UNPKG

xcraft-core-bus

Version:
496 lines (427 loc) 13.8 kB
'use strict'; const moduleName = 'bus'; const path = require('path'); const watt = require('gigawatts'); const orcish = require('./orcish.js'); const xFs = require('xcraft-core-fs'); const xLog = require('xcraft-core-log')(moduleName, null); const xEtc = require('xcraft-core-etc')(); const busConfig = xEtc.load('xcraft-core-bus'); const {EventEmitter} = require('events'); class Bus extends EventEmitter { constructor() { super(); const Notifier = require('./notifier.js'); const Commander = require('./commander.js'); const acceptIncoming = !!busConfig.acceptIncoming; this._token = ''; this._notifier = new Notifier('greathall', acceptIncoming); this._commander = new Commander('greathall', acceptIncoming); this._loaded = false; this._commander .onInsertOrc((orcName) => { /* We ignore orcs which come from the autoconnect first step */ if (orcName.indexOf('@') !== -1) { const busClient = require('xcraft-core-busclient').getGlobal(); busClient.events.send('greathall::<axon-orc-added>', orcName, null, { router: 'ee', originRouter: 'ee', }); } }) .onDeleteOrc((orcName) => { const busClient = require('xcraft-core-busclient').getGlobal(); busClient.events.send('greathall::<axon-orc-removed>', orcName, null, { router: 'ee', originRouter: 'ee', }); }); watt.wrapAll(this); } get loaded() { return this._loaded; } _registerCommand(name, location, info, rc, handler) { /* register commands as activity */ this._commander.registerCommandHandler( name, location, info, { desc: (rc && rc.desc) || null, options: (rc && rc.options) || {}, activity: true, parallel: (rc && rc.parallel) || false, registrar: (rc && rc.registrar) || 'xcraft-core-bus', questOptions: (rc && rc.questOptions) || {}, }, handler ); } /** * Browse /scripts for zog modules, and register exported xcraftCommands. * (Activities). * * @yields * @param {Resp} resp response object. * @param {string} moduleName module's name. * @param {string} modulePath base module directory. * @param {string} moduleHot is hot module. * @param {string} moduleVersion package module version. * @param {RegExp} filterRegex regex for listing the directory. */ *_loadCommandsRegistry( resp, moduleName, modulePath, moduleHot, moduleVersion, filterRegex ) { const fileNames = xFs.ls(modulePath, filterRegex); try { yield this.loadModule(resp, fileNames, modulePath, { name: moduleName, version: moduleVersion, hot: moduleHot, }); } catch (ex) { xLog.warn(ex.stack || ex); } } *_loadRegistry(busClient, commandHandlers, next) { /* load some command handler from modules/scripts locations */ for (const index of Object.keys(commandHandlers)) { const resp = busClient.newResponse(moduleName, 'greathall'); this._loadCommandsRegistry( resp, commandHandlers[index].name, commandHandlers[index].path, commandHandlers[index].hot, commandHandlers[index].version, commandHandlers[index].pattern, next.parallel() ); } yield next.sync(); this._loaded = true; busClient.events.send('greathall::loaded'); } acceptIncoming() { this._notifier.acceptIncoming(); this._commander.acceptIncoming(); } /* It can be the current commands registry or from the horde */ notifyCmdsRegistry() { const busClient = require('xcraft-core-busclient').getGlobal(); if (!busClient.isConnected()) { return; } const registry = this._commander.getFullRegistry(); busClient.events.send('greathall::bus.commands.registry', { registry, token: this._token, }); } /* It can be the current server token or from the horde */ notifyTokenChanged(busConfig) { const busClient = require('xcraft-core-busclient').getGlobal(); if (!busClient.isConnected()) { return; } busClient.events.send('greathall::bus.token.changed', {busConfig}); } notifyOrcnameChanged(oldOrcName, newOrcName, busConfig) { const busClient = require('xcraft-core-busclient').getGlobal(); if (!busClient.isConnected()) { return; } busClient.events.send('greathall::bus.orcname.changed', { oldOrcName, newOrcName, token: this.getToken(), busConfig, }); } notifyReconnect(status) { const busClient = require('xcraft-core-busclient').getGlobal(); if (!busClient.isConnected()) { return; } busClient.events.send('greathall::bus.reconnect', {status}); } _getModules(onlyHot) { const registry = this._commander.getRegistry(); const list = Object.keys(registry) .filter((key) => !/^bus\./.test(key)) .map((key) => registry[key]); return onlyHot ? list.filter((cmd) => cmd.info.hot && !!cmd.location) : list; } getModuleInfo(name, userModulePath) { let location; if (!userModulePath) { location = require .resolve(path.join(name, 'package.json')) .replace(new RegExp(`(.*[/\\\\]${name})[/\\\\].*`), '$1'); } else { location = path .join(userModulePath, name, 'package.json') .replace(new RegExp(`(.*[/\\\\]${name})[/\\\\].*`), '$1'); } return { path: location, pattern: /^(?!([a-zA-Z0-9]+.)?config\.js|\.).*\.js$/, }; } generateOrcName() { return orcish.generateOrcName(this.getToken()); } getCommander() { return this._commander; } getNotifier() { return this._notifier; } getToken() { return this._token; } getRegistry() { return this._commander.getRegistry(); } getBusTokenFromId(cmd, id) { const routingKey = this._commander.getRoutingKeyFromId(cmd, id); if (routingKey) { const xHorde = require('xcraft-core-horde'); const slave = xHorde.getSlave(routingKey); if (!slave) { throw new Error('routingKey without slave'); } return slave.busClient.getToken(); } return this.getToken(); } *loadModule(resp, files, root, info, next) { const clearModule = require('clear-module'); if (!files || !files.length || !root) { throw new Error( `bad arguments because no JS files are available but xcraftCommands is set` ); } const loaded = []; for (const file of files) { const location = path.join(root, file); const handle = require(location); if (!handle.hasOwnProperty('xcraftCommands')) { clearModule.single(location); xLog.warn(`skip ${location} which is not a valid Xcraft module`); continue; } const name = file.replace(/\.js$/, ''); /* HACK: accept in the case of wizard module (special behaviour) * But note that the module system has a bad design because * everything is flat. It's just impossible to reload only * specific wizard commands. */ if (this._commander.isModuleRegistered(name) && name !== 'wizard') { xLog.warn( `skip ${location} because a module with the same name is already registered` ); continue; } let cmds = {}; try { cmds = handle.xcraftCommands(); } catch (ex) { clearModule(location); xLog.err(ex.stack || ex); continue; } const rc = cmds.rc || {}; /* If at least one command is already registered, this module is * fully skipped. */ Object.keys(cmds.handlers) .map((cmd) => { if (this._commander.isCommandRegistered(cmd)) { throw new Error(`command ${cmd} already registered`); } return cmd; }) .forEach((cmd) => { const cmdName = `${name}.${cmd}`; this._registerCommand( cmdName, location, info, rc[cmd], cmds.handlers[cmd] ); }); loaded.push({name, cmds}); } this.notifyCmdsRegistry(); if (!resp) { return; } for (const item of loaded) { if (item.cmds.handlers._postload) { yield resp.command.nestedSend(`${item.name}._postload`, null, next); } } } runningModuleNames(onlyHot = false) { return this._getModules(onlyHot) .map((cmd) => cmd.name.replace(/(^[^.]*)\..*/, '$1')) .reduce((acc, name) => { if (!acc.includes(name)) { acc.push(name); } return acc; }, []); } runningModuleLocations(onlyHot = false) { return this._getModules(onlyHot) .map((cmd) => cmd.location) .reduce((acc, location) => { if (!acc.includes(location)) { acc.push(location); } return acc; }, []); } runningModuleDirnames(onlyHot = false) { return this._getModules(onlyHot).map((cmd) => path.dirname(cmd.location).reduce((acc, dirname) => { if (!acc.includes(dirname)) { acc.push(dirname); } return acc; }, []) ); } /** * Boot buses. * * @yields * @param {Object[]} commandHandlers list of modules. * @param {function(err)} next watt's callback. */ *boot(commandHandlers, next) { const fs = require('fs'); const path = require('path'); const xHost = require('xcraft-core-host'); const {resourcesPath} = xHost; const appArgs = xHost.appArgs(); xLog.verb('Booting...'); /* Generate the token */ const genToken = orcish.generateGreatHall(); xLog.info('Great Hall created: %s', genToken); this._token = genToken; /* The server key and certificate are ignored in case of unix socket use */ if ( appArgs.tls !== false && !busConfig.noTLS && !busConfig.unixSocketId && !busConfig.keyPath && !busConfig.certPath ) { const resKeyPath = path.join(resourcesPath, 'server-key.pem'); const resCertPath = path.join(resourcesPath, 'server-cert.pem'); if (fs.existsSync(resKeyPath) && fs.existsSync(resCertPath)) { busConfig.keyPath = resKeyPath; busConfig.certPath = resCertPath; } } const options = { timeout: parseInt(busConfig.timeout), serverKeepAlive: typeof busConfig.serverKeepAlive === 'string' ? parseInt(busConfig.serverKeepAlive) : busConfig.serverKeepAlive, }; if (busConfig.keyPath && busConfig.certPath) { options.keyPath = busConfig.keyPath; options.certPath = busConfig.certPath; } if (appArgs.tribe >= 1) { const tribe = appArgs.tribe - 1; busConfig.commanderPort = busConfig.tribes[tribe].commanderPort; busConfig.notifierPort = busConfig.tribes[tribe].notifierPort; } /* Start the bus commander */ this._commander.start( busConfig.commanderHost || busConfig.host, parseInt(busConfig.commanderPort), busConfig.unixSocketId, options, genToken, next.parallel() ); /* Start the bus notifier */ this._notifier.start( busConfig.notifierHost || busConfig.host, parseInt(busConfig.notifierPort), busConfig.unixSocketId, options, next.parallel() ); yield next.sync(); xEtc.saveRun('xcraft-core-bus', { host: busConfig.host, commanderPort: this._commander.ports[0] ?? busConfig.commanderPort, //FIXME: multi-ports backends case notifierPort: this._notifier.ports[0] ?? busConfig.notifierPort, //FIXME: multi-ports backends case serverKeepAlive: busConfig.serverKeepAlive, clientKeepAlive: busConfig.clientKeepAlive, noTLS: busConfig.noTLS, unixSocketId: busConfig.unixSocketId, keyPath: busConfig.keyPath, certPath: busConfig.certPath, policiesPath: busConfig.policiesPath, }); this.emit('ready', (busClient, callback) => this._loadRegistry(busClient, commandHandlers, (...args) => { const busConfig = require('xcraft-core-etc')().load('xcraft-core-bus'); if (!process.env.XCRAFT_CONFIG) { process.env.XCRAFT_CONFIG = '{}'; } const xcraftConfig = JSON.parse(process.env.XCRAFT_CONFIG); xcraftConfig['xcraft-core-bus'] = busConfig; process.env.XCRAFT_CONFIG = JSON.stringify(xcraftConfig); const xConfig = require('xcraft-core-etc')().load('xcraft'); xLog.dbg(`Xcraft root: ${xConfig.xcraftRoot}`); xLog.dbg(`Token: ${this._token}`); xLog.dbg( `- commander: ${busConfig.commanderHost || busConfig.host}:${ busConfig.commanderPort } [timeout:${busConfig.timeout}]` ); xLog.dbg( `- notifier: ${busConfig.notifierHost || busConfig.host}:${ busConfig.notifierPort } [timeout:${busConfig.timeout}]` ); callback(...args); }) ); } *stop() { xLog.verb('Buses stop called, stopping services and sending GameOver...'); const busClient = require('xcraft-core-busclient').getGlobal(); const msg = busClient.newMessage(); this._notifier.send('gameover', msg); if (this._commander.isModuleRegistered('probe')) { const xProbe = require('xcraft-core-probe'); xProbe.close(); } if (this._commander.isModuleRegistered('horde')) { const xHorde = require('xcraft-core-horde'); yield xHorde.stop(!!busConfig.shutdownRemotes); } this._commander.stop(); this._notifier.stop(); this.emit('stop'); } } module.exports = new Bus();