harnery 0.43.0 → 0.45.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.js +1 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +41 -48
- 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/files.d.ts +2 -2
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +7 -4
- 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 +26 -17
- 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 +15 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +453 -153
- package/dist/commands/turn-rollback.d.ts +5 -0
- package/dist/commands/turn-rollback.d.ts.map +1 -0
- package/dist/commands/turn-rollback.js +76 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +68 -30
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +18 -0
- 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 +8 -12
- 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 +7 -30
- 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/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 -1
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +46 -1
- package/dist/core/agents/state/process-handle.d.ts +26 -0
- package/dist/core/agents/state/process-handle.d.ts.map +1 -0
- package/dist/core/agents/state/process-handle.js +121 -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/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +2 -1
- 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/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 +33 -5
- 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 +7 -13
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +34 -91
- 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/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +2 -9
- package/dist/core/servers/index.d.ts +34 -22
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +297 -144
- package/dist/core/servers/tunnels.d.ts +7 -7
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +47 -10
- 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 +7 -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/turn-rollback/content.d.ts +53 -0
- package/dist/core/turn-rollback/content.d.ts.map +1 -0
- package/dist/core/turn-rollback/content.js +396 -0
- package/dist/core/turn-rollback/index.d.ts +72 -0
- package/dist/core/turn-rollback/index.d.ts.map +1 -0
- package/dist/core/turn-rollback/index.js +443 -0
- 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 +5 -3
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +3 -2
- 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/recovery-audit.d.ts +30 -0
- package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
- package/dist/core/workflow/recovery-audit.js +116 -0
- package/dist/core/workflow/run-state.d.ts +20 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +141 -32
- 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 +328 -8
- 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 +40 -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 +2 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +42 -10
- 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/state.d.ts +13 -6
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +58 -12
- 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 +12 -1
- package/src/commander.ts +10 -0
- package/src/commands/agents.ts +54 -63
- package/src/commands/browse.ts +50 -14
- package/src/commands/diagnostics.ts +10 -0
- package/src/commands/doctor.ts +26 -7
- package/src/commands/files.ts +13 -5
- 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 +27 -26
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/tunnel.ts +533 -166
- package/src/commands/turn-rollback.ts +111 -0
- package/src/commands/workflow.ts +89 -30
- package/src/core/agents/coord-client.ts +20 -0
- 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 +10 -12
- package/src/core/agents/render/session-context.ts +9 -32
- 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/pidmap.ts +13 -0
- package/src/core/agents/state/proc-start.ts +62 -2
- package/src/core/agents/state/process-handle.ts +165 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- package/src/core/artifacts/delivery-card.ts +7 -1
- 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/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 +33 -5
- 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 +57 -95
- package/src/core/resources/status.ts +57 -8
- package/src/core/semantic/service.ts +2 -8
- package/src/core/servers/index.ts +376 -141
- package/src/core/servers/tunnels.ts +57 -13
- 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 +87 -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/turn-rollback/content.ts +499 -0
- package/src/core/turn-rollback/index.ts +580 -0
- 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 +12 -2
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/recovery-audit.ts +181 -0
- package/src/core/workflow/run-state.ts +165 -34
- 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 +388 -7
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +62 -0
- package/src/core/workflow/workspaces/recovery.ts +45 -10
- 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/state.ts +70 -15
- package/src/lib/zip/index.ts +483 -0
|
@@ -14,30 +14,30 @@
|
|
|
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
|
-
import {
|
|
23
|
-
import { createHash } from "node:crypto";
|
|
22
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
24
23
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
25
24
|
import { hostname } from "node:os";
|
|
26
25
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
27
|
-
import { listStates } from "../../lib/tunnel/state.js";
|
|
28
26
|
import { resolveCoordRoot } from "../agents/coord-client.js";
|
|
29
27
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
|
|
30
|
-
import {
|
|
28
|
+
import { processStartToken, signalIfSameGeneration, verifyProcessGeneration, } from "../agents/state/proc-start.js";
|
|
31
29
|
import { serverPortRange } from "../config.js";
|
|
32
30
|
import { readCoordinationViewV3, requireAuthoritySafeCoordinationViewV3, } from "../events/v3/coordination-view.js";
|
|
33
31
|
import { liveInstanceIdV3 } from "../events/v3/live-route-observer.js";
|
|
34
32
|
import { resolveOwner } from "../hooks/resolve/owner.js";
|
|
35
33
|
import { writePrivateJsonAtomic } from "../storage/atomic-json.js";
|
|
34
|
+
import { acquireNoClobberLease } from "../workflow/workspaces/leases.js";
|
|
36
35
|
import { ancestry, establishedConnectionCounts, listListeningPorts, listListeningSockets, processStartedAt, readProcessInfo, scanSupport, } from "./net.js";
|
|
37
|
-
|
|
36
|
+
export { processStartToken, signalIfSameGeneration, verifyProcessGeneration, } from "../agents/state/proc-start.js";
|
|
37
|
+
export { acquireNoClobberLease } from "../workflow/workspaces/leases.js";
|
|
38
38
|
export { ancestry, establishedConnectionCounts, listListeningPorts, listListeningSockets, lsofConnectionCounts, lsofListeningSockets, parseElapsed, parseLsofListen, parseProcNetTcp, processStartedAt, psProcessInfo, readProcessInfo, scanSupport, } from "./net.js";
|
|
39
|
-
export { tunnelServerId, tunnelServerInput } from "./tunnels.js";
|
|
40
|
-
export const SERVER_RECORD_SCHEMA_VERSION =
|
|
39
|
+
export { createTunnelReadinessVerifier, tunnelServerId, tunnelServerInput } from "./tunnels.js";
|
|
40
|
+
export const SERVER_RECORD_SCHEMA_VERSION = 2;
|
|
41
41
|
export const SERVER_DEFAULT_IDLE_HOURS = 2;
|
|
42
42
|
export const SERVER_DEFAULT_OWNER_STALE_HOURS = 24;
|
|
43
43
|
const TOUCH_INTERVAL_MS = 60_000;
|
|
@@ -86,34 +86,54 @@ export function registerServer(input, options) {
|
|
|
86
86
|
!(Number.isInteger(input.port) && input.port > 0 && input.port < 65536))
|
|
87
87
|
throw new Error(`invalid server port: ${input.port}`);
|
|
88
88
|
const id = input.id ?? serverId(input.type, input.scope);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
89
|
+
return withServerMutation(coordRoot, id, () => {
|
|
90
|
+
const path = recordPath(coordRoot, id);
|
|
91
|
+
const now = new Date().toISOString();
|
|
92
|
+
const startToken = input.start_token ?? processStartToken(pid);
|
|
93
|
+
const pids = [...new Set(input.pids?.filter((p) => Number.isSafeInteger(p) && p > 0))];
|
|
94
|
+
const pidsStartTokens = {};
|
|
95
|
+
for (const auxiliaryPid of pids) {
|
|
96
|
+
const token = processStartToken(auxiliaryPid);
|
|
97
|
+
if (token)
|
|
98
|
+
pidsStartTokens[auxiliaryPid] = token;
|
|
99
|
+
}
|
|
100
|
+
const owner = input.owner === undefined ? callingOwner(coordRoot) : (input.owner ?? undefined);
|
|
101
|
+
const record = {
|
|
102
|
+
schema_version: SERVER_RECORD_SCHEMA_VERSION,
|
|
103
|
+
id,
|
|
104
|
+
registration_id: randomUUID(),
|
|
105
|
+
kind: input.kind,
|
|
106
|
+
type: input.type,
|
|
107
|
+
label: input.label ?? id,
|
|
108
|
+
...(input.url ? { url: input.url } : {}),
|
|
109
|
+
...(input.port ? { port: input.port } : {}),
|
|
110
|
+
pid,
|
|
111
|
+
...(startToken ? { start_token: startToken } : {}),
|
|
112
|
+
...(pids.length ? { pids, pids_start_tokens: pidsStartTokens } : {}),
|
|
113
|
+
host: hostname(),
|
|
114
|
+
...(input.scope ? { scope: resolve(input.scope) } : {}),
|
|
115
|
+
...(input.cwd ? { cwd: resolve(input.cwd) } : {}),
|
|
116
|
+
...(input.log ? { log: resolve(input.log) } : {}),
|
|
117
|
+
...(owner ? { owner } : {}),
|
|
118
|
+
started_at: input.started_at ?? now,
|
|
119
|
+
registered_at: now,
|
|
120
|
+
last_active_at: now,
|
|
121
|
+
};
|
|
122
|
+
if (input.start_token !== undefined &&
|
|
123
|
+
verifyProcessGeneration(pid, input.start_token) !== "same")
|
|
124
|
+
throw new Error(`pid ${pid} generation could not be verified before registration`);
|
|
125
|
+
if (existsSync(path)) {
|
|
126
|
+
const existing = readRecord(path);
|
|
127
|
+
if (!existing)
|
|
128
|
+
throw new Error(`registration ${id} custody is unknown: invalid or older server record retained`);
|
|
129
|
+
if (sameProcessCustody(existing, record) && serverState(existing) === "running")
|
|
130
|
+
return existing;
|
|
131
|
+
if (existing.host !== hostname() || !allProcessesAbsent(existing))
|
|
132
|
+
throw new Error(`registration ${id} custody is held by a live or unverifiable server`);
|
|
133
|
+
}
|
|
134
|
+
writePrivateJsonAtomic(path, record);
|
|
135
|
+
return record;
|
|
136
|
+
});
|
|
117
137
|
}
|
|
118
138
|
/** Best-effort registration that never throws (for server start paths). */
|
|
119
139
|
export function tryRegisterServer(input, options) {
|
|
@@ -136,43 +156,106 @@ function callingOwner(coordRoot) {
|
|
|
136
156
|
return undefined;
|
|
137
157
|
}
|
|
138
158
|
}
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
* is removed, so a replacement that re-registered the same id survives.
|
|
142
|
-
*/
|
|
143
|
-
export function unregisterServer(id, options) {
|
|
159
|
+
/** Remove only the exact registration receipt observed by the caller. */
|
|
160
|
+
export function unregisterServer(record, options) {
|
|
144
161
|
const coordRoot = rootOf(options);
|
|
145
|
-
if (!coordRoot || !isValidServerId(id))
|
|
162
|
+
if (!coordRoot || !record || !isValidServerId(record.id))
|
|
146
163
|
return false;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
return withServerMutation(coordRoot, record.id, () => removeServerReceipt(coordRoot, record));
|
|
165
|
+
}
|
|
166
|
+
export function withServerRegistrationCustody(record, options, effect) {
|
|
167
|
+
const coordRoot = rootOf(options);
|
|
168
|
+
if (!coordRoot)
|
|
169
|
+
throw new Error("registration custody root is unavailable");
|
|
170
|
+
const lease = serverMutationLease(coordRoot, record.id);
|
|
171
|
+
try {
|
|
172
|
+
const current = readServer(record.id, { coordRoot });
|
|
173
|
+
if (!current ||
|
|
174
|
+
current.registration_id !== record.registration_id ||
|
|
175
|
+
!sameProcessCustody(current, record))
|
|
176
|
+
throw new Error(`registration ${record.id} custody changed`);
|
|
177
|
+
const result = effect();
|
|
178
|
+
if (result !== null &&
|
|
179
|
+
(typeof result === "object" || typeof result === "function") &&
|
|
180
|
+
typeof result.then === "function")
|
|
181
|
+
return Promise.resolve(result).finally(() => lease.release());
|
|
182
|
+
lease.release();
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
lease.release();
|
|
187
|
+
throw error;
|
|
152
188
|
}
|
|
153
|
-
|
|
189
|
+
}
|
|
190
|
+
function removeServerReceipt(coordRoot, record) {
|
|
191
|
+
const path = recordPath(coordRoot, record.id);
|
|
192
|
+
const current = readRecord(path);
|
|
193
|
+
if (!current ||
|
|
194
|
+
current.registration_id !== record.registration_id ||
|
|
195
|
+
!sameProcessCustody(current, record))
|
|
154
196
|
return false;
|
|
155
|
-
rmSync(path
|
|
197
|
+
rmSync(path);
|
|
156
198
|
return true;
|
|
157
199
|
}
|
|
200
|
+
function serverMutationLease(coordRoot, id) {
|
|
201
|
+
recordPath(coordRoot, id);
|
|
202
|
+
const scope = `server-registration-${id}`;
|
|
203
|
+
return acquireNoClobberLease({
|
|
204
|
+
path: join(serversDir(coordRoot), ".custody", id),
|
|
205
|
+
scope,
|
|
206
|
+
authoritySha256: createHash("sha256").update(scope).digest("hex"),
|
|
207
|
+
staleAfterMs: 1,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function withServerMutation(coordRoot, id, operation) {
|
|
211
|
+
const lease = serverMutationLease(coordRoot, id);
|
|
212
|
+
try {
|
|
213
|
+
return operation();
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
lease.release();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function sameProcessCustody(a, b) {
|
|
220
|
+
const helpers = (record) => [...new Set(record.pids ?? [])]
|
|
221
|
+
.sort((x, y) => x - y)
|
|
222
|
+
.map((pid) => [pid, record.pids_start_tokens?.[pid] ?? null]);
|
|
223
|
+
return (a.schema_version === SERVER_RECORD_SCHEMA_VERSION &&
|
|
224
|
+
b.schema_version === SERVER_RECORD_SCHEMA_VERSION &&
|
|
225
|
+
a.id === b.id &&
|
|
226
|
+
a.pid === b.pid &&
|
|
227
|
+
a.start_token === b.start_token &&
|
|
228
|
+
a.host === b.host &&
|
|
229
|
+
a.kind === b.kind &&
|
|
230
|
+
a.type === b.type &&
|
|
231
|
+
a.scope === b.scope &&
|
|
232
|
+
a.owner?.instance_id === b.owner?.instance_id &&
|
|
233
|
+
JSON.stringify(helpers(a)) === JSON.stringify(helpers(b)));
|
|
234
|
+
}
|
|
158
235
|
export function readServer(id, options) {
|
|
159
236
|
const coordRoot = rootOf(options);
|
|
160
237
|
if (!coordRoot || !isValidServerId(id))
|
|
161
238
|
return null;
|
|
162
|
-
|
|
239
|
+
const path = recordPath(coordRoot, id);
|
|
240
|
+
const record = readRecord(path);
|
|
241
|
+
if (!record && existsSync(path))
|
|
242
|
+
throw new Error(`server record ${id} is invalid or uses an older schema; retained without custody`);
|
|
243
|
+
return record;
|
|
163
244
|
}
|
|
164
245
|
/** Mark a server as used now. */
|
|
165
246
|
export function touchServer(id, options) {
|
|
166
247
|
const coordRoot = rootOf(options);
|
|
167
248
|
if (!coordRoot || !isValidServerId(id))
|
|
168
249
|
return;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
250
|
+
withServerMutation(coordRoot, id, () => {
|
|
251
|
+
const path = recordPath(coordRoot, id);
|
|
252
|
+
const record = readRecord(path);
|
|
253
|
+
if (!record)
|
|
254
|
+
return;
|
|
255
|
+
writePrivateJsonAtomic(path, {
|
|
256
|
+
...record,
|
|
257
|
+
last_active_at: (options?.now ?? new Date()).toISOString(),
|
|
258
|
+
});
|
|
176
259
|
});
|
|
177
260
|
}
|
|
178
261
|
/**
|
|
@@ -201,6 +284,8 @@ function readRecord(path) {
|
|
|
201
284
|
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
202
285
|
if (value?.schema_version !== SERVER_RECORD_SCHEMA_VERSION ||
|
|
203
286
|
typeof value.id !== "string" ||
|
|
287
|
+
typeof value.registration_id !== "string" ||
|
|
288
|
+
!/^[0-9a-f-]{36}$/.test(value.registration_id) ||
|
|
204
289
|
!Number.isSafeInteger(value.pid) ||
|
|
205
290
|
(value.kind !== "service" && value.kind !== "session"))
|
|
206
291
|
return null;
|
|
@@ -219,25 +304,42 @@ export function readServerRecords(options) {
|
|
|
219
304
|
return [];
|
|
220
305
|
return readdirSync(dir)
|
|
221
306
|
.filter((name) => name.endsWith(".json"))
|
|
222
|
-
.map((name) =>
|
|
223
|
-
|
|
307
|
+
.map((name) => {
|
|
308
|
+
const record = readRecord(join(dir, name));
|
|
309
|
+
if (!record)
|
|
310
|
+
throw new Error(`server record ${name} is invalid or uses an older schema; retained without custody`);
|
|
311
|
+
return record;
|
|
312
|
+
})
|
|
224
313
|
.sort((a, b) => a.kind.localeCompare(b.kind) || a.type.localeCompare(b.type) || a.id.localeCompare(b.id));
|
|
225
314
|
}
|
|
226
315
|
export function pidAlive(pid) {
|
|
316
|
+
return processPresence(pid) !== "absent";
|
|
317
|
+
}
|
|
318
|
+
function processPresence(pid, probePid) {
|
|
227
319
|
try {
|
|
320
|
+
if (probePid)
|
|
321
|
+
return probePid(pid) ? "present" : "absent";
|
|
228
322
|
process.kill(pid, 0);
|
|
229
|
-
return
|
|
323
|
+
return "present";
|
|
230
324
|
}
|
|
231
325
|
catch (error) {
|
|
232
|
-
|
|
326
|
+
const code = error.code;
|
|
327
|
+
return code === "ESRCH" ? "absent" : code === "EPERM" ? "present" : "unknown";
|
|
233
328
|
}
|
|
234
329
|
}
|
|
235
|
-
|
|
330
|
+
function allProcessesAbsent(record, probePid) {
|
|
331
|
+
return [record.pid, ...(record.pids ?? [])].every((pid) => processPresence(pid, probePid) === "absent");
|
|
332
|
+
}
|
|
333
|
+
export function serverState(record, readToken, probePid) {
|
|
236
334
|
if (record.host !== hostname())
|
|
237
335
|
return "other-host";
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
336
|
+
const presence = processPresence(record.pid, probePid);
|
|
337
|
+
if (presence === "absent")
|
|
338
|
+
return allProcessesAbsent(record, probePid) ? "dead" : "unknown";
|
|
339
|
+
if (presence === "unknown")
|
|
340
|
+
return "unknown";
|
|
341
|
+
const verdict = verifyProcessGeneration(record.pid, record.start_token, readToken);
|
|
342
|
+
return verdict === "same" ? "running" : verdict === "different" ? "dead" : "unknown";
|
|
241
343
|
}
|
|
242
344
|
function ownerReader(coordRoot, ownerStaleMs, nowMs) {
|
|
243
345
|
let view = null;
|
|
@@ -268,7 +370,6 @@ export function listServers(options) {
|
|
|
268
370
|
const now = options?.now ?? new Date();
|
|
269
371
|
const nowMs = now.getTime();
|
|
270
372
|
const ownerStaleMs = hours(options?.ownerStaleHours ?? SERVER_DEFAULT_OWNER_STALE_HOURS);
|
|
271
|
-
adoptRunningTunnels(coordRoot);
|
|
272
373
|
const records = readServerRecords({ coordRoot });
|
|
273
374
|
const ownerOf = ownerReader(coordRoot, ownerStaleMs, nowMs);
|
|
274
375
|
const states = records.map((record) => serverState(record));
|
|
@@ -278,8 +379,8 @@ export function listServers(options) {
|
|
|
278
379
|
const servers = [];
|
|
279
380
|
records.forEach((record, i) => {
|
|
280
381
|
const state = states[i];
|
|
281
|
-
if (state === "dead" && options?.prune !== false) {
|
|
282
|
-
unregisterServer(record
|
|
382
|
+
if (state === "dead" && allProcessesAbsent(record) && options?.prune !== false) {
|
|
383
|
+
unregisterServer(record, { coordRoot });
|
|
283
384
|
pruned.push(record.id);
|
|
284
385
|
return;
|
|
285
386
|
}
|
|
@@ -316,27 +417,6 @@ export function listServers(options) {
|
|
|
316
417
|
pruned,
|
|
317
418
|
};
|
|
318
419
|
}
|
|
319
|
-
/** Register running tunnels that have no current record. Returns adopted ids. */
|
|
320
|
-
export function adoptRunningTunnels(coordRoot) {
|
|
321
|
-
const adopted = [];
|
|
322
|
-
let states;
|
|
323
|
-
try {
|
|
324
|
-
states = listStates(coordRoot);
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
return adopted;
|
|
328
|
-
}
|
|
329
|
-
for (const state of states) {
|
|
330
|
-
if (!state.gate_pid || !pidAlive(state.gate_pid))
|
|
331
|
-
continue;
|
|
332
|
-
const id = tunnelServerId(state.name);
|
|
333
|
-
if (readServer(id, { coordRoot })?.pid === state.gate_pid)
|
|
334
|
-
continue;
|
|
335
|
-
if (tryRegisterServer(tunnelServerInput(state, coordRoot), { coordRoot }))
|
|
336
|
-
adopted.push(id);
|
|
337
|
-
}
|
|
338
|
-
return adopted;
|
|
339
|
-
}
|
|
340
420
|
/** Processes that browser automation and editors start; never project servers. */
|
|
341
421
|
const IGNORED_LISTENER_COMMANDS = [
|
|
342
422
|
/(^|\/)(chrome|chromium|chrome-headless-shell|headless_shell|msedge|firefox)( |$)/i,
|
|
@@ -350,10 +430,10 @@ const IGNORED_LISTENER_COMMANDS = [
|
|
|
350
430
|
* a recorded port.
|
|
351
431
|
*/
|
|
352
432
|
export function findUnregisteredListeners(coordRoot, servers) {
|
|
353
|
-
const
|
|
433
|
+
const registered = servers.filter((server) => server.record.host === hostname());
|
|
354
434
|
const covered = new Set();
|
|
355
435
|
const ports = new Set();
|
|
356
|
-
for (const { record } of
|
|
436
|
+
for (const { record } of registered) {
|
|
357
437
|
covered.add(record.pid);
|
|
358
438
|
for (const pid of record.pids ?? [])
|
|
359
439
|
covered.add(pid);
|
|
@@ -363,8 +443,9 @@ export function findUnregisteredListeners(coordRoot, servers) {
|
|
|
363
443
|
const results = [];
|
|
364
444
|
const seenPids = new Map();
|
|
365
445
|
for (const socket of listListeningSockets()) {
|
|
366
|
-
if (ports.has(socket.port) || socket.pid === process.pid)
|
|
446
|
+
if (ports.has(socket.port) || covered.has(socket.pid) || socket.pid === process.pid)
|
|
367
447
|
continue;
|
|
448
|
+
const startToken = processStartToken(socket.pid);
|
|
368
449
|
let inside = seenPids.get(socket.pid);
|
|
369
450
|
const info = readProcessInfo(socket.pid);
|
|
370
451
|
if (!info)
|
|
@@ -380,6 +461,7 @@ export function findUnregisteredListeners(coordRoot, servers) {
|
|
|
380
461
|
continue;
|
|
381
462
|
results.push({
|
|
382
463
|
pid: socket.pid,
|
|
464
|
+
...(startToken ? { start_token: startToken } : {}),
|
|
383
465
|
port: socket.port,
|
|
384
466
|
address: socket.address,
|
|
385
467
|
command: info.command.slice(0, 300),
|
|
@@ -430,72 +512,138 @@ export function planServerGc(views, policy) {
|
|
|
430
512
|
});
|
|
431
513
|
}
|
|
432
514
|
/**
|
|
433
|
-
* Stop
|
|
434
|
-
*
|
|
435
|
-
*
|
|
515
|
+
* Stop a verified server with SIGTERM, then SIGKILL after the grace period.
|
|
516
|
+
* Every signal targets one retained process handle, including auxiliary processes.
|
|
517
|
+
* Each target requires a fresh generation check. Unknown or different generations retain the record.
|
|
436
518
|
*/
|
|
437
519
|
export async function stopServer(record, options) {
|
|
438
520
|
const coordRoot = rootOf(options);
|
|
521
|
+
if (!coordRoot)
|
|
522
|
+
return {
|
|
523
|
+
id: record.id,
|
|
524
|
+
stopped: false,
|
|
525
|
+
method: "refused",
|
|
526
|
+
generation: "unknown",
|
|
527
|
+
detail: "registration custody root is unavailable",
|
|
528
|
+
};
|
|
529
|
+
let lease;
|
|
530
|
+
try {
|
|
531
|
+
lease = serverMutationLease(coordRoot, record.id);
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
return {
|
|
535
|
+
id: record.id,
|
|
536
|
+
stopped: false,
|
|
537
|
+
method: "refused",
|
|
538
|
+
generation: "unknown",
|
|
539
|
+
detail: "registration custody is unavailable",
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
const current = readServer(record.id, { coordRoot });
|
|
544
|
+
if (!current || current.registration_id !== record.registration_id)
|
|
545
|
+
return {
|
|
546
|
+
id: record.id,
|
|
547
|
+
stopped: false,
|
|
548
|
+
method: "refused",
|
|
549
|
+
generation: "unknown",
|
|
550
|
+
detail: "registration custody changed; retained current server",
|
|
551
|
+
};
|
|
552
|
+
return await stopServerWithCustody(record, coordRoot, options);
|
|
553
|
+
}
|
|
554
|
+
finally {
|
|
555
|
+
lease.release();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function stopServerWithCustody(record, coordRoot, options) {
|
|
439
559
|
const graceMs = options?.graceMs ?? 5_000;
|
|
560
|
+
const dependencies = options?.signalDependencies ?? {};
|
|
561
|
+
const targets = [
|
|
562
|
+
{ pid: record.pid, token: record.start_token },
|
|
563
|
+
...[...new Set(record.pids ?? [])]
|
|
564
|
+
.filter((pid) => pid !== record.pid)
|
|
565
|
+
.map((pid) => ({ pid, token: record.pids_start_tokens?.[pid] })),
|
|
566
|
+
];
|
|
567
|
+
const refuse = (pid, generation) => ({
|
|
568
|
+
id: record.id,
|
|
569
|
+
stopped: false,
|
|
570
|
+
method: "refused",
|
|
571
|
+
generation,
|
|
572
|
+
detail: `pid ${pid} generation is ${generation}; left the server record in place`,
|
|
573
|
+
});
|
|
440
574
|
const finish = (result) => {
|
|
441
575
|
if (result.stopped || result.method === "not-running")
|
|
442
|
-
|
|
576
|
+
removeServerReceipt(coordRoot, record);
|
|
443
577
|
return result;
|
|
444
578
|
};
|
|
445
|
-
|
|
446
|
-
if (state === "other-host")
|
|
579
|
+
if (record.host !== hostname())
|
|
447
580
|
return { id: record.id, stopped: false, method: "refused", detail: `runs on ${record.host}` };
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
581
|
+
const leaderPresence = processPresence(record.pid, dependencies.probePid);
|
|
582
|
+
if (leaderPresence === "unknown" ||
|
|
583
|
+
(leaderPresence === "absent" && !allProcessesAbsent(record, dependencies.probePid)))
|
|
584
|
+
return refuse(record.pid, "unknown");
|
|
585
|
+
// Check every recorded live process before sending the first signal.
|
|
586
|
+
for (const target of targets) {
|
|
587
|
+
const presence = processPresence(target.pid, dependencies.probePid);
|
|
588
|
+
if (presence === "absent")
|
|
589
|
+
continue;
|
|
590
|
+
if (presence === "unknown")
|
|
591
|
+
return refuse(target.pid, "unknown");
|
|
592
|
+
const verdict = verifyProcessGeneration(target.pid, target.token, dependencies.readToken);
|
|
593
|
+
if (verdict !== "same")
|
|
594
|
+
return refuse(target.pid, verdict);
|
|
455
595
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (
|
|
460
|
-
return
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
596
|
+
if (targets.every((target) => processPresence(target.pid, dependencies.probePid) === "absent"))
|
|
597
|
+
return finish({ id: record.id, stopped: false, method: "not-running" });
|
|
598
|
+
const current = readServer(record.id, { coordRoot });
|
|
599
|
+
if (!current || !sameProcessCustody(current, record))
|
|
600
|
+
return refuse(record.pid, "unknown");
|
|
601
|
+
for (const name of ["SIGTERM", "SIGKILL"]) {
|
|
602
|
+
for (const target of targets) {
|
|
603
|
+
const presence = processPresence(target.pid, dependencies.probePid);
|
|
604
|
+
if (presence === "absent")
|
|
605
|
+
continue;
|
|
606
|
+
if (presence === "unknown")
|
|
607
|
+
return refuse(target.pid, "unknown");
|
|
608
|
+
const result = signalIfSameGeneration(target.pid, target.token, name, dependencies);
|
|
609
|
+
if (!result.sent || result.verdict !== "same")
|
|
610
|
+
return refuse(target.pid, result.verdict === "same" ? "unknown" : result.verdict);
|
|
611
|
+
}
|
|
612
|
+
const observed = await waitForExit(targets, name === "SIGTERM" ? graceMs : 2_000, dependencies);
|
|
613
|
+
if (observed.refused)
|
|
614
|
+
return refuse(observed.refused.pid, observed.refused.verdict);
|
|
615
|
+
if (observed.exited)
|
|
616
|
+
return finish({ id: record.id, stopped: true, method: "signal", generation: "same" });
|
|
465
617
|
}
|
|
466
|
-
|
|
467
|
-
return finish({
|
|
618
|
+
return {
|
|
468
619
|
id: record.id,
|
|
469
|
-
stopped,
|
|
620
|
+
stopped: false,
|
|
470
621
|
method: "signal",
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
function signal(pid, name) {
|
|
475
|
-
// Detached servers lead their own process group; signal the group so a
|
|
476
|
-
// wrapper (npm, bunx) does not orphan the real listener.
|
|
477
|
-
try {
|
|
478
|
-
process.kill(-pid, name);
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
482
|
-
// Not a group leader, or already gone.
|
|
483
|
-
}
|
|
484
|
-
try {
|
|
485
|
-
process.kill(pid, name);
|
|
486
|
-
}
|
|
487
|
-
catch {
|
|
488
|
-
// Already gone.
|
|
489
|
-
}
|
|
622
|
+
generation: "same",
|
|
623
|
+
detail: "process survived SIGKILL",
|
|
624
|
+
};
|
|
490
625
|
}
|
|
491
|
-
async function
|
|
626
|
+
async function waitForExit(targets, waitMs, dependencies) {
|
|
492
627
|
const deadline = Date.now() + waitMs;
|
|
493
|
-
while (
|
|
494
|
-
|
|
495
|
-
|
|
628
|
+
while (true) {
|
|
629
|
+
let live = false;
|
|
630
|
+
for (const target of targets) {
|
|
631
|
+
const presence = processPresence(target.pid, dependencies.probePid);
|
|
632
|
+
if (presence === "absent")
|
|
633
|
+
continue;
|
|
634
|
+
if (presence === "unknown")
|
|
635
|
+
return { exited: false, refused: { pid: target.pid, verdict: "unknown" } };
|
|
636
|
+
const verdict = verifyProcessGeneration(target.pid, target.token, dependencies.readToken);
|
|
637
|
+
if (verdict !== "same")
|
|
638
|
+
return { exited: false, refused: { pid: target.pid, verdict } };
|
|
639
|
+
live = true;
|
|
640
|
+
}
|
|
641
|
+
if (!live)
|
|
642
|
+
return { exited: true };
|
|
643
|
+
if (Date.now() >= deadline)
|
|
644
|
+
return { exited: false };
|
|
496
645
|
await new Promise((r) => setTimeout(r, 100));
|
|
497
646
|
}
|
|
498
|
-
return serverState(record) !== "running";
|
|
499
647
|
}
|
|
500
648
|
/**
|
|
501
649
|
* The first free port in the configured range for `type`
|
|
@@ -510,7 +658,8 @@ export function allocateServerPort(type, options) {
|
|
|
510
658
|
return null;
|
|
511
659
|
const taken = listListeningPorts();
|
|
512
660
|
for (const record of readServerRecords({ coordRoot })) {
|
|
513
|
-
|
|
661
|
+
const state = serverState(record);
|
|
662
|
+
if (record.port && (state === "running" || state === "unknown"))
|
|
514
663
|
taken.add(record.port);
|
|
515
664
|
}
|
|
516
665
|
for (let port = range[0]; port <= range[1]; port++)
|
|
@@ -526,12 +675,15 @@ export function adoptServer(pid, options) {
|
|
|
526
675
|
const coordRoot = rootOf(options);
|
|
527
676
|
if (!coordRoot)
|
|
528
677
|
throw new Error("no Harnery project found (coordination root did not resolve)");
|
|
529
|
-
const report = listServers({ coordRoot, sample: false });
|
|
678
|
+
const report = (options?.dependencies?.listServers ?? listServers)({ coordRoot, sample: false });
|
|
530
679
|
const listeners = report.unregistered.filter((item) => item.pid === pid);
|
|
531
680
|
if (!listeners.length)
|
|
532
681
|
throw new Error(`pid ${pid} is not an unregistered listener inside this project`);
|
|
533
682
|
const ports = listeners.map((item) => item.port).sort((a, b) => a - b);
|
|
534
683
|
const first = listeners[0];
|
|
684
|
+
const generation = verifyProcessGeneration(pid, first.start_token, options?.dependencies?.readToken);
|
|
685
|
+
if (generation !== "same")
|
|
686
|
+
throw new Error(`pid ${pid} generation is ${generation}; adoption refused`);
|
|
535
687
|
const type = options?.type ?? "adopted";
|
|
536
688
|
const scope = options?.scope ?? first.cwd ?? undefined;
|
|
537
689
|
const started = processStartedAt(pid);
|
|
@@ -543,6 +695,7 @@ export function adoptServer(pid, options) {
|
|
|
543
695
|
url: `http://127.0.0.1:${ports[0]}/`,
|
|
544
696
|
port: ports[0],
|
|
545
697
|
pid,
|
|
698
|
+
start_token: first.start_token,
|
|
546
699
|
...(scope ? { scope } : {}),
|
|
547
700
|
...(first.cwd ? { cwd: first.cwd } : {}),
|
|
548
701
|
...(options?.owner === "none" ? { owner: null } : {}),
|
|
@@ -1,14 +1,14 @@
|
|
|
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. Missing original witnesses remain unknown and cannot authorize actions.
|
|
8
6
|
*/
|
|
9
|
-
import { type TunnelState } from "../../lib/tunnel/state.js";
|
|
10
|
-
import type
|
|
7
|
+
import { type TunnelReadinessVerifier, type TunnelState } from "../../lib/tunnel/state.js";
|
|
8
|
+
import { type RegisterServerInput, type ServerOptions } from "./index.js";
|
|
9
|
+
/** Verify only witnesses saved by the original tunnel registration. */
|
|
10
|
+
export declare function createTunnelReadinessVerifier(options: ServerOptions): TunnelReadinessVerifier;
|
|
11
11
|
export declare function tunnelServerId(name: string): string;
|
|
12
|
-
/**
|
|
12
|
+
/** Register the new gate; saved helper PIDs have no generation witnesses. */
|
|
13
13
|
export declare function tunnelServerInput(state: TunnelState, coordRoot: string): RegisterServerInput;
|
|
14
14
|
//# sourceMappingURL=tunnels.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnels.d.ts","sourceRoot":"","sources":["../../../src/core/servers/tunnels.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"tunnels.d.ts","sourceRoot":"","sources":["../../../src/core/servers/tunnels.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,KAAK,mBAAmB,EAExB,KAAK,aAAa,EAGnB,MAAM,YAAY,CAAC;AAEpB,uEAAuE;AACvE,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,aAAa,GAAG,uBAAuB,CAiC7F;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,mBAAmB,CAmB5F"}
|