UNPKG

container.ts

Version:
341 lines 14.8 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); var childProcess = require("child_process"); var path = require("path"); var container_1 = require("../../container"); var RxJS_1 = require("../../container/RxJS"); var error_1 = require("../error"); var node_validate_1 = require("../node-validate"); var ChildProcess_1 = require("./ChildProcess"); /** Scripts error class. */ var ScriptsError = /** @class */ (function (_super) { __extends(ScriptsError, _super); function ScriptsError(cause) { return _super.call(this, { name: "ScriptsError" }, cause) || this; } return ScriptsError; }(error_1.ErrorChain)); exports.ScriptsError = ScriptsError; /** ScriptsProcess error class. */ var ScriptsProcessError = /** @class */ (function (_super) { __extends(ScriptsProcessError, _super); function ScriptsProcessError(target, cause) { return _super.call(this, { name: "ScriptsProcessError", value: target }, cause) || this; } return ScriptsProcessError; }(error_1.ErrorChain)); exports.ScriptsProcessError = ScriptsProcessError; /** Spawned scripts process interface. */ var ScriptsProcess = /** @class */ (function () { function ScriptsProcess(scripts, target, process, options) { var _this = this; this.scripts = scripts; this.target = target; this.process = process; this.options = options; this.messages$ = new RxJS_1.Subject(); this.events$ = new RxJS_1.Subject(); this.currentIdentifier = 0; // Accumulate multiple callback arguments into array. var accumulator = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return args; }; // Listen for process exit, reduce code/signal for next argument. this.exit$ = RxJS_1.Observable.fromEvent(process, "exit", accumulator) .take(1) .map(function (args) { var code = args[0], signal = args[1]; var value = (typeof code === "number") ? code : signal; return (value != null) ? value : 1; }); this.exit$.subscribe(function (code) { // Log error if script exits with error code. if (code !== 0) { var error = new ScriptsProcessError(_this.target); _this.scripts.log.error(error); } }); // Listen for process error, forward to scripts logger. RxJS_1.Observable.fromEvent(process, "error") .takeUntil(this.exit$) .subscribe(function (error) { var chained = new ScriptsProcessError(_this.target, error); _this.scripts.log.error(chained); }); // If socket provided, configure parent as message receiver. // Send socket as handle to child process. if (options.sockets != null) { this.socket = ChildProcess_1.ChildProcess.socketConfigure({ socket: options.sockets.parent, onError: function (error) { return _this.scripts.log.error(error); }, onData: function (data) { return _this.messages$.next(data); }, }); this.process.send(ChildProcess_1.ChildProcess.EVENT.SOCKET, options.sockets.child); } // Listen for and handle process messages. RxJS_1.Observable.fromEvent(process, "message") .takeUntil(this.exit$) .subscribe(function (message) { return _this.messages$.next(message); }); this.messages$ .subscribe(function (message) { return _this.handleMessage(message); }); } Object.defineProperty(ScriptsProcess.prototype, "isConnected", { get: function () { return this.process.connected; }, enumerable: true, configurable: true }); /** End child process with signal. */ ScriptsProcess.prototype.kill = function (signal) { this.process.kill(signal); return this.exit$; }; /** Send message to child process. */ ScriptsProcess.prototype.send = function (type, data) { if (this.socket != null) { this.socket.write(ChildProcess_1.ChildProcess.socketSerialise({ type: type, data: data })); } else { this.process.send({ type: type, data: data }); } }; /** Send socket channel to child process. */ ScriptsProcess.prototype.sendChannel = function (name, socket) { var _this = this; this.send(ChildProcess_1.EProcessMessageType.Socket, name); return RxJS_1.Observable.of(false) .delay(1000) .map(function () { return _this.process.send(ChildProcess_1.ChildProcess.EVENT.SOCKET, socket); }) .switchMap(function () { return _this.listen(ChildProcess_1.ChildProcess.EVENT.CHANNEL); }) .take(1) .map(function (channel) { return name === channel; }); }; /** Make call to module.method in child process. */ ScriptsProcess.prototype.call = function (target, method, options) { if (options === void 0) { options = {}; } return ChildProcess_1.ChildProcess.sendCallRequest(this, this.scripts, target, method, this.nextIdentifier, options); }; /** Send event with optional data to child process. */ ScriptsProcess.prototype.event = function (name, options) { if (options === void 0) { options = {}; } ChildProcess_1.ChildProcess.sendEvent(this, this.scripts, name, options); }; /** Listen for event sent by child process. */ ScriptsProcess.prototype.listen = function (name) { return ChildProcess_1.ChildProcess.listenForEvent(this.events$, name); }; Object.defineProperty(ScriptsProcess.prototype, "nextIdentifier", { /** Incrementing counter for unique identifiers. */ get: function () { return ++this.currentIdentifier; }, enumerable: true, configurable: true }); /** Handle messages received from child process. */ ScriptsProcess.prototype.handleMessage = function (message) { switch (message.type) { // Send received log and metric messages to container. case ChildProcess_1.EProcessMessageType.Log: { var data = message.data; this.scripts.container.sendLog(data.level, data.message, data.metadata, data.args); break; } case ChildProcess_1.EProcessMessageType.Metric: { var data = message.data; this.scripts.container.sendMetric(data.type, data.name, data.value, data.tags); break; } // Call request received from child. case ChildProcess_1.EProcessMessageType.CallRequest: { ChildProcess_1.ChildProcess.handleCallRequest(this, this.scripts, message.data, message.channel); break; } // Send event on internal event bus. case ChildProcess_1.EProcessMessageType.Event: { var event_1 = message.data; this.events$.next(event_1); break; } } }; return ScriptsProcess; }()); exports.ScriptsProcess = ScriptsProcess; /** Node.js scripts interface. */ var Scripts = /** @class */ (function (_super) { __extends(Scripts, _super); function Scripts(options) { var _this = _super.call(this, options) || this; _this.path = _this.envPath; _this.workers = {}; // Debug environment variables. _this.debug(Scripts.ENV.PATH + "=\"" + _this.path + "\""); return _this; } Scripts.prototype.moduleDown = function () { var _this = this; var observables$ = []; // Wait for worker processes to exit if connected. Object.keys(this.workers).map(function (name) { var worker = _this.workers[name]; worker.unsubscribe$.next(); worker.unsubscribe$.complete(); if ((worker.process.isConnected)) { observables$.push(worker.process.exit$); } }); if (observables$.length > 0) { return RxJS_1.Observable.forkJoin.apply(RxJS_1.Observable, observables$).map(function () { return undefined; }); } }; /** Spawn new Node.js process using script file. */ Scripts.prototype.fork = function (target, options) { if (options === void 0) { options = {}; } var forkEnv = this.environment.copy(options.env); // Check script file exists and fork. var filePath = node_validate_1.NodeValidate.isFile(path.resolve(this.path, target)); var process = childProcess.fork(filePath, options.args || [], { env: forkEnv.variables }); return new ScriptsProcess(this, target, process, options); }; Scripts.prototype.startWorker = function (name, target, options) { var _this = this; if (options === void 0) { options = {}; } var uptimeLimit = this.getUptimeLimit(options.uptimeLimit); var process = this.fork(target, options); if (this.workers[name] == null) { // New worker, create new observables in workers state. var unsubscribe$ = new RxJS_1.Subject(); var next$ = new RxJS_1.BehaviorSubject(process); this.workers[name] = { process: process, unsubscribe$: unsubscribe$, next$: next$, restarts: 0 }; // Log worker start. var metadata = this.getWorkerLogMetadata({ name: name, worker: this.workers[name], options: options }); this.log.info(Scripts.LOG.WORKER_START, metadata); } else { // Restarted worker, reassign process in workers state. this.workers[name].unsubscribe$.next(); this.workers[name].process = process; this.workers[name].next$.next(process); this.workers[name].restarts += 1; } var worker = this.workers[name]; // Handle worker restarts. process.exit$ .takeUntil(worker.unsubscribe$) .subscribe(function (code) { // Log worker exit. var metadata = _this.getWorkerLogMetadata({ name: name, worker: worker, code: code }); _this.log.info(Scripts.LOG.WORKER_EXIT, metadata); // Restart worker process by default. if ((options.restart == null) || !!options.restart) { // Do not restart process if limit reached. if ((options.restartLimit == null) || (worker.restarts < options.restartLimit)) { _this.log.info(Scripts.LOG.WORKER_RESTART, metadata); _this.startWorker(name, target, options); } else { _this.log.error(Scripts.LOG.WORKER_RESTART_LIMIT, metadata); _this.stopWorker(name); } } }); // Track worker process uptime. process.listen(ChildProcess_1.ChildProcess.EVENT.STATUS) .takeUntil(worker.unsubscribe$) .subscribe(function (status) { // Kill worker process if uptime limit exceeded. if ((uptimeLimit != null) && (status.uptime > uptimeLimit)) { var metadata = _this.getWorkerLogMetadata({ name: name, worker: worker }); _this.log.info(Scripts.LOG.WORKER_UPTIME_LIMIT, metadata); process.kill(); } }); return worker.next$; }; Scripts.prototype.stopWorker = function (name) { var worker = this.workers[name]; var observable$ = RxJS_1.Observable.of(0); if (worker != null) { // Observables clean up. worker.unsubscribe$.next(); worker.unsubscribe$.complete(); worker.next$.complete(); // End process if connected. if (worker.process.isConnected) { worker.process.kill(); observable$ = worker.process.exit$; } // Log worker stop and delete in state. var metadata = this.getWorkerLogMetadata({ name: name, worker: worker }); this.log.info(Scripts.LOG.WORKER_STOP, metadata); delete this.workers[name]; } return observable$; }; Object.defineProperty(Scripts.prototype, "envPath", { get: function () { return node_validate_1.NodeValidate.isDirectory(path.resolve(this.environment.get(Scripts.ENV.PATH))); }, enumerable: true, configurable: true }); Scripts.prototype.getUptimeLimit = function (limit) { if (limit != null) { try { var duration = node_validate_1.NodeValidate.isDuration(limit); return duration.asSeconds(); } catch (error) { throw new ScriptsError(error); } } return null; }; Scripts.prototype.getWorkerLogMetadata = function (data) { var metadata = { name: data.name, target: data.worker.process.target, restarts: data.worker.restarts, }; if (data.options != null) { metadata.restart = data.options.restart; metadata.restartLimit = data.options.restartLimit; metadata.uptimeLimit = data.options.uptimeLimit; } if (data.code != null) { metadata.code = data.code; } return metadata; }; /** Default module name. */ Scripts.moduleName = "Scripts"; /** Environment variable names. */ Scripts.ENV = { /** Scripts directory path (required). */ PATH: "SCRIPTS_PATH", }; /** Log names. */ Scripts.LOG = { WORKER_START: "ScriptsWorkerStart", WORKER_STOP: "ScriptsWorkerStop", WORKER_EXIT: "ScriptsWorkerExit", WORKER_RESTART: "ScriptsWorkerRestart", WORKER_RESTART_LIMIT: "ScriptsWorkerRestartLimit", WORKER_UPTIME_LIMIT: "ScriptsWorkerUptimeLimit", }; return Scripts; }(container_1.Module)); exports.Scripts = Scripts; //# sourceMappingURL=Scripts.js.map