Repository navigation
Expand file tree
/
Copy pathrun-command-with-timeout.cjs
More file actions
90 lines (78 loc) · 2.73 KB
/
Copy pathrun-command-with-timeout.cjs
File metadata and controls
90 lines (78 loc) · 2.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/usr/bin/env node
const { spawn } = require("node:child_process");
const { writeSync } = require("node:fs");
const { signalPosixProcessGroup } = require("./process-group-signal.cjs");
const [, , timeoutValue, command, ...args] = process.argv;
const timeoutMs = Number(timeoutValue);
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || !command) {
writeSync(
3,
`${JSON.stringify({ setupError: "Invalid timeout runner arguments." })}\n`,
);
process.exit(1);
}
let child;
let timedOut = false;
let terminationTimer;
let killTimer;
let forwardedSignal;
function signalProcessGroup(signal, { leaderExited = false } = {}) {
if (!child?.pid) return;
if (process.platform === "win32") child.kill(signal);
else signalPosixProcessGroup(process.kill, child.pid, signal, { leaderExited });
}
function beginTermination(signal, timeoutExpired = false) {
if (timeoutExpired) timedOut = true;
if (signal && !forwardedSignal) forwardedSignal = signal;
// A timed-out command must not outlive the synchronous caller. The caller
// returns as soon as the direct child closes, so a delayed SIGKILL would
// leave grandchildren running after runProcess() has already returned.
if (timeoutExpired) {
signalProcessGroup("SIGKILL");
return;
}
signalProcessGroup(signal || "SIGTERM");
if (!killTimer) {
killTimer = setTimeout(() => signalProcessGroup("SIGKILL"), 5_000);
killTimer.unref();
}
}
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => beginTermination(signal));
}
try {
child = spawn(command, args, {
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
});
} catch (error) {
writeSync(3, `${JSON.stringify({ setupError: error.message })}\n`);
process.exit(1);
}
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
terminationTimer = setTimeout(
() => beginTermination("SIGTERM", true),
timeoutMs,
);
terminationTimer.unref();
child.on("error", (error) => {
writeSync(3, `${JSON.stringify({ setupError: error.message })}\n`);
});
child.on("exit", () => {
// A descendant can be created concurrently with the first group signal.
// Once the direct child has exited it cannot fork again, so signaling the
// group a second time closes that race before the runner reports completion.
// macOS can report EPERM after the leader is reaped even though the earlier
// group signal succeeded, so only this idempotent sweep treats it as gone.
if (forwardedSignal) signalProcessGroup("SIGKILL", { leaderExited: true });
});
child.on("close", (code, signal) => {
clearTimeout(terminationTimer);
clearTimeout(killTimer);
writeSync(
3,
`${JSON.stringify({ code, forwardedSignal, signal, timedOut })}\n`,
);
process.exitCode = 0;
});