@pmouli/isy-matter-server
Version:
Service to expose an ISY device as a Matter Border router
329 lines (325 loc) • 13.3 kB
JavaScript
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