lisk-framework
Version:
Lisk blockchain application platform
253 lines • 10.4 kB
JavaScript
"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