From bea03af8a0df9330b04ca1feddc9d931c27ae077 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sat, 26 Oct 2024 18:52:38 +0100 Subject: [PATCH] feat: satellite ws implementation #3101 --- Dockerfile | 2 +- Dockerfile.prebuild | 2 +- companion/lib/Service/Controller.ts | 12 +- companion/lib/Service/SatelliteApi.ts | 4 +- .../Service/{Satellite.ts => SatelliteTcp.ts} | 26 ++-- companion/lib/Service/SatelliteWebsocket.ts | 116 ++++++++++++++++++ webui/src/UserConfig/SatelliteConfig.tsx | 7 +- 7 files changed, 147 insertions(+), 22 deletions(-) rename companion/lib/Service/{Satellite.ts => SatelliteTcp.ts} (79%) create mode 100644 companion/lib/Service/SatelliteWebsocket.ts diff --git a/Dockerfile b/Dockerfile index 2772193f85..a4d2908dac 100755 --- a/Dockerfile +++ b/Dockerfile @@ -53,7 +53,7 @@ RUN mkdir $COMPANION_CONFIG_BASEDIR && chown companion:companion $COMPANION_CONF USER companion # Export ports for web, Satellite API and WebSocket (Elgato Plugin) -EXPOSE 8000 16622 28492 +EXPOSE 8000 16622 16623 28492 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD [ "curl", "-fSsq", "http://localhost:8000/" ] diff --git a/Dockerfile.prebuild b/Dockerfile.prebuild index 1b9c3b6e5f..ebd02dcf21 100755 --- a/Dockerfile.prebuild +++ b/Dockerfile.prebuild @@ -42,7 +42,7 @@ RUN useradd -ms /bin/bash companion \ USER companion # Export ports for web, Satellite API and WebSocket (Elgato Plugin) -EXPOSE 8000 16622 28492 +EXPOSE 8000 16622 16623 28492 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD [ "curl", "-fSsq", "http://localhost:8000/" ] diff --git a/companion/lib/Service/Controller.ts b/companion/lib/Service/Controller.ts index bbe09aa658..4249814948 100644 --- a/companion/lib/Service/Controller.ts +++ b/companion/lib/Service/Controller.ts @@ -7,7 +7,7 @@ import { ServiceHttps } from './Https.js' import { ServiceOscListener } from './OscListener.js' import { ServiceOscSender } from './OscSender.js' import { ServiceRosstalk } from './Rosstalk.js' -import { ServiceSatellite } from './Satellite.js' +import { ServiceSatelliteTcp } from './SatelliteTcp.js' import { ServiceSharedUdpManager } from './SharedUdpManager.js' import { ServiceSurfaceDiscovery } from './SurfaceDiscovery.js' import { ServiceTcp } from './Tcp.js' @@ -15,6 +15,7 @@ import { ServiceUdp } from './Udp.js' import { ServiceVideohubPanel } from './VideohubPanel.js' import type { Registry } from '../Registry.js' import type { ClientSocket } from '../UI/Handler.js' +import { ServiceSatelliteWebsocket } from './SatelliteWebsocket.js' /** * Class that manages all of the services. @@ -46,7 +47,8 @@ export class ServiceController { readonly emberplus: ServiceEmberPlus readonly artnet: ServiceArtnet readonly rosstalk: ServiceRosstalk - readonly satellite: ServiceSatellite + readonly satelliteTcp: ServiceSatelliteTcp + readonly satelliteWebsocket: ServiceSatelliteWebsocket readonly elgatoPlugin: ServiceElgatoPlugin readonly videohubPanel: ServiceVideohubPanel readonly bonjourDiscovery: ServiceBonjourDiscovery @@ -63,7 +65,8 @@ export class ServiceController { this.emberplus = new ServiceEmberPlus(registry) this.artnet = new ServiceArtnet(registry) this.rosstalk = new ServiceRosstalk(registry) - this.satellite = new ServiceSatellite(registry) + this.satelliteTcp = new ServiceSatelliteTcp(registry) + this.satelliteWebsocket = new ServiceSatelliteWebsocket(registry) this.elgatoPlugin = new ServiceElgatoPlugin(registry) this.videohubPanel = new ServiceVideohubPanel(registry) this.bonjourDiscovery = new ServiceBonjourDiscovery(registry) @@ -85,7 +88,8 @@ export class ServiceController { this.oscListener.updateUserConfig(key, value) this.oscSender.updateUserConfig(key, value) this.rosstalk.updateUserConfig(key, value) - this.satellite.updateUserConfig(key, value) + this.satelliteTcp.updateUserConfig(key, value) + this.satelliteWebsocket.updateUserConfig(key, value) this.tcp.updateUserConfig(key, value) this.udp.updateUserConfig(key, value) this.videohubPanel.updateUserConfig(key, value) diff --git a/companion/lib/Service/SatelliteApi.ts b/companion/lib/Service/SatelliteApi.ts index e26870964c..054d2fa32c 100644 --- a/companion/lib/Service/SatelliteApi.ts +++ b/companion/lib/Service/SatelliteApi.ts @@ -39,7 +39,7 @@ export interface SatelliteSocketWrapper { } export interface SatelliteInitSocketResult { - processMessage(data: Buffer): void + processMessage(data: string): void cleanupDevices(): number } @@ -225,7 +225,7 @@ export class ServiceSatelliteApi extends CoreBase { let receivebuffer = '' return { processMessage: (data) => { - receivebuffer += data.toString() + receivebuffer += data let i = 0, line = '', diff --git a/companion/lib/Service/Satellite.ts b/companion/lib/Service/SatelliteTcp.ts similarity index 79% rename from companion/lib/Service/Satellite.ts rename to companion/lib/Service/SatelliteTcp.ts index 710202ca96..0889685812 100644 --- a/companion/lib/Service/Satellite.ts +++ b/companion/lib/Service/SatelliteTcp.ts @@ -5,7 +5,7 @@ import type { Registry } from '../Registry.js' import { ServiceSatelliteApi } from './SatelliteApi.js' /** - * Class providing the Satellite/Remote Surface api. + * Class providing the Satellite/Remote Surface api over tcp. * * @author Håkon Nessjøen * @author Keith Rocheck @@ -24,15 +24,15 @@ import { ServiceSatelliteApi } from './SatelliteApi.js' * develop commercial activities involving the Companion software without * disclosing the source code of your own applications. */ -export class ServiceSatellite extends ServiceBase { +export class ServiceSatelliteTcp extends ServiceBase { readonly #api: ServiceSatelliteApi - server: net.Server | undefined = undefined + #server: net.Server | undefined = undefined readonly #clients = new Set() constructor(registry: Registry) { - super(registry, 'Service/Satellite', null, null) + super(registry, 'Service/SatelliteTcp', null, null) this.#api = new ServiceSatelliteApi(registry) @@ -42,9 +42,9 @@ export class ServiceSatellite extends ServiceBase { } listen() { - this.server = net.createServer((socket) => { + this.#server = net.createServer((socket) => { const name = socket.remoteAddress + ':' + socket.remotePort - const socketLogger = LogController.createLogger(`Service/Satellite/${name}`) + const socketLogger = LogController.createLogger(`Service/SatelliteTcp/${name}`) this.#clients.add(socket) @@ -75,23 +75,23 @@ export class ServiceSatellite extends ServiceBase { socket.on('close', doCleanup) - socket.on('data', processMessage) + socket.on('data', (data) => processMessage(data.toString())) }) - this.server.on('error', (e) => { + this.#server.on('error', (e) => { this.logger.debug(`listen-socket error: ${e}`) }) try { - this.server.listen(this.port) + this.#server.listen(this.port) } catch (e) { - this.logger.debug(`ERROR opening port this.port for companion satellite devices`) + this.logger.debug(`ERROR opening tcp port ${this.port} for companion satellite devices`) } } close(): void { - if (this.server) { - this.server.close() - this.server = undefined + if (this.#server) { + this.#server.close() + this.#server = undefined } // Disconnect all clients diff --git a/companion/lib/Service/SatelliteWebsocket.ts b/companion/lib/Service/SatelliteWebsocket.ts new file mode 100644 index 0000000000..0af68181c3 --- /dev/null +++ b/companion/lib/Service/SatelliteWebsocket.ts @@ -0,0 +1,116 @@ +import { ServiceBase } from './Base.js' +import LogController from '../Log/Controller.js' +import type { Registry } from '../Registry.js' +import { ServiceSatelliteApi } from './SatelliteApi.js' +import { WebSocketServer } from 'ws' + +/** + * Class providing the Satellite/Remote Surface api over websockets. + * + * @author Håkon Nessjøen + * @author Keith Rocheck + * @author William Viker + * @author Julian Waller + * @since 2.2.0 + * @copyright 2022 Bitfocus AS + * @license + * This program is free software. + * You should have received a copy of the MIT licence as well as the Bitfocus + * Individual Contributor License Agreement for Companion along with + * this program. + * + * You can be released from the requirements of the license by purchasing + * a commercial license. Buying such a license is mandatory as soon as you + * develop commercial activities involving the Companion software without + * disclosing the source code of your own applications. + */ +export class ServiceSatelliteWebsocket extends ServiceBase { + readonly #api: ServiceSatelliteApi + + #server: WebSocketServer | undefined = undefined + + constructor(registry: Registry) { + super(registry, 'Service/SatelliteWebsocket', null, null) + + this.#api = new ServiceSatelliteApi(registry) + + this.port = 16623 + + this.init() + } + + listen() { + if (this.#server === undefined) { + try { + this.#server = new WebSocketServer({ + port: this.port, + }) + + this.#server.on('error', (e) => { + this.logger.debug(`listen-socket error: ${e}`) + }) + + this.#server.on('connection', (socket) => { + // @ts-expect-error This works but isn't in the types.. TODO: verify this + const name = socket.remoteAddress + ':' + socket.remotePort + const socketLogger = LogController.createLogger(`Service/SatelliteWs/${name}`) + + let lastReceived = Date.now() + + // socket.setTimeout(5000) + socket.on('error', (e) => { + socketLogger.silly('socket error:', e) + }) + + const { processMessage, cleanupDevices } = this.#api.initSocket(socketLogger, { + // @ts-expect-error The property exists but not in the types + remoteAddress: socket.remoteAddress, + destroy: () => socket.terminate(), + write: (data) => socket.send(data), + }) + + const timeoutCheck = setInterval(() => { + if (lastReceived < Date.now() - 5000) { + socketLogger.debug('socket timeout') + socket.terminate() + doCleanup() + } + }, 3000) + + const doCleanup = () => { + const count = cleanupDevices() + socketLogger.info(`connection closed with ${count} connected surfaces`) + + socket.removeAllListeners('data') + socket.removeAllListeners('close') + + clearInterval(timeoutCheck) + } + + socket.on('close', doCleanup) + + socket.on('message', (data) => { + lastReceived = Date.now() + + processMessage(data.toString()) + }) + }) + } catch (e) { + this.logger.debug(`ERROR opening ws port ${this.port} for companion satellite devices`) + } + } + } + + close(): void { + if (this.#server) { + this.logger.info('Shutting down') + + for (const client of this.#server.clients) { + client.terminate() + } + + this.#server.close() + this.#server = undefined + } + } +} diff --git a/webui/src/UserConfig/SatelliteConfig.tsx b/webui/src/UserConfig/SatelliteConfig.tsx index 74377ae34c..4f70ac14eb 100644 --- a/webui/src/UserConfig/SatelliteConfig.tsx +++ b/webui/src/UserConfig/SatelliteConfig.tsx @@ -11,9 +11,14 @@ export const SatelliteConfig = observer(function SatelliteConfig(_props: UserCon Satellite Listen Port} + label={Satellite TCP Listen Port} text={16622} /> + + Satellite Websocket Listen Port} + text={16623} + /> ) })