@showbridge/cli
Version:
Simple Protocol Router /s
305 lines (272 loc) • 8.29 kB
JavaScript
import { Config, Router, Utils } from '@showbridge/lib';
import { Option, program } from 'commander';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { createRequire } from 'module';
import path from 'node:path';
// TODO(jwetzell): this seems error prone....
function readJSON(relativePath) {
return JSON.parse(readFileSync(path.join(import.meta.dirname, relativePath)));
}
export const defaultConfig = readJSON('./sample/config/default.json');
export const defaultVars = readJSON('./sample/vars/default.json');
export const schema = readJSON('./schema/config.schema.json');
export const packageInfo = readJSON('./package.json');
// TODO(jwetzell): would be nice to get rid of this require
const require = createRequire(import.meta.url);
const defaultWebUIPath = path.dirname(require.resolve('@showbridge/webui/dist/webui/browser/index.html'));
program.name(packageInfo.name);
program.version(packageInfo.version);
program.description('Simple protocol router /s');
program.option('-c, --config <path>', 'location of config file', undefined);
program.option('-v, --vars <path>', 'location of file containing vars', undefined);
program.option('-w, --webui <path>', 'location of webui html to serve', defaultWebUIPath);
program.option('--disable-action <action-type...>', 'action type(s) to disable');
program.option('--disable-protocol <protocol-type...>', 'protocol type(s) to disable');
program.option('--disable-trigger <trigger-type...>', 'trigger type(s) to disable');
program.option('--disable-transform <transform-type...>', 'transform type(s) to disable');
program.addOption(
new Option('-l, --log-level <level>', 'log level')
.choices(['trace', 'debug', 'info', 'warn', 'error', 'fatal'])
.default('info')
);
program.parse(process.argv);
const options = program.opts();
const isChildProcess = process.send !== undefined;
const logger = Utils.logger;
if (options.logLevel) {
switch (options.logLevel) {
case 'trace':
logger.level = 10;
break;
case 'debug':
logger.level = 20;
break;
case 'info':
logger.level = 30;
break;
case 'warn':
logger.level = 40;
break;
case 'error':
logger.level = 50;
break;
case 'fatal':
logger.level = 60;
break;
default:
logger.level = 30;
break;
}
}
logger.info(`app: starting ${packageInfo.name} version: ${packageInfo.version}`);
let config = {};
// NOTE(jwetzell): if there is a config argument load it as the config
if (options.config) {
try {
logger.debug(`app: loading config from ${options.config}`);
const configToLoad = JSON.parse(readFileSync(options.config));
config = new Config(configToLoad, schema);
} catch (error) {
logger.error(`app: could not load config from ${options.config}`);
logger.error(error);
}
} else {
// NOTE(jwetzell): if not load a default
logger.debug(`app: loading default config`);
config = new Config(defaultConfig, schema);
}
const router = new Router(config);
if (options.vars) {
try {
logger.debug(`app: loading vars from ${options.vars}`);
const varsToLoad = JSON.parse(readFileSync(options.vars));
router.vars = varsToLoad;
router.emit('varsUpdated', router.vars);
} catch (error) {
logger.error(`app: could not load vars from ${options.vars}`);
logger.error(error);
}
} else {
// NOTE(jwetzell): if not load a default
logger.debug(`app: loading default vars`);
router.vars = defaultVars;
}
if (options.webui) {
if (existsSync(options.webui)) {
const filePath = path.resolve(options.webui);
router.servePath(filePath);
} else {
logger.error(`app: provided webui path = ${options.webui} does not seem to exist skipping...`);
}
}
router.on('configUpdated', (updatedConfig) => {
if (isChildProcess) {
process.send({
eventName: 'configUpdated',
data: updatedConfig,
});
} else if (options.config) {
try {
writeFileSync(options.config, JSON.stringify(updatedConfig, undefined, 2));
logger.debug(`app: updated config written to ${options.config}`);
} catch (error) {
logger.error(`app: problem saving config to ${options.config}`);
logger.error(error);
}
}
});
router.on('varsUpdated', (updatedVars) => {
if (isChildProcess) {
process.send({
eventName: 'varsUpdated',
data: updatedVars,
});
} else if (options.vars) {
try {
writeFileSync(options.vars, JSON.stringify(updatedVars, undefined, 2));
logger.debug(`app: updated vars written to ${options.vars}`);
} catch (error) {
logger.error(`app: problem saving vars to ${options.vars}`);
logger.error(error);
}
}
});
router.on('messageIn', (messageEvent) => {
if (isChildProcess) {
process.send({
eventName: 'messageIn',
data: messageEvent,
});
}
});
router.on('trigger', (triggerEvent) => {
if (isChildProcess) {
process.send({
eventName: 'trigger',
data: triggerEvent,
});
}
});
router.on('action', (actionEvent) => {
if (isChildProcess) {
process.send({
eventName: 'action',
data: actionEvent,
});
}
});
router.on('transform', (transformEvent) => {
if (isChildProcess) {
process.send({
eventName: 'transform',
data: transformEvent,
});
}
});
router.on('protocolStarted', (protocol) => {
if (isChildProcess) {
process.send({
eventName: 'protocolStarted',
data: protocol,
});
}
});
process.on('message', (message) => {
switch (message.eventName) {
case 'checkConfig':
try {
const newConfig = new Config(message.data, schema);
if (isChildProcess) {
process.send({
eventName: 'configValid',
data: newConfig.toJSON(),
});
}
} catch (errors) {
if (isChildProcess) {
process.send({
eventName: 'configError',
data: errors,
});
}
logger.error(`app: problem loading new config`);
logger.error(errors.toString());
}
break;
case 'updateConfig':
try {
router.config = new Config(message.data, schema);
logger.info('app: new config applied router reload');
} catch (errors) {
logger.error('app: errors loading config');
if (isChildProcess) {
process.send({
eventName: 'configError',
data: errors,
});
}
}
break;
case 'varsUpdated':
if (message.vars) {
router.vars = message.data;
router.emit('varsUpdated', router.vars);
}
break;
case 'destroy':
logger.info('app: process received a request to tear down');
router.stop();
break;
case 'reloadProtocols':
if (message.data && Array.isArray(message.data)) {
message.data.forEach((protocolType) => {
router.reloadProtocol(protocolType);
});
}
break;
default:
logger.error(`app: unhandled process event type = ${message.eventName}`);
break;
}
});
if (options.disableAction?.length > 0) {
options.disableAction.forEach((type) => {
logger.debug(`app: disabling action ${type}`);
router.disableAction(type);
});
}
if (options.disableProtocol?.length > 0) {
options.disableProtocol.forEach((type) => {
logger.debug(`app: disabling ${type} protocol`);
router.disableProtocol(type);
});
}
if (options.disableTrigger?.length > 0) {
options.disableTrigger.forEach((type) => {
logger.debug(`app: disabling ${type} trigger`);
router.disableTrigger(type);
});
}
if (options.disableTransform?.length > 0) {
options.disableTransform.forEach((type) => {
logger.debug(`app: disabling ${type} transform`);
router.disableTransform(type);
});
}
router.start();
router.on('stopped', () => {
logger.info('app: router has stopped exiting...');
process.exit(0);
});
process.once('SIGINT', () => {
logger.info('app: shutting down router');
router.stop();
});
if (isChildProcess) {
// NOTE(jwetzell): this seems to detect the parent process disappearing
process.on('disconnect', () => {
logger.info('app: parent process exited');
router.stop();
});
}