container.ts
Version:
Modular application framework
341 lines • 14.8 kB
JavaScript
"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