container.ts
Version:
Modular application framework
315 lines • 15.1 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
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 extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __spreadArrays = (this && this.__spreadArrays) || function () {
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
for (var r = Array(s), k = 0, i = 0; i < il; i++)
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
r[k] = a[j];
return r;
};
Object.defineProperty(exports, "__esModule", { value: true });
var child_process_1 = require("child_process");
var lodash_1 = require("lodash");
var path_1 = require("path");
var rxjs_1 = require("rxjs");
var operators_1 = require("rxjs/operators");
var container_1 = require("../../../container");
var error_chain_1 = require("../../error/error-chain");
var validate_1 = require("../validate");
var process_1 = require("./process");
/** Scripts log names. */
var EScriptsLog;
(function (EScriptsLog) {
EScriptsLog["Information"] = "Scripts.Information";
})(EScriptsLog = exports.EScriptsLog || (exports.EScriptsLog = {}));
/** ScriptsProcess error codes. */
var EScriptsProcessError;
(function (EScriptsProcessError) {
EScriptsProcessError["Exit"] = "ScriptsProcessError.Exit";
EScriptsProcessError["Error"] = "ScriptsProcessError.Error";
})(EScriptsProcessError = exports.EScriptsProcessError || (exports.EScriptsProcessError = {}));
/** ScriptsProcess error class. */
var ScriptsProcessError = /** @class */ (function (_super) {
__extends(ScriptsProcessError, _super);
function ScriptsProcessError(code, cause, context) {
return _super.call(this, { name: "ScriptsProcessError", value: __assign({ code: code }, context) }, cause) || this;
}
return ScriptsProcessError;
}(error_chain_1.ErrorChain));
exports.ScriptsProcessError = ScriptsProcessError;
/** Spawned scripts process. */
var ScriptsProcess = /** @class */ (function () {
function ScriptsProcess(scripts, fileName, process) {
var _this = this;
this.scripts = scripts;
this.fileName = fileName;
this.process = process;
// Connect process events to subjects.
this.process.on("exit", function (code, signal) {
_this.scripts.processExit$.next({ pid: _this.process.pid, code: code, signal: signal });
});
this.process.on("error", function (error) {
_this.scripts.processError$.next({ pid: _this.process.pid, error: error });
});
// Create exit observable, reduce code/signal argument.
this.exit$ = this.scripts.rxTakeUntilModuleDown(this.scripts.processExit$).pipe(operators_1.filter(function (exit) { return exit.pid === _this.process.pid; }), operators_1.take(1), operators_1.map(function (exit) {
var code = exit.code != null ? exit.code : exit.signal;
return code != null ? code : 1;
}));
// Subscribe to exit observable to log errors.
this.exit$.subscribe(function (code) {
if (code !== 0) {
var error = new ScriptsProcessError(EScriptsProcessError.Exit, undefined, { code: code, fileName: _this.fileName });
_this.scripts.log.error(error, { code: code });
}
});
// Subscribe to error observable to forward to scripts logger.
this.scripts
.rxTakeUntilModuleDown(this.scripts.processError$)
.pipe(operators_1.takeUntil(this.exit$), operators_1.filter(function (exit) { return exit.pid === _this.process.pid; }))
.subscribe(function (error) {
var chained = new ScriptsProcessError(EScriptsProcessError.Error, error.error, { fileName: _this.fileName });
_this.scripts.log.error(chained);
});
}
/** End child process with signal. */
ScriptsProcess.prototype.kill = function (signal) {
this.process.kill(signal);
return this.exit$;
};
return ScriptsProcess;
}());
exports.ScriptsProcess = ScriptsProcess;
/** Scripts environment variable names. */
var EScriptsEnv;
(function (EScriptsEnv) {
/** Scripts directory path (required). */
EScriptsEnv["Path"] = "SCRIPTS_PATH";
})(EScriptsEnv = exports.EScriptsEnv || (exports.EScriptsEnv = {}));
/** Scripts log names. */
(function (EScriptsLog) {
EScriptsLog["WorkerStart"] = "Scripts.WorkerStart";
EScriptsLog["WorkerStop"] = "Scripts.WorkerStop";
EScriptsLog["WorkerExit"] = "Scripts.WorkerExit";
EScriptsLog["WorkerRestart"] = "Scripts.WorkerRestart";
EScriptsLog["WorkerRestartLimit"] = "Scripts.WorkerRestartLimit";
})(EScriptsLog = exports.EScriptsLog || (exports.EScriptsLog = {}));
/** Node.js scripts module. */
var Scripts = /** @class */ (function (_super) {
__extends(Scripts, _super);
function Scripts() {
var _this = _super !== null && _super.apply(this, arguments) || this;
/** Absolute path to script files directory. */
_this.envPath = validate_1.isDirectory(_this.environment.get(EScriptsEnv.Path));
/** Observable stream of process exit events. */
_this.processExit$ = new rxjs_1.Subject();
/** Observable stream of process error events. */
_this.processError$ = new rxjs_1.Subject();
/** Workers state. */
_this.scriptsWorkers = {};
return _this;
}
Scripts.prototype.moduleDependencies = function () {
var previous = [];
for (var _i = 0; _i < arguments.length; _i++) {
previous[_i] = arguments[_i];
}
return _super.prototype.moduleDependencies.apply(this, __spreadArrays(previous, [{ process: process_1.Process }]));
};
Scripts.prototype.moduleUp = function () {
var _this = this;
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
return _super.prototype.moduleUp.apply(this, __spreadArrays(args, [function () { return __awaiter(_this, void 0, void 0, function () {
return __generator(this, function (_a) {
this.log.debug(EScriptsLog.Information, { path: this.envPath });
return [2 /*return*/];
});
}); }]));
};
Scripts.prototype.moduleDown = function () {
var _this = this;
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
// Wait for worker processes to exit.
return _super.prototype.moduleDown.apply(this, __spreadArrays(args, [function () { return __awaiter(_this, void 0, void 0, function () {
var promises;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
promises = lodash_1.keys(this.scriptsWorkers).map(function (name) {
return _this.stopWorker(name).toPromise();
});
return [4 /*yield*/, Promise.all(promises)];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); }]));
};
Scripts.prototype.moduleDestroy = function () {
var _this = this;
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
return _super.prototype.moduleDestroy.apply(this, __spreadArrays(args, [function () {
_this.processExit$.complete();
_this.processError$.complete();
}]));
};
/** Spawn new Node.js process using script file. */
Scripts.prototype.fork = function (fileName, options) {
if (options === void 0) { options = {}; }
var forkEnv = this.environment.copy(options.env || {});
// Check script file exists and fork.
var filePath = validate_1.isFile(path_1.resolve(this.envPath, fileName));
var process = child_process_1.fork(filePath, options.args || [], { env: forkEnv.variables });
return new ScriptsProcess(this, fileName, process);
};
Scripts.prototype.startWorker = function (name, fileName, options) {
var _this = this;
if (options === void 0) { options = {}; }
var process = this.fork(fileName, options);
if (this.scriptsWorkers[name] == null) {
// New worker, create new observables.
var next$ = new rxjs_1.BehaviorSubject(process);
var unsubscribe$ = new rxjs_1.Subject();
this.scriptsWorkers[name] = { next$: next$, unsubscribe$: unsubscribe$, restarts: 0 };
// Log worker start.
var metadata = this.scriptsWorkerLogMetadata({ name: name, worker: this.scriptsWorkers[name], options: options });
this.log.info(EScriptsLog.WorkerStart, metadata);
}
else {
// Restarted worker, update process in state.
this.scriptsWorkers[name].unsubscribe$.next();
this.scriptsWorkers[name].next$.next(process);
this.scriptsWorkers[name].restarts += 1;
}
var worker = this.scriptsWorkers[name];
// Handle worker restarts.
process.exit$.pipe(operators_1.takeUntil(worker.unsubscribe$)).subscribe(function (code) {
// Log worker exit.
// TODO(L): Better use of log.notice/error here?
var metadata = _this.scriptsWorkerLogMetadata({ name: name, worker: worker, code: code });
_this.log.info(EScriptsLog.WorkerExit, 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(EScriptsLog.WorkerRestart, metadata);
_this.startWorker(name, fileName, options);
}
else {
_this.log.error(EScriptsLog.WorkerRestartLimit, metadata);
_this.stopWorker(name);
}
}
});
return worker.next$;
};
Scripts.prototype.stopWorker = function (name) {
var worker = this.scriptsWorkers[name];
var observable$ = rxjs_1.of(0);
if (worker != null) {
var process_2 = worker.next$.value;
// Observables clean up.
worker.unsubscribe$.next();
worker.unsubscribe$.complete();
worker.next$.complete();
// End process if connected.
if (process_2.process.connected) {
observable$ = process_2.kill();
}
// Log worker stop and delete in state.
var metadata = this.scriptsWorkerLogMetadata({ name: name, worker: worker });
this.log.info(EScriptsLog.WorkerStop, metadata);
delete this.scriptsWorkers[name];
}
return observable$;
};
Scripts.prototype.scriptsWorkerLogMetadata = function (data) {
var metadata = {
name: data.name,
restarts: data.worker.restarts
};
if (data.options != null) {
metadata.restart = data.options.restart;
metadata.restartLimit = data.options.restartLimit;
}
if (data.code != null) {
metadata.code = data.code;
}
return metadata;
};
/** Default module name. */
Scripts.moduleName = "Scripts";
return Scripts;
}(container_1.RxModule));
exports.Scripts = Scripts;
//# sourceMappingURL=scripts.js.map