94 lines
2.9 KiB
JavaScript
94 lines
2.9 KiB
JavaScript
import {setTimeout} from 'node:timers/promises';
|
|
import {isErrorInstance} from '../return/final-error.js';
|
|
import {normalizeSignalArgument} from './signal.js';
|
|
|
|
// Normalize the `forceKillAfterDelay` option
|
|
export const normalizeForceKillAfterDelay = forceKillAfterDelay => {
|
|
if (forceKillAfterDelay === false) {
|
|
return forceKillAfterDelay;
|
|
}
|
|
|
|
if (forceKillAfterDelay === true) {
|
|
return DEFAULT_FORCE_KILL_TIMEOUT;
|
|
}
|
|
|
|
if (!Number.isFinite(forceKillAfterDelay) || forceKillAfterDelay < 0) {
|
|
throw new TypeError(`Expected the \`forceKillAfterDelay\` option to be a non-negative integer, got \`${forceKillAfterDelay}\` (${typeof forceKillAfterDelay})`);
|
|
}
|
|
|
|
return forceKillAfterDelay;
|
|
};
|
|
|
|
const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5;
|
|
|
|
// Monkey-patches `subprocess.kill()` to add `forceKillAfterDelay` behavior and `.kill(error)`
|
|
export const subprocessKill = (
|
|
{kill, options: {forceKillAfterDelay, killSignal}, onInternalError, context, controller},
|
|
signalOrError,
|
|
errorArgument,
|
|
) => {
|
|
const {signal, error} = parseKillArguments(signalOrError, errorArgument, killSignal);
|
|
emitKillError(error, onInternalError);
|
|
const killResult = kill(signal);
|
|
setKillTimeout({
|
|
kill,
|
|
signal,
|
|
forceKillAfterDelay,
|
|
killSignal,
|
|
killResult,
|
|
context,
|
|
controller,
|
|
});
|
|
return killResult;
|
|
};
|
|
|
|
const parseKillArguments = (signalOrError, errorArgument, killSignal) => {
|
|
const [signal = killSignal, error] = isErrorInstance(signalOrError)
|
|
? [undefined, signalOrError]
|
|
: [signalOrError, errorArgument];
|
|
|
|
if (typeof signal !== 'string' && !Number.isInteger(signal)) {
|
|
throw new TypeError(`The first argument must be an error instance or a signal name string/integer: ${String(signal)}`);
|
|
}
|
|
|
|
if (error !== undefined && !isErrorInstance(error)) {
|
|
throw new TypeError(`The second argument is optional. If specified, it must be an error instance: ${error}`);
|
|
}
|
|
|
|
return {signal: normalizeSignalArgument(signal), error};
|
|
};
|
|
|
|
// Fails right away when calling `subprocess.kill(error)`.
|
|
// Does not wait for actual signal termination.
|
|
// Uses a deferred promise instead of the `error` event on the subprocess, as this is less intrusive.
|
|
const emitKillError = (error, onInternalError) => {
|
|
if (error !== undefined) {
|
|
onInternalError.reject(error);
|
|
}
|
|
};
|
|
|
|
const setKillTimeout = async ({kill, signal, forceKillAfterDelay, killSignal, killResult, context, controller}) => {
|
|
if (signal === killSignal && killResult) {
|
|
killOnTimeout({
|
|
kill,
|
|
forceKillAfterDelay,
|
|
context,
|
|
controllerSignal: controller.signal,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Forcefully terminate a subprocess after a timeout
|
|
export const killOnTimeout = async ({kill, forceKillAfterDelay, context, controllerSignal}) => {
|
|
if (forceKillAfterDelay === false) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await setTimeout(forceKillAfterDelay, undefined, {signal: controllerSignal});
|
|
if (kill('SIGKILL')) {
|
|
context.isForcefullyTerminated ??= true;
|
|
}
|
|
} catch {}
|
|
};
|