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 {} };