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