harnery 0.42.0 → 0.44.0
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 +3 -3
- package/dist/commander.d.ts +16 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +18 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +95 -52
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +38 -11
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +10 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +22 -7
- package/dist/commands/governor.js +22 -10
- package/dist/commands/inbox.d.ts +1 -1
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +55 -2
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +28 -4
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +16 -1
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +7 -2
- package/dist/commands/toc.d.ts +14 -0
- package/dist/commands/toc.d.ts.map +1 -1
- package/dist/commands/toc.js +7 -4
- package/dist/commands/tunnel.d.ts +65 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +290 -101
- package/dist/commands/web-fetch.d.ts +3 -2
- package/dist/commands/web-fetch.d.ts.map +1 -1
- package/dist/commands/web-fetch.js +25 -19
- package/dist/commands/web.d.ts +7 -1
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +9 -3
- package/dist/commands/workflow.js +24 -30
- package/dist/core/agents/live-authority-v3.d.ts +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +28 -7
- package/dist/core/agents/mailbox.d.ts +9 -8
- package/dist/core/agents/mailbox.d.ts.map +1 -1
- package/dist/core/agents/mailbox.js +24 -17
- package/dist/core/agents/render/prompt-context.d.ts +2 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +19 -17
- package/dist/core/agents/render/session-context.d.ts +2 -8
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +64 -43
- package/dist/core/agents/rules/stop-hook.js +3 -3
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +25 -14
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/agents/state/names.d.ts +43 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +87 -3
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +5 -0
- package/dist/core/agents/state/proc-start.d.ts +19 -0
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +180 -70
- package/dist/core/agents/state/unchanged-file.d.ts +44 -0
- package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
- package/dist/core/agents/state/unchanged-file.js +390 -0
- package/dist/core/context/index.d.ts +22 -10
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +381 -119
- package/dist/core/context/observation.d.ts +26 -0
- package/dist/core/context/observation.d.ts.map +1 -0
- package/dist/core/context/observation.js +11 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +25 -14
- package/dist/core/diagnostics/path-identity.d.ts +3 -0
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
- package/dist/core/diagnostics/path-identity.js +12 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
- package/dist/core/diagnostics/sanitize.js +4 -3
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +29 -0
- package/dist/core/events/v3/generation-custody.d.ts +5 -0
- package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
- package/dist/core/events/v3/generation-custody.js +50 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +77 -64
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +14 -1
- package/dist/core/governor/service-read.d.ts +10 -2
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +105 -22
- package/dist/core/governor/service.d.ts +20 -2
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +72 -47
- package/dist/core/hooks/cli.js +120 -23
- package/dist/core/hooks/fork-lineage.d.ts +40 -0
- package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
- package/dist/core/hooks/fork-lineage.js +62 -0
- package/dist/core/hooks/resolve/transcript.d.ts +16 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +84 -0
- package/dist/core/hooks/shell-rm.d.ts +2 -0
- package/dist/core/hooks/shell-rm.d.ts.map +1 -0
- package/dist/core/hooks/shell-rm.js +105 -0
- package/dist/core/hooks/shell-tokens.d.ts +12 -0
- package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
- package/dist/core/hooks/shell-tokens.js +97 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
- package/dist/core/hooks/shell-waiter.js +3 -93
- package/dist/core/inbox/contract.d.ts +2 -1
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/index.d.ts +2 -0
- package/dist/core/inbox/index.d.ts.map +1 -1
- package/dist/core/inbox/index.js +2 -0
- package/dist/core/inbox/peer-messages.d.ts +37 -0
- package/dist/core/inbox/peer-messages.d.ts.map +1 -0
- package/dist/core/inbox/peer-messages.js +72 -0
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +67 -21
- package/dist/core/inbox/targets.d.ts +22 -0
- package/dist/core/inbox/targets.d.ts.map +1 -0
- package/dist/core/inbox/targets.js +108 -0
- package/dist/core/inbox/watcher.js +7 -3
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +17 -2
- package/dist/core/resources/contract.d.ts +3 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/service-status.d.ts +14 -1
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +23 -13
- package/dist/core/resources/service.d.ts +9 -1
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +47 -27
- package/dist/core/resources/status.d.ts +2 -5
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +36 -7
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/servers/index.d.ts +24 -13
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +132 -92
- package/dist/core/servers/tunnels.d.ts +3 -5
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +3 -9
- package/dist/core/storage/process-log.d.ts +12 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +10 -2
- package/dist/core/supervisor/contract.d.ts +12 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.js +2 -1
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +54 -9
- package/dist/core/supervisor/service.d.ts +9 -1
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +59 -29
- package/dist/core/supervisor/services.d.ts.map +1 -1
- package/dist/core/supervisor/services.js +22 -1
- package/dist/core/supervisor/status.d.ts +17 -2
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +70 -12
- package/dist/core/supervisor/timeline.d.ts.map +1 -1
- package/dist/core/supervisor/timeline.js +2 -1
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +105 -32
- package/dist/core/workflow/failure-evidence.d.ts +16 -0
- package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
- package/dist/core/workflow/failure-evidence.js +114 -0
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -1
- package/dist/core/workflow/live-session-v3.d.ts +6 -0
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +126 -5
- package/dist/core/workflow/proof.d.ts +5 -5
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +115 -20
- package/dist/core/workflow/run-state.d.ts +3 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +31 -27
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +19 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +13 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +19 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +14 -0
- package/dist/core/workflow/types.d.ts +64 -17
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.d.ts +0 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.js +0 -1
- package/dist/core/workflow/workspaces/leases.d.ts +3 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +30 -16
- package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +270 -6
- package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/ownership.js +194 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +14 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +21 -13
- package/dist/core/workflow/workspaces/state.d.ts +1 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +21 -1
- package/dist/lib/browser/windows-chrome.d.ts +100 -12
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +262 -73
- package/dist/lib/docs-links.d.ts.map +1 -1
- package/dist/lib/docs-links.js +45 -2
- package/dist/lib/guarded-remove.d.ts +7 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +106 -12
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -0
- package/dist/lib/tunnel/allowlist.d.ts +44 -0
- package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
- package/dist/lib/tunnel/allowlist.js +204 -0
- package/dist/lib/tunnel/current-address.d.ts +41 -0
- package/dist/lib/tunnel/current-address.d.ts.map +1 -0
- package/dist/lib/tunnel/current-address.js +123 -0
- package/dist/lib/tunnel/gate.js +13 -6
- package/dist/lib/tunnel/processes.d.ts +39 -0
- package/dist/lib/tunnel/processes.d.ts.map +1 -0
- package/dist/lib/tunnel/processes.js +121 -0
- package/dist/lib/tunnel/state.d.ts +28 -3
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +98 -27
- package/dist/lib/zip/index.d.ts +41 -0
- package/dist/lib/zip/index.d.ts.map +1 -0
- package/dist/lib/zip/index.js +398 -0
- package/package.json +8 -1
- package/src/commander.ts +32 -0
- package/src/commands/agents.ts +151 -74
- package/src/commands/browse.ts +50 -14
- package/src/commands/diagnostics.ts +10 -0
- package/src/commands/doctor.ts +26 -7
- package/src/commands/governor.ts +23 -10
- package/src/commands/inbox.ts +52 -2
- package/src/commands/rm.ts +43 -4
- package/src/commands/servers.ts +16 -4
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/tunnel.ts +394 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- package/src/commands/workflow.ts +30 -30
- package/src/core/agents/live-authority-v3.ts +17 -0
- package/src/core/agents/live-lifecycle-v3.ts +52 -14
- package/src/core/agents/mailbox.ts +26 -17
- package/src/core/agents/render/prompt-context.ts +35 -20
- package/src/core/agents/render/session-context.ts +92 -48
- package/src/core/agents/rules/stop-hook.ts +3 -3
- package/src/core/agents/state/heartbeat-writer.ts +29 -12
- package/src/core/agents/state/live-coordination-writer.ts +29 -0
- package/src/core/agents/state/names.ts +117 -6
- package/src/core/agents/state/pidmap.ts +13 -0
- package/src/core/agents/state/proc-start.ts +57 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- package/src/core/context/index.ts +446 -136
- package/src/core/context/observation.ts +29 -0
- package/src/core/diagnostics/bundle.ts +23 -18
- package/src/core/diagnostics/path-identity.ts +15 -0
- package/src/core/diagnostics/sanitize.ts +4 -3
- package/src/core/events/v3/authority-outbox.ts +56 -0
- package/src/core/events/v3/generation-custody.ts +59 -0
- package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
- package/src/core/events/v3/producers/recorder.ts +94 -68
- package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
- package/src/core/governor/planning.ts +20 -8
- package/src/core/governor/service-read.ts +128 -20
- package/src/core/governor/service.ts +119 -42
- package/src/core/hooks/cli.ts +139 -22
- package/src/core/hooks/fork-lineage.ts +82 -0
- package/src/core/hooks/resolve/transcript.ts +88 -0
- package/src/core/hooks/shell-rm.ts +103 -0
- package/src/core/hooks/shell-tokens.ts +96 -0
- package/src/core/hooks/shell-waiter.ts +4 -91
- package/src/core/inbox/contract.ts +2 -1
- package/src/core/inbox/index.ts +2 -0
- package/src/core/inbox/peer-messages.ts +94 -0
- package/src/core/inbox/service.ts +88 -22
- package/src/core/inbox/targets.ts +148 -0
- package/src/core/inbox/watcher.ts +8 -9
- package/src/core/journal/index.ts +19 -2
- package/src/core/resources/contract.ts +4 -0
- package/src/core/resources/service-status.ts +35 -12
- package/src/core/resources/service.ts +71 -24
- package/src/core/resources/status.ts +57 -8
- package/src/core/servers/index.ts +177 -91
- package/src/core/servers/tunnels.ts +3 -11
- package/src/core/storage/process-log.ts +17 -2
- package/src/core/supervisor/contract.ts +13 -0
- package/src/core/supervisor/explanations.ts +4 -1
- package/src/core/supervisor/findings.ts +68 -10
- package/src/core/supervisor/service.ts +88 -26
- package/src/core/supervisor/services.ts +26 -8
- package/src/core/supervisor/status.ts +89 -12
- package/src/core/supervisor/timeline.ts +2 -1
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +131 -49
- package/src/core/workflow/failure-evidence.ts +141 -0
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/run-state.ts +31 -29
- package/src/core/workflow/spawn-claude.ts +18 -0
- package/src/core/workflow/spawn-codex.ts +13 -0
- package/src/core/workflow/spawn-cursor.ts +18 -0
- package/src/core/workflow/spawn-opencode.ts +14 -0
- package/src/core/workflow/types.ts +51 -17
- package/src/core/workflow/workspaces/index.ts +0 -1
- package/src/core/workflow/workspaces/leases.ts +48 -17
- package/src/core/workflow/workspaces/local-git.ts +315 -5
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +58 -0
- package/src/core/workflow/workspaces/recovery.ts +25 -13
- package/src/core/workflow/workspaces/state.ts +20 -0
- package/src/lib/browser/windows-chrome.ts +346 -86
- package/src/lib/docs-links.ts +49 -2
- package/src/lib/guarded-remove.ts +132 -14
- package/src/lib/identities/assume.ts +4 -0
- package/src/lib/tunnel/allowlist.ts +208 -0
- package/src/lib/tunnel/current-address.ts +158 -0
- package/src/lib/tunnel/gate.ts +16 -8
- package/src/lib/tunnel/processes.ts +149 -0
- package/src/lib/tunnel/state.ts +123 -27
- package/src/lib/zip/index.ts +483 -0
|
@@ -14,21 +14,25 @@
|
|
|
14
14
|
* for `gc` once its starting agent has ended or been unobserved for a long
|
|
15
15
|
* time AND nobody has used the server for the idle window.
|
|
16
16
|
*
|
|
17
|
-
* Fail-closed rules: a record whose pid was recycled is
|
|
18
|
-
*
|
|
17
|
+
* Fail-closed rules: a record whose pid was recycled is never signalled;
|
|
18
|
+
* an unreadable start token or coordination view is `unknown`,
|
|
19
19
|
* which `gc` never stops; a record without an owner is never stopped
|
|
20
20
|
* automatically.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { spawnSync } from "node:child_process";
|
|
24
23
|
import { createHash } from "node:crypto";
|
|
25
24
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
26
25
|
import { hostname } from "node:os";
|
|
27
26
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
28
|
-
import { listStates } from "../../lib/tunnel/state.ts";
|
|
29
27
|
import { resolveCoordRoot } from "../agents/coord-client.ts";
|
|
30
28
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
|
|
31
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
type ProcessGenerationSignalDependencies,
|
|
31
|
+
type ProcessGenerationVerdict,
|
|
32
|
+
processStartToken,
|
|
33
|
+
signalIfSameGeneration,
|
|
34
|
+
verifyProcessGeneration,
|
|
35
|
+
} from "../agents/state/proc-start.ts";
|
|
32
36
|
import { serverPortRange } from "../config.ts";
|
|
33
37
|
import {
|
|
34
38
|
readCoordinationViewV3,
|
|
@@ -47,7 +51,6 @@ import {
|
|
|
47
51
|
type ScanSupport,
|
|
48
52
|
scanSupport,
|
|
49
53
|
} from "./net.ts";
|
|
50
|
-
import { tunnelServerId, tunnelServerInput } from "./tunnels.ts";
|
|
51
54
|
|
|
52
55
|
export {
|
|
53
56
|
ancestry,
|
|
@@ -87,13 +90,13 @@ export interface ServerRecord {
|
|
|
87
90
|
start_token?: string;
|
|
88
91
|
/** Further processes that belong to this server (helpers, children). */
|
|
89
92
|
pids?: number[];
|
|
93
|
+
/** Generation witnesses captured when the further processes registered. */
|
|
94
|
+
pids_start_tokens?: Record<string, string>;
|
|
90
95
|
host: string;
|
|
91
96
|
/** What the server serves, usually a directory. */
|
|
92
97
|
scope?: string;
|
|
93
98
|
cwd?: string;
|
|
94
99
|
log?: string;
|
|
95
|
-
/** Graceful stop command; signals are the fallback. */
|
|
96
|
-
stop_argv?: string[];
|
|
97
100
|
owner?: { instance_id: string; name?: string };
|
|
98
101
|
started_at: string;
|
|
99
102
|
registered_at: string;
|
|
@@ -108,11 +111,12 @@ export interface RegisterServerInput {
|
|
|
108
111
|
url?: string;
|
|
109
112
|
port?: number;
|
|
110
113
|
pid?: number;
|
|
114
|
+
/** Preserve an observed witness when adopting, rather than probing a new generation. */
|
|
115
|
+
start_token?: string;
|
|
111
116
|
pids?: number[];
|
|
112
117
|
scope?: string;
|
|
113
118
|
cwd?: string;
|
|
114
119
|
log?: string;
|
|
115
|
-
stop_argv?: string[];
|
|
116
120
|
started_at?: string;
|
|
117
121
|
/** `null` records no owner; omitted resolves the calling agent session. */
|
|
118
122
|
owner?: { instance_id: string; name?: string } | null;
|
|
@@ -122,7 +126,7 @@ export interface ServerOptions {
|
|
|
122
126
|
coordRoot?: string | null;
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
export type ServerState = "running" | "dead" | "other-host";
|
|
129
|
+
export type ServerState = "running" | "dead" | "other-host" | "unknown";
|
|
126
130
|
export type OwnerState = "live" | "abandoned" | "ended" | "unknown" | "none";
|
|
127
131
|
|
|
128
132
|
export interface ServerView {
|
|
@@ -136,6 +140,8 @@ export interface ServerView {
|
|
|
136
140
|
|
|
137
141
|
export interface UnregisteredListener {
|
|
138
142
|
pid: number;
|
|
143
|
+
/** Captured during the scan, before a stop can be authorized. */
|
|
144
|
+
start_token?: string;
|
|
139
145
|
port: number;
|
|
140
146
|
address: string;
|
|
141
147
|
command: string;
|
|
@@ -204,7 +210,13 @@ export function registerServer(
|
|
|
204
210
|
throw new Error(`invalid server port: ${input.port}`);
|
|
205
211
|
const id = input.id ?? serverId(input.type, input.scope);
|
|
206
212
|
const now = new Date().toISOString();
|
|
207
|
-
const startToken = processStartToken(pid);
|
|
213
|
+
const startToken = input.start_token ?? processStartToken(pid);
|
|
214
|
+
const pids = [...new Set(input.pids?.filter((p) => Number.isSafeInteger(p) && p > 0))];
|
|
215
|
+
const pidsStartTokens: Record<string, string> = {};
|
|
216
|
+
for (const auxiliaryPid of pids) {
|
|
217
|
+
const token = processStartToken(auxiliaryPid);
|
|
218
|
+
if (token) pidsStartTokens[auxiliaryPid] = token;
|
|
219
|
+
}
|
|
208
220
|
const owner = input.owner === undefined ? callingOwner(coordRoot) : (input.owner ?? undefined);
|
|
209
221
|
const record: ServerRecord = {
|
|
210
222
|
schema_version: SERVER_RECORD_SCHEMA_VERSION,
|
|
@@ -216,19 +228,18 @@ export function registerServer(
|
|
|
216
228
|
...(input.port ? { port: input.port } : {}),
|
|
217
229
|
pid,
|
|
218
230
|
...(startToken ? { start_token: startToken } : {}),
|
|
219
|
-
...(
|
|
220
|
-
? { pids: input.pids.filter((p) => Number.isSafeInteger(p) && p > 0) }
|
|
221
|
-
: {}),
|
|
231
|
+
...(pids.length ? { pids, pids_start_tokens: pidsStartTokens } : {}),
|
|
222
232
|
host: hostname(),
|
|
223
233
|
...(input.scope ? { scope: resolve(input.scope) } : {}),
|
|
224
234
|
...(input.cwd ? { cwd: resolve(input.cwd) } : {}),
|
|
225
235
|
...(input.log ? { log: resolve(input.log) } : {}),
|
|
226
|
-
...(input.stop_argv?.length ? { stop_argv: input.stop_argv } : {}),
|
|
227
236
|
...(owner ? { owner } : {}),
|
|
228
237
|
started_at: input.started_at ?? now,
|
|
229
238
|
registered_at: now,
|
|
230
239
|
last_active_at: now,
|
|
231
240
|
};
|
|
241
|
+
if (input.start_token !== undefined && verifyProcessGeneration(pid, input.start_token) !== "same")
|
|
242
|
+
throw new Error(`pid ${pid} generation could not be verified before registration`);
|
|
232
243
|
writePrivateJsonAtomic(recordPath(coordRoot, id), record);
|
|
233
244
|
return record;
|
|
234
245
|
}
|
|
@@ -343,18 +354,40 @@ export function readServerRecords(options?: ServerOptions): ServerRecord[] {
|
|
|
343
354
|
}
|
|
344
355
|
|
|
345
356
|
export function pidAlive(pid: number): boolean {
|
|
357
|
+
return processPresence(pid) !== "absent";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function processPresence(
|
|
361
|
+
pid: number,
|
|
362
|
+
probePid?: (pid: number) => boolean,
|
|
363
|
+
): "present" | "absent" | "unknown" {
|
|
346
364
|
try {
|
|
365
|
+
if (probePid) return probePid(pid) ? "present" : "absent";
|
|
347
366
|
process.kill(pid, 0);
|
|
348
|
-
return
|
|
367
|
+
return "present";
|
|
349
368
|
} catch (error) {
|
|
350
|
-
|
|
369
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
370
|
+
return code === "ESRCH" ? "absent" : code === "EPERM" ? "present" : "unknown";
|
|
351
371
|
}
|
|
352
372
|
}
|
|
353
373
|
|
|
354
|
-
|
|
374
|
+
function allProcessesAbsent(record: ServerRecord, probePid?: (pid: number) => boolean): boolean {
|
|
375
|
+
return [record.pid, ...(record.pids ?? [])].every(
|
|
376
|
+
(pid) => processPresence(pid, probePid) === "absent",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function serverState(
|
|
381
|
+
record: ServerRecord,
|
|
382
|
+
readToken?: (pid: number) => string | null,
|
|
383
|
+
probePid?: (pid: number) => boolean,
|
|
384
|
+
): ServerState {
|
|
355
385
|
if (record.host !== hostname()) return "other-host";
|
|
356
|
-
|
|
357
|
-
return
|
|
386
|
+
const presence = processPresence(record.pid, probePid);
|
|
387
|
+
if (presence === "absent") return allProcessesAbsent(record, probePid) ? "dead" : "unknown";
|
|
388
|
+
if (presence === "unknown") return "unknown";
|
|
389
|
+
const verdict = verifyProcessGeneration(record.pid, record.start_token, readToken);
|
|
390
|
+
return verdict === "same" ? "running" : verdict === "different" ? "dead" : "unknown";
|
|
358
391
|
}
|
|
359
392
|
|
|
360
393
|
type OwnerReader = (instanceId: string) => OwnerState;
|
|
@@ -395,7 +428,6 @@ export function listServers(options?: ListServersOptions): ServersReport {
|
|
|
395
428
|
const now = options?.now ?? new Date();
|
|
396
429
|
const nowMs = now.getTime();
|
|
397
430
|
const ownerStaleMs = hours(options?.ownerStaleHours ?? SERVER_DEFAULT_OWNER_STALE_HOURS);
|
|
398
|
-
adoptRunningTunnels(coordRoot);
|
|
399
431
|
const records = readServerRecords({ coordRoot });
|
|
400
432
|
const ownerOf = ownerReader(coordRoot, ownerStaleMs, nowMs);
|
|
401
433
|
const states = records.map((record) => serverState(record));
|
|
@@ -407,7 +439,7 @@ export function listServers(options?: ListServersOptions): ServersReport {
|
|
|
407
439
|
const servers: ServerView[] = [];
|
|
408
440
|
records.forEach((record, i) => {
|
|
409
441
|
const state = states[i]!;
|
|
410
|
-
if (state === "dead" && options?.prune !== false) {
|
|
442
|
+
if (state === "dead" && allProcessesAbsent(record) && options?.prune !== false) {
|
|
411
443
|
unregisterServer(record.id, { coordRoot, pid: record.pid });
|
|
412
444
|
pruned.push(record.id);
|
|
413
445
|
return;
|
|
@@ -446,24 +478,6 @@ export function listServers(options?: ListServersOptions): ServersReport {
|
|
|
446
478
|
};
|
|
447
479
|
}
|
|
448
480
|
|
|
449
|
-
/** Register running tunnels that have no current record. Returns adopted ids. */
|
|
450
|
-
export function adoptRunningTunnels(coordRoot: string): string[] {
|
|
451
|
-
const adopted: string[] = [];
|
|
452
|
-
let states: ReturnType<typeof listStates>;
|
|
453
|
-
try {
|
|
454
|
-
states = listStates(coordRoot);
|
|
455
|
-
} catch {
|
|
456
|
-
return adopted;
|
|
457
|
-
}
|
|
458
|
-
for (const state of states) {
|
|
459
|
-
if (!state.gate_pid || !pidAlive(state.gate_pid)) continue;
|
|
460
|
-
const id = tunnelServerId(state.name);
|
|
461
|
-
if (readServer(id, { coordRoot })?.pid === state.gate_pid) continue;
|
|
462
|
-
if (tryRegisterServer(tunnelServerInput(state, coordRoot), { coordRoot })) adopted.push(id);
|
|
463
|
-
}
|
|
464
|
-
return adopted;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
481
|
/** Processes that browser automation and editors start; never project servers. */
|
|
468
482
|
const IGNORED_LISTENER_COMMANDS = [
|
|
469
483
|
/(^|\/)(chrome|chromium|chrome-headless-shell|headless_shell|msedge|firefox)( |$)/i,
|
|
@@ -481,10 +495,10 @@ export function findUnregisteredListeners(
|
|
|
481
495
|
coordRoot: string,
|
|
482
496
|
servers: Array<Pick<ServerView, "record" | "state">>,
|
|
483
497
|
): UnregisteredListener[] {
|
|
484
|
-
const
|
|
498
|
+
const registered = servers.filter((server) => server.record.host === hostname());
|
|
485
499
|
const covered = new Set<number>();
|
|
486
500
|
const ports = new Set<number>();
|
|
487
|
-
for (const { record } of
|
|
501
|
+
for (const { record } of registered) {
|
|
488
502
|
covered.add(record.pid);
|
|
489
503
|
for (const pid of record.pids ?? []) covered.add(pid);
|
|
490
504
|
if (record.port) ports.add(record.port);
|
|
@@ -492,7 +506,8 @@ export function findUnregisteredListeners(
|
|
|
492
506
|
const results: UnregisteredListener[] = [];
|
|
493
507
|
const seenPids = new Map<number, boolean>();
|
|
494
508
|
for (const socket of listListeningSockets()) {
|
|
495
|
-
if (ports.has(socket.port) || socket.pid === process.pid) continue;
|
|
509
|
+
if (ports.has(socket.port) || covered.has(socket.pid) || socket.pid === process.pid) continue;
|
|
510
|
+
const startToken = processStartToken(socket.pid);
|
|
496
511
|
let inside = seenPids.get(socket.pid);
|
|
497
512
|
const info = readProcessInfo(socket.pid);
|
|
498
513
|
if (!info) continue;
|
|
@@ -506,6 +521,7 @@ export function findUnregisteredListeners(
|
|
|
506
521
|
if (!inside) continue;
|
|
507
522
|
results.push({
|
|
508
523
|
pid: socket.pid,
|
|
524
|
+
...(startToken ? { start_token: startToken } : {}),
|
|
509
525
|
port: socket.port,
|
|
510
526
|
address: socket.address,
|
|
511
527
|
command: info.command.slice(0, 300),
|
|
@@ -568,77 +584,134 @@ export function planServerGc(views: ServerView[], policy?: GcPolicy): GcDecision
|
|
|
568
584
|
export interface StopResult {
|
|
569
585
|
id: string;
|
|
570
586
|
stopped: boolean;
|
|
571
|
-
method: "not-running" | "
|
|
587
|
+
method: "not-running" | "signal" | "refused";
|
|
588
|
+
generation?: ProcessGenerationVerdict;
|
|
572
589
|
detail?: string;
|
|
573
590
|
}
|
|
574
591
|
|
|
592
|
+
export interface ServerProcessDependencies extends ProcessGenerationSignalDependencies {
|
|
593
|
+
probePid?: (pid: number) => boolean;
|
|
594
|
+
}
|
|
595
|
+
|
|
575
596
|
/**
|
|
576
|
-
* Stop
|
|
577
|
-
*
|
|
578
|
-
*
|
|
597
|
+
* Stop a verified server with SIGTERM, then SIGKILL after the grace period.
|
|
598
|
+
* Each signal, including the process group and auxiliary processes, requires
|
|
599
|
+
* a fresh generation check. Unknown or different generations retain the record.
|
|
579
600
|
*/
|
|
580
601
|
export async function stopServer(
|
|
581
602
|
record: ServerRecord,
|
|
582
|
-
options?: ServerOptions & {
|
|
603
|
+
options?: ServerOptions & {
|
|
604
|
+
graceMs?: number;
|
|
605
|
+
signalDependencies?: ServerProcessDependencies;
|
|
606
|
+
},
|
|
583
607
|
): Promise<StopResult> {
|
|
584
608
|
const coordRoot = rootOf(options);
|
|
585
609
|
const graceMs = options?.graceMs ?? 5_000;
|
|
610
|
+
const dependencies = options?.signalDependencies ?? {};
|
|
611
|
+
const targets = [
|
|
612
|
+
{ pid: record.pid, token: record.start_token },
|
|
613
|
+
...[...new Set(record.pids ?? [])]
|
|
614
|
+
.filter((pid) => pid !== record.pid)
|
|
615
|
+
.map((pid) => ({ pid, token: record.pids_start_tokens?.[pid] })),
|
|
616
|
+
];
|
|
617
|
+
const refuse = (pid: number, generation: ProcessGenerationVerdict): StopResult => ({
|
|
618
|
+
id: record.id,
|
|
619
|
+
stopped: false,
|
|
620
|
+
method: "refused",
|
|
621
|
+
generation,
|
|
622
|
+
detail: `pid ${pid} generation is ${generation}; left the server record in place`,
|
|
623
|
+
});
|
|
586
624
|
const finish = (result: StopResult): StopResult => {
|
|
587
625
|
if (result.stopped || result.method === "not-running")
|
|
588
626
|
unregisterServer(record.id, { coordRoot, pid: record.pid });
|
|
589
627
|
return result;
|
|
590
628
|
};
|
|
591
|
-
|
|
592
|
-
if (state === "other-host")
|
|
629
|
+
if (record.host !== hostname())
|
|
593
630
|
return { id: record.id, stopped: false, method: "refused", detail: `runs on ${record.host}` };
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
631
|
+
const leaderPresence = processPresence(record.pid, dependencies.probePid);
|
|
632
|
+
if (
|
|
633
|
+
leaderPresence === "unknown" ||
|
|
634
|
+
(leaderPresence === "absent" && !allProcessesAbsent(record, dependencies.probePid))
|
|
635
|
+
)
|
|
636
|
+
return refuse(record.pid, "unknown");
|
|
637
|
+
// Check every recorded live process before sending the first signal.
|
|
638
|
+
for (const target of targets) {
|
|
639
|
+
const presence = processPresence(target.pid, dependencies.probePid);
|
|
640
|
+
if (presence === "absent") continue;
|
|
641
|
+
if (presence === "unknown") return refuse(target.pid, "unknown");
|
|
642
|
+
const verdict = verifyProcessGeneration(target.pid, target.token, dependencies.readToken);
|
|
643
|
+
if (verdict !== "same") return refuse(target.pid, verdict);
|
|
601
644
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
645
|
+
if (targets.every((target) => processPresence(target.pid, dependencies.probePid) === "absent"))
|
|
646
|
+
return finish({ id: record.id, stopped: false, method: "not-running" });
|
|
647
|
+
|
|
648
|
+
for (const name of ["SIGTERM", "SIGKILL"] as const) {
|
|
649
|
+
for (const target of targets) {
|
|
650
|
+
const presence = processPresence(target.pid, dependencies.probePid);
|
|
651
|
+
if (presence === "absent") continue;
|
|
652
|
+
if (presence === "unknown") return refuse(target.pid, "unknown");
|
|
653
|
+
const result =
|
|
654
|
+
target.pid === record.pid
|
|
655
|
+
? signalServerLeader(target.pid, target.token, name, dependencies)
|
|
656
|
+
: signalIfSameGeneration(target.pid, target.token, name, dependencies);
|
|
657
|
+
if (result.verdict !== "same") return refuse(target.pid, result.verdict);
|
|
658
|
+
}
|
|
659
|
+
const observed = await waitForExit(targets, name === "SIGTERM" ? graceMs : 2_000, dependencies);
|
|
660
|
+
if (observed.refused) return refuse(observed.refused.pid, observed.refused.verdict);
|
|
661
|
+
if (observed.exited)
|
|
662
|
+
return finish({ id: record.id, stopped: true, method: "signal", generation: "same" });
|
|
609
663
|
}
|
|
610
|
-
|
|
611
|
-
return finish({
|
|
664
|
+
return {
|
|
612
665
|
id: record.id,
|
|
613
|
-
stopped,
|
|
666
|
+
stopped: false,
|
|
614
667
|
method: "signal",
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function signal(pid: number, name: NodeJS.Signals): void {
|
|
620
|
-
// Detached servers lead their own process group; signal the group so a
|
|
621
|
-
// wrapper (npm, bunx) does not orphan the real listener.
|
|
622
|
-
try {
|
|
623
|
-
process.kill(-pid, name);
|
|
624
|
-
return;
|
|
625
|
-
} catch {
|
|
626
|
-
// Not a group leader, or already gone.
|
|
627
|
-
}
|
|
628
|
-
try {
|
|
629
|
-
process.kill(pid, name);
|
|
630
|
-
} catch {
|
|
631
|
-
// Already gone.
|
|
632
|
-
}
|
|
668
|
+
generation: "same",
|
|
669
|
+
detail: "process survived SIGKILL",
|
|
670
|
+
};
|
|
633
671
|
}
|
|
634
672
|
|
|
635
|
-
|
|
673
|
+
function signalServerLeader(
|
|
674
|
+
pid: number,
|
|
675
|
+
token: string | undefined,
|
|
676
|
+
name: NodeJS.Signals,
|
|
677
|
+
dependencies: ServerProcessDependencies,
|
|
678
|
+
): { sent: boolean; verdict: ProcessGenerationVerdict } {
|
|
679
|
+
// The leader's generation authorizes its group. A failed group signal must
|
|
680
|
+
// not reuse that observation for the direct-PID fallback.
|
|
681
|
+
const kill = dependencies.kill ?? process.kill;
|
|
682
|
+
const group = signalIfSameGeneration(pid, token, name, {
|
|
683
|
+
readToken: dependencies.readToken,
|
|
684
|
+
kill: (leader, signal) => kill(-leader, signal),
|
|
685
|
+
});
|
|
686
|
+
return group.sent || group.verdict !== "same"
|
|
687
|
+
? group
|
|
688
|
+
: signalIfSameGeneration(pid, token, name, dependencies);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function waitForExit(
|
|
692
|
+
targets: Array<{ pid: number; token: string | undefined }>,
|
|
693
|
+
waitMs: number,
|
|
694
|
+
dependencies: ServerProcessDependencies,
|
|
695
|
+
): Promise<{
|
|
696
|
+
exited: boolean;
|
|
697
|
+
refused?: { pid: number; verdict: ProcessGenerationVerdict };
|
|
698
|
+
}> {
|
|
636
699
|
const deadline = Date.now() + waitMs;
|
|
637
|
-
while (
|
|
638
|
-
|
|
700
|
+
while (true) {
|
|
701
|
+
let live = false;
|
|
702
|
+
for (const target of targets) {
|
|
703
|
+
const presence = processPresence(target.pid, dependencies.probePid);
|
|
704
|
+
if (presence === "absent") continue;
|
|
705
|
+
if (presence === "unknown")
|
|
706
|
+
return { exited: false, refused: { pid: target.pid, verdict: "unknown" } };
|
|
707
|
+
const verdict = verifyProcessGeneration(target.pid, target.token, dependencies.readToken);
|
|
708
|
+
if (verdict !== "same") return { exited: false, refused: { pid: target.pid, verdict } };
|
|
709
|
+
live = true;
|
|
710
|
+
}
|
|
711
|
+
if (!live) return { exited: true };
|
|
712
|
+
if (Date.now() >= deadline) return { exited: false };
|
|
639
713
|
await new Promise((r) => setTimeout(r, 100));
|
|
640
714
|
}
|
|
641
|
-
return serverState(record) !== "running";
|
|
642
715
|
}
|
|
643
716
|
|
|
644
717
|
/**
|
|
@@ -653,7 +726,8 @@ export function allocateServerPort(type: string, options?: ServerOptions): numbe
|
|
|
653
726
|
if (!range) return null;
|
|
654
727
|
const taken = listListeningPorts();
|
|
655
728
|
for (const record of readServerRecords({ coordRoot })) {
|
|
656
|
-
|
|
729
|
+
const state = serverState(record);
|
|
730
|
+
if (record.port && (state === "running" || state === "unknown")) taken.add(record.port);
|
|
657
731
|
}
|
|
658
732
|
for (let port = range[0]; port <= range[1]; port++) if (!taken.has(port)) return port;
|
|
659
733
|
throw new Error(`no free port for ${type} in ${range[0]}-${range[1]}`);
|
|
@@ -666,6 +740,10 @@ export interface AdoptOptions extends ServerOptions {
|
|
|
666
740
|
scope?: string;
|
|
667
741
|
/** `self` (default) records the adopting agent as owner; `none` records no owner. */
|
|
668
742
|
owner?: "self" | "none";
|
|
743
|
+
dependencies?: {
|
|
744
|
+
listServers?: typeof listServers;
|
|
745
|
+
readToken?: (pid: number) => string | null;
|
|
746
|
+
};
|
|
669
747
|
}
|
|
670
748
|
|
|
671
749
|
/**
|
|
@@ -675,12 +753,19 @@ export interface AdoptOptions extends ServerOptions {
|
|
|
675
753
|
export function adoptServer(pid: number, options?: AdoptOptions): ServerRecord {
|
|
676
754
|
const coordRoot = rootOf(options);
|
|
677
755
|
if (!coordRoot) throw new Error("no Harnery project found (coordination root did not resolve)");
|
|
678
|
-
const report = listServers({ coordRoot, sample: false });
|
|
756
|
+
const report = (options?.dependencies?.listServers ?? listServers)({ coordRoot, sample: false });
|
|
679
757
|
const listeners = report.unregistered.filter((item) => item.pid === pid);
|
|
680
758
|
if (!listeners.length)
|
|
681
759
|
throw new Error(`pid ${pid} is not an unregistered listener inside this project`);
|
|
682
760
|
const ports = listeners.map((item) => item.port).sort((a, b) => a - b);
|
|
683
761
|
const first = listeners[0]!;
|
|
762
|
+
const generation = verifyProcessGeneration(
|
|
763
|
+
pid,
|
|
764
|
+
first.start_token,
|
|
765
|
+
options?.dependencies?.readToken,
|
|
766
|
+
);
|
|
767
|
+
if (generation !== "same")
|
|
768
|
+
throw new Error(`pid ${pid} generation is ${generation}; adoption refused`);
|
|
684
769
|
const type = options?.type ?? "adopted";
|
|
685
770
|
const scope = options?.scope ?? first.cwd ?? undefined;
|
|
686
771
|
const started = processStartedAt(pid);
|
|
@@ -693,6 +778,7 @@ export function adoptServer(pid: number, options?: AdoptOptions): ServerRecord {
|
|
|
693
778
|
url: `http://127.0.0.1:${ports[0]}/`,
|
|
694
779
|
port: ports[0],
|
|
695
780
|
pid,
|
|
781
|
+
start_token: first.start_token,
|
|
696
782
|
...(scope ? { scope } : {}),
|
|
697
783
|
...(first.cwd ? { cwd: first.cwd } : {}),
|
|
698
784
|
...(options?.owner === "none" ? { owner: null } : {}),
|
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tunnels as registry services.
|
|
3
3
|
*
|
|
4
|
-
* `tunnel up` and `tunnel reload` register the
|
|
5
|
-
*
|
|
6
|
-
* time the registry is read, so a long-lived tunnel never shows as an
|
|
7
|
-
* unregistered listener.
|
|
4
|
+
* `tunnel up` and `tunnel reload` register the new gate; `tunnel down` removes
|
|
5
|
+
* its record. Older tunnels need an explicit adoption of an observed listener.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
import { resolve } from "node:path";
|
|
11
9
|
import { gateLogFile, type TunnelState } from "../../lib/tunnel/state.ts";
|
|
12
|
-
import { resolveBinName } from "../config.ts";
|
|
13
10
|
import type { RegisterServerInput } from "./index.ts";
|
|
14
11
|
|
|
15
12
|
export function tunnelServerId(name: string): string {
|
|
16
13
|
return `tunnel-${name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-")}`;
|
|
17
14
|
}
|
|
18
15
|
|
|
19
|
-
/**
|
|
16
|
+
/** Register the new gate; saved helper PIDs have no generation witnesses. */
|
|
20
17
|
export function tunnelServerInput(state: TunnelState, coordRoot: string): RegisterServerInput {
|
|
21
|
-
const helpers = [state.cloudflared_pid, state.provider_pid].filter(
|
|
22
|
-
(pid): pid is number => typeof pid === "number" && pid > 0,
|
|
23
|
-
);
|
|
24
18
|
return {
|
|
25
19
|
id: tunnelServerId(state.name),
|
|
26
20
|
kind: "service",
|
|
@@ -29,10 +23,8 @@ export function tunnelServerInput(state: TunnelState, coordRoot: string): Regist
|
|
|
29
23
|
url: state.url,
|
|
30
24
|
port: state.gate_port,
|
|
31
25
|
pid: state.gate_pid,
|
|
32
|
-
...(helpers.length ? { pids: helpers } : {}),
|
|
33
26
|
log: resolve(coordRoot, ".cache", "tunnel", gateLogFile(state.name)),
|
|
34
27
|
cwd: coordRoot,
|
|
35
|
-
stop_argv: [resolveBinName(coordRoot), "tunnel", "down", "--name", state.name],
|
|
36
28
|
started_at: state.started_at,
|
|
37
29
|
owner: null,
|
|
38
30
|
};
|
|
@@ -38,6 +38,13 @@ export interface RotatingProcessOptions {
|
|
|
38
38
|
env?: Readonly<Record<string, string | undefined>>;
|
|
39
39
|
max_bytes?: number;
|
|
40
40
|
backups?: number;
|
|
41
|
+
/**
|
|
42
|
+
* How to start the log wrapper, as an argv prefix that takes the JSON
|
|
43
|
+
* specification as its final argument and ends in `runProcessLogWorker`. The
|
|
44
|
+
* default runs this module's own file, which does not exist inside a
|
|
45
|
+
* single-file host bundle; such a host supplies a hidden task of its own.
|
|
46
|
+
*/
|
|
47
|
+
worker?: { command: string; arguments: string[] };
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
const DEFAULT_PROCESS_LOG_BYTES = 5 * 1024 * 1024;
|
|
@@ -136,7 +143,14 @@ export function spawnRotatingProcess(options: RotatingProcessOptions): ChildProc
|
|
|
136
143
|
command: options.command,
|
|
137
144
|
arguments: options.arguments ?? [],
|
|
138
145
|
});
|
|
139
|
-
|
|
146
|
+
// Run the wrapper with the Bun that is running this code: a host may carry a
|
|
147
|
+
// managed Bun that is not on PATH.
|
|
148
|
+
const bun = typeof process.versions.bun === "string" ? process.execPath : "bun";
|
|
149
|
+
const launcher = options.worker ?? {
|
|
150
|
+
command: bun,
|
|
151
|
+
arguments: [fileURLToPath(import.meta.url), "--run"],
|
|
152
|
+
};
|
|
153
|
+
return spawn(launcher.command, [...launcher.arguments, specification], {
|
|
140
154
|
detached: true,
|
|
141
155
|
stdio: "ignore",
|
|
142
156
|
env: { ...(options.env ?? process.env) } as NodeJS.ProcessEnv,
|
|
@@ -162,7 +176,8 @@ export function runRotatingProcessSync(options: RotatingProcessOptions): number
|
|
|
162
176
|
return result.status;
|
|
163
177
|
}
|
|
164
178
|
|
|
165
|
-
|
|
179
|
+
/** The wrapper's body: run the specified command and rotate its output into the log. */
|
|
180
|
+
export async function runProcessLogWorker(raw: string | undefined): Promise<void> {
|
|
166
181
|
const parsed = JSON.parse(raw ?? "null") as RotatingProcessOptions | null;
|
|
167
182
|
if (!parsed || typeof parsed.command !== "string" || !Array.isArray(parsed.arguments)) {
|
|
168
183
|
throw new Error("invalid process log worker specification");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProcessGenerationVerdict } from "../agents/state/proc-start.ts";
|
|
1
2
|
import type {
|
|
2
3
|
PressureAssessment,
|
|
3
4
|
PressureHysteresisState,
|
|
@@ -47,10 +48,16 @@ export const SUPERVISOR_RESOURCE_BUDGET = {
|
|
|
47
48
|
|
|
48
49
|
export type SupervisorProcessState = "starting" | "running" | "stopping" | "stopped" | "error";
|
|
49
50
|
|
|
51
|
+
export interface SupervisorBuild {
|
|
52
|
+
version: string;
|
|
53
|
+
revision: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
export interface SupervisorServiceStatusRecord {
|
|
51
57
|
schema_version: typeof SUPERVISOR_STATUS_SCHEMA_VERSION;
|
|
52
58
|
pid: number;
|
|
53
59
|
start_token?: string;
|
|
60
|
+
running_build?: SupervisorBuild;
|
|
54
61
|
host: string;
|
|
55
62
|
nonce: string;
|
|
56
63
|
state: SupervisorProcessState;
|
|
@@ -70,6 +77,12 @@ export interface SupervisorServiceStatusRecord {
|
|
|
70
77
|
export interface SupervisorStatus {
|
|
71
78
|
running: boolean;
|
|
72
79
|
stale: boolean;
|
|
80
|
+
generation: ProcessGenerationVerdict;
|
|
81
|
+
liveness: "running" | "stopped" | "unknown";
|
|
82
|
+
running_build: SupervisorBuild | null;
|
|
83
|
+
installed_build: SupervisorBuild;
|
|
84
|
+
restart_needed: boolean;
|
|
85
|
+
build_status: string;
|
|
73
86
|
record?: SupervisorServiceStatusRecord;
|
|
74
87
|
status_path: string;
|
|
75
88
|
snapshot_path: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalizeDiagnosticIdentity } from "../diagnostics/path-identity.ts";
|
|
2
3
|
import {
|
|
3
4
|
SUPERVISOR_DIAGNOSTIC_LIMITS,
|
|
4
5
|
SUPERVISOR_EXPLANATION_SCHEMA_VERSION,
|
|
@@ -94,7 +95,9 @@ function statement(
|
|
|
94
95
|
): SupervisorExplanationStatement {
|
|
95
96
|
return {
|
|
96
97
|
id: `ex_${createHash("sha256")
|
|
97
|
-
.update(
|
|
98
|
+
.update(
|
|
99
|
+
canonicalizeDiagnosticIdentity(`${code}\u0000${summary}\u0000${evidenceIds.join(",")}`),
|
|
100
|
+
)
|
|
98
101
|
.digest("hex")
|
|
99
102
|
.slice(0, 24)}`,
|
|
100
103
|
code,
|