UNPKG

lisk-framework

Version:

Lisk blockchain application platform

253 lines 10.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Controller = void 0; const childProcess = require("child_process"); const path = require("path"); const constants_1 = require("../constants"); const endpoint_1 = require("../endpoint"); const base_plugin_1 = require("../plugins/base_plugin"); const system_dirs_1 = require("../system_dirs"); const bus_1 = require("./bus"); const channels_1 = require("./channels"); const ipc_server_1 = require("./ipc/ipc_server"); const constants_2 = require("./constants"); class Controller { constructor(options) { var _a; this._plugins = {}; this._inMemoryPlugins = {}; this._childProcesses = {}; this._endpointHandlers = {}; this._appConfig = options.appConfig; this._pluginConfigs = (_a = options.pluginConfigs) !== null && _a !== void 0 ? _a : {}; this._chainID = options.chainID; const dirs = (0, system_dirs_1.systemDirs)(options.appConfig.system.dataPath); this._config = { dataPath: dirs.dataPath, dirs, }; this._internalIPCServer = new ipc_server_1.IPCServer({ socketsDir: this._config.dirs.sockets, name: 'bus', }); this._bus = new bus_1.Bus({ internalIPCServer: this._internalIPCServer, chainID: this._chainID, }); } get channel() { if (!this._channel) { throw new Error('Channel is not initialized.'); } return this._channel; } getEndpoints() { return this._bus.getEndpoints(); } getEvents() { return this._bus.getEvents(); } init(arg) { this._stateDB = arg.stateDB; this._moduleDB = arg.moduleDB; this._logger = arg.logger; this._channel = new channels_1.InMemoryChannel(this._logger, this._stateDB, this._moduleDB, constants_1.APP_IDENTIFIER, arg.events, arg.endpoints, this._chainID); } registerPlugin(plugin, options) { const pluginName = plugin.name; if (Object.keys(this._plugins).includes(pluginName)) { throw new Error(`A plugin with name "${pluginName}" is already registered.`); } if (options.loadAsChildProcess) { if (!(0, base_plugin_1.getPluginExportPath)(plugin)) { throw new Error(`Unable to register plugin "${pluginName}" to load as child process. Package name or __filename must be specified in nodeModulePath.`); } } this._pluginConfigs[pluginName] = { ...this._pluginConfigs[pluginName], ...options }; (0, base_plugin_1.validatePluginSpec)(plugin); this._plugins[pluginName] = plugin; } getRegisteredPlugins() { return this._plugins; } registerEndpoint(namespace, handlers) { if (this._endpointHandlers[namespace]) { throw new Error(`Endpoint for ${namespace} is already registered.`); } this._endpointHandlers[namespace] = handlers; } async start() { var _a; this._logger.info('Starting controller'); await this.channel.registerToBus(this._bus); for (const [namespace, handlers] of Object.entries(this._endpointHandlers)) { const channel = new channels_1.InMemoryChannel(this._logger, this._stateDB, this._moduleDB, namespace, [], handlers, this._chainID); await channel.registerToBus(this._bus); } await this._bus.start(this._logger); for (const name of Object.keys(this._plugins)) { const plugin = this._plugins[name]; const config = (_a = this._pluginConfigs[name]) !== null && _a !== void 0 ? _a : {}; if (config.loadAsChildProcess) { await this._loadChildProcessPlugin(plugin, config, this._appConfig); } else { await this._loadInMemoryPlugin(plugin, config, this._appConfig); } } } async stop(_code, reason) { this._logger.info('Stopping Controller'); if (reason) { this._logger.debug(`Reason: ${reason}`); } try { this._logger.debug('Plugins cleanup started'); await this._unloadPlugins(); this._logger.debug('Plugins cleanup completed'); this._logger.debug('Bus cleanup started'); await this._bus.cleanup(); this._logger.debug('Bus cleanup completed'); this._logger.info('Controller cleanup completed'); } catch (err) { this._logger.error(err, 'Controller cleanup failed'); } } async _unloadPlugins() { const pluginsToUnload = [ ...Object.keys(this._inMemoryPlugins), ...Object.keys(this._childProcesses), ]; let hasError = false; for (const name of pluginsToUnload) { try { if (this._inMemoryPlugins[name]) { await this._unloadInMemoryPlugin(name); } else if (this._childProcesses[name]) { await this._unloadChildProcessPlugin(name); } else { throw new Error(`Unknown plugin "${name}" was asked to unload.`); } } catch (error) { this._logger.error(error); hasError = true; } } if (hasError) { throw new Error('Unload Plugins failed'); } } async _loadInMemoryPlugin(plugin, config, appConfig) { const { name } = plugin; this._logger.info(name, 'Loading in-memory plugin'); const channel = new channels_1.InMemoryChannel(this._logger, this._stateDB, this._moduleDB, name, plugin.events, plugin.endpoint ? (0, endpoint_1.getEndpointHandlers)(plugin.endpoint) : new Map(), this._chainID); await channel.registerToBus(this._bus); this._logger.debug({ plugin: name }, 'Plugin is registered to bus'); await plugin.init({ config, appConfig, logger: this._logger }); await plugin.load(); this._logger.debug({ plugin: name }, 'Plugin is successfully loaded'); this._inMemoryPlugins[name] = { plugin, channel }; } async _loadChildProcessPlugin(plugin, config, appConfig) { const { name } = plugin; this._logger.info(name, 'Loading child-process plugin'); const program = path.resolve(__dirname, 'child_process_loader'); const parameters = [(0, base_plugin_1.getPluginExportPath)(plugin), plugin.constructor.name]; const forkedProcessOptions = { execArgv: undefined, }; const maxPort = 20000; const minPort = 10000; if (process.env.NODE_DEBUG) { forkedProcessOptions.execArgv = [ `--inspect=${Math.floor(Math.random() * (maxPort - minPort) + minPort)}`, ]; } const child = childProcess.fork(program, parameters, forkedProcessOptions); child.send({ action: 'load', config, appConfig, }); this._childProcesses[name] = child; child.on('exit', (code, signal) => { if (code !== null && code !== undefined && code !== 0) { this._logger.error({ name, code, signal: signal !== null && signal !== void 0 ? signal : '' }, 'Child process plugin exited'); } }); child.on('error', error => { this._logger.error(error, `Child process for "${name}" faced error.`); }); await Promise.race([ new Promise(resolve => { child.on('message', ({ action }) => { if (action === 'loaded') { this._logger.info({ name }, 'Loaded child-process plugin'); resolve(); } }); }), new Promise((_, reject) => { setTimeout(() => { reject(new Error('Child process plugin loading timeout')); }, constants_2.IPC_EVENTS.RPC_REQUEST_TIMEOUT); }), ]); } async _unloadInMemoryPlugin(name) { this._logger.debug({ plugin: name }, 'Unloading plugin'); try { await this._inMemoryPlugins[name].plugin.unload(); this._logger.debug({ plugin: name }, 'Successfully unloaded plugin'); } catch (error) { this._logger.debug({ plugin: name, err: error }, 'Fail to unload plugin'); } finally { delete this._inMemoryPlugins[name]; } } async _unloadChildProcessPlugin(name) { if (!this._childProcesses[name].connected) { this._childProcesses[name].kill('SIGTERM'); delete this._childProcesses[name]; throw new Error('Child process is not connected any more.'); } this._childProcesses[name].send({ action: 'unload', }); await Promise.race([ new Promise((resolve, reject) => { this._childProcesses[name].on('message', ({ action, err }) => { if (action !== 'unloaded' && action !== 'unloadedWithError') { resolve(); return; } delete this._childProcesses[name]; if (action === 'unloaded') { this._logger.info(`Child process plugin "${name}" unloaded`); resolve(); } else { this._logger.info(`Child process plugin "${name}" unloaded with error`); this._logger.error({ err }, 'Unloading plugin error.'); reject(err); } }); }), new Promise((_, reject) => { setTimeout(() => { this._childProcesses[name].kill('SIGTERM'); delete this._childProcesses[name]; reject(new Error('Child process plugin unload timeout')); }, constants_2.IPC_EVENTS.RPC_REQUEST_TIMEOUT); }), ]); } } exports.Controller = Controller; //# sourceMappingURL=controller.js.map