import { io, Socket } from 'socket.io-client'; import { Main } from './Main'; import { delay, ServiceState } from './Utils'; const PREFIX = `[CameraRunner]`; export class CameraRunner { private _Main: Main; socket: Socket; errorTriggerStartupDelay = 10000; bootTime = Date.now(); state: ServiceState = 'DISCONNECTED'; message?: string; error?: string; processStatus: ProcessStatus = { state: 'STOPPED', startTime: -1, output: { current: [], last: [] }, }; constructor(Main: Main) { this._Main = Main; } handle(command: string, ...args: any[]) { switch (command) { case 'restart': case 'reboot': const callback: (response: { succeed: boolean; message?: string; }) => void = args[0]; if (typeof callback !== 'function') return; this.sendCommand(command, callback); break; } } sendCommand( command: 'reboot' | 'restart', callback: (response: { succeed: boolean; message?: string }) => void ) { if (this.socket == null || !this.socket.connected) return callback({ succeed: false, message: 'Not connected to CameraRunner', }); this.socket.emit( command, (response: { succeed: boolean; message?: string }) => { callback(response); } ); } broadcastState() { this._Main.WebServer.socket.emit('cameraRunnerState', this.getState()); } getState(): CameraRunnerStatus { return { state: this.state, message: this.message, error: this.error, processStatus: this.processStatus, }; } setInfo(message: string, error: string, state: ServiceState = 'FAILED') { this.message = message; this.error = error; this.state = state; this.broadcastState(); if ( error != null && Date.now() - this.bootTime > this.errorTriggerStartupDelay ) this._Main.Twilio.sendError('CameraRunner', error); if (state == 'FAILED' || state == 'DISCONNECTED') console.warn(PREFIX, message ?? error); else console.log(PREFIX, message ?? error); } private pollClock: NodeJS.Timeout; startPollClock() { const poll = async () => { const data: ProcessStatus = await new Promise((resolve) => { this.socket.emit('getStatus', (response: ProcessStatus) => resolve(response) ); }); this.processStatus = data; this.broadcastState(); }; this.socket.on('simpleStatus', (simpleStatus: ProcessStatusSimple) => { this.processStatus.state = simpleStatus.state; this.processStatus.message = simpleStatus.message; this.processStatus.error = simpleStatus.error; this.broadcastState(); }); clearInterval(this.pollClock); this.pollClock = setInterval( () => poll(), this._Main.Config.cameraRunner.pollInterval ); poll(); } async reconnect() { clearInterval(this.pollClock); if (this.socket != null) { this.socket.disconnect(); this.socket.close(); this.socket = null; } await delay(2000); this.message = 'Reconnecting in 10 seconds...'; this.broadcastState(); await delay(5000); await this.connect(); } async connect() { this.setInfo('Connecting...', null, 'CONNECTING'); await delay(1000); const ip = this._Main.Config.cameraRunner.webSocket.ip; const port = this._Main.Config.cameraRunner.webSocket.port; if (ip == null || port == null) { return this.setInfo( 'Connection problem', 'Camera Runner WebSocket IP or port is not configured.' ); } this.socket = io(`ws://${ip}:${port}`, { reconnectionAttempts: 0, reconnectionDelay: 2000, }); this.socket.on('connect', () => { this._Main.Twilio.resetError('CameraRunner'); this.setInfo('Connected', null, 'CONNECTED'); this.startPollClock(); }); this.socket.on('disconnect', () => { this.setInfo( 'Disconnected', 'Camera Runner was disconnected unexpectedly', 'DISCONNECTED' ); this.reconnect(); }); this.socket.on('reconnect', () => { this.setInfo('Connected (reconnection)', null, 'CONNECTED'); }); this.socket.on('connect_error', (err) => { this.setInfo('Connection problem', `Connection error: ${err}`); this.reconnect(); }); this.socket.on('error', (err) => { this.setInfo('Connection problem', `Socket error: ${err}`); this.reconnect(); }); this.socket.on('reconnect_attempt', () => { this.setInfo('Reconnecting', null, 'CONNECTING'); }); this.socket.on('reconnect_failed', () => { this.setInfo('Connection problem', 'Reconnection failed'); this.reconnect(); }); this.socket.on('close', () => { this.setInfo('Disconnected', null, 'DISCONNECTED'); this.reconnect(); }); } } interface CameraRunnerStatus { state: ServiceState; message?: string; error?: string; processStatus: ProcessStatus; } export type ProcessStatusState = 'RUNNING' | 'STOPPED' | 'STARTING' | 'PROBLEM'; interface ProcessStatusSimple { state: ProcessStatusState; message?: string; error?: string; } interface ProcessStatus extends ProcessStatusSimple { startTime: number; output: { current: string[]; last: string[] }; }