UNPKG

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