UNPKG

@mos-connection/connector

Version:
523 lines 24.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MosConnection = void 0; const connectionConfig_1 = require("./config/connectionConfig"); const mosSocketServer_1 = require("./connection/mosSocketServer"); const model_1 = require("@mos-connection/model"); const MosDevice_1 = require("./MosDevice"); const socketConnection_1 = require("./connection/socketConnection"); const NCSServerConnection_1 = require("./connection/NCSServerConnection"); const helper_1 = require("@mos-connection/helper"); const eventemitter3_1 = require("eventemitter3"); const iconv = require("iconv-lite"); const mosMessageParser_1 = require("./connection/mosMessageParser"); const lib_1 = require("./lib"); class MosConnection extends eventemitter3_1.EventEmitter { /** */ constructor(configOptions) { super(); this._debug = false; this._disposed = false; this._scheduleCheckProfileValidnessTimeout = null; this._incomingSockets = {}; this._ncsConnections = {}; this._mosDevices = {}; this._initialized = false; this._isListening = false; this._isOpenMediaHotStandby = false; this._conf = new connectionConfig_1.ConnectionConfig(configOptions); if (this._conf.debug) { this._debug = this._conf.debug; } // Setup utility functions for handling MosTypes: this.mosTypes = (0, model_1.getMosTypes)(configOptions.strict ?? false); if (this._conf.strict) { const orgStack = new Error(); this._scheduleCheckProfileValidness(orgStack); } } /** * Initiate the MosConnection, start accepting connections */ async init() { this.emit('info', `Initializing MOS-Connection, id: ${this._conf.mosID}`); this._initialized = true; if (this._conf.acceptsConnections) { await this._initiateIncomingConnections(); this._isListening = true; return true; } return false; } /** * Establish a new connection to a MOS-device (NCS-server). When established, the new MOS-device will be emitted to this.onConnection() * @param connectionOptions Connection options */ async connect(connectionOptions) { if (!this._initialized) throw Error('Not initialized, run .init() first!'); this._isOpenMediaHotStandby = connectionOptions.secondary?.openMediaHotStandby ?? false; // Connect to MOS-device: const primary = new NCSServerConnection_1.NCSServerConnection(connectionOptions.primary.id, connectionOptions.primary.host, this._conf.mosID, connectionOptions.primary.timeout, connectionOptions.primary.heartbeatInterval, this._debug, this.mosTypes.strict, this._isOpenMediaHotStandby); const secondary = connectionOptions.secondary ? new NCSServerConnection_1.NCSServerConnection(connectionOptions.secondary.id, connectionOptions.secondary.host, this._conf.mosID, connectionOptions.secondary.timeout, connectionOptions.secondary.heartbeatInterval, this._debug, this.mosTypes.strict, this._isOpenMediaHotStandby) : null; this._ncsConnections[connectionOptions.primary.host] = primary; primary.on('rawMessage', (type, message) => { this.emit('rawMessage', 'primary', type, message); }); primary.on('warning', (str) => { this.emit('warning', 'primary: ' + str); }); primary.on('error', (error) => { error.message = `primary: ${error.message}`; this.emit('error', error); }); primary.on('info', (str) => { this.emit('info', 'primary: ' + str); }); primary.createClient(MosConnection.nextSocketID, connectionOptions.primary.ports?.lower ?? MosConnection.CONNECTION_PORT_LOWER, 'lower', true); primary.createClient(MosConnection.nextSocketID, connectionOptions.primary.ports?.upper ?? MosConnection.CONNECTION_PORT_UPPER, 'upper', true); if (!connectionOptions.primary.dontUseQueryPort) { primary.createClient(MosConnection.nextSocketID, connectionOptions.primary.ports?.query ?? MosConnection.CONNECTION_PORT_QUERY, 'query', false); } if (secondary && connectionOptions.secondary) { this._ncsConnections[connectionOptions.secondary.host] = secondary; secondary.on('rawMessage', (type, message) => { this.emit('rawMessage', 'secondary', type, message); }); secondary.on('warning', (str) => { this.emit('warning', 'secondary: ' + str); }); secondary.on('error', (error) => { error.message = `secondary: ${error.message}`; this.emit('error', error); }); secondary.on('info', (str) => { this.emit('info', 'secondary: ' + str); }); secondary.createClient(MosConnection.nextSocketID, connectionOptions.secondary.ports?.lower ?? MosConnection.CONNECTION_PORT_LOWER, 'lower', !connectionOptions.secondary.openMediaHotStandby); secondary.createClient(MosConnection.nextSocketID, connectionOptions.secondary.ports?.upper ?? MosConnection.CONNECTION_PORT_UPPER, 'upper', !connectionOptions.secondary.openMediaHotStandby); if (!connectionOptions.primary.dontUseQueryPort) { secondary.createClient(MosConnection.nextSocketID, connectionOptions.secondary.ports?.query ?? MosConnection.CONNECTION_PORT_QUERY, 'query', false); } // Handle that .openMediaHotStandby should not check for heartbeats on // the secondary connection when the primary is connected // And disable heartbeats on the primary when the primary is disconnected if (connectionOptions.secondary?.openMediaHotStandby) { // Initially disable heartbeats on secondary since primary should be attempted first secondary.disableHeartbeats(); primary.enableHeartbeats(); primary.on('connectionChanged', () => { if (primary.connected) { secondary?.disableHeartbeats(); primary.enableHeartbeats(); } else { secondary?.enableHeartbeats(); primary.disableHeartbeats(); } }); // Handle secondary connection changes setTimeout(() => { secondary?.on('connectionChanged', () => { if (!primary.connected) { // Secondary is active if (secondary?.connected) { secondary.enableHeartbeats(); primary.disableHeartbeats(); } else { // Secondary disconnected - try to re-enable primary primary.enableHeartbeats(); secondary?.disableHeartbeats(); } } }); }, 50); } } return this._registerMosDevice(this._conf.mosID, connectionOptions.primary.id, connectionOptions.secondary ? connectionOptions.secondary.id : null, primary, secondary); } /** Callback is called when a new connection is established */ onConnection(cb) { this._onConnection = cb; } /** True if mosConnection is listening for connections */ get isListening() { return this._isListening; } /** TO BE IMPLEMENTED: True if mosConnection is mos-compliant */ get isCompliant() { return false; } /** True if mosConnection is configured to accept connections */ get acceptsConnections() { return this._conf.acceptsConnections; } /** A list of the profiles mosConnection is currently configured to use */ get profiles() { return this._conf.profiles; } /** Close all connections and clear all data */ async dispose() { this._disposed = true; const sockets = []; for (const socketID in this._incomingSockets) { const e = this._incomingSockets[socketID]; if (e) { sockets.push(e.socket); } } const disposePromises0 = sockets.map(async (socket) => { return new Promise((resolve) => { socket.on('close', resolve); socket.end(); socket.destroy(); }); }); const disposePromises1 = [ this._lowerSocketServer ? this._lowerSocketServer.dispose([]) : Promise.resolve(), this._upperSocketServer ? this._upperSocketServer.dispose([]) : Promise.resolve(), this._querySocketServer ? this._querySocketServer.dispose([]) : Promise.resolve(), ]; const disposePromises2 = []; for (const deviceId of Object.keys(this._mosDevices)) { const device = this._mosDevices[deviceId]; disposePromises2.push(this.disposeMosDevice(device)); } await Promise.all(disposePromises0); await Promise.all(disposePromises1); await Promise.all(disposePromises2); } /** Return a specific MOS-device */ getDevice(id) { return this._mosDevices[id]; } /** Get a list of all MOS-devices */ getDevices() { return Object.keys(this._mosDevices).map((id) => { return this._mosDevices[id]; }); } async disposeMosDevice(myMosIDOrMosDevice, theirMosId0, theirMosId1) { let id0; let id1; if (myMosIDOrMosDevice && myMosIDOrMosDevice instanceof MosDevice_1.MosDevice) { // myMosID = myMosIDOrMosDevice const mosDevice = myMosIDOrMosDevice; id0 = mosDevice.idPrimary; id1 = mosDevice.idSecondary; } else { const myMosID = myMosIDOrMosDevice; id0 = myMosID + '_' + theirMosId0; id1 = theirMosId1 ? myMosID + '_' + theirMosId1 : null; } if (this._mosDevices[id0]) { await this._mosDevices[id0].dispose(); delete this._mosDevices[id0]; } else if (id1 && this._mosDevices[id1]) { await this._mosDevices[id1].dispose(); delete this._mosDevices[id1]; } else { throw new Error(`Device not found ("${id0}", "${id1}")`); } } /** * Do a check if the profile is valid. Throws if not. * Optionally called after a mosConnection has been set up to ensure that all callbacks have been set up properly. */ checkProfileValidness() { if (this._scheduleCheckProfileValidnessTimeout) { clearTimeout(this._scheduleCheckProfileValidnessTimeout); this._scheduleCheckProfileValidnessTimeout = null; } const orgStack = new Error(); this._checkProfileValidness(orgStack); } /** TO BE IMPLEMENTED */ get complianceText() { if (this.isCompliant) { const profiles = []; for (const nextSocketID in this._conf.profiles) { if (this._conf.profiles[nextSocketID] === true) { profiles.push(nextSocketID); } } return `MOS Compatible - Profiles ${profiles.join(',')}`; } return 'Warning: Not MOS compatible'; } setDebug(debug) { this._debug = debug; this.getDevices().forEach((device) => { device.setDebug(debug); }); Object.keys(this._ncsConnections).forEach((host) => { const conn = this._ncsConnections[host]; if (conn) { conn.setDebug(debug); } }); if (this._lowerSocketServer) this._lowerSocketServer.setDebug(debug); if (this._upperSocketServer) this._upperSocketServer.setDebug(debug); if (this._querySocketServer) this._querySocketServer.setDebug(debug); } _registerMosDevice(myMosID, theirMosId0, theirMosId1, primary, secondary) { const id0 = myMosID + '_' + theirMosId0; const id1 = theirMosId1 ? myMosID + '_' + theirMosId1 : null; const mosDevice = new MosDevice_1.MosDevice(id0, id1, this._conf, primary, secondary, this._conf.offspecFailover, this.mosTypes.strict); mosDevice.setDebug(this._debug); // Add mosDevice to register: if (this._mosDevices[id0]) { throw new Error('Unable to register MosDevice "' + id0 + '": The device already exists!'); } if (id1 && this._mosDevices[id1]) { throw new Error('Unable to register MosDevice "' + id1 + '": The device already exists!'); } this._mosDevices[id0] = mosDevice; if (id1) this._mosDevices[id1] = mosDevice; mosDevice.connect(); // emit to .onConnection: if (this._onConnection) this._onConnection(mosDevice); return mosDevice; } /** Set up TCP-server */ async _initiateIncomingConnections() { if (!this._conf.acceptsConnections) { return Promise.reject(new Error('Not configured for accepting connections')); } const initSocket = (port, portType) => { const socketServer = new mosSocketServer_1.MosSocketServer(port, portType, this._debug); socketServer.on(socketConnection_1.SocketServerEvent.CLIENT_CONNECTED, (e) => this._registerIncomingClient(e)); socketServer.on(socketConnection_1.SocketServerEvent.ERROR, (err) => { // handle error this.emit('error', err); }); return socketServer; }; this._lowerSocketServer = initSocket(this._conf.ports?.lower ?? MosConnection.CONNECTION_PORT_LOWER, 'lower'); this._upperSocketServer = initSocket(this._conf.ports?.upper ?? MosConnection.CONNECTION_PORT_UPPER, 'upper'); this._querySocketServer = initSocket(this._conf.ports?.query ?? MosConnection.CONNECTION_PORT_QUERY, 'query'); const handleListen = async (socketServer) => { await socketServer.listen(); this.emit('info', 'Listening on port ' + socketServer.port + ' (' + socketServer.portDescription + ')'); }; await Promise.all([ handleListen(this._lowerSocketServer), handleListen(this._upperSocketServer), handleListen(this._querySocketServer), ]); // All sockets are open and listening at this point } /** */ _registerIncomingClient(client) { const socketID = MosConnection.nextSocketID; this.emit('rawMessage', 'incoming_' + socketID, 'newConnection', 'From ' + client.socket.remoteAddress + ':' + client.socket.remotePort); const messageParser = new mosMessageParser_1.MosMessageParser(`${socketID}, ${client.socket.remoteAddress}, ${client.portDescription}`); // messageParser.debug = this._debug messageParser.on('message', (message, messageString) => { // Handle incoming data handleMessage(message, messageString).catch((err) => this.emit('error', err)); }); // handles socket listeners client.socket.on('close', ( /*hadError: boolean*/) => { this._disposeIncomingSocket(socketID); this.emit('rawMessage', 'incoming_' + socketID, 'closedConnection', ''); }); client.socket.on('end', () => { this.debugTrace('Socket End'); }); client.socket.on('drain', () => { this.debugTrace('Socket Drain'); }); client.socket.on('data', (data) => { const messageString = iconv.decode(data, 'utf16-be'); this.emit('rawMessage', 'incoming', 'recieved', messageString); try { messageParser.debug = this._debug; messageParser.parseMessage(messageString); } catch (err) { this.emit('error', err instanceof Error ? err : new Error(`${err}`)); } }); const handleMessage = async (parsed, _messageString) => { const remoteAddressContent = client.socket.remoteAddress ? client.socket.remoteAddress.split(':') : undefined; const remoteAddress = remoteAddressContent ? remoteAddressContent[remoteAddressContent.length - 1] : ''; const ncsID = parsed.mos.ncsID; const mosID = parsed.mos.mosID; const mosMessageId = parsed.mos.messageID ? parseInt(parsed.mos.messageID, 10) : undefined; let mosDevice = this._mosDevices[ncsID + '_' + mosID] || this._mosDevices[mosID + '_' + ncsID]; const sendReply = (message) => { message.ncsID = ncsID; message.mosID = mosID; message.prepare(mosMessageId); const sendMessageString = message.toString(); const buf = iconv.encode(sendMessageString, 'utf16-be'); client.socket.write(buf); this.emit('rawMessage', 'incoming_' + socketID, 'sent', sendMessageString); }; if (!mosDevice && this._conf.openRelay) { // No MOS-device found in the register // Register a new mosDevice to use for this connection: if (ncsID === this._conf.mosID) { // Setup a "primary" connection back to the mos-device, so that we can automatically // send commands to it through the mosDevice const primary = new NCSServerConnection_1.NCSServerConnection(mosID, remoteAddress, this._conf.mosID, undefined, undefined, this._debug, this.mosTypes.strict, this._isOpenMediaHotStandby); this._ncsConnections[remoteAddress] = primary; primary.on('rawMessage', (type, message) => { this.emit('rawMessage', 'primary', type, message); }); primary.on('warning', (str) => { this.emit('warning', 'primary: ' + str); }); primary.on('error', (err) => { err.message = `primary: ${err.message}`; this.emit('error', err); }); const openRelayOptions = typeof this._conf.openRelay === 'object' ? this._conf.openRelay.options : undefined; primary.createClient(MosConnection.nextSocketID, openRelayOptions?.ports?.lower ?? MosConnection.CONNECTION_PORT_LOWER, 'lower', true); primary.createClient(MosConnection.nextSocketID, openRelayOptions?.ports?.upper ?? MosConnection.CONNECTION_PORT_UPPER, 'upper', true); mosDevice = this._registerMosDevice(this._conf.mosID, mosID, null, primary, null); } else if (mosID === this._conf.mosID) { mosDevice = await this.connect({ primary: { id: ncsID, host: remoteAddress, }, }); } } if (mosDevice) { mosDevice .routeData(parsed.mos, client.portDescription) .catch((err) => { if (helper_1.MosModel.ParseError.isParseError(err)) { // Try to add the main mos message key to the error: const breadcrumbs = Object.keys(parsed.mos).filter((key) => !['messageID', 'mosID', 'ncsID'].includes(key) && typeof parsed.mos[key] === 'object'); throw helper_1.MosModel.ParseError.handleCaughtError(breadcrumbs.join('/'), err); } else throw err; }) .then((message) => { sendReply(message); }) .catch((err) => { // Something went wrong if (err instanceof helper_1.MosModel.MosMessage) { sendReply(err); } else { // Internal/parsing error // Log error: this.emit('warning', `Error when handling incoming data: ${err} ${err?.stack}`); // reply with NACK: // TODO: implement ACK // https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOS_Protocol_Version_2.8.5_Final.htm#mosAck const msg = new helper_1.MosModel.MOSAck({ ID: this.mosTypes.mosString128.create('0'), Revision: 0, Description: this.mosTypes.mosString128.create(`Internal error: ${err}`), Status: model_1.IMOSAckStatus.NACK, }, this.mosTypes.strict); sendReply(msg); // TODO: Need tests } }); } else { // No MOS-device found in the register // We can't handle the message, reply with a NACK: const msg = new helper_1.MosModel.MOSAck({ ID: this.mosTypes.mosString128.create('0'), Revision: 0, Description: this.mosTypes.mosString128.create('MosDevice not found'), Status: model_1.IMOSAckStatus.NACK, }, this.mosTypes.strict); sendReply(msg); // TODO: Need tests } }; client.socket.on('error', (e) => { e.message = `Socket had error (${socketID}, ${client.socket.remoteAddress}, ${client.portDescription}): ${e.message}`; this.emit('error', e); this.debugTrace(`Socket had error (${socketID}, ${client.socket.remoteAddress}, ${client.portDescription}): ${e}`); }); // Register this socket: this._incomingSockets[socketID + ''] = client; this.debugTrace('Added socket: ', socketID); } /** Close socket and clean up */ _disposeIncomingSocket(socketID) { const e = this._incomingSockets[socketID + '']; if (e) { e.socket.removeAllListeners(); e.socket.destroy(); } delete this._incomingSockets[socketID + '']; this.debugTrace('removed: ', socketID, '\n'); } /** Get new unique id */ static get nextSocketID() { return this._nextSocketID++ + ''; } debugTrace(...strs) { // eslint-disable-next-line no-console if (this._debug) console.log(...strs); } _scheduleCheckProfileValidness(orgStack) { if (this._scheduleCheckProfileValidnessTimeout) return; this._scheduleCheckProfileValidnessTimeout = setTimeout(() => { this._scheduleCheckProfileValidnessTimeout = null; if (this._disposed) return; try { this._checkProfileValidness(orgStack); } catch (e) { // eslint-disable-next-line no-console console.error(e); } }, lib_1.PROFILE_VALIDNESS_CHECK_WAIT_TIME); } /** * Checks that all callbacks have been set up properly, according to which MOS-profile have been set in the options. * throws if something's wrong */ _checkProfileValidness(orgStack) { if (!this._conf.strict) return; const fixError = (message) => { // Change the stack of the error, so that it points to the original call to the MosDevice: const err = new Error(message); err.stack = message + orgStack.stack; return err; }; if (this.listenerCount('error') === 0) { throw fixError(`Error: no listener for the "error" event has been set up for MosConnection!`); } if (this.listenerCount('warning') === 0) { throw fixError(`Error: no listener for the "warning" event has been set up for MosConnection!`); } } } exports.MosConnection = MosConnection; MosConnection.CONNECTION_PORT_LOWER = 10540; MosConnection.CONNECTION_PORT_UPPER = 10541; MosConnection.CONNECTION_PORT_QUERY = 10542; MosConnection._nextSocketID = 0; //# sourceMappingURL=MosConnection.js.map