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