@rushstack/node-core-library
Version:
Core libraries that every NodeJS toolchain project should use
190 lines • 8.74 kB
JavaScript
;
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubprocessTerminator = void 0;
const process_1 = __importDefault(require("process"));
const Executable_1 = require("./Executable");
/**
* When a child process is created, registering it with the SubprocessTerminator will ensure
* that the child gets terminated when the current process terminates.
*
* @remarks
* This works by hooking the current process's events for SIGTERM/SIGINT/exit, and ensuring the
* child process gets terminated in those cases.
*
* SubprocessTerminator doesn't do anything on Windows, since by default Windows automatically
* terminates child processes when their parent is terminated.
*
* @beta
*/
class SubprocessTerminator {
/**
* Registers a child process so that it will be terminated automatically if the current process
* is terminated.
*/
static killProcessTreeOnExit(subprocess, subprocessOptions) {
if (typeof subprocess.exitCode === 'number') {
// Process has already been killed
return;
}
SubprocessTerminator._validateSubprocessOptions(subprocessOptions);
SubprocessTerminator._ensureInitialized();
// Closure variable
const pid = subprocess.pid;
if (pid === undefined) {
// The process failed to spawn.
return;
}
subprocess.on('close', (exitCode, signal) => {
if (SubprocessTerminator._subprocessesByPid.delete(pid)) {
SubprocessTerminator._logDebug(`untracking #${pid}`);
}
});
SubprocessTerminator._subprocessesByPid.set(pid, {
subprocess,
subprocessOptions
});
SubprocessTerminator._logDebug(`tracking #${pid}`);
}
/**
* Terminate the child process and all of its children.
*/
static killProcessTree(subprocess, subprocessOptions) {
const pid = subprocess.pid;
if (pid === undefined) {
// The process failed to spawn.
return;
}
// Don't attempt to kill the same process twice
if (SubprocessTerminator._subprocessesByPid.delete(pid)) {
SubprocessTerminator._logDebug(`untracking #${pid} via killProcessTree()`);
}
SubprocessTerminator._validateSubprocessOptions(subprocessOptions);
if (typeof subprocess.exitCode === 'number') {
// Process has already been killed
return;
}
SubprocessTerminator._logDebug(`terminating #${pid}`);
if (SubprocessTerminator._isWindows) {
// On Windows we have a problem that CMD.exe launches child processes, but when CMD.exe is killed
// the child processes may continue running. Also if we send signals to CMD.exe the child processes
// will not receive them. The safest solution is not to attempt a graceful shutdown, but simply
// kill the entire process tree.
const result = Executable_1.Executable.spawnSync('TaskKill.exe', [
'/T', // "Terminates the specified process and any child processes which were started by it."
'/F', // Without this, TaskKill will try to use WM_CLOSE which doesn't work with CLI tools
'/PID',
pid.toString()
]);
if (result.status) {
const output = result.output.join('\n');
// Nonzero exit code
if (output.indexOf('not found') >= 0) {
// The PID does not exist
}
else {
// Another error occurred, for example TaskKill.exe does not support
// the expected CLI syntax
throw new Error(`TaskKill.exe returned exit code ${result.status}:\n` + output + '\n');
}
}
}
else {
// Passing a negative PID terminates the entire group instead of just the one process
process_1.default.kill(-pid, 'SIGKILL');
}
}
// Install the hooks
static _ensureInitialized() {
if (!SubprocessTerminator._initialized) {
SubprocessTerminator._initialized = true;
SubprocessTerminator._logDebug('initialize');
process_1.default.prependListener('SIGTERM', SubprocessTerminator._onTerminateSignal);
process_1.default.prependListener('SIGINT', SubprocessTerminator._onTerminateSignal);
process_1.default.prependListener('exit', SubprocessTerminator._onExit);
}
}
// Uninstall the hooks and perform cleanup
static _cleanupChildProcesses() {
if (SubprocessTerminator._initialized) {
SubprocessTerminator._initialized = false;
process_1.default.removeListener('SIGTERM', SubprocessTerminator._onTerminateSignal);
process_1.default.removeListener('SIGINT', SubprocessTerminator._onTerminateSignal);
const trackedSubprocesses = Array.from(SubprocessTerminator._subprocessesByPid.values());
let firstError = undefined;
for (const trackedSubprocess of trackedSubprocesses) {
try {
SubprocessTerminator.killProcessTree(trackedSubprocess.subprocess, { detached: true });
}
catch (error) {
if (firstError === undefined) {
firstError = error;
}
}
}
if (firstError !== undefined) {
// This is generally an unexpected error such as the TaskKill.exe command not being found,
// not a trivial issue such as a nonexistent PID. Since this occurs during process shutdown,
// we should not interfere with control flow by throwing an exception or calling process.exit().
// So simply write to STDERR and ensure our exit code indicates the problem.
// eslint-disable-next-line no-console
console.error('\nAn unexpected error was encountered while attempting to clean up child processes:');
// eslint-disable-next-line no-console
console.error(firstError.toString());
if (!process_1.default.exitCode) {
process_1.default.exitCode = 1;
}
}
}
}
static _validateSubprocessOptions(subprocessOptions) {
if (!SubprocessTerminator._isWindows) {
if (!subprocessOptions.detached) {
// Setting detached=true is what creates the process group that we use to kill the children
throw new Error('killProcessTree() requires detached=true on this operating system');
}
}
}
static _onExit(exitCode) {
SubprocessTerminator._logDebug(`received exit(${exitCode})`);
SubprocessTerminator._cleanupChildProcesses();
SubprocessTerminator._logDebug(`finished exit()`);
}
static _onTerminateSignal(signal) {
SubprocessTerminator._logDebug(`received signal ${signal}`);
SubprocessTerminator._cleanupChildProcesses();
// When a listener is added to SIGTERM, Node.js strangely provides no way to reference
// the original handler. But we can invoke it by removing our listener and then resending
// the signal to our own process.
SubprocessTerminator._logDebug(`relaying ${signal}`);
process_1.default.kill(process_1.default.pid, signal);
}
// For debugging
static _logDebug(message) {
//const logLine: string = `SubprocessTerminator: [${process.pid}] ${message}`;
// fs.writeFileSync('trace.log', logLine + '\n', { flag: 'a' });
//console.log(logLine);
}
}
exports.SubprocessTerminator = SubprocessTerminator;
/**
* Whether the hooks are installed
*/
SubprocessTerminator._initialized = false;
/**
* The list of registered child processes. Processes are removed from this set if they
* terminate on their own.
*/
SubprocessTerminator._subprocessesByPid = new Map();
SubprocessTerminator._isWindows = process_1.default.platform === 'win32';
/**
* The recommended options when creating a child process.
*/
SubprocessTerminator.RECOMMENDED_OPTIONS = {
detached: process_1.default.platform !== 'win32'
};
//# sourceMappingURL=SubprocessTerminator.js.map