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