@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 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.kill("SIGTERM");
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
- active.process.kill("SIGTERM");
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: true,
2559
- message: active ? `Sent SIGTERM to ${active.command}` : "Cancellation queued before command start"
2592
+ cancelled,
2593
+ message: cancelMessage
2560
2594
  })
2561
2595
  );
2562
2596
  return;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "type": "commonjs"
5
5
  }
@@ -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.kill("SIGTERM");
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
- active.process.kill("SIGTERM");
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: true,
2516
- message: active ? `Sent SIGTERM to ${active.command}` : "Cancellation queued before command start"
2549
+ cancelled,
2550
+ message: cancelMessage
2517
2551
  })
2518
2552
  );
2519
2553
  return;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "type": "module"
5
5
  }
@@ -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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",