| 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 | };
|
|---|