@ricsam/r5d-worker 0.0.29 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/main.cjs +39 -5
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/process-tree.cjs +117 -0
- package/dist/mjs/main.mjs +39 -5
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/process-tree.mjs +93 -0
- package/dist/types/process-tree.d.ts +16 -0
- package/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -49,6 +49,7 @@ var import_node_child_process = require("node:child_process");
|
|
|
49
49
|
var import_cli_update = require("./cli-update.cjs");
|
|
50
50
|
var import_git_identity = require("./git-identity.cjs");
|
|
51
51
|
var import_heartbeat = require("./heartbeat.cjs");
|
|
52
|
+
var import_process_tree = require("./process-tree.cjs");
|
|
52
53
|
var import_supervisor = require("./supervisor.cjs");
|
|
53
54
|
const import_meta = {};
|
|
54
55
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
@@ -1817,6 +1818,7 @@ async function executeCommand(input) {
|
|
|
1817
1818
|
cwd,
|
|
1818
1819
|
stdout: "pipe",
|
|
1819
1820
|
stderr: "pipe",
|
|
1821
|
+
detached: true,
|
|
1820
1822
|
env: {
|
|
1821
1823
|
...process.env,
|
|
1822
1824
|
...containerRegistryEnv(),
|
|
@@ -1838,7 +1840,12 @@ async function executeCommand(input) {
|
|
|
1838
1840
|
});
|
|
1839
1841
|
if (input.message.timeoutMs) {
|
|
1840
1842
|
timeout = setTimeout(() => {
|
|
1841
|
-
subprocess.
|
|
1843
|
+
void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
|
|
1844
|
+
process.stderr.write(
|
|
1845
|
+
`[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
1846
|
+
`
|
|
1847
|
+
);
|
|
1848
|
+
});
|
|
1842
1849
|
}, input.message.timeoutMs);
|
|
1843
1850
|
}
|
|
1844
1851
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
@@ -1874,6 +1881,8 @@ async function executeCommand(input) {
|
|
|
1874
1881
|
async function executeStreamingCommand(input) {
|
|
1875
1882
|
const startedAt = Date.now();
|
|
1876
1883
|
let started = false;
|
|
1884
|
+
let timedOut = false;
|
|
1885
|
+
let timeout;
|
|
1877
1886
|
try {
|
|
1878
1887
|
if (cancelledProcessRuns.delete(input.message.runId)) {
|
|
1879
1888
|
sendWorkerMessage(input.ws, {
|
|
@@ -1914,6 +1923,7 @@ async function executeStreamingCommand(input) {
|
|
|
1914
1923
|
cwd,
|
|
1915
1924
|
stdout: "pipe",
|
|
1916
1925
|
stderr: "pipe",
|
|
1926
|
+
detached: true,
|
|
1917
1927
|
env: {
|
|
1918
1928
|
...process.env,
|
|
1919
1929
|
...containerRegistryEnv(),
|
|
@@ -1940,6 +1950,17 @@ async function executeStreamingCommand(input) {
|
|
|
1940
1950
|
requestId: input.message.requestId,
|
|
1941
1951
|
runId: input.message.runId
|
|
1942
1952
|
});
|
|
1953
|
+
if (input.message.timeoutMs) {
|
|
1954
|
+
timeout = setTimeout(() => {
|
|
1955
|
+
timedOut = true;
|
|
1956
|
+
void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
|
|
1957
|
+
process.stderr.write(
|
|
1958
|
+
`[r5d-worker] failed to terminate timed-out process ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
1959
|
+
`
|
|
1960
|
+
);
|
|
1961
|
+
});
|
|
1962
|
+
}, input.message.timeoutMs);
|
|
1963
|
+
}
|
|
1943
1964
|
const [exitCode] = await Promise.all([
|
|
1944
1965
|
subprocess.exited,
|
|
1945
1966
|
streamCommandOutput(subprocess.stdout, (data) => {
|
|
@@ -1963,7 +1984,8 @@ async function executeStreamingCommand(input) {
|
|
|
1963
1984
|
type: "exec_exit",
|
|
1964
1985
|
runId: input.message.runId,
|
|
1965
1986
|
exitCode,
|
|
1966
|
-
durationMs: Date.now() - startedAt
|
|
1987
|
+
durationMs: Date.now() - startedAt,
|
|
1988
|
+
...timedOut ? { timedOut: true } : {}
|
|
1967
1989
|
});
|
|
1968
1990
|
} catch (error) {
|
|
1969
1991
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1983,6 +2005,9 @@ async function executeStreamingCommand(input) {
|
|
|
1983
2005
|
});
|
|
1984
2006
|
}
|
|
1985
2007
|
} finally {
|
|
2008
|
+
if (timeout) {
|
|
2009
|
+
clearTimeout(timeout);
|
|
2010
|
+
}
|
|
1986
2011
|
activeProcesses.delete(input.message.runId);
|
|
1987
2012
|
cancelledProcessRuns.delete(input.message.runId);
|
|
1988
2013
|
}
|
|
@@ -2545,18 +2570,27 @@ async function startWorker(options) {
|
|
|
2545
2570
|
}
|
|
2546
2571
|
if (message.type === "cancel") {
|
|
2547
2572
|
const active = activeProcesses.get(message.runId);
|
|
2573
|
+
let cancelled = true;
|
|
2574
|
+
let cancelMessage;
|
|
2548
2575
|
if (active) {
|
|
2549
|
-
|
|
2576
|
+
try {
|
|
2577
|
+
await (0, import_process_tree.terminateProcessTree)(active.process);
|
|
2578
|
+
cancelMessage = `Stopped ${active.command}`;
|
|
2579
|
+
} catch (error) {
|
|
2580
|
+
cancelled = false;
|
|
2581
|
+
cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
|
|
2582
|
+
}
|
|
2550
2583
|
} else {
|
|
2551
2584
|
cancelledProcessRuns.add(message.runId);
|
|
2585
|
+
cancelMessage = "Cancellation queued before command start";
|
|
2552
2586
|
}
|
|
2553
2587
|
ws.send(
|
|
2554
2588
|
JSON.stringify({
|
|
2555
2589
|
type: "cancel_result",
|
|
2556
2590
|
requestId: message.requestId,
|
|
2557
2591
|
runId: message.runId,
|
|
2558
|
-
cancelled
|
|
2559
|
-
message:
|
|
2592
|
+
cancelled,
|
|
2593
|
+
message: cancelMessage
|
|
2560
2594
|
})
|
|
2561
2595
|
);
|
|
2562
2596
|
return;
|
package/dist/cjs/package.json
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var process_tree_exports = {};
|
|
20
|
+
__export(process_tree_exports, {
|
|
21
|
+
terminateProcessTree: () => terminateProcessTree
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(process_tree_exports);
|
|
24
|
+
const DEFAULT_TERMINATION_GRACE_MS = 2e3;
|
|
25
|
+
const DEFAULT_FORCE_KILL_WAIT_MS = 2e3;
|
|
26
|
+
const PROCESS_GROUP_POLL_INTERVAL_MS = 25;
|
|
27
|
+
const processTerminations = /* @__PURE__ */ new WeakMap();
|
|
28
|
+
function errorCode(error) {
|
|
29
|
+
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
return typeof error.code === "string" ? error.code : void 0;
|
|
33
|
+
}
|
|
34
|
+
function delay(ms) {
|
|
35
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
}
|
|
37
|
+
function isPosixProcessGroupRunning(processGroupId) {
|
|
38
|
+
try {
|
|
39
|
+
process.kill(-processGroupId, 0);
|
|
40
|
+
return true;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (errorCode(error) === "ESRCH") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (errorCode(error) === "EPERM") {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function signalPosixProcessGroup(processGroupId, signal) {
|
|
52
|
+
try {
|
|
53
|
+
process.kill(-processGroupId, signal);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (errorCode(error) !== "ESRCH") {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function waitForPosixProcessGroupExit(processGroupId, timeoutMs) {
|
|
61
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
62
|
+
while (isPosixProcessGroupRunning(processGroupId)) {
|
|
63
|
+
const remainingMs = deadline - Date.now();
|
|
64
|
+
if (remainingMs <= 0) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
await delay(Math.min(PROCESS_GROUP_POLL_INTERVAL_MS, remainingMs));
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
async function runWindowsTaskkill(pid) {
|
|
72
|
+
const taskkill = Bun.spawn(["taskkill", "/PID", String(pid), "/T", "/F"], {
|
|
73
|
+
stdout: "ignore",
|
|
74
|
+
stderr: "pipe"
|
|
75
|
+
});
|
|
76
|
+
const [exitCode, stderr] = await Promise.all([
|
|
77
|
+
taskkill.exited,
|
|
78
|
+
taskkill.stderr ? new Response(taskkill.stderr).text() : Promise.resolve("")
|
|
79
|
+
]);
|
|
80
|
+
if (exitCode !== 0 && !/not found|no running instance/i.test(stderr)) {
|
|
81
|
+
throw new Error(`Failed to terminate process tree ${pid}: ${stderr.trim() || `taskkill exited ${exitCode}`}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function terminateProcessTreeOnce(subprocess, options) {
|
|
85
|
+
if (!Number.isSafeInteger(subprocess.pid) || subprocess.pid <= 0) {
|
|
86
|
+
throw new Error(`Cannot terminate invalid subprocess PID: ${subprocess.pid}`);
|
|
87
|
+
}
|
|
88
|
+
if (process.platform === "win32") {
|
|
89
|
+
await runWindowsTaskkill(subprocess.pid);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const graceMs = options.graceMs ?? DEFAULT_TERMINATION_GRACE_MS;
|
|
93
|
+
const forceKillWaitMs = options.forceKillWaitMs ?? DEFAULT_FORCE_KILL_WAIT_MS;
|
|
94
|
+
signalPosixProcessGroup(subprocess.pid, "SIGTERM");
|
|
95
|
+
if (await waitForPosixProcessGroupExit(subprocess.pid, graceMs)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
signalPosixProcessGroup(subprocess.pid, "SIGKILL");
|
|
99
|
+
if (!await waitForPosixProcessGroupExit(subprocess.pid, forceKillWaitMs)) {
|
|
100
|
+
throw new Error(`Process group ${subprocess.pid} remained active after SIGKILL`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function terminateProcessTree(subprocess, options = {}) {
|
|
104
|
+
const pending = processTerminations.get(subprocess);
|
|
105
|
+
if (pending) {
|
|
106
|
+
return pending;
|
|
107
|
+
}
|
|
108
|
+
const termination = terminateProcessTreeOnce(subprocess, options).finally(() => {
|
|
109
|
+
processTerminations.delete(subprocess);
|
|
110
|
+
});
|
|
111
|
+
processTerminations.set(subprocess, termination);
|
|
112
|
+
return termination;
|
|
113
|
+
}
|
|
114
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
115
|
+
0 && (module.exports = {
|
|
116
|
+
terminateProcessTree
|
|
117
|
+
});
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { spawn as spawnChildProcess } from "node:child_process";
|
|
|
7
7
|
import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
|
|
8
8
|
import { configureVisibleGitIdentity } from "./git-identity.mjs";
|
|
9
9
|
import { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
|
|
10
|
+
import { terminateProcessTree } from "./process-tree.mjs";
|
|
10
11
|
import { superviseWorkerRuntime, WORKER_RELOAD_EXIT_CODE, WORKER_RUNTIME_ENV } from "./supervisor.mjs";
|
|
11
12
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
12
13
|
const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
@@ -1774,6 +1775,7 @@ async function executeCommand(input) {
|
|
|
1774
1775
|
cwd,
|
|
1775
1776
|
stdout: "pipe",
|
|
1776
1777
|
stderr: "pipe",
|
|
1778
|
+
detached: true,
|
|
1777
1779
|
env: {
|
|
1778
1780
|
...process.env,
|
|
1779
1781
|
...containerRegistryEnv(),
|
|
@@ -1795,7 +1797,12 @@ async function executeCommand(input) {
|
|
|
1795
1797
|
});
|
|
1796
1798
|
if (input.message.timeoutMs) {
|
|
1797
1799
|
timeout = setTimeout(() => {
|
|
1798
|
-
subprocess.
|
|
1800
|
+
void terminateProcessTree(subprocess).catch((error) => {
|
|
1801
|
+
process.stderr.write(
|
|
1802
|
+
`[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
1803
|
+
`
|
|
1804
|
+
);
|
|
1805
|
+
});
|
|
1799
1806
|
}, input.message.timeoutMs);
|
|
1800
1807
|
}
|
|
1801
1808
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
@@ -1831,6 +1838,8 @@ async function executeCommand(input) {
|
|
|
1831
1838
|
async function executeStreamingCommand(input) {
|
|
1832
1839
|
const startedAt = Date.now();
|
|
1833
1840
|
let started = false;
|
|
1841
|
+
let timedOut = false;
|
|
1842
|
+
let timeout;
|
|
1834
1843
|
try {
|
|
1835
1844
|
if (cancelledProcessRuns.delete(input.message.runId)) {
|
|
1836
1845
|
sendWorkerMessage(input.ws, {
|
|
@@ -1871,6 +1880,7 @@ async function executeStreamingCommand(input) {
|
|
|
1871
1880
|
cwd,
|
|
1872
1881
|
stdout: "pipe",
|
|
1873
1882
|
stderr: "pipe",
|
|
1883
|
+
detached: true,
|
|
1874
1884
|
env: {
|
|
1875
1885
|
...process.env,
|
|
1876
1886
|
...containerRegistryEnv(),
|
|
@@ -1897,6 +1907,17 @@ async function executeStreamingCommand(input) {
|
|
|
1897
1907
|
requestId: input.message.requestId,
|
|
1898
1908
|
runId: input.message.runId
|
|
1899
1909
|
});
|
|
1910
|
+
if (input.message.timeoutMs) {
|
|
1911
|
+
timeout = setTimeout(() => {
|
|
1912
|
+
timedOut = true;
|
|
1913
|
+
void terminateProcessTree(subprocess).catch((error) => {
|
|
1914
|
+
process.stderr.write(
|
|
1915
|
+
`[r5d-worker] failed to terminate timed-out process ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
1916
|
+
`
|
|
1917
|
+
);
|
|
1918
|
+
});
|
|
1919
|
+
}, input.message.timeoutMs);
|
|
1920
|
+
}
|
|
1900
1921
|
const [exitCode] = await Promise.all([
|
|
1901
1922
|
subprocess.exited,
|
|
1902
1923
|
streamCommandOutput(subprocess.stdout, (data) => {
|
|
@@ -1920,7 +1941,8 @@ async function executeStreamingCommand(input) {
|
|
|
1920
1941
|
type: "exec_exit",
|
|
1921
1942
|
runId: input.message.runId,
|
|
1922
1943
|
exitCode,
|
|
1923
|
-
durationMs: Date.now() - startedAt
|
|
1944
|
+
durationMs: Date.now() - startedAt,
|
|
1945
|
+
...timedOut ? { timedOut: true } : {}
|
|
1924
1946
|
});
|
|
1925
1947
|
} catch (error) {
|
|
1926
1948
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1940,6 +1962,9 @@ async function executeStreamingCommand(input) {
|
|
|
1940
1962
|
});
|
|
1941
1963
|
}
|
|
1942
1964
|
} finally {
|
|
1965
|
+
if (timeout) {
|
|
1966
|
+
clearTimeout(timeout);
|
|
1967
|
+
}
|
|
1943
1968
|
activeProcesses.delete(input.message.runId);
|
|
1944
1969
|
cancelledProcessRuns.delete(input.message.runId);
|
|
1945
1970
|
}
|
|
@@ -2502,18 +2527,27 @@ async function startWorker(options) {
|
|
|
2502
2527
|
}
|
|
2503
2528
|
if (message.type === "cancel") {
|
|
2504
2529
|
const active = activeProcesses.get(message.runId);
|
|
2530
|
+
let cancelled = true;
|
|
2531
|
+
let cancelMessage;
|
|
2505
2532
|
if (active) {
|
|
2506
|
-
|
|
2533
|
+
try {
|
|
2534
|
+
await terminateProcessTree(active.process);
|
|
2535
|
+
cancelMessage = `Stopped ${active.command}`;
|
|
2536
|
+
} catch (error) {
|
|
2537
|
+
cancelled = false;
|
|
2538
|
+
cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
|
|
2539
|
+
}
|
|
2507
2540
|
} else {
|
|
2508
2541
|
cancelledProcessRuns.add(message.runId);
|
|
2542
|
+
cancelMessage = "Cancellation queued before command start";
|
|
2509
2543
|
}
|
|
2510
2544
|
ws.send(
|
|
2511
2545
|
JSON.stringify({
|
|
2512
2546
|
type: "cancel_result",
|
|
2513
2547
|
requestId: message.requestId,
|
|
2514
2548
|
runId: message.runId,
|
|
2515
|
-
cancelled
|
|
2516
|
-
message:
|
|
2549
|
+
cancelled,
|
|
2550
|
+
message: cancelMessage
|
|
2517
2551
|
})
|
|
2518
2552
|
);
|
|
2519
2553
|
return;
|
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const DEFAULT_TERMINATION_GRACE_MS = 2e3;
|
|
2
|
+
const DEFAULT_FORCE_KILL_WAIT_MS = 2e3;
|
|
3
|
+
const PROCESS_GROUP_POLL_INTERVAL_MS = 25;
|
|
4
|
+
const processTerminations = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
function errorCode(error) {
|
|
6
|
+
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
7
|
+
return void 0;
|
|
8
|
+
}
|
|
9
|
+
return typeof error.code === "string" ? error.code : void 0;
|
|
10
|
+
}
|
|
11
|
+
function delay(ms) {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
function isPosixProcessGroupRunning(processGroupId) {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(-processGroupId, 0);
|
|
17
|
+
return true;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (errorCode(error) === "ESRCH") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (errorCode(error) === "EPERM") {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function signalPosixProcessGroup(processGroupId, signal) {
|
|
29
|
+
try {
|
|
30
|
+
process.kill(-processGroupId, signal);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (errorCode(error) !== "ESRCH") {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function waitForPosixProcessGroupExit(processGroupId, timeoutMs) {
|
|
38
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
39
|
+
while (isPosixProcessGroupRunning(processGroupId)) {
|
|
40
|
+
const remainingMs = deadline - Date.now();
|
|
41
|
+
if (remainingMs <= 0) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
await delay(Math.min(PROCESS_GROUP_POLL_INTERVAL_MS, remainingMs));
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
async function runWindowsTaskkill(pid) {
|
|
49
|
+
const taskkill = Bun.spawn(["taskkill", "/PID", String(pid), "/T", "/F"], {
|
|
50
|
+
stdout: "ignore",
|
|
51
|
+
stderr: "pipe"
|
|
52
|
+
});
|
|
53
|
+
const [exitCode, stderr] = await Promise.all([
|
|
54
|
+
taskkill.exited,
|
|
55
|
+
taskkill.stderr ? new Response(taskkill.stderr).text() : Promise.resolve("")
|
|
56
|
+
]);
|
|
57
|
+
if (exitCode !== 0 && !/not found|no running instance/i.test(stderr)) {
|
|
58
|
+
throw new Error(`Failed to terminate process tree ${pid}: ${stderr.trim() || `taskkill exited ${exitCode}`}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function terminateProcessTreeOnce(subprocess, options) {
|
|
62
|
+
if (!Number.isSafeInteger(subprocess.pid) || subprocess.pid <= 0) {
|
|
63
|
+
throw new Error(`Cannot terminate invalid subprocess PID: ${subprocess.pid}`);
|
|
64
|
+
}
|
|
65
|
+
if (process.platform === "win32") {
|
|
66
|
+
await runWindowsTaskkill(subprocess.pid);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const graceMs = options.graceMs ?? DEFAULT_TERMINATION_GRACE_MS;
|
|
70
|
+
const forceKillWaitMs = options.forceKillWaitMs ?? DEFAULT_FORCE_KILL_WAIT_MS;
|
|
71
|
+
signalPosixProcessGroup(subprocess.pid, "SIGTERM");
|
|
72
|
+
if (await waitForPosixProcessGroupExit(subprocess.pid, graceMs)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
signalPosixProcessGroup(subprocess.pid, "SIGKILL");
|
|
76
|
+
if (!await waitForPosixProcessGroupExit(subprocess.pid, forceKillWaitMs)) {
|
|
77
|
+
throw new Error(`Process group ${subprocess.pid} remained active after SIGKILL`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function terminateProcessTree(subprocess, options = {}) {
|
|
81
|
+
const pending = processTerminations.get(subprocess);
|
|
82
|
+
if (pending) {
|
|
83
|
+
return pending;
|
|
84
|
+
}
|
|
85
|
+
const termination = terminateProcessTreeOnce(subprocess, options).finally(() => {
|
|
86
|
+
processTerminations.delete(subprocess);
|
|
87
|
+
});
|
|
88
|
+
processTerminations.set(subprocess, termination);
|
|
89
|
+
return termination;
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
terminateProcessTree
|
|
93
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type KillableSubprocess = {
|
|
2
|
+
readonly pid: number;
|
|
3
|
+
readonly exited: Promise<number>;
|
|
4
|
+
kill(signal?: number | NodeJS.Signals): void;
|
|
5
|
+
};
|
|
6
|
+
type TerminateProcessTreeOptions = {
|
|
7
|
+
graceMs?: number;
|
|
8
|
+
forceKillWaitMs?: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Terminates a subprocess spawned with `detached: true` and every descendant
|
|
12
|
+
* that remains in its process group. Concurrent termination requests share one
|
|
13
|
+
* cleanup attempt.
|
|
14
|
+
*/
|
|
15
|
+
export declare function terminateProcessTree(subprocess: KillableSubprocess, options?: TerminateProcessTreeOptions): Promise<void>;
|
|
16
|
+
export {};
|