116 lines
3.0 KiB
JavaScript
116 lines
3.0 KiB
JavaScript
|
'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
|
||
|
};
|