UNPKG

@tableau/taco-toolkit

Version:
211 lines 17.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getSortedProcessesToStop = exports.EPSProcessManager = exports.ProcessType = void 0; var child_process_1 = require("child_process"); var error_codes_1 = require("../../../shared/errors/error-codes"); var error_source_1 = require("../../../shared/errors/error-source"); var status_code_1 = require("../../../shared/errors/status-code"); var eps_logger_1 = require("../../logging/eps-logger"); var eps_process_types_1 = require("../../types/eps-process-types"); var exit_code_1 = require("../../types/exit-code"); var ProcessType; (function (ProcessType) { /** * The parent process of eps. The process manager monitors the process. * If a parent process is killed, the process manager will terminate all * eps related processes. */ ProcessType["parent"] = "parent"; /** * The sibling process of the main eps process. If a sibling process is killed, * the process manager will terminate all sibling and child processes. */ ProcessType["sibling"] = "sibling"; /** * The independent child process of eps. e.g. extractor-app. If a child process is * killed, the process manager will removes it from the watching list. A child process * will never affect other eps processes. */ ProcessType["child"] = "child"; })(ProcessType = exports.ProcessType || (exports.ProcessType = {})); var EPSProcessManager = /** @class */ (function () { function EPSProcessManager(tempDataManager) { this.processes = []; this.tempDataManager = tempDataManager; } EPSProcessManager.prototype.start = function () { var _this = this; this.setupMainProcess(); // Checks processes every second setInterval(function () { _this.checkProcesses(); }, 1000); }; EPSProcessManager.prototype.fork = function (epsProcessName, type, modulePath, args, forkOptions) { if (forkOptions === void 0) { forkOptions = { detached: false, silent: false, execArgv: [], }; } try { var child = (0, child_process_1.fork)(modulePath, args, forkOptions); child.on('error', function (e) { // When child process crashes during spawn, error event is fired, which will // lead to the process exiting in the event cycle. Just log the error here. e.message = "Error forking process ".concat(epsProcessName, ": ").concat(e); eps_logger_1.Logger.error(e); }); if (child.pid !== undefined) { this.processes.push({ name: epsProcessName, type: type, pid: child.pid, process: child, }); } return child; } catch (e) { // based on observation, fork() may throw when the input validations fail e.message = "Error forking process ".concat(epsProcessName, ": ").concat(e); eps_logger_1.Logger.logEPSError(status_code_1.StatusCode.Aborted, error_source_1.ErrorSources.System, error_codes_1.ErrorCodes.M54O33RX, e); throw e; } }; EPSProcessManager.prototype.setupMainProcess = function () { var _this = this; process.title = eps_process_types_1.EPSProcessName.EPSMain; process.on('SIGINT', function () { _this.stopAllProcesses(); }); // This will probably never occur, log here for debugging purpose, just in case process.on('error', function (e) { e.message = "Process '".concat(process.title, "' (pid:").concat(process.pid, ") encountered error with code ").concat(e.code, ": ").concat(e); eps_logger_1.Logger.error(e); }); this.processes.push({ name: process.title, type: ProcessType.sibling, pid: process.pid, process: process, }); }; EPSProcessManager.prototype.checkProcesses = function () { var _this = this; this.processes.forEach(function (_a) { var name = _a.name, pid = _a.pid, type = _a.type; if (_this.processExists(pid) === false) { if (type === ProcessType.child) { // child process might be terminated ungracefully, so remove it from the list _this.processes = _this.processes.filter(function (p) { return p.pid !== pid; }); return; } // stop EPS process when parent or any sibling process has terminated if (type === ProcessType.sibling) { eps_logger_1.Logger.error("Process '".concat(name, "' (pid:").concat(pid, ") no longer exists; shutting down EPS.")); } else if (type === ProcessType.parent) { eps_logger_1.Logger.info("EPS parent process '".concat(name, "' (pid:").concat(pid, ") has terminated; shutting down EPS.")); } _this.stopAllProcesses(); } }); }; EPSProcessManager.prototype.addPID = function (pid, name, type) { this.processes.push({ name: name, pid: pid, type: type }); eps_logger_1.Logger.info("Added ".concat(type, " process '").concat(name, "' (pid:").concat(pid, ") to be watched; watching ").concat(this.processes.length, " processes.")); return this.processes; }; EPSProcessManager.prototype.stopChildProcess = function (pid) { var processInfo = this.processes.find(function (p) { return p.pid === pid; }); if (processInfo === undefined) { return; } var name = processInfo.name, type = processInfo.type; if (type !== ProcessType.child) { eps_logger_1.Logger.warn("Only child process can be stopped. '".concat(name, "' (pid: ").concat(pid, ") is a ").concat(type, " process. Skipping...")); return; } try { // May shift to SIGTERM if needed process.kill(pid, 'SIGKILL'); this.processes = this.processes.filter(function (p) { return p.pid !== pid; }); } catch (e) { eps_logger_1.Logger.error("Error killing process '".concat(processInfo.name, "' (pid:").concat(processInfo.pid, "): ").concat(e)); } }; EPSProcessManager.prototype.stopAllProcesses = function () { var epsProcesses = getSortedProcessesToStop(this.processes); this.tempDataManager.deleteEpsTempDataDir(); eps_logger_1.Logger.info("Killing all EPS processes: ".concat(epsProcesses.map(function (_a) { var pid = _a.pid; return pid; }))); epsProcesses.forEach(function (_a) { var name = _a.name, pid = _a.pid; try { eps_logger_1.Logger.info("Shutting down '".concat(name, "' (pid:").concat(pid, ").")); process.kill(pid, 'SIGKILL'); } catch (e) { if (e.code === 'ESRCH') { eps_logger_1.Logger.info("Process ".concat(name, " has already exited.")); } else { // We don't want to throw an exception because we may have other pids to clean up still eps_logger_1.Logger.logEPSError(status_code_1.StatusCode.Internal, error_source_1.ErrorSources.System, error_codes_1.ErrorCodes.EZV90I5V, "Error killing process '".concat(name, "' (pid:").concat(pid, "): ").concat(e)); } } }); eps_logger_1.Logger.info("All other processes closed, shutting down main process '".concat(process.title, "' (pid:").concat(process.pid, ").")); process.exit(exit_code_1.ExitCode.PARENT_PID_TERMINATED); }; /** * processExists provides a mechanism to check whether a pid is an active process. * Advantage of this is that it does not require a separate executable to work. * The 3PL `processExists` has a dependency on fastlist, which on windows requires * fastlist.exe. Bundling the fastlist executable with the EPS executable is not * simple on Windows, so replacing 3PL solution with this fn avoids that dependency. */ EPSProcessManager.prototype.processExists = function (pid) { try { // Process.kill with 0 code checks process existence, may throw an error as outlined here: // https://nodejs.org/api/process.html#process_process_kill_pid_signal // Windows platforms will throw an error if the pid is used to kill a process group. // An error by process.kill doesn't mean anything is wrong, so we ignore the error and // compare the code for the returned value. process.kill(pid, 0); } catch (e) { // This is the expected code path. return e.code === 'EPERM'; } // Some processes don't throw error on 0 signal on Windows, so in that case, we know it's true return true; }; return EPSProcessManager; }()); exports.EPSProcessManager = EPSProcessManager; function getSortedProcessesToStop(processInfos) { return processInfos .filter(function (_a) { var type = _a.type, pid = _a.pid; // do not kill the parent process and do not self terminate until the end of the function return type !== ProcessType.parent && pid !== process.pid; }) .sort(function (first, second) { // children need to be killed first if (first.type === ProcessType.child) { return -1; } else if (second.type === ProcessType.child) { return 1; } else { return 0; } }); } exports.getSortedProcessesToStop = getSortedProcessesToStop; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJvY2Vzcy1tYW5hZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2Vwcy9tYWluL2NvbXBvbmVudHMvcHJvY2Vzcy1tYW5hZ2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLCtDQUErRDtBQUUvRCxrRUFBK0Q7QUFDL0Qsb0VBQWtFO0FBQ2xFLGtFQUErRDtBQUMvRCx1REFBaUQ7QUFFakQsbUVBQThEO0FBQzlELG1EQUFnRDtBQUVoRCxJQUFZLFdBa0JYO0FBbEJELFdBQVksV0FBVztJQUNyQjs7OztPQUlHO0lBQ0gsZ0NBQWlCLENBQUE7SUFDakI7OztPQUdHO0lBQ0gsa0NBQW1CLENBQUE7SUFDbkI7Ozs7T0FJRztJQUNILDhCQUFlLENBQUE7QUFDakIsQ0FBQyxFQWxCVyxXQUFXLEdBQVgsbUJBQVcsS0FBWCxtQkFBVyxRQWtCdEI7QUFTRDtJQUlFLDJCQUFZLGVBQWdDO1FBSHBDLGNBQVMsR0FBcUIsRUFBRSxDQUFBO1FBSXRDLElBQUksQ0FBQyxlQUFlLEdBQUcsZUFBZSxDQUFBO0lBQ3hDLENBQUM7SUFFRCxpQ0FBSyxHQUFMO1FBQUEsaUJBTUM7UUFMQyxJQUFJLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQTtRQUN2QixnQ0FBZ0M7UUFDaEMsV0FBVyxDQUFDO1lBQ1YsS0FBSSxDQUFDLGNBQWMsRUFBRSxDQUFBO1FBQ3ZCLENBQUMsRUFBRSxJQUFJLENBQUMsQ0FBQTtJQUNWLENBQUM7SUFFRCxnQ0FBSSxHQUFKLFVBQ0UsY0FBOEIsRUFDOUIsSUFBNkMsRUFDN0MsVUFBa0IsRUFDbEIsSUFBYyxFQUNkLFdBSUM7UUFKRCw0QkFBQSxFQUFBO1lBQ0UsUUFBUSxFQUFFLEtBQUs7WUFDZixNQUFNLEVBQUUsS0FBSztZQUNiLFFBQVEsRUFBRSxFQUFFO1NBQ2I7UUFFRCxJQUFJO1lBQ0YsSUFBTSxLQUFLLEdBQUcsSUFBQSxvQkFBSSxFQUFDLFVBQVUsRUFBRSxJQUFJLEVBQUUsV0FBVyxDQUFD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