dave-dredd
Version:
HTTP API Testing Framework
206 lines (205 loc) • 7.88 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const cross_spawn_1 = __importDefault(require("cross-spawn"));
const ignorePipeErrors_1 = __importDefault(require("./ignorePipeErrors"));
const ASCII_CTRL_C = 3;
const IS_WINDOWS = process.platform === 'win32';
const TERM_FIRST_CHECK_TIMEOUT_MS = 1;
const TERM_DEFAULT_TIMEOUT_MS = 1000;
const TERM_DEFAULT_RETRY_MS = 300;
// Signals the child process to forcefully terminate
function signalKill(childProcess, callback) {
childProcess.emit('signalKill');
if (IS_WINDOWS) {
const taskkill = spawn('taskkill', ['/F', '/T', '/PID', childProcess.pid]);
taskkill.on('exit', (exitStatus) => {
if (exitStatus) {
return callback(new Error(`Unable to forcefully terminate process ${childProcess.pid}`));
}
callback();
});
}
else {
childProcess.kill('SIGKILL');
process.nextTick(callback);
}
}
exports.signalKill = signalKill;
// Signals the child process to gracefully terminate
function signalTerm(childProcess, callback) {
childProcess.emit('signalTerm');
if (IS_WINDOWS) {
// On Windows, there is no such way as SIGTERM or SIGINT. The closest
// thing is to interrupt the process with Ctrl+C. Under the hood, that
// generates '\u0003' character on stdin of the process and if
// the process listens on stdin, it's able to catch this as 'SIGINT'.
//
// However, that only works if user does it manually. There is no
// way to do it programmatically, at least not in Node.js (and even
// for C/C++, all the solutions are dirty hacks). Even if you send
// the very same character to stdin of the process, it's not
// recognized (the rl.on('SIGINT') event won't get triggered)
// for some reason.
//
// The only thing Dredd is left with is a convention. So when Dredd
// wants to gracefully signal to the child process it should terminate,
// it sends the '\u0003' to stdin of the child. It's up to the child
// to implement reading from stdin in such way it works both for
// programmatic and manual Ctrl+C.
childProcess.stdin.write(String.fromCharCode(ASCII_CTRL_C));
}
else {
childProcess.kill('SIGTERM');
}
process.nextTick(callback);
}
exports.signalTerm = signalTerm;
// Gracefully terminates a child process
//
// Sends a signal to the process as a heads up it should terminate.
// Then checks multiple times whether the process terminated. Retries
// sending the signal. In case it's not able to terminate the process
// within given timeout, it returns an error.
//
// If provided with the 'force' option, instead of returning an error,
// it kills the process unconditionally.
//
// Available options:
// - timeout (number) - Time period in ms for which the termination
// attempts will be done
// - retryDelay (number) - Delay in ms between termination attempts
// - force (boolean) - Kills the process forcefully after the timeout
function terminate(childProcess, options = {}, callback) {
if (typeof options === 'function') {
[callback, options] = Array.from([options, {}]);
}
const force = options.force || false;
// If the timeout is zero or less then the delay for waiting between
// retries, there will be just one termination attempt
const timeout = options.timeout ? options.timeout : TERM_DEFAULT_TIMEOUT_MS;
const retryDelay = options.retryDelay
? options.retryDelay
: TERM_DEFAULT_RETRY_MS;
let terminated = false;
const onExit = () => {
terminated = true;
childProcess.removeListener('exit', onExit);
};
childProcess.on('exit', onExit);
const start = Date.now();
let t;
// A function representing one check, whether the process already
// ended or not. It is repeatedly called until the timeout has passed.
function check() {
if (terminated) {
// Successfully terminated
clearTimeout(t);
return callback();
}
if (Date.now() - start < timeout) {
// Still not terminated, try again
signalTerm(childProcess, (err) => {
if (err) {
return callback(err);
}
t = setTimeout(check, retryDelay);
});
}
else {
// Still not terminated and the timeout has passed, either
// kill the process (force) or provide an error
clearTimeout(t);
if (force) {
signalKill(childProcess, callback);
}
else {
callback(new Error(`Unable to gracefully terminate process ${childProcess.pid}`));
}
}
}
// Fire the first termination attempt and check the result
signalTerm(childProcess, (err) => {
if (err) {
return callback(err);
}
t = setTimeout(check, TERM_FIRST_CHECK_TIMEOUT_MS);
});
}
exports.terminate = terminate;
function spawn(...args) {
const childProcess = cross_spawn_1.default.spawn.apply(null, args);
ignorePipeErrors_1.default(childProcess);
childProcess.spawned = true;
childProcess.terminated = false;
let killedIntentionally = false;
let terminatedIntentionally = false;
childProcess.on('signalKill', () => {
killedIntentionally = true;
});
childProcess.on('signalTerm', () => {
terminatedIntentionally = true;
});
childProcess.signalKill = () => {
signalKill(childProcess, (err) => {
if (err) {
childProcess.emit('error', err);
}
});
};
childProcess.signalTerm = () => {
signalTerm(childProcess, (err) => {
if (err) {
childProcess.emit('error', err);
}
});
};
childProcess.terminate = (options) => {
terminate(childProcess, options, (err) => {
if (err) {
childProcess.emit('error', err);
}
});
};
childProcess.on('error', (err) => {
if (err.syscall && err.syscall.indexOf('spawn') >= 0) {
childProcess.spawned = false;
}
});
childProcess.on('exit', (exitStatus, signal) => {
childProcess.terminated = true;
childProcess.killedIntentionally = killedIntentionally;
childProcess.terminatedIntentionally = terminatedIntentionally;
// Crash detection. Emits a 'crash' event in case the process
// unintentionally terminated with non-zero status code.
// The 'crash' event's signature:
//
// - exitStatus (number, nullable) - The non-zero status code
// - killed (boolean) - Whether the process was killed or not
//
// How to distinguish a process was killed?
//
// UNIX:
// - exitStatus is null or 137 or... https://github.com/apiaryio/dredd/issues/735
// - signal is 'SIGKILL'
//
// Windows:
// - exitStatus is usually 1
// - signal isn't set (Windows do not have signals)
//
// Yes, you got it - on Windows there's no way to distinguish
// a process was forcefully killed...
if (!killedIntentionally && !terminatedIntentionally) {
if (signal === 'SIGKILL') {
childProcess.emit('crash', null, true);
}
else if (exitStatus !== 0) {
childProcess.emit('crash', exitStatus, false);
}
}
});
return childProcess;
}
exports.spawn = spawn;