@platless/axom-cli 0.3.13 → 0.5.9
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/axom-patch/cli/README.md +30 -8
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
- package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
- package/dist/axom-patch/cli/qoder-steering.json +15 -15
- package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
- package/dist/axom-patch/cli/runtime.json +34 -8
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +33 -6
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2163 -611
- package/dist/workstation/acp/acpClient.js +60 -497
- package/dist/workstation/acp/adoption.js +10 -396
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +76 -160
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +20 -3
- package/dist/workstation/acp/permission.js +42 -10
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +1 -1
- package/dist/workstation/acp/registry/catalog.js +154 -32
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +16 -42
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1324 -474
- package/dist/workstation/channels/agent.js +7 -5
- package/dist/workstation/channels/computer.js +69 -21
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +84 -4
- package/dist/workstation/daemon.js +288 -7
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/{fileOps.js → files/text.js} +117 -38
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +48 -4
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
- package/dist/workstation/qoderAdapter/history.js +1 -3
- package/dist/workstation/qoderAdapter/main.js +4 -2
- package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +1 -1
- package/dist/workstation/qoderAdapter/subwork.js +15 -7
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/client.js +9 -1
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +15 -5
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +37 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/package.json +10 -7
- package/dist/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/sessionScope.js +0 -19
- package/dist/workstation/filePaths.js +0 -174
- package/dist/workstation/fileWorker.js +0 -81
- package/dist/workstation/fileWorkerClient.js +0 -121
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Daemon-side: the legacy tools keeper (`axom-tools-keeper-<id>`, tools/keeperProcess.ts from CLIs before keeper-terminal) that
|
|
2
|
+
// may still hold live PTYs on an upgraded machine. Spec §9, analysis C §2.3. The daemon serves those PTYs until they end, then
|
|
3
|
+
// stops the keeper; nothing here can start one.
|
|
4
|
+
//
|
|
5
|
+
// Read-only discovery: the old socket and pid manifest (tools/client.ts `keeperPaths(…, 'terminal')`), `keeper.ping` with an
|
|
6
|
+
// identity check, `terminal.list`. It speaks the old one-request-per-connection protocol through `keeperRpc` only and NEVER
|
|
7
|
+
// through `ToolsClient('terminal').request`, which lazily starts that keeper. It never unlinks anything: a keeper that does not
|
|
8
|
+
// answer a ping while its manifest pid is alive is busy, and is asked again later.
|
|
9
|
+
import { execFile, spawn } from 'node:child_process';
|
|
10
|
+
import { promises as fs } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { keeperPaths, keeperRpc } from '../tools/client.js';
|
|
13
|
+
import { pidAlive } from './kit/lock.js';
|
|
14
|
+
/** A legacy keeper with no open PTY is stopped once nothing was routed to it for this long (the user may still read an exited one). */
|
|
15
|
+
export const LEGACY_IDLE_MS = 10 * 60_000;
|
|
16
|
+
/**
|
|
17
|
+
* Bound on one router-side lookup (probe + `terminal.list`), well under the COMPUTER channel's 7.5 s tool budget: a hung legacy
|
|
18
|
+
* keeper must not time out terminal.list / create / unknown-id requests, which would leave the terminal tab unusable.
|
|
19
|
+
*/
|
|
20
|
+
export const LEGACY_LOOKUP_MS = 1500;
|
|
21
|
+
/** Today's wording when no keeper holds a session id. */
|
|
22
|
+
export const TERMINAL_NOT_FOUND = 'Terminal session not found; keeper restart loses live PTYs';
|
|
23
|
+
function runSystemctl(args, timeoutMs) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const child = spawn('systemctl', ['--user', ...args], { stdio: 'ignore' });
|
|
26
|
+
let done = false;
|
|
27
|
+
const finish = (ok) => { if (done)
|
|
28
|
+
return; done = true; clearTimeout(timer); resolve(ok); };
|
|
29
|
+
const timer = setTimeout(() => { child.kill(); finish(false); }, timeoutMs);
|
|
30
|
+
child.on('error', () => finish(false));
|
|
31
|
+
child.on('exit', (code) => finish(code === 0));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export const userSystemd = {
|
|
35
|
+
available: () => runSystemctl(['show-environment'], 1000),
|
|
36
|
+
isActive: (unit) => runSystemctl(['is-active', '--quiet', unit], 2000),
|
|
37
|
+
stop: (unit) => runSystemctl(['stop', unit], 15_000),
|
|
38
|
+
};
|
|
39
|
+
function psCommandLine(pid) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
execFile('ps', ['-ww', '-o', 'command=', '-p', String(pid)], { timeout: 2000 }, (error, stdout) => resolve(error ? null : stdout.trim() || null));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
|
+
export class LegacyToolsKeeper {
|
|
46
|
+
options;
|
|
47
|
+
paths;
|
|
48
|
+
current = 'unknown';
|
|
49
|
+
pid;
|
|
50
|
+
open = null;
|
|
51
|
+
lastTrafficAt = 0;
|
|
52
|
+
probing;
|
|
53
|
+
ids = null;
|
|
54
|
+
listed = [];
|
|
55
|
+
retiring;
|
|
56
|
+
now;
|
|
57
|
+
rpc;
|
|
58
|
+
constructor(options) {
|
|
59
|
+
this.options = options;
|
|
60
|
+
this.paths = options.paths ?? keeperPaths({ ...options.tools }, undefined, undefined, 'terminal');
|
|
61
|
+
this.now = options.now ?? Date.now;
|
|
62
|
+
this.rpc = options.rpc ?? keeperRpc;
|
|
63
|
+
}
|
|
64
|
+
get state() { return this.current; }
|
|
65
|
+
/** Present or busy: it may hold sessions, so the router asks it. */
|
|
66
|
+
get live() { return this.current === 'present' || this.current === 'busy'; }
|
|
67
|
+
/**
|
|
68
|
+
* Ids of the last successful list (null until one succeeded). The legacy keeper gains no session after the upgrade (every create
|
|
69
|
+
* goes to keeper-terminal except a same-id retry), so this answers "is it one of its sessions?" even while it is busy or hung.
|
|
70
|
+
*/
|
|
71
|
+
get knownIds() { return this.ids; }
|
|
72
|
+
/** The sessions of the last successful list (for the heartbeat's items; no IO). Empty once it is gone or retired. */
|
|
73
|
+
lastSessions() { return this.live ? this.listed : []; }
|
|
74
|
+
/** For the heartbeat: state, pid and the number of non-exited sessions at the last successful list (null = unknown). */
|
|
75
|
+
status() {
|
|
76
|
+
return { state: this.current, pid: this.pid ?? null, open: this.live ? this.open : 0 };
|
|
77
|
+
}
|
|
78
|
+
log(line) { this.options.log?.(`[legacy-tools-keeper] ${line}`); }
|
|
79
|
+
async ping(timeoutMs = this.options.probeTimeoutMs ?? 1000) {
|
|
80
|
+
try {
|
|
81
|
+
const reply = await this.rpc(this.paths.socket, 'keeper.ping', {}, timeoutMs);
|
|
82
|
+
return reply.identity === this.paths.unit && Number.isSafeInteger(reply.pid) && reply.pid > 0 ? reply.pid : null;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** The pid of a live process the manifest names under our identity, else null. Reads only. */
|
|
89
|
+
async manifestPid() {
|
|
90
|
+
try {
|
|
91
|
+
const stat = await fs.lstat(this.paths.pidFile);
|
|
92
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096)
|
|
93
|
+
return null;
|
|
94
|
+
const value = JSON.parse(await fs.readFile(this.paths.pidFile, 'utf8'));
|
|
95
|
+
if (value.identity !== this.paths.unit || !Number.isSafeInteger(value.pid))
|
|
96
|
+
return null;
|
|
97
|
+
return pidAlive(value.pid) ? value.pid : null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Probes (single-flight) unless the answer is final. A keeper found for the first time starts its idle window now, so a
|
|
105
|
+
* daemon restart never shortens the time a user has to look at an exited session's output.
|
|
106
|
+
*/
|
|
107
|
+
detect(timeoutMs) {
|
|
108
|
+
if (this.current === 'absent' || this.current === 'retired' || this.current === 'stopping')
|
|
109
|
+
return Promise.resolve(this.current);
|
|
110
|
+
this.probing ??= (async () => {
|
|
111
|
+
const pid = await this.ping(timeoutMs);
|
|
112
|
+
if (this.current === 'stopping' || this.current === 'retired')
|
|
113
|
+
return this.current;
|
|
114
|
+
if (pid !== null) {
|
|
115
|
+
if (this.current !== 'present' && this.current !== 'busy') {
|
|
116
|
+
this.lastTrafficAt = this.now();
|
|
117
|
+
this.log(`found pid ${pid}`);
|
|
118
|
+
}
|
|
119
|
+
this.pid = pid;
|
|
120
|
+
this.current = 'present';
|
|
121
|
+
}
|
|
122
|
+
else if (await this.manifestPid() !== null) {
|
|
123
|
+
if (this.current === 'unknown')
|
|
124
|
+
this.lastTrafficAt = this.now();
|
|
125
|
+
this.current = 'busy';
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (this.current !== 'unknown')
|
|
129
|
+
this.log('gone');
|
|
130
|
+
this.current = 'absent';
|
|
131
|
+
this.open = 0;
|
|
132
|
+
}
|
|
133
|
+
return this.current;
|
|
134
|
+
})().finally(() => { this.probing = undefined; });
|
|
135
|
+
return this.probing;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* `terminal.list` of the legacy keeper, or null when it is not there or did not answer now. Not routed traffic. Bounded by
|
|
139
|
+
* `budgetMs` in total (probe included). Only a keeper that answered its last probe is listed: a busy one gives null at once
|
|
140
|
+
* (the periodic tick re-probes it), and a list that fails or times out sends it back to probing in the background.
|
|
141
|
+
*/
|
|
142
|
+
async sessions(budgetMs = LEGACY_LOOKUP_MS) {
|
|
143
|
+
const deadline = Date.now() + budgetMs;
|
|
144
|
+
if (this.current === 'unknown')
|
|
145
|
+
await this.detect(Math.min(this.options.probeTimeoutMs ?? 1000, Math.floor(budgetMs / 2)));
|
|
146
|
+
if (this.current !== 'present')
|
|
147
|
+
return null;
|
|
148
|
+
try {
|
|
149
|
+
const reply = await this.rpc(this.paths.socket, 'terminal.list', {}, Math.max(1, deadline - Date.now()));
|
|
150
|
+
const sessions = Array.isArray(reply.sessions) ? reply.sessions : [];
|
|
151
|
+
this.open = sessions.filter((session) => !session.exited).length;
|
|
152
|
+
this.ids = new Set(sessions.map((session) => session.id));
|
|
153
|
+
this.listed = sessions;
|
|
154
|
+
return sessions;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
void this.detect();
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** One routed `terminal.*` request, sent exactly once (no retry: input may have been delivered even if the reply was lost). */
|
|
162
|
+
async request(method, params) {
|
|
163
|
+
if (!this.live)
|
|
164
|
+
throw new Error(TERMINAL_NOT_FOUND);
|
|
165
|
+
this.lastTrafficAt = this.now();
|
|
166
|
+
try {
|
|
167
|
+
const result = await this.rpc(this.paths.socket, method, params, this.options.requestTimeoutMs ?? 7000);
|
|
168
|
+
if (method === 'terminal.close' && typeof params.id === 'string')
|
|
169
|
+
this.ids?.delete(params.id);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
void this.detect(); // a failed connection (or a keeper that went away mid-request) shows up on the next probe
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Retirement check (call periodically): once no session is open (only exited ones, or none) and nothing was routed to it for
|
|
179
|
+
* the idle window, stop it. Also re-probes a busy keeper.
|
|
180
|
+
*/
|
|
181
|
+
async tick() {
|
|
182
|
+
if (this.retiring)
|
|
183
|
+
return this.retiring;
|
|
184
|
+
if (await this.detect() !== 'present')
|
|
185
|
+
return;
|
|
186
|
+
const sessions = await this.sessions(this.options.requestTimeoutMs ?? 7000);
|
|
187
|
+
if (!sessions)
|
|
188
|
+
return;
|
|
189
|
+
const idle = () => this.open === 0 && this.now() - this.lastTrafficAt >= (this.options.idleMs ?? LEGACY_IDLE_MS);
|
|
190
|
+
if (idle())
|
|
191
|
+
await this.retire('idle, no open terminal', idle);
|
|
192
|
+
}
|
|
193
|
+
/** Stops it now whatever is open (the management page's "replace now"). Returns the open sessions that end with it. */
|
|
194
|
+
async replaceNow() {
|
|
195
|
+
if (this.retiring) {
|
|
196
|
+
await this.retiring;
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
const state = await this.detect();
|
|
200
|
+
if (state !== 'present' && state !== 'busy')
|
|
201
|
+
return [];
|
|
202
|
+
const affected = (await this.sessions() ?? []).filter((session) => !session.exited);
|
|
203
|
+
await this.retire(`replaced now with ${affected.length} open terminal(s)`);
|
|
204
|
+
return affected;
|
|
205
|
+
}
|
|
206
|
+
retire(reason, stillWanted) {
|
|
207
|
+
this.retiring ??= this.stop(reason, stillWanted).finally(() => { this.retiring = undefined; });
|
|
208
|
+
return this.retiring;
|
|
209
|
+
}
|
|
210
|
+
async stop(reason, stillWanted) {
|
|
211
|
+
const previous = this.current;
|
|
212
|
+
this.current = 'stopping'; // from here on nothing more is routed to it
|
|
213
|
+
// A request routed between the check and now (it stamped the traffic time before `stopping`) cancels an idle stop.
|
|
214
|
+
if (stillWanted && !stillWanted()) {
|
|
215
|
+
this.current = previous;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
let pid = this.pid ?? null;
|
|
219
|
+
let stopped = false;
|
|
220
|
+
try {
|
|
221
|
+
const systemd = this.options.systemd === undefined ? (process.platform === 'linux' ? userSystemd : null) : this.options.systemd;
|
|
222
|
+
if (systemd && await systemd.available() && await systemd.isActive(this.paths.unit)) {
|
|
223
|
+
// SIGTERM through the unit: the old keeper exits 0, so `Restart=on-failure` does not bring it back.
|
|
224
|
+
this.log(`stopping unit ${this.paths.unit} (${reason})`);
|
|
225
|
+
stopped = await systemd.stop(this.paths.unit) && await this.gone(pid);
|
|
226
|
+
}
|
|
227
|
+
if (!stopped) {
|
|
228
|
+
// Kill only a process that is provably the keeper: the manifest names it under our identity AND it answers the ping
|
|
229
|
+
// as that same pid. Anything less (pid reuse, a stale manifest, a keeper too busy to answer) is left alone for now.
|
|
230
|
+
// The manifest may be gone while the keeper lives: macOS's daily /tmp cleaner deletes regular files not accessed for
|
|
231
|
+
// 3 days and leaves the socket. Only then is the process's own launch argument (socket + identity) the proof instead;
|
|
232
|
+
// a manifest that exists but names another pid or identity still refuses.
|
|
233
|
+
const [manifest, answered] = [await this.manifestPid(), await this.ping()];
|
|
234
|
+
if (answered === null || !(manifest !== null ? manifest === answered
|
|
235
|
+
: await this.manifestAbsent() && await this.launchedAsUs(answered))) {
|
|
236
|
+
this.log(`cannot verify the keeper process (manifest ${manifest}, ping ${answered}); retrying later`);
|
|
237
|
+
this.current = previous === 'busy' ? 'busy' : 'present';
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
pid = answered;
|
|
241
|
+
this.log(`SIGTERM pid ${pid} (${reason})`);
|
|
242
|
+
(this.options.signal ?? ((target, signal) => process.kill(target, signal)))(pid, 'SIGTERM');
|
|
243
|
+
stopped = await this.gone(pid);
|
|
244
|
+
}
|
|
245
|
+
if (!stopped) {
|
|
246
|
+
this.log(`pid ${pid} did not exit; retrying later`);
|
|
247
|
+
this.current = 'present';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.current = 'retired';
|
|
251
|
+
this.open = 0;
|
|
252
|
+
this.log('retired');
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
this.log(`stop failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
256
|
+
if (this.current === 'stopping')
|
|
257
|
+
this.current = 'present';
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (this.options.recoverUploads) {
|
|
261
|
+
// Its own stop() already cleaned the uploads it knew about; this catches what an earlier crash left behind.
|
|
262
|
+
await this.options.recoverUploads(path.join(this.paths.dir, 'uploads')).catch((error) => this.log(`uploads recover failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
manifestAbsent() {
|
|
266
|
+
return fs.lstat(this.paths.pidFile).then(() => false, (error) => error.code === 'ENOENT');
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* True when `pid` runs the old keeper with our launch config: the old ToolsClient passed it as the last argument, base64 JSON
|
|
270
|
+
* naming this socket and identity. Reads only.
|
|
271
|
+
*/
|
|
272
|
+
async launchedAsUs(pid) {
|
|
273
|
+
const command = await (this.options.commandLine ?? psCommandLine)(pid);
|
|
274
|
+
const last = command?.trim().split(/\s+/).at(-1);
|
|
275
|
+
if (!last)
|
|
276
|
+
return false;
|
|
277
|
+
try {
|
|
278
|
+
const config = JSON.parse(Buffer.from(last, 'base64').toString('utf8'));
|
|
279
|
+
return config.socket === this.paths.socket && config.identity === this.paths.unit;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Waits (bounded, wall clock) until `pid` is gone and the socket no longer answers as the keeper. */
|
|
286
|
+
async gone(pid) {
|
|
287
|
+
const deadline = Date.now() + (this.options.stopTimeoutMs ?? 10_000);
|
|
288
|
+
for (;;) {
|
|
289
|
+
if ((pid === null || !pidAlive(pid)) && await this.ping() === null)
|
|
290
|
+
return true;
|
|
291
|
+
if (Date.now() >= deadline)
|
|
292
|
+
return false;
|
|
293
|
+
await delay(50);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|