1 | 'use strict';
|
2 | const os = require('os');
|
3 | const onExit = require('signal-exit');
|
4 | const pFinally = require('p-finally');
|
5 |
|
6 | const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5;
|
7 |
|
8 |
|
9 | const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => {
|
10 | const killResult = kill(signal);
|
11 | setKillTimeout(kill, signal, options, killResult);
|
12 | return killResult;
|
13 | };
|
14 |
|
15 | const 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 |
|
26 | const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => {
|
27 | return isSigterm(signal) && forceKillAfterTimeout !== false && killResult;
|
28 | };
|
29 |
|
30 | const isSigterm = signal => {
|
31 | return signal === os.constants.signals.SIGTERM ||
|
32 | (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM');
|
33 | };
|
34 |
|
35 | const 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 |
|
48 | const spawnedCancel = (spawned, context) => {
|
49 | const killResult = spawned.kill();
|
50 |
|
51 | if (killResult) {
|
52 | context.isCanceled = true;
|
53 | }
|
54 | };
|
55 |
|
56 | const timeoutKill = (spawned, signal, reject) => {
|
57 | spawned.kill(signal);
|
58 | reject(Object.assign(new Error('Timed out'), {timedOut: true, signal}));
|
59 | };
|
60 |
|
61 |
|
62 | const 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 |
|
86 | const setExitHandler = (spawned, {cleanup, detached}, timedPromise) => {
|
87 | if (!cleanup || detached) {
|
88 | return;
|
89 | }
|
90 |
|
91 | const removeExitHandler = onExit(() => {
|
92 | spawned.kill();
|
93 | });
|
94 |
|
95 |
|
96 | return pFinally(timedPromise, removeExitHandler);
|
97 | };
|
98 |
|
99 | module.exports = {
|
100 | spawnedKill,
|
101 | spawnedCancel,
|
102 | setupTimeout,
|
103 | setExitHandler
|
104 | };
|