UNPKG

stryker

Version:
204 lines 9.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var os = require("os"); var child_process_1 = require("child_process"); var core_1 = require("stryker-api/core"); var log4js_1 = require("log4js"); var messageProtocol_1 = require("./messageProtocol"); var objectUtils_1 = require("../utils/objectUtils"); var Task_1 = require("../utils/Task"); var ChildProcessCrashedError_1 = require("./ChildProcessCrashedError"); var util_1 = require("@stryker-mutator/util"); var OutOfMemoryError_1 = require("./OutOfMemoryError"); var StringBuilder_1 = require("../utils/StringBuilder"); var BROKEN_PIPE_ERROR_CODE = 'EPIPE'; var IPC_CHANNEL_CLOSED_ERROR_CODE = 'ERR_IPC_CHANNEL_CLOSED'; var TIMEOUT_FOR_DISPOSE = 2000; var ChildProcessProxy = /** @class */ (function () { function ChildProcessProxy(requirePath, requireName, loggingContext, options, additionalInjectableValues, workingDirectory) { this.workerTasks = []; this.log = log4js_1.getLogger(ChildProcessProxy.name); this.stdoutAndStderrBuilder = new StringBuilder_1.default(); this.isDisposed = false; this.worker = child_process_1.fork(require.resolve('./ChildProcessProxyWorker'), [messageProtocol_1.autoStart], { silent: true, execArgv: [] }); this.initTask = new Task_1.Task(); this.log.debug('Starting %s in child process %s', requirePath, this.worker.pid); this.send({ additionalInjectableValues: additionalInjectableValues, kind: messageProtocol_1.WorkerMessageKind.Init, loggingContext: loggingContext, options: options, requireName: requireName, requirePath: requirePath, workingDirectory: workingDirectory }); this.listenForMessages(); this.listenToStdoutAndStderr(); // This is important! Be sure to bind to `this` this.handleUnexpectedExit = this.handleUnexpectedExit.bind(this); this.handleError = this.handleError.bind(this); this.worker.on('exit', this.handleUnexpectedExit); this.worker.on('error', this.handleError); this.proxy = this.initProxy(); } /** * @description Creates a proxy where each function of the object created using the constructorFunction arg is ran inside of a child process */ ChildProcessProxy.create = function (requirePath, loggingContext, options, additionalInjectableValues, workingDirectory, InjectableClass) { return new ChildProcessProxy(requirePath, InjectableClass.name, loggingContext, options, additionalInjectableValues, workingDirectory); }; ChildProcessProxy.prototype.send = function (message) { this.worker.send(objectUtils_1.serialize(message, [core_1.File])); }; ChildProcessProxy.prototype.initProxy = function () { // This proxy is a genuine javascript `Proxy` class // More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy var self = this; return new Proxy({}, { get: function (_, propertyKey) { if (typeof propertyKey === 'string') { return self.forward(propertyKey); } else { return undefined; } } }); }; ChildProcessProxy.prototype.forward = function (methodName) { var _this = this; return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } if (_this.currentError) { return Promise.reject(_this.currentError); } else { var workerTask = new Task_1.Task(); var correlationId_1 = _this.workerTasks.push(workerTask) - 1; _this.initTask.promise.then(function () { _this.send({ args: args, correlationId: correlationId_1, kind: messageProtocol_1.WorkerMessageKind.Call, methodName: methodName }); }); return workerTask.promise; } }; }; ChildProcessProxy.prototype.listenForMessages = function () { var _this = this; this.worker.on('message', function (serializedMessage) { var message = objectUtils_1.deserialize(serializedMessage, [core_1.File]); switch (message.kind) { case messageProtocol_1.ParentMessageKind.Initialized: _this.initTask.resolve(undefined); break; case messageProtocol_1.ParentMessageKind.Result: _this.workerTasks[message.correlationId].resolve(message.result); delete _this.workerTasks[message.correlationId]; break; case messageProtocol_1.ParentMessageKind.Rejection: _this.workerTasks[message.correlationId].reject(new Error(message.error)); delete _this.workerTasks[message.correlationId]; break; case messageProtocol_1.ParentMessageKind.DisposeCompleted: if (_this.disposeTask) { _this.disposeTask.resolve(undefined); } break; default: _this.logUnidentifiedMessage(message); break; } }); }; ChildProcessProxy.prototype.listenToStdoutAndStderr = function () { var _this = this; var handleData = function (data) { var output = data.toString(); _this.stdoutAndStderrBuilder.append(output); if (_this.log.isTraceEnabled()) { _this.log.trace(output); } }; if (this.worker.stdout) { this.worker.stdout.on('data', handleData); } if (this.worker.stderr) { this.worker.stderr.on('data', handleData); } }; ChildProcessProxy.prototype.reportError = function (error) { this.workerTasks .filter(function (task) { return !task.isCompleted; }) .forEach(function (task) { return task.reject(error); }); }; ChildProcessProxy.prototype.handleUnexpectedExit = function (code, signal) { this.isDisposed = true; var output = this.stdoutAndStderrBuilder.toString(); if (processOutOfMemory()) { this.currentError = new OutOfMemoryError_1.default(this.worker.pid, code); this.log.warn("Child process [pid " + this.currentError.pid + "] ran out of memory. Stdout and stderr are logged on debug level."); this.log.debug(stdoutAndStderr()); } else { this.currentError = new ChildProcessCrashedError_1.default(this.worker.pid, "Child process [pid " + this.worker.pid + "] exited unexpectedly with exit code " + code + " (" + (signal || 'without signal') + "). " + stdoutAndStderr(), code, signal); this.log.warn(this.currentError.message, this.currentError); } this.reportError(this.currentError); function processOutOfMemory() { return output.indexOf('JavaScript heap out of memory') >= 0; } function stdoutAndStderr() { if (output.length) { return "Last part of stdout and stderr was:" + os.EOL + objectUtils_1.padLeft(output); } else { return 'Stdout and stderr were empty.'; } } }; ChildProcessProxy.prototype.handleError = function (error) { if (this.innerProcessIsCrashed(error)) { this.log.warn("Child process [pid " + this.worker.pid + "] has crashed. See other warning messages for more info.", error); this.reportError(new ChildProcessCrashedError_1.default(this.worker.pid, "Child process [pid " + this.worker.pid + "] has crashed", undefined, undefined, error)); } else { this.reportError(error); } }; ChildProcessProxy.prototype.innerProcessIsCrashed = function (error) { return util_1.isErrnoException(error) && (error.code === BROKEN_PIPE_ERROR_CODE || error.code === IPC_CHANNEL_CLOSED_ERROR_CODE); }; ChildProcessProxy.prototype.dispose = function () { var _this = this; this.worker.removeListener('exit', this.handleUnexpectedExit); if (this.isDisposed) { return Promise.resolve(); } else { this.log.debug('Disposing of worker process %s', this.worker.pid); var killWorker = function () { _this.log.debug('Kill %s', _this.worker.pid); objectUtils_1.kill(_this.worker.pid); _this.isDisposed = true; }; this.disposeTask = new Task_1.ExpirableTask(TIMEOUT_FOR_DISPOSE); this.send({ kind: messageProtocol_1.WorkerMessageKind.Dispose }); return this.disposeTask.promise .then(killWorker) .catch(killWorker); } }; ChildProcessProxy.prototype.logUnidentifiedMessage = function (message) { this.log.error("Received unidentified message " + message); }; return ChildProcessProxy; }()); exports.default = ChildProcessProxy; //# sourceMappingURL=ChildProcessProxy.js.map