kill.js
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 if (!Number.isFinite(timeout) || timeout < 0) { 75 throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`); 76 } 77 78 let timeoutId; 79 const timeoutPromise = new Promise((resolve, reject) => { 80 timeoutId = setTimeout(() => { 81 timeoutKill(spawned, killSignal, reject); 82 }, timeout); 83 }); 84 85 const safeSpawnedPromise = spawnedPromise.finally(() => { 86 clearTimeout(timeoutId); 87 }); 88 89 return Promise.race([timeoutPromise, safeSpawnedPromise]); 90 }; 91 92 // `cleanup` option handling 93 const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { 94 if (!cleanup || detached) { 95 return timedPromise; 96 } 97 98 const removeExitHandler = onExit(() => { 99 spawned.kill(); 100 }); 101 102 return timedPromise.finally(() => { 103 removeExitHandler(); 104 }); 105 }; 106 107 module.exports = { 108 spawnedKill, 109 spawnedCancel, 110 setupTimeout, 111 setExitHandler 112 };