UNPKG

@pmouli/isy-matter-server

Version:

Service to expose an ISY device as a Matter Border router

329 lines (325 loc) 13.3 kB
#!/usr/bin/env node import { fork } from 'child_process'; import { stat, unlink } from 'fs'; import { chmod } from 'fs/promises'; import { logStringify, writeDebugFile, writeFile } from 'isy-nodejs/Utils'; import { createServer } from 'net'; import { exit } from 'process'; import { promisify } from 'util'; import { authenticate } from './authenticate.js'; import path from 'path'; import * as Utils from './utils.js'; /* const program = new Command(); program.option('-a, --autoStart', 'Start matter bridge server on startup', false).option('-l, --dependencies', 'Load dependencies - static (from local node_modules), plugin (from plugin node_modules)', 'static').option('-e, --env [PATH]', 'Path to environment file', '.env').option('-d, --devices <DEVICES>', 'list of device (addresses) to expose', []).option('--filter [FILTER]', 'devices names to exclude', '').option('-r, --requireAuth', 'Require authentication to start bridge server', false).option('-s, --openSocket', 'Open socket to receive requests from plugin/client', false).option('-f, --factoryReset', 'Reset bridge server to initial state', false).option('-p --subprocess', 'run as subprocess', false); program.parse(); export let options: ProgramOptions = { autoStart: false, dependencies: 'static', env: '.env', requireAuth: true, openSocket: false, reset: false, devices: [], filter: '', subprocess: false, factoryReset: false }; options = program.opts<ProgramOptions & { subprocess: boolean }>(); */ await Utils.initialize(); async function launchBridge() { if (Utils.serverConfig.subprocess) { if (cp) { logger.info('Bridge already started as subprocess'); return; } logger.info('Running bridge as subprocess'); await startChildProcess(); } else { logger.info('Running bridge in main process'); await (await import('./server.js')).startBridgeServer(); //let pc = (await import('./server.js')).matterServer.getPairingCode(); //if (client) client.write(JSON.stringify({ pairingInfo: pc })); } } async function shutdownBridge() { if (Utils.serverConfig.subprocess) { await new Promise((resolve) => { logger.info('Bridge was started as a child process. Terminating...'); if (cp) { cp.once('exit', (code, signal) => { logger.info(`Child process exited with code ${code} and signal ${signal}`); cp = undefined; resolve(); }); cp.kill('SIGABRT'); } else { logger.info('No child process to kill'); resolve(); } }); } else { console.log('Bridge started in process'); await (await import('./server.js')).stopBridgeServer(); } } let cp; let pairingInfo; let abortController; // This is the main entry point for the Matter server. async function startChildProcess() { let serverjsloc = path.relative(process.cwd(), new URL(import.meta.url).pathname).replace('main', 'server'); logger.info('Starting child process: ' + serverjsloc); abortController = new AbortController(); cp = fork(serverjsloc, process.argv.slice(2), { killSignal: 'SIGABRT', signal: abortController.signal }); return await new Promise((resolve) => { cp.on('spawn', () => { console.log('Child process spawned'); resolve(cp); }) .on('message', (msg) => { //logger.info('Message from child process:', msg); //console.log('Message from child process:', msg); if (typeof msg === 'string') { logger.info('Message from child process:', msg); } else if ('pairingInfo' in msg) { pairingInfo = msg.pairingInfo; if (client) client.write(JSON.stringify({ pairingInfo: msg.pairingInfo })); } else { if (client) client.write(JSON.stringify(msg)); //console.log('Message from child process:', msg); } }) .on('close', (code, signal) => { console.log(`Child process closed connection with code ${code} and ${signal}.`); // Restart the child process //startChildProcess(); }) .on('exit', (code, signal) => { console.log(`Child process exited with code ${code} and ${signal}`); //process.exit(1); }); /*if (client) { cp.send('client', client); }*/ }); } export async function processMessage(msg) { try { if (msg.type !== 'auth' && !authenticated) { Utils.logger.warn('Not authenticated. Ignoring message: ' + logStringify(msg)); return; } //console.log(data.toString()); switch (msg.type) { case 'auth': console.log('Authenticating: ' + logStringify(msg)); authenticated = await authenticate(msg.credential); if (authenticated) Utils.logger.info('Client successfully authenticated.'); else Utils.logger.warn('Authentication failed. Terminating client connection.'); await promisify(client?.end)(); break; case 'isyConfig': delete msg.type; Utils.updateConfigs({ isyConfig: msg }); console.log('ISY api config update: ' + logStringify(Utils.isyConfig)); await writeDebugFile(logStringify(Utils.isyConfig), 'isyConfig.json', Utils.logger, Utils.serverConfig.workingDir); break; case 'matterConfig': delete msg.type; Utils.updateConfigs({ matterConfig: msg }); console.log('Matter bridge config update: ' + logStringify(Utils.matterConfig)); await writeFile(logStringify(Utils.overrides), 'overrides.json', Utils.serverConfig.workingDir); await writeDebugFile(logStringify(Utils.matterConfig), 'matterConfig.json', Utils.logger, Utils.serverConfig.workingDir); Utils.logger.info('Matter bridge config updated. Restarting bridge server'); await shutdownBridge(); await launchBridge(); Utils.logger.info('Matter bridge server restarted'); break; case 'serverConfig': delete msg.type; Utils.updateConfigs({ serverConfig: msg }); Utils.logger.transports[2].level = Utils.serverConfig.logLevel; //Utils.logger.transports[1].filename = Utils.serverConfig.logPath; console.log('Server config update: ' + logStringify(Utils.serverConfig)); await writeDebugFile(logStringify(Utils.serverConfig), 'serverConfig.json', Utils.logger, Utils.serverConfig.workingDir); break; case 'clientEnv': pluginEnv = msg.env; console.log('Plugin environment variables: ' + logStringify(pluginEnv)); break; case 'command': switch (msg.command) { case 'start': Utils.logger.info('Matter bridge start requested'); await launchBridge(); break; case 'stop': Utils.logger.info('Matter bridge stop requested'); await shutdownBridge(); break; case 'requestPairingCode': client.write(JSON.stringify({ pairingInfo: matterServer.getPairingCode() })); break; } break; } } catch (e) { Utils.logger.error(`Error processing msg: ${msg}: ${e.message}`); } } export let authenticated = false; export const matterServiceSockPath = '/tmp/ns2matter'; export let socketServer; export let matterServer; export let client; export let pluginEnv; export function parseConcatenatedJSON(json) { try { let s = '[' + json.trim().replace(/}\s*{/g, '},{') + ']'; return JSON.parse(s); //NOTE: will not handle brackets in quoted strings } catch { console.error('Error parsing concatenated JSON: ' + json); return null; } } export async function startSocketServer() { socketServer = createServer(async (socket) => { //socket.write('Echo server\r\n'); //let Utils.loggerStream = pipeline(addLogHeaderStream,socket); Utils.addClientLogTransport(socket); if (client) { Utils.logger.error('Previous client already connected. Disconnecting previous client.'); await new Promise((resolve) => { client.end(() => { client.destroy(); resolve(); }); }); } Utils.logger.info('Client connected'); client = socket; client .on('data', async (data) => { Utils.logger.debug(`Received: ${data.toString()}`); let s = data.toString(); let messages = parseConcatenatedJSON(s); for (const message of messages) { await processMessage(message); } return; }) .on('end', () => { Utils.removeClientLogTransport(); Utils.logger.info('Client disconnected'); client = null; authenticated = false; }); }); socketServer.on('error', (err) => { Utils.logger.error('Socket server error: ' + err); exit(1); }); try { await promisify(stat)(matterServiceSockPath); Utils.logger.info('Removing leftover socket.'); try { await promisify(unlink)(matterServiceSockPath); } catch (e) { Utils.logger.error(`Error unlinking socket. ${e.message}`); process.exit(0); } } finally { return new Promise((resolve, reject) => { socketServer.listen(matterServiceSockPath, async () => { try { Utils.logger.info('Socket bound.'); Utils.logger.info('Setting socket permissions.'); await chmod(matterServiceSockPath, 0o777); //TODO: Set to group only resolve(socketServer); } catch (e) { reject(e); } }); }); } } export async function stopSocketServer() { try { if (socketServer) { Utils.logger.info('Stopping socket server'); try { delete Utils.logger.transports[2]; Utils.removeClientLogTransport(); } finally { if (client) await promisify(client?.end)(); if (socketServer) await promisify(socketServer.close)(); client = null; Utils.logger.info('Socket server stopped'); } } } catch (e) { Utils.logger.error(`Error stopping socket server ${e.message}`); } } /*stat(matterServiceSockPath, function (err, stats) { if (err) { // start server console.log('No leftover socket found.'); server.listen(matterServiceSockPath, () => { console.log('Server bound'); }); } else { // remove file then start server console.log('Removing leftover socket.'); unlink(matterServiceSockPath, function (err) { if (err) { // This should never happen. console.error(err); process.exit(0); } server.listen(matterServiceSockPath, () => { console.log('Server bound'); }); }); } });*/ let logger = Utils.logger; logger.info(`ISY config: ${logStringify(Utils.isyConfig)}`); logger.info(`Matter config: ${logStringify(Utils.matterConfig)}`); logger.info(`Server config: ${logStringify(Utils.serverConfig)}`); if (Utils.options.openSocket) { Utils.logger.info('Starting socket server'); await startSocketServer(); } else { Utils.logger.info('Running standalone'); } if (!Utils.options.requireAuth) { authenticated = true; } if (Utils.options.autoStart) { Utils.logger.info('Auto start enabled. Starting bridge server'); await launchBridge(); } /* /*async function handleExit(signal: NodeJS.Signals) { Utils.logger.info(`Received ${signal}. Shutting down.`); logger.info('SIGINT received, stopping bridge server'); await shutdownBridge(); await stopSocketServer(); logger.info('Bridge server stopped', 'exiting'); logger.once('close', () => { console.log('logger closed'); process.exit(0); }); logger.close(); }*/ process.once('SIGINT', Utils.handleExit(shutdownBridge, stopSocketServer)); process.once('SIGTERM', Utils.handleExit(shutdownBridge, stopSocketServer)); //# sourceMappingURL=main.js.map