| 'use strict'; |
| const os = require('os'); |
| const onExit = require('signal-exit'); |
| |
| const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; |
| |
| // Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior |
| const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { |
| const killResult = kill(signal); |
| setKillTimeout(kill, signal, options, killResult); |
| return killResult; |
| }; |
| |
| const setKillTimeout = (kill, signal, options, killResult) => { |
| if (!shouldForceKill(signal, options, killResult)) { |
| return; |
| } |
| |
| const timeout = getForceKillAfterTimeout(options); |
| const t = setTimeout(() => { |
| kill('SIGKILL'); |
| }, timeout); |
| |
| // Guarded because there's no `.unref()` when `execa` is used in the renderer |
| // process in Electron. This cannot be tested since we don't run tests in |
| // Electron. |
| // istanbul ignore else |
| if (t.unref) { |
| t.unref(); |
| } |
| }; |
| |
| const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => { |
| return isSigterm(signal) && forceKillAfterTimeout !== false && killResult; |
| }; |
| |
| const isSigterm = signal => { |
| return signal === os.constants.signals.SIGTERM || |
| (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); |
| }; |
| |
| const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { |
| if (forceKillAfterTimeout === true) { |
| return DEFAULT_FORCE_KILL_TIMEOUT; |
| } |
| |
| if (!Number.isFinite(forceKillAfterTimeout) || forceKillAfterTimeout < 0) { |
| throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`); |
| } |
| |
| return forceKillAfterTimeout; |
| }; |
| |
| // `childProcess.cancel()` |
| const spawnedCancel = (spawned, context) => { |
| const killResult = spawned.kill(); |
| |
| if (killResult) { |
| context.isCanceled = true; |
| } |
| }; |
| |
| const timeoutKill = (spawned, signal, reject) => { |
| spawned.kill(signal); |
| reject(Object.assign(new Error('Timed out'), {timedOut: true, signal})); |
| }; |
| |
| // `timeout` option handling |
| const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => { |
| if (timeout === 0 || timeout === undefined) { |
| return spawnedPromise; |
| } |
| |
| let timeoutId; |
| const timeoutPromise = new Promise((resolve, reject) => { |
| timeoutId = setTimeout(() => { |
| timeoutKill(spawned, killSignal, reject); |
| }, timeout); |
| }); |
| |
| const safeSpawnedPromise = spawnedPromise.finally(() => { |
| clearTimeout(timeoutId); |
| }); |
| |
| return Promise.race([timeoutPromise, safeSpawnedPromise]); |
| }; |
| |
| 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})`); |
| } |
| }; |
| |
| // `cleanup` option handling |
| const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { |
| if (!cleanup || detached) { |
| return timedPromise; |
| } |
| |
| const removeExitHandler = onExit(() => { |
| spawned.kill(); |
| }); |
| |
| return timedPromise.finally(() => { |
| removeExitHandler(); |
| }); |
| }; |
| |
| module.exports = { |
| spawnedKill, |
| spawnedCancel, |
| setupTimeout, |
| validateTimeout, |
| setExitHandler |
| }; |