stryker
Version:
The extendable JavaScript mutation testing framework
204 lines • 9.52 kB
JavaScript
"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