kill.js (2983B)
1 import os from 'node:os'; 2 import onExit from 'signal-exit'; 3 4 const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; 5 6 // Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior 7 export const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { 8 const killResult = kill(signal); 9 setKillTimeout(kill, signal, options, killResult); 10 return killResult; 11 }; 12 13 const 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 32 const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => isSigterm(signal) && forceKillAfterTimeout !== false && killResult; 33 34 const isSigterm = signal => signal === os.constants.signals.SIGTERM 35 || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); 36 37 const 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()` 50 export const spawnedCancel = (spawned, context) => { 51 const killResult = spawned.kill(); 52 53 if (killResult) { 54 context.isCanceled = true; 55 } 56 }; 57 58 const 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 64 export 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 83 export 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 90 export 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 };