UNPKG

2.98 kBJavaScriptView Raw
1import os from 'node:os';
2import {onExit} from 'signal-exit';
3
4const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5;
5
6// Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior
7export const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => {
8 const killResult = kill(signal);
9 setKillTimeout(kill, signal, options, killResult);
10 return killResult;
11};
12
13const setKillTimeout = (kill, signal, options, killResult) => {
14 if (!shouldForceKill(signal, options, killResult)) {
15 return;
16 }
17
18 const timeout = getForceKillAfterTimeout(options);
19 const t = setTimeout(() => {
20 kill('SIGKILL');
21 }, timeout);
22
23 // Guarded because there's no `.unref()` when `execa` is used in the renderer
24 // process in Electron. This cannot be tested since we don't run tests in
25 // Electron.
26 // istanbul ignore else
27 if (t.unref) {
28 t.unref();
29 }
30};
31
32const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => isSigterm(signal) && forceKillAfterTimeout !== false && killResult;
33
34const isSigterm = signal => signal === os.constants.signals.SIGTERM
35 || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM');
36
37const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => {
38 if (forceKillAfterTimeout === true) {
39 return DEFAULT_FORCE_KILL_TIMEOUT;
40 }
41
42 if (!Number.isFinite(forceKillAfterTimeout) || forceKillAfterTimeout < 0) {
43 throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`);
44 }
45
46 return forceKillAfterTimeout;
47};
48
49// `childProcess.cancel()`
50export const spawnedCancel = (spawned, context) => {
51 const killResult = spawned.kill();
52
53 if (killResult) {
54 context.isCanceled = true;
55 }
56};
57
58const timeoutKill = (spawned, signal, reject) => {
59 spawned.kill(signal);
60 reject(Object.assign(new Error('Timed out'), {timedOut: true, signal}));
61};
62
63// `timeout` option handling
64export const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => {
65 if (timeout === 0 || timeout === undefined) {
66 return spawnedPromise;
67 }
68
69 let timeoutId;
70 const timeoutPromise = new Promise((resolve, reject) => {
71 timeoutId = setTimeout(() => {
72 timeoutKill(spawned, killSignal, reject);
73 }, timeout);
74 });
75
76 const safeSpawnedPromise = spawnedPromise.finally(() => {
77 clearTimeout(timeoutId);
78 });
79
80 return Promise.race([timeoutPromise, safeSpawnedPromise]);
81};
82
83export const validateTimeout = ({timeout}) => {
84 if (timeout !== undefined && (!Number.isFinite(timeout) || timeout < 0)) {
85 throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`);
86 }
87};
88
89// `cleanup` option handling
90export const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => {
91 if (!cleanup || detached) {
92 return timedPromise;
93 }
94
95 const removeExitHandler = onExit(() => {
96 spawned.kill();
97 });
98
99 return timedPromise.finally(() => {
100 removeExitHandler();
101 });
102};