init project
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
import {onAbortedSignal} from '../utils/abort-signal.js';
|
||||
|
||||
// Validate the `cancelSignal` option
|
||||
export const validateCancelSignal = ({cancelSignal}) => {
|
||||
if (cancelSignal !== undefined && Object.prototype.toString.call(cancelSignal) !== '[object AbortSignal]') {
|
||||
throw new Error(`The \`cancelSignal\` option must be an AbortSignal: ${String(cancelSignal)}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Terminate the subprocess when aborting the `cancelSignal` option and `gracefulSignal` is `false`
|
||||
export const throwOnCancel = ({kill, cancelSignal, gracefulCancel, context, controller}) => cancelSignal === undefined || gracefulCancel
|
||||
? []
|
||||
: [terminateOnCancel(kill, cancelSignal, context, controller)];
|
||||
|
||||
const terminateOnCancel = async (kill, cancelSignal, context, {signal}) => {
|
||||
await onAbortedSignal(cancelSignal, signal);
|
||||
context.terminationReason ??= 'cancel';
|
||||
kill();
|
||||
throw cancelSignal.reason;
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import {addAbortListener} from 'node:events';
|
||||
import {onExit} from 'signal-exit';
|
||||
|
||||
// If the `cleanup` option is used, call `subprocess.kill()` when the parent process exits
|
||||
export const cleanupOnExit = (kill, {cleanup, detached}, {signal}) => {
|
||||
if (!cleanup || detached) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removeExitHandler = onExit(() => {
|
||||
kill();
|
||||
});
|
||||
addAbortListener(signal, () => {
|
||||
removeExitHandler();
|
||||
});
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import {onAbortedSignal} from '../utils/abort-signal.js';
|
||||
import {sendAbort} from '../ipc/graceful.js';
|
||||
import {killOnTimeout} from './kill.js';
|
||||
|
||||
// Validate the `gracefulCancel` option
|
||||
export const validateGracefulCancel = ({gracefulCancel, cancelSignal, ipc, serialization}) => {
|
||||
if (!gracefulCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cancelSignal === undefined) {
|
||||
throw new Error('The `cancelSignal` option must be defined when setting the `gracefulCancel` option.');
|
||||
}
|
||||
|
||||
if (!ipc) {
|
||||
throw new Error('The `ipc` option cannot be false when setting the `gracefulCancel` option.');
|
||||
}
|
||||
|
||||
if (serialization === 'json') {
|
||||
throw new Error('The `serialization` option cannot be \'json\' when setting the `gracefulCancel` option.');
|
||||
}
|
||||
};
|
||||
|
||||
// Send abort reason to the subprocess when aborting the `cancelSignal` option and `gracefulCancel` is `true`
|
||||
export const throwOnGracefulCancel = ({
|
||||
subprocess,
|
||||
kill,
|
||||
cancelSignal,
|
||||
gracefulCancel,
|
||||
forceKillAfterDelay,
|
||||
context,
|
||||
controller,
|
||||
}) => gracefulCancel
|
||||
? [sendOnAbort({
|
||||
subprocess,
|
||||
kill,
|
||||
cancelSignal,
|
||||
forceKillAfterDelay,
|
||||
context,
|
||||
controller,
|
||||
})]
|
||||
: [];
|
||||
|
||||
const sendOnAbort = async ({subprocess, kill, cancelSignal, forceKillAfterDelay, context, controller: {signal}}) => {
|
||||
await onAbortedSignal(cancelSignal, signal);
|
||||
const reason = getReason(cancelSignal);
|
||||
await sendAbort(subprocess, reason);
|
||||
killOnTimeout({
|
||||
kill,
|
||||
forceKillAfterDelay,
|
||||
context,
|
||||
controllerSignal: signal,
|
||||
});
|
||||
context.terminationReason ??= 'gracefulCancel';
|
||||
throw cancelSignal.reason;
|
||||
};
|
||||
|
||||
// The default `reason` is a DOMException, which is not serializable with V8
|
||||
// See https://github.com/nodejs/node/issues/53225
|
||||
const getReason = ({reason}) => {
|
||||
if (!(reason instanceof DOMException)) {
|
||||
return reason;
|
||||
}
|
||||
|
||||
const error = new Error(reason.message);
|
||||
Object.defineProperty(error, 'stack', {
|
||||
value: reason.stack,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
return error;
|
||||
};
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import process from 'node:process';
|
||||
import {execFile} from 'node:child_process';
|
||||
import path from 'node:path/win32';
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// The `killDescendants` option terminates the whole process tree, not just the direct child.
|
||||
// On Unix, this requires spawning the subprocess in its own process group, so we override the
|
||||
// `detached` argument passed to `child_process.spawn()`.
|
||||
// This is kept separate from the user-facing `detached` option, which must keep its own value,
|
||||
// so the `cleanup` behavior is not affected.
|
||||
export const getSpawnOptions = options => options.killDescendants && !isWindows
|
||||
? {...options, detached: true}
|
||||
: options;
|
||||
|
||||
// Returns the low-level function used to send a signal to the subprocess.
|
||||
// With the `killDescendants` option, the signal is sent to the whole process tree.
|
||||
export const getKillFunction = (subprocess, {killDescendants}) => {
|
||||
if (!killDescendants) {
|
||||
return subprocess.kill.bind(subprocess);
|
||||
}
|
||||
|
||||
const killDescendantsFunction = isWindows ? killDescendantsWindows : killDescendantsUnix;
|
||||
return killDescendantsFunction.bind(undefined, subprocess);
|
||||
};
|
||||
|
||||
// On Unix, the subprocess is its own process group leader (its PGID equals its PID), since it
|
||||
// was spawned with `detached: true`. Sending the signal to `-pid` targets the whole group.
|
||||
const killDescendantsUnix = (subprocess, signal) => {
|
||||
if (subprocess.pid === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return process.kill(-subprocess.pid, signal);
|
||||
} catch {
|
||||
// The process group might already be gone, or signaling it might not be permitted, so we
|
||||
// fall back to the direct child. Like `ChildProcess.kill()`, this returns `false` instead of throwing.
|
||||
return subprocess.kill(signal);
|
||||
}
|
||||
};
|
||||
|
||||
// Windows has no process groups. Instead, `taskkill /T` recursively terminates the process tree.
|
||||
// It must run while the tree is still intact, so direct subprocess termination is only used as a
|
||||
// fallback: killing the direct subprocess first would orphan its descendants before `taskkill` could enumerate them.
|
||||
// If `taskkill` is unavailable or fails, the fallback only terminates the direct subprocess.
|
||||
// `taskkill` ignores the signal (`/F` terminates the tree).
|
||||
const killDescendantsWindows = (subprocess, signal) => {
|
||||
if (subprocess.pid === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const taskkillFile = getTaskkillFile();
|
||||
if (taskkillFile === undefined) {
|
||||
return subprocess.kill(signal);
|
||||
}
|
||||
|
||||
// This is best-effort: if `taskkill` fails, still try the direct subprocess.
|
||||
execFile(taskkillFile, ['/pid', `${subprocess.pid}`, '/T', '/F'], error => {
|
||||
if (error) {
|
||||
subprocess.kill(signal);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getTaskkillFile = () => {
|
||||
const windowsDirectory = [process.env.SystemRoot, process.env.windir]
|
||||
.find(directory => directory && isWindowsDriveAbsolutePath(directory));
|
||||
|
||||
return windowsDirectory === undefined
|
||||
? undefined
|
||||
: path.join(windowsDirectory, 'System32', 'taskkill.exe');
|
||||
};
|
||||
|
||||
const isWindowsDriveAbsolutePath = directory => {
|
||||
const {root} = path.parse(directory);
|
||||
return /^[a-z]:[/\\]/i.test(root);
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
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 {}
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import {constants} from 'node:os';
|
||||
import {signalsByName} from 'human-signals';
|
||||
|
||||
// Normalize signals for comparison purpose.
|
||||
// Also validate the signal exists.
|
||||
export const normalizeKillSignal = killSignal => {
|
||||
const optionName = 'option `killSignal`';
|
||||
if (killSignal === 0) {
|
||||
throw new TypeError(`Invalid ${optionName}: 0 cannot be used.`);
|
||||
}
|
||||
|
||||
return normalizeSignal(killSignal, optionName);
|
||||
};
|
||||
|
||||
export const normalizeSignalArgument = signal => signal === 0
|
||||
? signal
|
||||
: normalizeSignal(signal, '`subprocess.kill()`\'s argument');
|
||||
|
||||
const normalizeSignal = (signalNameOrInteger, optionName) => {
|
||||
if (Number.isInteger(signalNameOrInteger)) {
|
||||
return normalizeSignalInteger(signalNameOrInteger, optionName);
|
||||
}
|
||||
|
||||
if (typeof signalNameOrInteger === 'string') {
|
||||
return normalizeSignalName(signalNameOrInteger, optionName);
|
||||
}
|
||||
|
||||
throw new TypeError(`Invalid ${optionName} ${String(signalNameOrInteger)}: it must be a string or an integer.\n${getAvailableSignals()}`);
|
||||
};
|
||||
|
||||
const normalizeSignalInteger = (signalInteger, optionName) => {
|
||||
if (signalsIntegerToName.has(signalInteger)) {
|
||||
return signalsIntegerToName.get(signalInteger);
|
||||
}
|
||||
|
||||
throw new TypeError(`Invalid ${optionName} ${signalInteger}: this signal integer does not exist.\n${getAvailableSignals()}`);
|
||||
};
|
||||
|
||||
const getSignalsIntegerToName = () => new Map(Object.entries(constants.signals)
|
||||
.reverse()
|
||||
.map(([signalName, signalInteger]) => [signalInteger, signalName]));
|
||||
|
||||
const signalsIntegerToName = getSignalsIntegerToName();
|
||||
|
||||
const normalizeSignalName = (signalName, optionName) => {
|
||||
if (signalName in constants.signals) {
|
||||
return signalName;
|
||||
}
|
||||
|
||||
if (signalName.toUpperCase() in constants.signals) {
|
||||
throw new TypeError(`Invalid ${optionName} '${signalName}': please rename it to '${signalName.toUpperCase()}'.`);
|
||||
}
|
||||
|
||||
throw new TypeError(`Invalid ${optionName} '${signalName}': this signal name does not exist.\n${getAvailableSignals()}`);
|
||||
};
|
||||
|
||||
const getAvailableSignals = () => `Available signal names: ${getAvailableSignalNames()}.
|
||||
Available signal numbers: ${getAvailableSignalIntegers()}.`;
|
||||
|
||||
const getAvailableSignalNames = () => Object.keys(constants.signals)
|
||||
.sort()
|
||||
.map(signalName => `'${signalName}'`)
|
||||
.join(', ');
|
||||
|
||||
const getAvailableSignalIntegers = () => [...new Set(Object.values(constants.signals)
|
||||
.sort((signalInteger, signalIntegerTwo) => signalInteger - signalIntegerTwo))]
|
||||
.join(', ');
|
||||
|
||||
// Human-friendly description of a signal
|
||||
export const getSignalDescription = signal => signalsByName[signal].description;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import {setTimeout} from 'node:timers/promises';
|
||||
import {DiscardedError} from '../return/final-error.js';
|
||||
|
||||
// Validate `timeout` option
|
||||
export const validateTimeout = ({timeout}) => {
|
||||
if (timeout !== undefined && (!Number.isFinite(timeout) || timeout < 0)) {
|
||||
throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`);
|
||||
}
|
||||
};
|
||||
|
||||
// Fails when the `timeout` option is exceeded
|
||||
export const throwOnTimeout = (kill, timeout, context, controller) => timeout === 0 || timeout === undefined
|
||||
? []
|
||||
: [killAfterTimeout(kill, timeout, context, controller)];
|
||||
|
||||
const killAfterTimeout = async (kill, timeout, context, {signal}) => {
|
||||
await setTimeout(timeout, undefined, {signal});
|
||||
context.terminationReason ??= 'timeout';
|
||||
kill();
|
||||
throw new DiscardedError();
|
||||
};
|
||||
Reference in New Issue
Block a user