@ricsam/r5d-worker 0.0.28 → 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/README.md +4 -0
- package/dist/cjs/cli-update.cjs +108 -0
- package/dist/cjs/main.cjs +164 -28
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/process-tree.cjs +117 -0
- package/dist/cjs/supervisor.cjs +58 -0
- package/dist/mjs/cli-update.mjs +80 -0
- package/dist/mjs/main.mjs +164 -28
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/process-tree.mjs +93 -0
- package/dist/mjs/supervisor.mjs +32 -0
- package/dist/types/cli-update.d.ts +21 -0
- package/dist/types/process-tree.d.ts +16 -0
- package/dist/types/supervisor.d.ts +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,10 @@ Visible branch checkouts are the user/agent workbench. Their `origin` remains th
|
|
|
19
19
|
|
|
20
20
|
Worker labels are mandatory and unique per user. Choose labels that describe host capabilities, such as `macos`, `linux`, `ios`, or `ec2-build`.
|
|
21
21
|
|
|
22
|
+
`r5d-worker start` keeps a lightweight supervisor process attached to the launching terminal or service. When both r5d CLIs are updated from User Settings, the connected runtime verifies the installed versions, exits with a reload signal, and the supervisor reconnects using the new worker package. Updates only run while the worker has no active commands or shells.
|
|
23
|
+
|
|
24
|
+
One-click updates use the current user's global npm prefix and never invoke `sudo`. If that prefix is not writable, User Settings provides the exact command to run manually.
|
|
25
|
+
|
|
22
26
|
The agent runs commands through the virtual shell command:
|
|
23
27
|
|
|
24
28
|
```sh
|
|
@@ -0,0 +1,108 @@
|
|
|
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 cli_update_exports = {};
|
|
20
|
+
__export(cli_update_exports, {
|
|
21
|
+
getManualCliUpdateCommand: () => getManualCliUpdateCommand,
|
|
22
|
+
installCliUpdate: () => installCliUpdate,
|
|
23
|
+
parseCliVersion: () => parseCliVersion,
|
|
24
|
+
readInstalledCliVersion: () => readInstalledCliVersion,
|
|
25
|
+
runCommand: () => runCommand
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(cli_update_exports);
|
|
28
|
+
const UPDATE_OUTPUT_LIMIT = 8e3;
|
|
29
|
+
function collectOutput(value) {
|
|
30
|
+
if (!value || typeof value === "number") {
|
|
31
|
+
return Promise.resolve("");
|
|
32
|
+
}
|
|
33
|
+
return new Response(value).text();
|
|
34
|
+
}
|
|
35
|
+
async function runCommand(argv) {
|
|
36
|
+
const child = Bun.spawn(argv, {
|
|
37
|
+
stdout: "pipe",
|
|
38
|
+
stderr: "pipe",
|
|
39
|
+
env: process.env
|
|
40
|
+
});
|
|
41
|
+
const [stdout, stderr, exitCode] = await Promise.all([collectOutput(child.stdout), collectOutput(child.stderr), child.exited]);
|
|
42
|
+
return { exitCode, stdout, stderr };
|
|
43
|
+
}
|
|
44
|
+
function parseCliVersion(cliName, output) {
|
|
45
|
+
const escapedName = cliName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
46
|
+
const match = output.trim().match(new RegExp(`(?:^|\\n)${escapedName}\\s+(\\S+)`));
|
|
47
|
+
return match?.[1]?.trim() || null;
|
|
48
|
+
}
|
|
49
|
+
function readInstalledCliVersion(cliName) {
|
|
50
|
+
try {
|
|
51
|
+
const result = Bun.spawnSync([cliName, "--version"], {
|
|
52
|
+
stdout: "pipe",
|
|
53
|
+
stderr: "pipe",
|
|
54
|
+
env: process.env
|
|
55
|
+
});
|
|
56
|
+
if (result.exitCode !== 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return parseCliVersion(cliName, Buffer.from(result.stdout).toString("utf8"));
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function shellQuote(value) {
|
|
65
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
66
|
+
}
|
|
67
|
+
function getManualCliUpdateCommand(input) {
|
|
68
|
+
return `npm install -g --omit=dev ${shellQuote(input.workerPackageSpec)} ${shellQuote(input.r5dctlPackageSpec)}`;
|
|
69
|
+
}
|
|
70
|
+
function commandErrorDetail(result) {
|
|
71
|
+
const detail = `${result.stderr.trim()}${result.stderr.trim() && result.stdout.trim() ? "\n" : ""}${result.stdout.trim()}`.trim();
|
|
72
|
+
if (!detail) {
|
|
73
|
+
return `exit code ${result.exitCode}`;
|
|
74
|
+
}
|
|
75
|
+
return detail.slice(-UPDATE_OUTPUT_LIMIT);
|
|
76
|
+
}
|
|
77
|
+
async function readVerifiedVersion(cliName, targetVersion, runner) {
|
|
78
|
+
const result = await runner([cliName, "--version"]);
|
|
79
|
+
const installedVersion = result.exitCode === 0 ? parseCliVersion(cliName, result.stdout) : null;
|
|
80
|
+
if (!installedVersion) {
|
|
81
|
+
throw new Error(`${cliName} was installed but is not runnable: ${commandErrorDetail(result)}`);
|
|
82
|
+
}
|
|
83
|
+
if (installedVersion !== targetVersion) {
|
|
84
|
+
throw new Error(`${cliName} ${installedVersion} was installed; expected ${targetVersion}`);
|
|
85
|
+
}
|
|
86
|
+
return installedVersion;
|
|
87
|
+
}
|
|
88
|
+
async function installCliUpdate(input, runner = runCommand) {
|
|
89
|
+
const installArgs = ["npm", "install", "-g", "--omit=dev", input.workerPackageSpec, input.r5dctlPackageSpec];
|
|
90
|
+
const result = await runner(installArgs);
|
|
91
|
+
if (result.exitCode !== 0) {
|
|
92
|
+
throw new Error(`CLI update failed: ${commandErrorDetail(result)}
|
|
93
|
+
Run manually: ${getManualCliUpdateCommand(input)}`);
|
|
94
|
+
}
|
|
95
|
+
const [installedWorkerVersion, installedR5dctlVersion] = await Promise.all([
|
|
96
|
+
readVerifiedVersion("r5d-worker", input.targetWorkerVersion, runner),
|
|
97
|
+
readVerifiedVersion("r5dctl", input.targetR5dctlVersion, runner)
|
|
98
|
+
]);
|
|
99
|
+
return { installedWorkerVersion, installedR5dctlVersion };
|
|
100
|
+
}
|
|
101
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
102
|
+
0 && (module.exports = {
|
|
103
|
+
getManualCliUpdateCommand,
|
|
104
|
+
installCliUpdate,
|
|
105
|
+
parseCliVersion,
|
|
106
|
+
readInstalledCliVersion,
|
|
107
|
+
runCommand
|
|
108
|
+
});
|
package/dist/cjs/main.cjs
CHANGED
|
@@ -46,8 +46,11 @@ var import_node_path = __toESM(require("node:path"), 1);
|
|
|
46
46
|
var import_node_crypto = require("node:crypto");
|
|
47
47
|
var import_node_os = __toESM(require("node:os"), 1);
|
|
48
48
|
var import_node_child_process = require("node:child_process");
|
|
49
|
+
var import_cli_update = require("./cli-update.cjs");
|
|
49
50
|
var import_git_identity = require("./git-identity.cjs");
|
|
50
51
|
var import_heartbeat = require("./heartbeat.cjs");
|
|
52
|
+
var import_process_tree = require("./process-tree.cjs");
|
|
53
|
+
var import_supervisor = require("./supervisor.cjs");
|
|
51
54
|
const import_meta = {};
|
|
52
55
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
53
56
|
const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
@@ -1815,6 +1818,7 @@ async function executeCommand(input) {
|
|
|
1815
1818
|
cwd,
|
|
1816
1819
|
stdout: "pipe",
|
|
1817
1820
|
stderr: "pipe",
|
|
1821
|
+
detached: true,
|
|
1818
1822
|
env: {
|
|
1819
1823
|
...process.env,
|
|
1820
1824
|
...containerRegistryEnv(),
|
|
@@ -1836,7 +1840,12 @@ async function executeCommand(input) {
|
|
|
1836
1840
|
});
|
|
1837
1841
|
if (input.message.timeoutMs) {
|
|
1838
1842
|
timeout = setTimeout(() => {
|
|
1839
|
-
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
|
+
});
|
|
1840
1849
|
}, input.message.timeoutMs);
|
|
1841
1850
|
}
|
|
1842
1851
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
@@ -1872,6 +1881,8 @@ async function executeCommand(input) {
|
|
|
1872
1881
|
async function executeStreamingCommand(input) {
|
|
1873
1882
|
const startedAt = Date.now();
|
|
1874
1883
|
let started = false;
|
|
1884
|
+
let timedOut = false;
|
|
1885
|
+
let timeout;
|
|
1875
1886
|
try {
|
|
1876
1887
|
if (cancelledProcessRuns.delete(input.message.runId)) {
|
|
1877
1888
|
sendWorkerMessage(input.ws, {
|
|
@@ -1912,6 +1923,7 @@ async function executeStreamingCommand(input) {
|
|
|
1912
1923
|
cwd,
|
|
1913
1924
|
stdout: "pipe",
|
|
1914
1925
|
stderr: "pipe",
|
|
1926
|
+
detached: true,
|
|
1915
1927
|
env: {
|
|
1916
1928
|
...process.env,
|
|
1917
1929
|
...containerRegistryEnv(),
|
|
@@ -1938,6 +1950,17 @@ async function executeStreamingCommand(input) {
|
|
|
1938
1950
|
requestId: input.message.requestId,
|
|
1939
1951
|
runId: input.message.runId
|
|
1940
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
|
+
}
|
|
1941
1964
|
const [exitCode] = await Promise.all([
|
|
1942
1965
|
subprocess.exited,
|
|
1943
1966
|
streamCommandOutput(subprocess.stdout, (data) => {
|
|
@@ -1961,7 +1984,8 @@ async function executeStreamingCommand(input) {
|
|
|
1961
1984
|
type: "exec_exit",
|
|
1962
1985
|
runId: input.message.runId,
|
|
1963
1986
|
exitCode,
|
|
1964
|
-
durationMs: Date.now() - startedAt
|
|
1987
|
+
durationMs: Date.now() - startedAt,
|
|
1988
|
+
...timedOut ? { timedOut: true } : {}
|
|
1965
1989
|
});
|
|
1966
1990
|
} catch (error) {
|
|
1967
1991
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1981,6 +2005,9 @@ async function executeStreamingCommand(input) {
|
|
|
1981
2005
|
});
|
|
1982
2006
|
}
|
|
1983
2007
|
} finally {
|
|
2008
|
+
if (timeout) {
|
|
2009
|
+
clearTimeout(timeout);
|
|
2010
|
+
}
|
|
1984
2011
|
activeProcesses.delete(input.message.runId);
|
|
1985
2012
|
cancelledProcessRuns.delete(input.message.runId);
|
|
1986
2013
|
}
|
|
@@ -2358,6 +2385,9 @@ async function startWorker(options) {
|
|
|
2358
2385
|
import_node_fs.default.mkdirSync(planRoot, { recursive: true });
|
|
2359
2386
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
2360
2387
|
let repositorySyncQueue = Promise.resolve();
|
|
2388
|
+
let repositorySyncInProgress = false;
|
|
2389
|
+
let cliUpdateInProgress = false;
|
|
2390
|
+
let reloadAfterClose = false;
|
|
2361
2391
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
2362
2392
|
headers: {
|
|
2363
2393
|
Authorization: `Bearer ${token}`
|
|
@@ -2391,6 +2421,10 @@ async function startWorker(options) {
|
|
|
2391
2421
|
arch: process.arch,
|
|
2392
2422
|
pid: process.pid,
|
|
2393
2423
|
version: getWorkerVersion(),
|
|
2424
|
+
r5dctlVersion: (0, import_cli_update.readInstalledCliVersion)("r5dctl"),
|
|
2425
|
+
capabilities: {
|
|
2426
|
+
updateClis: true
|
|
2427
|
+
},
|
|
2394
2428
|
projectRoot: projectsRoot,
|
|
2395
2429
|
artifactRoot,
|
|
2396
2430
|
planRoot
|
|
@@ -2418,32 +2452,49 @@ async function startWorker(options) {
|
|
|
2418
2452
|
return;
|
|
2419
2453
|
}
|
|
2420
2454
|
if (message.type === "sync_projects") {
|
|
2455
|
+
if (cliUpdateInProgress) {
|
|
2456
|
+
sendWorkerMessage(ws, {
|
|
2457
|
+
type: "sync_projects_result",
|
|
2458
|
+
requestId: message.requestId,
|
|
2459
|
+
result: {
|
|
2460
|
+
status: "blocked",
|
|
2461
|
+
results: [],
|
|
2462
|
+
blockedBy: []
|
|
2463
|
+
}
|
|
2464
|
+
});
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2421
2467
|
let syncResult = { status: "completed", results: [], blockedBy: [] };
|
|
2422
2468
|
const executeSync = async () => {
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2469
|
+
repositorySyncInProgress = true;
|
|
2470
|
+
try {
|
|
2471
|
+
syncResult = await syncManifestProjectsFromInternal({
|
|
2472
|
+
baseUrl,
|
|
2473
|
+
token,
|
|
2474
|
+
projectsRoot,
|
|
2475
|
+
syncRoot,
|
|
2476
|
+
manifests: [...manifestByProjectId.values()],
|
|
2477
|
+
projectIds: message.projectIds
|
|
2478
|
+
});
|
|
2479
|
+
for (const result of syncResult.results) {
|
|
2480
|
+
if (result.status !== "synced") continue;
|
|
2481
|
+
try {
|
|
2482
|
+
await syncProjectPlans({
|
|
2483
|
+
baseUrl,
|
|
2484
|
+
token,
|
|
2485
|
+
projectId: result.projectId,
|
|
2486
|
+
branchName: result.branchName,
|
|
2487
|
+
planRoot
|
|
2488
|
+
});
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
process.stderr.write(
|
|
2491
|
+
`[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
2444
2492
|
`
|
|
2445
|
-
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2446
2495
|
}
|
|
2496
|
+
} finally {
|
|
2497
|
+
repositorySyncInProgress = false;
|
|
2447
2498
|
}
|
|
2448
2499
|
};
|
|
2449
2500
|
const queued = repositorySyncQueue.then(executeSync, executeSync);
|
|
@@ -2475,6 +2526,43 @@ async function startWorker(options) {
|
|
|
2475
2526
|
});
|
|
2476
2527
|
return;
|
|
2477
2528
|
}
|
|
2529
|
+
if (message.type === "update_clis") {
|
|
2530
|
+
if (cliUpdateInProgress) {
|
|
2531
|
+
sendWorkerMessage(ws, {
|
|
2532
|
+
type: "update_clis_result",
|
|
2533
|
+
requestId: message.requestId,
|
|
2534
|
+
error: "A CLI update is already in progress"
|
|
2535
|
+
});
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
if (activeProcesses.size > 0 || activePtys.size > 0 || repositorySyncInProgress) {
|
|
2539
|
+
sendWorkerMessage(ws, {
|
|
2540
|
+
type: "update_clis_result",
|
|
2541
|
+
requestId: message.requestId,
|
|
2542
|
+
error: "Worker is busy. Stop active commands and shells, then retry the CLI update."
|
|
2543
|
+
});
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
cliUpdateInProgress = true;
|
|
2547
|
+
try {
|
|
2548
|
+
const result = await (0, import_cli_update.installCliUpdate)(message);
|
|
2549
|
+
sendWorkerMessage(ws, {
|
|
2550
|
+
type: "update_clis_result",
|
|
2551
|
+
requestId: message.requestId,
|
|
2552
|
+
...result
|
|
2553
|
+
});
|
|
2554
|
+
reloadAfterClose = true;
|
|
2555
|
+
ws.close(1e3, "Activating updated CLIs");
|
|
2556
|
+
} catch (error) {
|
|
2557
|
+
cliUpdateInProgress = false;
|
|
2558
|
+
sendWorkerMessage(ws, {
|
|
2559
|
+
type: "update_clis_result",
|
|
2560
|
+
requestId: message.requestId,
|
|
2561
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2478
2566
|
if (message.type === "ping") {
|
|
2479
2567
|
lastServerHeartbeatAt = Date.now();
|
|
2480
2568
|
ws.send(JSON.stringify({ type: "pong" }));
|
|
@@ -2482,23 +2570,41 @@ async function startWorker(options) {
|
|
|
2482
2570
|
}
|
|
2483
2571
|
if (message.type === "cancel") {
|
|
2484
2572
|
const active = activeProcesses.get(message.runId);
|
|
2573
|
+
let cancelled = true;
|
|
2574
|
+
let cancelMessage;
|
|
2485
2575
|
if (active) {
|
|
2486
|
-
|
|
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
|
+
}
|
|
2487
2583
|
} else {
|
|
2488
2584
|
cancelledProcessRuns.add(message.runId);
|
|
2585
|
+
cancelMessage = "Cancellation queued before command start";
|
|
2489
2586
|
}
|
|
2490
2587
|
ws.send(
|
|
2491
2588
|
JSON.stringify({
|
|
2492
2589
|
type: "cancel_result",
|
|
2493
2590
|
requestId: message.requestId,
|
|
2494
2591
|
runId: message.runId,
|
|
2495
|
-
cancelled
|
|
2496
|
-
message:
|
|
2592
|
+
cancelled,
|
|
2593
|
+
message: cancelMessage
|
|
2497
2594
|
})
|
|
2498
2595
|
);
|
|
2499
2596
|
return;
|
|
2500
2597
|
}
|
|
2501
2598
|
if (message.type === "pty_open") {
|
|
2599
|
+
if (cliUpdateInProgress) {
|
|
2600
|
+
sendWorkerMessage(ws, {
|
|
2601
|
+
type: "pty_error",
|
|
2602
|
+
requestId: message.requestId,
|
|
2603
|
+
ptyId: message.ptyId,
|
|
2604
|
+
error: "Worker is activating a CLI update"
|
|
2605
|
+
});
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2502
2608
|
await repositorySyncQueue;
|
|
2503
2609
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2504
2610
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
@@ -2520,6 +2626,16 @@ async function startWorker(options) {
|
|
|
2520
2626
|
return;
|
|
2521
2627
|
}
|
|
2522
2628
|
if (message.type === "exec") {
|
|
2629
|
+
if (cliUpdateInProgress) {
|
|
2630
|
+
sendWorkerMessage(ws, {
|
|
2631
|
+
type: "exec_result",
|
|
2632
|
+
requestId: message.requestId,
|
|
2633
|
+
stdout: "",
|
|
2634
|
+
stderr: "Worker is activating a CLI update",
|
|
2635
|
+
exitCode: 1
|
|
2636
|
+
});
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2523
2639
|
await repositorySyncQueue;
|
|
2524
2640
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2525
2641
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
@@ -2540,6 +2656,15 @@ async function startWorker(options) {
|
|
|
2540
2656
|
return;
|
|
2541
2657
|
}
|
|
2542
2658
|
if (message.type === "exec_start") {
|
|
2659
|
+
if (cliUpdateInProgress) {
|
|
2660
|
+
sendWorkerMessage(ws, {
|
|
2661
|
+
type: "exec_start_error",
|
|
2662
|
+
requestId: message.requestId,
|
|
2663
|
+
runId: message.runId,
|
|
2664
|
+
error: "Worker is activating a CLI update"
|
|
2665
|
+
});
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2543
2668
|
await repositorySyncQueue;
|
|
2544
2669
|
sendWorkerMessage(ws, {
|
|
2545
2670
|
type: "exec_accepted",
|
|
@@ -2637,6 +2762,9 @@ async function startWorker(options) {
|
|
|
2637
2762
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
2638
2763
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
2639
2764
|
`);
|
|
2765
|
+
if (reloadAfterClose) {
|
|
2766
|
+
process.exit(import_supervisor.WORKER_RELOAD_EXIT_CODE);
|
|
2767
|
+
}
|
|
2640
2768
|
if (activeProcesses.size > 0) {
|
|
2641
2769
|
const delayMs = 2e3;
|
|
2642
2770
|
process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
|
|
@@ -2675,7 +2803,15 @@ async function main() {
|
|
|
2675
2803
|
printVersion();
|
|
2676
2804
|
return;
|
|
2677
2805
|
}
|
|
2678
|
-
|
|
2806
|
+
if (process.env[import_supervisor.WORKER_RUNTIME_ENV] === "1") {
|
|
2807
|
+
delete process.env[import_supervisor.WORKER_RUNTIME_ENV];
|
|
2808
|
+
await startWorker(parsed.options);
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
const exitCode = await (0, import_supervisor.superviseWorkerRuntime)(process.argv.slice(2));
|
|
2812
|
+
if (exitCode !== 0) {
|
|
2813
|
+
process.exit(exitCode);
|
|
2814
|
+
}
|
|
2679
2815
|
}
|
|
2680
2816
|
function isCliEntrypoint() {
|
|
2681
2817
|
if (import_meta.main) {
|
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
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
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 supervisor_exports = {};
|
|
20
|
+
__export(supervisor_exports, {
|
|
21
|
+
WORKER_RELOAD_EXIT_CODE: () => WORKER_RELOAD_EXIT_CODE,
|
|
22
|
+
WORKER_RUNTIME_ENV: () => WORKER_RUNTIME_ENV,
|
|
23
|
+
superviseWorkerRuntime: () => superviseWorkerRuntime
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(supervisor_exports);
|
|
26
|
+
const WORKER_RUNTIME_ENV = "R5D_WORKER_INTERNAL_RUNTIME";
|
|
27
|
+
const WORKER_RELOAD_EXIT_CODE = 75;
|
|
28
|
+
function defaultRuntimeSpawner(argv, options) {
|
|
29
|
+
return Bun.spawn(argv, options);
|
|
30
|
+
}
|
|
31
|
+
async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner) {
|
|
32
|
+
if (!process.argv[1]) {
|
|
33
|
+
throw new Error("Cannot locate the r5d-worker entrypoint");
|
|
34
|
+
}
|
|
35
|
+
while (true) {
|
|
36
|
+
const runtime = spawnRuntime([process.execPath, process.argv[1], ...argv], {
|
|
37
|
+
stdin: "inherit",
|
|
38
|
+
stdout: "inherit",
|
|
39
|
+
stderr: "inherit",
|
|
40
|
+
env: {
|
|
41
|
+
...env,
|
|
42
|
+
[WORKER_RUNTIME_ENV]: "1"
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const exitCode = await runtime.exited;
|
|
46
|
+
if (exitCode === WORKER_RELOAD_EXIT_CODE) {
|
|
47
|
+
process.stdout.write("[r5d-worker] activating updated worker runtime\n");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
return exitCode;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
54
|
+
0 && (module.exports = {
|
|
55
|
+
WORKER_RELOAD_EXIT_CODE,
|
|
56
|
+
WORKER_RUNTIME_ENV,
|
|
57
|
+
superviseWorkerRuntime
|
|
58
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const UPDATE_OUTPUT_LIMIT = 8e3;
|
|
2
|
+
function collectOutput(value) {
|
|
3
|
+
if (!value || typeof value === "number") {
|
|
4
|
+
return Promise.resolve("");
|
|
5
|
+
}
|
|
6
|
+
return new Response(value).text();
|
|
7
|
+
}
|
|
8
|
+
async function runCommand(argv) {
|
|
9
|
+
const child = Bun.spawn(argv, {
|
|
10
|
+
stdout: "pipe",
|
|
11
|
+
stderr: "pipe",
|
|
12
|
+
env: process.env
|
|
13
|
+
});
|
|
14
|
+
const [stdout, stderr, exitCode] = await Promise.all([collectOutput(child.stdout), collectOutput(child.stderr), child.exited]);
|
|
15
|
+
return { exitCode, stdout, stderr };
|
|
16
|
+
}
|
|
17
|
+
function parseCliVersion(cliName, output) {
|
|
18
|
+
const escapedName = cliName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19
|
+
const match = output.trim().match(new RegExp(`(?:^|\\n)${escapedName}\\s+(\\S+)`));
|
|
20
|
+
return match?.[1]?.trim() || null;
|
|
21
|
+
}
|
|
22
|
+
function readInstalledCliVersion(cliName) {
|
|
23
|
+
try {
|
|
24
|
+
const result = Bun.spawnSync([cliName, "--version"], {
|
|
25
|
+
stdout: "pipe",
|
|
26
|
+
stderr: "pipe",
|
|
27
|
+
env: process.env
|
|
28
|
+
});
|
|
29
|
+
if (result.exitCode !== 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return parseCliVersion(cliName, Buffer.from(result.stdout).toString("utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function shellQuote(value) {
|
|
38
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
39
|
+
}
|
|
40
|
+
function getManualCliUpdateCommand(input) {
|
|
41
|
+
return `npm install -g --omit=dev ${shellQuote(input.workerPackageSpec)} ${shellQuote(input.r5dctlPackageSpec)}`;
|
|
42
|
+
}
|
|
43
|
+
function commandErrorDetail(result) {
|
|
44
|
+
const detail = `${result.stderr.trim()}${result.stderr.trim() && result.stdout.trim() ? "\n" : ""}${result.stdout.trim()}`.trim();
|
|
45
|
+
if (!detail) {
|
|
46
|
+
return `exit code ${result.exitCode}`;
|
|
47
|
+
}
|
|
48
|
+
return detail.slice(-UPDATE_OUTPUT_LIMIT);
|
|
49
|
+
}
|
|
50
|
+
async function readVerifiedVersion(cliName, targetVersion, runner) {
|
|
51
|
+
const result = await runner([cliName, "--version"]);
|
|
52
|
+
const installedVersion = result.exitCode === 0 ? parseCliVersion(cliName, result.stdout) : null;
|
|
53
|
+
if (!installedVersion) {
|
|
54
|
+
throw new Error(`${cliName} was installed but is not runnable: ${commandErrorDetail(result)}`);
|
|
55
|
+
}
|
|
56
|
+
if (installedVersion !== targetVersion) {
|
|
57
|
+
throw new Error(`${cliName} ${installedVersion} was installed; expected ${targetVersion}`);
|
|
58
|
+
}
|
|
59
|
+
return installedVersion;
|
|
60
|
+
}
|
|
61
|
+
async function installCliUpdate(input, runner = runCommand) {
|
|
62
|
+
const installArgs = ["npm", "install", "-g", "--omit=dev", input.workerPackageSpec, input.r5dctlPackageSpec];
|
|
63
|
+
const result = await runner(installArgs);
|
|
64
|
+
if (result.exitCode !== 0) {
|
|
65
|
+
throw new Error(`CLI update failed: ${commandErrorDetail(result)}
|
|
66
|
+
Run manually: ${getManualCliUpdateCommand(input)}`);
|
|
67
|
+
}
|
|
68
|
+
const [installedWorkerVersion, installedR5dctlVersion] = await Promise.all([
|
|
69
|
+
readVerifiedVersion("r5d-worker", input.targetWorkerVersion, runner),
|
|
70
|
+
readVerifiedVersion("r5dctl", input.targetR5dctlVersion, runner)
|
|
71
|
+
]);
|
|
72
|
+
return { installedWorkerVersion, installedR5dctlVersion };
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
getManualCliUpdateCommand,
|
|
76
|
+
installCliUpdate,
|
|
77
|
+
parseCliVersion,
|
|
78
|
+
readInstalledCliVersion,
|
|
79
|
+
runCommand
|
|
80
|
+
};
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -4,8 +4,11 @@ import path from "node:path";
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import os, { hostname } from "node:os";
|
|
6
6
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
7
|
+
import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
|
|
7
8
|
import { configureVisibleGitIdentity } from "./git-identity.mjs";
|
|
8
9
|
import { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
|
|
10
|
+
import { terminateProcessTree } from "./process-tree.mjs";
|
|
11
|
+
import { superviseWorkerRuntime, WORKER_RELOAD_EXIT_CODE, WORKER_RUNTIME_ENV } from "./supervisor.mjs";
|
|
9
12
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
10
13
|
const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
11
14
|
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
@@ -1772,6 +1775,7 @@ async function executeCommand(input) {
|
|
|
1772
1775
|
cwd,
|
|
1773
1776
|
stdout: "pipe",
|
|
1774
1777
|
stderr: "pipe",
|
|
1778
|
+
detached: true,
|
|
1775
1779
|
env: {
|
|
1776
1780
|
...process.env,
|
|
1777
1781
|
...containerRegistryEnv(),
|
|
@@ -1793,7 +1797,12 @@ async function executeCommand(input) {
|
|
|
1793
1797
|
});
|
|
1794
1798
|
if (input.message.timeoutMs) {
|
|
1795
1799
|
timeout = setTimeout(() => {
|
|
1796
|
-
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
|
+
});
|
|
1797
1806
|
}, input.message.timeoutMs);
|
|
1798
1807
|
}
|
|
1799
1808
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
@@ -1829,6 +1838,8 @@ async function executeCommand(input) {
|
|
|
1829
1838
|
async function executeStreamingCommand(input) {
|
|
1830
1839
|
const startedAt = Date.now();
|
|
1831
1840
|
let started = false;
|
|
1841
|
+
let timedOut = false;
|
|
1842
|
+
let timeout;
|
|
1832
1843
|
try {
|
|
1833
1844
|
if (cancelledProcessRuns.delete(input.message.runId)) {
|
|
1834
1845
|
sendWorkerMessage(input.ws, {
|
|
@@ -1869,6 +1880,7 @@ async function executeStreamingCommand(input) {
|
|
|
1869
1880
|
cwd,
|
|
1870
1881
|
stdout: "pipe",
|
|
1871
1882
|
stderr: "pipe",
|
|
1883
|
+
detached: true,
|
|
1872
1884
|
env: {
|
|
1873
1885
|
...process.env,
|
|
1874
1886
|
...containerRegistryEnv(),
|
|
@@ -1895,6 +1907,17 @@ async function executeStreamingCommand(input) {
|
|
|
1895
1907
|
requestId: input.message.requestId,
|
|
1896
1908
|
runId: input.message.runId
|
|
1897
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
|
+
}
|
|
1898
1921
|
const [exitCode] = await Promise.all([
|
|
1899
1922
|
subprocess.exited,
|
|
1900
1923
|
streamCommandOutput(subprocess.stdout, (data) => {
|
|
@@ -1918,7 +1941,8 @@ async function executeStreamingCommand(input) {
|
|
|
1918
1941
|
type: "exec_exit",
|
|
1919
1942
|
runId: input.message.runId,
|
|
1920
1943
|
exitCode,
|
|
1921
|
-
durationMs: Date.now() - startedAt
|
|
1944
|
+
durationMs: Date.now() - startedAt,
|
|
1945
|
+
...timedOut ? { timedOut: true } : {}
|
|
1922
1946
|
});
|
|
1923
1947
|
} catch (error) {
|
|
1924
1948
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1938,6 +1962,9 @@ async function executeStreamingCommand(input) {
|
|
|
1938
1962
|
});
|
|
1939
1963
|
}
|
|
1940
1964
|
} finally {
|
|
1965
|
+
if (timeout) {
|
|
1966
|
+
clearTimeout(timeout);
|
|
1967
|
+
}
|
|
1941
1968
|
activeProcesses.delete(input.message.runId);
|
|
1942
1969
|
cancelledProcessRuns.delete(input.message.runId);
|
|
1943
1970
|
}
|
|
@@ -2315,6 +2342,9 @@ async function startWorker(options) {
|
|
|
2315
2342
|
fs.mkdirSync(planRoot, { recursive: true });
|
|
2316
2343
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
2317
2344
|
let repositorySyncQueue = Promise.resolve();
|
|
2345
|
+
let repositorySyncInProgress = false;
|
|
2346
|
+
let cliUpdateInProgress = false;
|
|
2347
|
+
let reloadAfterClose = false;
|
|
2318
2348
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
2319
2349
|
headers: {
|
|
2320
2350
|
Authorization: `Bearer ${token}`
|
|
@@ -2348,6 +2378,10 @@ async function startWorker(options) {
|
|
|
2348
2378
|
arch: process.arch,
|
|
2349
2379
|
pid: process.pid,
|
|
2350
2380
|
version: getWorkerVersion(),
|
|
2381
|
+
r5dctlVersion: readInstalledCliVersion("r5dctl"),
|
|
2382
|
+
capabilities: {
|
|
2383
|
+
updateClis: true
|
|
2384
|
+
},
|
|
2351
2385
|
projectRoot: projectsRoot,
|
|
2352
2386
|
artifactRoot,
|
|
2353
2387
|
planRoot
|
|
@@ -2375,32 +2409,49 @@ async function startWorker(options) {
|
|
|
2375
2409
|
return;
|
|
2376
2410
|
}
|
|
2377
2411
|
if (message.type === "sync_projects") {
|
|
2412
|
+
if (cliUpdateInProgress) {
|
|
2413
|
+
sendWorkerMessage(ws, {
|
|
2414
|
+
type: "sync_projects_result",
|
|
2415
|
+
requestId: message.requestId,
|
|
2416
|
+
result: {
|
|
2417
|
+
status: "blocked",
|
|
2418
|
+
results: [],
|
|
2419
|
+
blockedBy: []
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2378
2424
|
let syncResult = { status: "completed", results: [], blockedBy: [] };
|
|
2379
2425
|
const executeSync = async () => {
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2426
|
+
repositorySyncInProgress = true;
|
|
2427
|
+
try {
|
|
2428
|
+
syncResult = await syncManifestProjectsFromInternal({
|
|
2429
|
+
baseUrl,
|
|
2430
|
+
token,
|
|
2431
|
+
projectsRoot,
|
|
2432
|
+
syncRoot,
|
|
2433
|
+
manifests: [...manifestByProjectId.values()],
|
|
2434
|
+
projectIds: message.projectIds
|
|
2435
|
+
});
|
|
2436
|
+
for (const result of syncResult.results) {
|
|
2437
|
+
if (result.status !== "synced") continue;
|
|
2438
|
+
try {
|
|
2439
|
+
await syncProjectPlans({
|
|
2440
|
+
baseUrl,
|
|
2441
|
+
token,
|
|
2442
|
+
projectId: result.projectId,
|
|
2443
|
+
branchName: result.branchName,
|
|
2444
|
+
planRoot
|
|
2445
|
+
});
|
|
2446
|
+
} catch (error) {
|
|
2447
|
+
process.stderr.write(
|
|
2448
|
+
`[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
2401
2449
|
`
|
|
2402
|
-
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2403
2452
|
}
|
|
2453
|
+
} finally {
|
|
2454
|
+
repositorySyncInProgress = false;
|
|
2404
2455
|
}
|
|
2405
2456
|
};
|
|
2406
2457
|
const queued = repositorySyncQueue.then(executeSync, executeSync);
|
|
@@ -2432,6 +2483,43 @@ async function startWorker(options) {
|
|
|
2432
2483
|
});
|
|
2433
2484
|
return;
|
|
2434
2485
|
}
|
|
2486
|
+
if (message.type === "update_clis") {
|
|
2487
|
+
if (cliUpdateInProgress) {
|
|
2488
|
+
sendWorkerMessage(ws, {
|
|
2489
|
+
type: "update_clis_result",
|
|
2490
|
+
requestId: message.requestId,
|
|
2491
|
+
error: "A CLI update is already in progress"
|
|
2492
|
+
});
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
if (activeProcesses.size > 0 || activePtys.size > 0 || repositorySyncInProgress) {
|
|
2496
|
+
sendWorkerMessage(ws, {
|
|
2497
|
+
type: "update_clis_result",
|
|
2498
|
+
requestId: message.requestId,
|
|
2499
|
+
error: "Worker is busy. Stop active commands and shells, then retry the CLI update."
|
|
2500
|
+
});
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
cliUpdateInProgress = true;
|
|
2504
|
+
try {
|
|
2505
|
+
const result = await installCliUpdate(message);
|
|
2506
|
+
sendWorkerMessage(ws, {
|
|
2507
|
+
type: "update_clis_result",
|
|
2508
|
+
requestId: message.requestId,
|
|
2509
|
+
...result
|
|
2510
|
+
});
|
|
2511
|
+
reloadAfterClose = true;
|
|
2512
|
+
ws.close(1e3, "Activating updated CLIs");
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
cliUpdateInProgress = false;
|
|
2515
|
+
sendWorkerMessage(ws, {
|
|
2516
|
+
type: "update_clis_result",
|
|
2517
|
+
requestId: message.requestId,
|
|
2518
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2435
2523
|
if (message.type === "ping") {
|
|
2436
2524
|
lastServerHeartbeatAt = Date.now();
|
|
2437
2525
|
ws.send(JSON.stringify({ type: "pong" }));
|
|
@@ -2439,23 +2527,41 @@ async function startWorker(options) {
|
|
|
2439
2527
|
}
|
|
2440
2528
|
if (message.type === "cancel") {
|
|
2441
2529
|
const active = activeProcesses.get(message.runId);
|
|
2530
|
+
let cancelled = true;
|
|
2531
|
+
let cancelMessage;
|
|
2442
2532
|
if (active) {
|
|
2443
|
-
|
|
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
|
+
}
|
|
2444
2540
|
} else {
|
|
2445
2541
|
cancelledProcessRuns.add(message.runId);
|
|
2542
|
+
cancelMessage = "Cancellation queued before command start";
|
|
2446
2543
|
}
|
|
2447
2544
|
ws.send(
|
|
2448
2545
|
JSON.stringify({
|
|
2449
2546
|
type: "cancel_result",
|
|
2450
2547
|
requestId: message.requestId,
|
|
2451
2548
|
runId: message.runId,
|
|
2452
|
-
cancelled
|
|
2453
|
-
message:
|
|
2549
|
+
cancelled,
|
|
2550
|
+
message: cancelMessage
|
|
2454
2551
|
})
|
|
2455
2552
|
);
|
|
2456
2553
|
return;
|
|
2457
2554
|
}
|
|
2458
2555
|
if (message.type === "pty_open") {
|
|
2556
|
+
if (cliUpdateInProgress) {
|
|
2557
|
+
sendWorkerMessage(ws, {
|
|
2558
|
+
type: "pty_error",
|
|
2559
|
+
requestId: message.requestId,
|
|
2560
|
+
ptyId: message.ptyId,
|
|
2561
|
+
error: "Worker is activating a CLI update"
|
|
2562
|
+
});
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2459
2565
|
await repositorySyncQueue;
|
|
2460
2566
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2461
2567
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
@@ -2477,6 +2583,16 @@ async function startWorker(options) {
|
|
|
2477
2583
|
return;
|
|
2478
2584
|
}
|
|
2479
2585
|
if (message.type === "exec") {
|
|
2586
|
+
if (cliUpdateInProgress) {
|
|
2587
|
+
sendWorkerMessage(ws, {
|
|
2588
|
+
type: "exec_result",
|
|
2589
|
+
requestId: message.requestId,
|
|
2590
|
+
stdout: "",
|
|
2591
|
+
stderr: "Worker is activating a CLI update",
|
|
2592
|
+
exitCode: 1
|
|
2593
|
+
});
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2480
2596
|
await repositorySyncQueue;
|
|
2481
2597
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2482
2598
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
@@ -2497,6 +2613,15 @@ async function startWorker(options) {
|
|
|
2497
2613
|
return;
|
|
2498
2614
|
}
|
|
2499
2615
|
if (message.type === "exec_start") {
|
|
2616
|
+
if (cliUpdateInProgress) {
|
|
2617
|
+
sendWorkerMessage(ws, {
|
|
2618
|
+
type: "exec_start_error",
|
|
2619
|
+
requestId: message.requestId,
|
|
2620
|
+
runId: message.runId,
|
|
2621
|
+
error: "Worker is activating a CLI update"
|
|
2622
|
+
});
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2500
2625
|
await repositorySyncQueue;
|
|
2501
2626
|
sendWorkerMessage(ws, {
|
|
2502
2627
|
type: "exec_accepted",
|
|
@@ -2594,6 +2719,9 @@ async function startWorker(options) {
|
|
|
2594
2719
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
2595
2720
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
2596
2721
|
`);
|
|
2722
|
+
if (reloadAfterClose) {
|
|
2723
|
+
process.exit(WORKER_RELOAD_EXIT_CODE);
|
|
2724
|
+
}
|
|
2597
2725
|
if (activeProcesses.size > 0) {
|
|
2598
2726
|
const delayMs = 2e3;
|
|
2599
2727
|
process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
|
|
@@ -2632,7 +2760,15 @@ async function main() {
|
|
|
2632
2760
|
printVersion();
|
|
2633
2761
|
return;
|
|
2634
2762
|
}
|
|
2635
|
-
|
|
2763
|
+
if (process.env[WORKER_RUNTIME_ENV] === "1") {
|
|
2764
|
+
delete process.env[WORKER_RUNTIME_ENV];
|
|
2765
|
+
await startWorker(parsed.options);
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
const exitCode = await superviseWorkerRuntime(process.argv.slice(2));
|
|
2769
|
+
if (exitCode !== 0) {
|
|
2770
|
+
process.exit(exitCode);
|
|
2771
|
+
}
|
|
2636
2772
|
}
|
|
2637
2773
|
function isCliEntrypoint() {
|
|
2638
2774
|
if (import.meta.main) {
|
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,32 @@
|
|
|
1
|
+
const WORKER_RUNTIME_ENV = "R5D_WORKER_INTERNAL_RUNTIME";
|
|
2
|
+
const WORKER_RELOAD_EXIT_CODE = 75;
|
|
3
|
+
function defaultRuntimeSpawner(argv, options) {
|
|
4
|
+
return Bun.spawn(argv, options);
|
|
5
|
+
}
|
|
6
|
+
async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner) {
|
|
7
|
+
if (!process.argv[1]) {
|
|
8
|
+
throw new Error("Cannot locate the r5d-worker entrypoint");
|
|
9
|
+
}
|
|
10
|
+
while (true) {
|
|
11
|
+
const runtime = spawnRuntime([process.execPath, process.argv[1], ...argv], {
|
|
12
|
+
stdin: "inherit",
|
|
13
|
+
stdout: "inherit",
|
|
14
|
+
stderr: "inherit",
|
|
15
|
+
env: {
|
|
16
|
+
...env,
|
|
17
|
+
[WORKER_RUNTIME_ENV]: "1"
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const exitCode = await runtime.exited;
|
|
21
|
+
if (exitCode === WORKER_RELOAD_EXIT_CODE) {
|
|
22
|
+
process.stdout.write("[r5d-worker] activating updated worker runtime\n");
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
return exitCode;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
WORKER_RELOAD_EXIT_CODE,
|
|
30
|
+
WORKER_RUNTIME_ENV,
|
|
31
|
+
superviseWorkerRuntime
|
|
32
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type CommandResult = {
|
|
2
|
+
exitCode: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
};
|
|
6
|
+
export type CommandRunner = (argv: string[]) => Promise<CommandResult>;
|
|
7
|
+
export type CliUpdateRequest = {
|
|
8
|
+
workerPackageSpec: string;
|
|
9
|
+
r5dctlPackageSpec: string;
|
|
10
|
+
targetWorkerVersion: string;
|
|
11
|
+
targetR5dctlVersion: string;
|
|
12
|
+
};
|
|
13
|
+
export type CliUpdateResult = {
|
|
14
|
+
installedWorkerVersion: string;
|
|
15
|
+
installedR5dctlVersion: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function runCommand(argv: string[]): Promise<CommandResult>;
|
|
18
|
+
export declare function parseCliVersion(cliName: "r5d-worker" | "r5dctl", output: string): string | null;
|
|
19
|
+
export declare function readInstalledCliVersion(cliName: "r5d-worker" | "r5dctl"): string | null;
|
|
20
|
+
export declare function getManualCliUpdateCommand(input: Pick<CliUpdateRequest, "workerPackageSpec" | "r5dctlPackageSpec">): string;
|
|
21
|
+
export declare function installCliUpdate(input: CliUpdateRequest, runner?: CommandRunner): Promise<CliUpdateResult>;
|
|
@@ -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 {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const WORKER_RUNTIME_ENV = "R5D_WORKER_INTERNAL_RUNTIME";
|
|
2
|
+
export declare const WORKER_RELOAD_EXIT_CODE = 75;
|
|
3
|
+
type RuntimeProcess = {
|
|
4
|
+
exited: Promise<number>;
|
|
5
|
+
};
|
|
6
|
+
export type RuntimeSpawner = (argv: string[], options: {
|
|
7
|
+
stdin: "inherit";
|
|
8
|
+
stdout: "inherit";
|
|
9
|
+
stderr: "inherit";
|
|
10
|
+
env: Record<string, string | undefined>;
|
|
11
|
+
}) => RuntimeProcess;
|
|
12
|
+
export declare function superviseWorkerRuntime(argv: string[], env?: Record<string, string | undefined>, spawnRuntime?: RuntimeSpawner): Promise<number>;
|
|
13
|
+
export {};
|