harnery 0.45.0 → 0.45.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.js +0 -1
- package/dist/commands/files.d.ts +2 -2
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +4 -7
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +17 -11
- package/dist/commands/tunnel.d.ts +0 -15
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +153 -453
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +6 -45
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +0 -18
- package/dist/core/agents/live-authority-v3.d.ts +0 -1
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +0 -11
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +7 -28
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +14 -25
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +0 -5
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +2 -7
- package/dist/core/agents/state/proc-start.d.ts +20 -31
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +37 -70
- package/dist/core/agents/state/service-process.d.ts +61 -0
- package/dist/core/agents/state/service-process.d.ts.map +1 -0
- package/dist/core/agents/state/service-process.js +143 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +70 -180
- package/dist/core/artifacts/activity.d.ts +4 -1
- package/dist/core/artifacts/activity.d.ts.map +1 -1
- package/dist/core/artifacts/activity.js +8 -2
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +1 -2
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +38 -12
- package/dist/core/context/index.d.ts +6 -4
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +194 -256
- package/dist/core/context/observation.d.ts +9 -9
- package/dist/core/context/observation.d.ts.map +1 -1
- package/dist/core/context/observation.js +37 -6
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +0 -29
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +64 -77
- package/dist/core/events/v3/runtime-build.d.ts +14 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +44 -0
- package/dist/core/governor/index.d.ts +2 -1
- package/dist/core/governor/index.d.ts.map +1 -1
- package/dist/core/governor/index.js +2 -1
- package/dist/core/governor/service-read.d.ts +3 -10
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +10 -94
- package/dist/core/governor/service.d.ts +3 -7
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +17 -182
- package/dist/core/hooks/cli.js +78 -48
- package/dist/core/hooks/resolve/owner.js +2 -2
- package/dist/core/resources/sampler.js +2 -2
- package/dist/core/resources/service-status.d.ts +2 -14
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +3 -23
- package/dist/core/resources/service.d.ts +3 -7
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +12 -101
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +3 -2
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +9 -2
- package/dist/core/servers/index.d.ts +11 -10
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +82 -192
- 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 +2 -45
- package/dist/core/storage/durable-history.js +56 -22
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +73 -17
- package/dist/core/supervisor/service.d.ts +4 -7
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +23 -122
- package/dist/core/supervisor/services.js +2 -2
- package/dist/core/supervisor/status.d.ts +3 -17
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +7 -70
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +23 -66
- package/dist/core/workflow/index.d.ts +3 -4
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -2
- package/dist/core/workflow/live-session-v3.d.ts +0 -6
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +5 -126
- package/dist/core/workflow/proof.d.ts +4 -4
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +34 -86
- package/dist/core/workflow/resume-lock.d.ts +17 -0
- package/dist/core/workflow/resume-lock.d.ts.map +1 -0
- package/dist/core/workflow/resume-lock.js +158 -0
- package/dist/core/workflow/run-state.d.ts +0 -21
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -174
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +0 -19
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +0 -13
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +0 -19
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +0 -14
- package/dist/core/workflow/types.d.ts +11 -54
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.d.ts +4 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -1
- package/dist/core/workflow/workspaces/local-git.js +91 -210
- package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/ownership.js +3 -130
- package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +0 -4
- package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +1 -4
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +0 -4
- package/dist/lib/tunnel/state.d.ts +6 -13
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +12 -58
- package/package.json +1 -6
- package/src/commander.ts +0 -10
- package/src/commands/files.ts +5 -13
- package/src/commands/servers.ts +23 -12
- package/src/commands/tunnel.ts +166 -533
- package/src/commands/workflow.ts +8 -58
- package/src/core/agents/coord-client.ts +0 -20
- package/src/core/agents/live-authority-v3.ts +0 -17
- package/src/core/agents/live-lifecycle-v3.ts +14 -52
- package/src/core/agents/state/heartbeat-writer.ts +12 -29
- package/src/core/agents/state/live-coordination-writer.ts +0 -29
- package/src/core/agents/state/pidmap.ts +2 -15
- package/src/core/agents/state/proc-start.ts +41 -69
- package/src/core/agents/state/service-process.ts +217 -0
- package/src/core/agents/state/stale-sweep.ts +82 -247
- package/src/core/artifacts/activity.ts +9 -2
- package/src/core/artifacts/delivery-card.ts +1 -7
- package/src/core/artifacts/index.ts +45 -12
- package/src/core/context/index.ts +207 -272
- package/src/core/context/observation.ts +40 -9
- package/src/core/events/v3/authority-outbox.ts +0 -56
- package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
- package/src/core/events/v3/producers/recorder.ts +68 -94
- package/src/core/events/v3/runtime-build.ts +62 -0
- package/src/core/governor/index.ts +1 -2
- package/src/core/governor/service-read.ts +15 -114
- package/src/core/governor/service.ts +36 -220
- package/src/core/hooks/cli.ts +85 -47
- package/src/core/hooks/resolve/owner.ts +2 -2
- package/src/core/resources/sampler.ts +3 -2
- package/src/core/resources/service-status.ts +6 -35
- package/src/core/resources/service.ts +23 -133
- package/src/core/resources/status.ts +3 -2
- package/src/core/semantic/service.ts +8 -2
- package/src/core/servers/index.ts +86 -228
- package/src/core/servers/tunnels.ts +3 -55
- package/src/core/storage/durable-history.ts +52 -20
- package/src/core/storage/segments.ts +79 -17
- package/src/core/supervisor/service.ts +33 -155
- package/src/core/supervisor/services.ts +2 -2
- package/src/core/supervisor/status.ts +10 -85
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +22 -74
- package/src/core/workflow/index.ts +2 -13
- package/src/core/workflow/live-session-v3.ts +5 -146
- package/src/core/workflow/proof.ts +40 -104
- package/src/core/workflow/resume-lock.ts +179 -0
- package/src/core/workflow/run-state.ts +3 -207
- package/src/core/workflow/spawn-claude.ts +0 -18
- package/src/core/workflow/spawn-codex.ts +0 -13
- package/src/core/workflow/spawn-cursor.ts +0 -18
- package/src/core/workflow/spawn-opencode.ts +0 -14
- package/src/core/workflow/types.ts +11 -41
- package/src/core/workflow/workspaces/leases.ts +1 -1
- package/src/core/workflow/workspaces/local-git.ts +120 -239
- package/src/core/workflow/workspaces/ownership.ts +3 -158
- package/src/core/workflow/workspaces/reclaim.ts +3 -9
- package/src/core/workflow/workspaces/recovery.ts +1 -1
- package/src/core/workflow/workspaces/state.ts +1 -5
- package/src/lib/identities/assume.ts +0 -4
- package/src/lib/tunnel/state.ts +15 -70
- package/dist/commands/turn-rollback.d.ts +0 -5
- package/dist/commands/turn-rollback.d.ts.map +0 -1
- package/dist/commands/turn-rollback.js +0 -76
- package/dist/core/agents/state/process-handle.d.ts +0 -26
- package/dist/core/agents/state/process-handle.d.ts.map +0 -1
- package/dist/core/agents/state/process-handle.js +0 -121
- package/dist/core/agents/state/unchanged-file.d.ts +0 -44
- package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
- package/dist/core/agents/state/unchanged-file.js +0 -390
- package/dist/core/events/v3/generation-custody.d.ts +0 -5
- package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
- package/dist/core/events/v3/generation-custody.js +0 -50
- package/dist/core/turn-rollback/content.d.ts +0 -53
- package/dist/core/turn-rollback/content.d.ts.map +0 -1
- package/dist/core/turn-rollback/content.js +0 -396
- package/dist/core/turn-rollback/index.d.ts +0 -72
- package/dist/core/turn-rollback/index.d.ts.map +0 -1
- package/dist/core/turn-rollback/index.js +0 -443
- package/dist/core/workflow/failure-evidence.d.ts +0 -16
- package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
- package/dist/core/workflow/failure-evidence.js +0 -114
- package/dist/core/workflow/recovery-audit.d.ts +0 -30
- package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
- package/dist/core/workflow/recovery-audit.js +0 -116
- package/src/commands/turn-rollback.ts +0 -111
- package/src/core/agents/state/process-handle.ts +0 -165
- package/src/core/agents/state/unchanged-file.ts +0 -456
- package/src/core/events/v3/generation-custody.ts +0 -59
- package/src/core/turn-rollback/content.ts +0 -499
- package/src/core/turn-rollback/index.ts +0 -580
- package/src/core/workflow/failure-evidence.ts +0 -141
- package/src/core/workflow/recovery-audit.ts +0 -181
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* automatically.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { createHash
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
24
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
25
25
|
import { hostname } from "node:os";
|
|
26
26
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -41,7 +41,6 @@ import {
|
|
|
41
41
|
import { liveInstanceIdV3 } from "../events/v3/live-route-observer.ts";
|
|
42
42
|
import { resolveOwner } from "../hooks/resolve/owner.ts";
|
|
43
43
|
import { writePrivateJsonAtomic } from "../storage/atomic-json.ts";
|
|
44
|
-
import { acquireNoClobberLease } from "../workflow/workspaces/leases.ts";
|
|
45
44
|
import {
|
|
46
45
|
ancestry,
|
|
47
46
|
establishedConnectionCounts,
|
|
@@ -53,6 +52,7 @@ import {
|
|
|
53
52
|
scanSupport,
|
|
54
53
|
} from "./net.ts";
|
|
55
54
|
|
|
55
|
+
// Host supervisors verify and signal their own children with the same checks.
|
|
56
56
|
export {
|
|
57
57
|
processStartToken,
|
|
58
58
|
signalIfSameGeneration,
|
|
@@ -74,9 +74,9 @@ export {
|
|
|
74
74
|
readProcessInfo,
|
|
75
75
|
scanSupport,
|
|
76
76
|
} from "./net.ts";
|
|
77
|
-
export {
|
|
77
|
+
export { tunnelServerId, tunnelServerInput } from "./tunnels.ts";
|
|
78
78
|
|
|
79
|
-
export const SERVER_RECORD_SCHEMA_VERSION =
|
|
79
|
+
export const SERVER_RECORD_SCHEMA_VERSION = 1;
|
|
80
80
|
export const SERVER_DEFAULT_IDLE_HOURS = 2;
|
|
81
81
|
export const SERVER_DEFAULT_OWNER_STALE_HOURS = 24;
|
|
82
82
|
const TOUCH_INTERVAL_MS = 60_000;
|
|
@@ -86,7 +86,6 @@ export type ServerKind = "service" | "session";
|
|
|
86
86
|
export interface ServerRecord {
|
|
87
87
|
schema_version: typeof SERVER_RECORD_SCHEMA_VERSION;
|
|
88
88
|
id: string;
|
|
89
|
-
registration_id: string;
|
|
90
89
|
kind: ServerKind;
|
|
91
90
|
/** Free-form family name: `preview`, `tunnel`, `dashboard`, ... */
|
|
92
91
|
type: string;
|
|
@@ -217,57 +216,39 @@ export function registerServer(
|
|
|
217
216
|
)
|
|
218
217
|
throw new Error(`invalid server port: ${input.port}`);
|
|
219
218
|
const id = input.id ?? serverId(input.type, input.scope);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
input.start_token !== undefined &&
|
|
254
|
-
verifyProcessGeneration(pid, input.start_token) !== "same"
|
|
255
|
-
)
|
|
256
|
-
throw new Error(`pid ${pid} generation could not be verified before registration`);
|
|
257
|
-
if (existsSync(path)) {
|
|
258
|
-
const existing = readRecord(path);
|
|
259
|
-
if (!existing)
|
|
260
|
-
throw new Error(
|
|
261
|
-
`registration ${id} custody is unknown: invalid or older server record retained`,
|
|
262
|
-
);
|
|
263
|
-
if (sameProcessCustody(existing, record) && serverState(existing) === "running")
|
|
264
|
-
return existing;
|
|
265
|
-
if (existing.host !== hostname() || !allProcessesAbsent(existing))
|
|
266
|
-
throw new Error(`registration ${id} custody is held by a live or unverifiable server`);
|
|
267
|
-
}
|
|
268
|
-
writePrivateJsonAtomic(path, record);
|
|
269
|
-
return record;
|
|
270
|
-
});
|
|
219
|
+
const now = new Date().toISOString();
|
|
220
|
+
const startToken = input.start_token ?? processStartToken(pid);
|
|
221
|
+
const pids = [...new Set(input.pids?.filter((p) => Number.isSafeInteger(p) && p > 0))];
|
|
222
|
+
const pidsStartTokens: Record<string, string> = {};
|
|
223
|
+
for (const auxiliaryPid of pids) {
|
|
224
|
+
const token = processStartToken(auxiliaryPid);
|
|
225
|
+
if (token) pidsStartTokens[auxiliaryPid] = token;
|
|
226
|
+
}
|
|
227
|
+
const owner = input.owner === undefined ? callingOwner(coordRoot) : (input.owner ?? undefined);
|
|
228
|
+
const record: ServerRecord = {
|
|
229
|
+
schema_version: SERVER_RECORD_SCHEMA_VERSION,
|
|
230
|
+
id,
|
|
231
|
+
kind: input.kind,
|
|
232
|
+
type: input.type,
|
|
233
|
+
label: input.label ?? id,
|
|
234
|
+
...(input.url ? { url: input.url } : {}),
|
|
235
|
+
...(input.port ? { port: input.port } : {}),
|
|
236
|
+
pid,
|
|
237
|
+
...(startToken ? { start_token: startToken } : {}),
|
|
238
|
+
...(pids.length ? { pids, pids_start_tokens: pidsStartTokens } : {}),
|
|
239
|
+
host: hostname(),
|
|
240
|
+
...(input.scope ? { scope: resolve(input.scope) } : {}),
|
|
241
|
+
...(input.cwd ? { cwd: resolve(input.cwd) } : {}),
|
|
242
|
+
...(input.log ? { log: resolve(input.log) } : {}),
|
|
243
|
+
...(owner ? { owner } : {}),
|
|
244
|
+
started_at: input.started_at ?? now,
|
|
245
|
+
registered_at: now,
|
|
246
|
+
last_active_at: now,
|
|
247
|
+
};
|
|
248
|
+
if (input.start_token !== undefined && verifyProcessGeneration(pid, input.start_token) !== "same")
|
|
249
|
+
throw new Error(`pid ${pid} generation could not be verified before registration`);
|
|
250
|
+
writePrivateJsonAtomic(recordPath(coordRoot, id), record);
|
|
251
|
+
return record;
|
|
271
252
|
}
|
|
272
253
|
|
|
273
254
|
/** Best-effort registration that never throws (for server start paths). */
|
|
@@ -293,132 +274,39 @@ function callingOwner(coordRoot: string): ServerRecord["owner"] {
|
|
|
293
274
|
}
|
|
294
275
|
}
|
|
295
276
|
|
|
296
|
-
/**
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Hold the exact registration receipt until a synchronous or asynchronous effect settles. */
|
|
304
|
-
export function withServerRegistrationCustody<T>(
|
|
305
|
-
record: ServerRecord,
|
|
306
|
-
options: ServerOptions,
|
|
307
|
-
effect: () => PromiseLike<T>,
|
|
308
|
-
): Promise<T>;
|
|
309
|
-
export function withServerRegistrationCustody<T>(
|
|
310
|
-
record: ServerRecord,
|
|
311
|
-
options: ServerOptions,
|
|
312
|
-
effect: () => T,
|
|
313
|
-
): T;
|
|
314
|
-
export function withServerRegistrationCustody<T>(
|
|
315
|
-
record: ServerRecord,
|
|
316
|
-
options: ServerOptions,
|
|
317
|
-
effect: () => T | PromiseLike<T>,
|
|
318
|
-
): T | Promise<T> {
|
|
277
|
+
/**
|
|
278
|
+
* Remove a server's record. With `pid`, only a record still naming that pid
|
|
279
|
+
* is removed, so a replacement that re-registered the same id survives.
|
|
280
|
+
*/
|
|
281
|
+
export function unregisterServer(id: string, options?: ServerOptions & { pid?: number }): boolean {
|
|
319
282
|
const coordRoot = rootOf(options);
|
|
320
|
-
if (!coordRoot
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
!current ||
|
|
326
|
-
current.registration_id !== record.registration_id ||
|
|
327
|
-
!sameProcessCustody(current, record)
|
|
328
|
-
)
|
|
329
|
-
throw new Error(`registration ${record.id} custody changed`);
|
|
330
|
-
const result = effect();
|
|
331
|
-
if (
|
|
332
|
-
result !== null &&
|
|
333
|
-
(typeof result === "object" || typeof result === "function") &&
|
|
334
|
-
typeof (result as PromiseLike<T>).then === "function"
|
|
335
|
-
)
|
|
336
|
-
return Promise.resolve(result).finally(() => lease.release());
|
|
337
|
-
lease.release();
|
|
338
|
-
return result as T;
|
|
339
|
-
} catch (error) {
|
|
340
|
-
lease.release();
|
|
341
|
-
throw error;
|
|
283
|
+
if (!coordRoot || !isValidServerId(id)) return false;
|
|
284
|
+
const path = recordPath(coordRoot, id);
|
|
285
|
+
if (options?.pid !== undefined) {
|
|
286
|
+
const record = readRecord(path);
|
|
287
|
+
if (record && record.pid !== options.pid) return false;
|
|
342
288
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
function removeServerReceipt(coordRoot: string, record: ServerRecord): boolean {
|
|
346
|
-
const path = recordPath(coordRoot, record.id);
|
|
347
|
-
const current = readRecord(path);
|
|
348
|
-
if (
|
|
349
|
-
!current ||
|
|
350
|
-
current.registration_id !== record.registration_id ||
|
|
351
|
-
!sameProcessCustody(current, record)
|
|
352
|
-
)
|
|
353
|
-
return false;
|
|
354
|
-
rmSync(path);
|
|
289
|
+
if (!existsSync(path)) return false;
|
|
290
|
+
rmSync(path, { force: true });
|
|
355
291
|
return true;
|
|
356
292
|
}
|
|
357
293
|
|
|
358
|
-
function serverMutationLease(coordRoot: string, id: string) {
|
|
359
|
-
recordPath(coordRoot, id);
|
|
360
|
-
const scope = `server-registration-${id}`;
|
|
361
|
-
return acquireNoClobberLease({
|
|
362
|
-
path: join(serversDir(coordRoot), ".custody", id),
|
|
363
|
-
scope,
|
|
364
|
-
authoritySha256: createHash("sha256").update(scope).digest("hex"),
|
|
365
|
-
staleAfterMs: 1,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function withServerMutation<T>(coordRoot: string, id: string, operation: () => T): T {
|
|
370
|
-
const lease = serverMutationLease(coordRoot, id);
|
|
371
|
-
try {
|
|
372
|
-
return operation();
|
|
373
|
-
} finally {
|
|
374
|
-
lease.release();
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function sameProcessCustody(a: ServerRecord, b: ServerRecord): boolean {
|
|
379
|
-
const helpers = (record: ServerRecord) =>
|
|
380
|
-
[...new Set(record.pids ?? [])]
|
|
381
|
-
.sort((x, y) => x - y)
|
|
382
|
-
.map((pid) => [pid, record.pids_start_tokens?.[pid] ?? null]);
|
|
383
|
-
return (
|
|
384
|
-
a.schema_version === SERVER_RECORD_SCHEMA_VERSION &&
|
|
385
|
-
b.schema_version === SERVER_RECORD_SCHEMA_VERSION &&
|
|
386
|
-
a.id === b.id &&
|
|
387
|
-
a.pid === b.pid &&
|
|
388
|
-
a.start_token === b.start_token &&
|
|
389
|
-
a.host === b.host &&
|
|
390
|
-
a.kind === b.kind &&
|
|
391
|
-
a.type === b.type &&
|
|
392
|
-
a.scope === b.scope &&
|
|
393
|
-
a.owner?.instance_id === b.owner?.instance_id &&
|
|
394
|
-
JSON.stringify(helpers(a)) === JSON.stringify(helpers(b))
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
294
|
export function readServer(id: string, options?: ServerOptions): ServerRecord | null {
|
|
399
295
|
const coordRoot = rootOf(options);
|
|
400
296
|
if (!coordRoot || !isValidServerId(id)) return null;
|
|
401
|
-
|
|
402
|
-
const record = readRecord(path);
|
|
403
|
-
if (!record && existsSync(path))
|
|
404
|
-
throw new Error(
|
|
405
|
-
`server record ${id} is invalid or uses an older schema; retained without custody`,
|
|
406
|
-
);
|
|
407
|
-
return record;
|
|
297
|
+
return readRecord(recordPath(coordRoot, id));
|
|
408
298
|
}
|
|
409
299
|
|
|
410
300
|
/** Mark a server as used now. */
|
|
411
301
|
export function touchServer(id: string, options?: ServerOptions & { now?: Date }): void {
|
|
412
302
|
const coordRoot = rootOf(options);
|
|
413
303
|
if (!coordRoot || !isValidServerId(id)) return;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
last_active_at: (options?.now ?? new Date()).toISOString(),
|
|
421
|
-
});
|
|
304
|
+
const path = recordPath(coordRoot, id);
|
|
305
|
+
const record = readRecord(path);
|
|
306
|
+
if (!record) return;
|
|
307
|
+
writePrivateJsonAtomic(path, {
|
|
308
|
+
...record,
|
|
309
|
+
last_active_at: (options?.now ?? new Date()).toISOString(),
|
|
422
310
|
});
|
|
423
311
|
}
|
|
424
312
|
|
|
@@ -447,8 +335,6 @@ function readRecord(path: string): ServerRecord | null {
|
|
|
447
335
|
if (
|
|
448
336
|
value?.schema_version !== SERVER_RECORD_SCHEMA_VERSION ||
|
|
449
337
|
typeof value.id !== "string" ||
|
|
450
|
-
typeof value.registration_id !== "string" ||
|
|
451
|
-
!/^[0-9a-f-]{36}$/.test(value.registration_id) ||
|
|
452
338
|
!Number.isSafeInteger(value.pid) ||
|
|
453
339
|
(value.kind !== "service" && value.kind !== "session")
|
|
454
340
|
)
|
|
@@ -466,14 +352,8 @@ export function readServerRecords(options?: ServerOptions): ServerRecord[] {
|
|
|
466
352
|
if (!existsSync(dir)) return [];
|
|
467
353
|
return readdirSync(dir)
|
|
468
354
|
.filter((name) => name.endsWith(".json"))
|
|
469
|
-
.map((name) =>
|
|
470
|
-
|
|
471
|
-
if (!record)
|
|
472
|
-
throw new Error(
|
|
473
|
-
`server record ${name} is invalid or uses an older schema; retained without custody`,
|
|
474
|
-
);
|
|
475
|
-
return record;
|
|
476
|
-
})
|
|
355
|
+
.map((name) => readRecord(join(dir, name)))
|
|
356
|
+
.filter((record): record is ServerRecord => record !== null)
|
|
477
357
|
.sort(
|
|
478
358
|
(a, b) =>
|
|
479
359
|
a.kind.localeCompare(b.kind) || a.type.localeCompare(b.type) || a.id.localeCompare(b.id),
|
|
@@ -567,7 +447,7 @@ export function listServers(options?: ListServersOptions): ServersReport {
|
|
|
567
447
|
records.forEach((record, i) => {
|
|
568
448
|
const state = states[i]!;
|
|
569
449
|
if (state === "dead" && allProcessesAbsent(record) && options?.prune !== false) {
|
|
570
|
-
unregisterServer(record, { coordRoot });
|
|
450
|
+
unregisterServer(record.id, { coordRoot, pid: record.pid });
|
|
571
451
|
pruned.push(record.id);
|
|
572
452
|
return;
|
|
573
453
|
}
|
|
@@ -722,8 +602,8 @@ export interface ServerProcessDependencies extends ProcessGenerationSignalDepend
|
|
|
722
602
|
|
|
723
603
|
/**
|
|
724
604
|
* Stop a verified server with SIGTERM, then SIGKILL after the grace period.
|
|
725
|
-
*
|
|
726
|
-
*
|
|
605
|
+
* Each signal, including the process group and auxiliary processes, requires
|
|
606
|
+
* a fresh generation check. Unknown or different generations retain the record.
|
|
727
607
|
*/
|
|
728
608
|
export async function stopServer(
|
|
729
609
|
record: ServerRecord,
|
|
@@ -733,47 +613,6 @@ export async function stopServer(
|
|
|
733
613
|
},
|
|
734
614
|
): Promise<StopResult> {
|
|
735
615
|
const coordRoot = rootOf(options);
|
|
736
|
-
if (!coordRoot)
|
|
737
|
-
return {
|
|
738
|
-
id: record.id,
|
|
739
|
-
stopped: false,
|
|
740
|
-
method: "refused",
|
|
741
|
-
generation: "unknown",
|
|
742
|
-
detail: "registration custody root is unavailable",
|
|
743
|
-
};
|
|
744
|
-
let lease: ReturnType<typeof serverMutationLease>;
|
|
745
|
-
try {
|
|
746
|
-
lease = serverMutationLease(coordRoot, record.id);
|
|
747
|
-
} catch {
|
|
748
|
-
return {
|
|
749
|
-
id: record.id,
|
|
750
|
-
stopped: false,
|
|
751
|
-
method: "refused",
|
|
752
|
-
generation: "unknown",
|
|
753
|
-
detail: "registration custody is unavailable",
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
try {
|
|
757
|
-
const current = readServer(record.id, { coordRoot });
|
|
758
|
-
if (!current || current.registration_id !== record.registration_id)
|
|
759
|
-
return {
|
|
760
|
-
id: record.id,
|
|
761
|
-
stopped: false,
|
|
762
|
-
method: "refused",
|
|
763
|
-
generation: "unknown",
|
|
764
|
-
detail: "registration custody changed; retained current server",
|
|
765
|
-
};
|
|
766
|
-
return await stopServerWithCustody(record, coordRoot, options);
|
|
767
|
-
} finally {
|
|
768
|
-
lease.release();
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
async function stopServerWithCustody(
|
|
773
|
-
record: ServerRecord,
|
|
774
|
-
coordRoot: string,
|
|
775
|
-
options?: ServerOptions & { graceMs?: number; signalDependencies?: ServerProcessDependencies },
|
|
776
|
-
): Promise<StopResult> {
|
|
777
616
|
const graceMs = options?.graceMs ?? 5_000;
|
|
778
617
|
const dependencies = options?.signalDependencies ?? {};
|
|
779
618
|
const targets = [
|
|
@@ -790,7 +629,8 @@ async function stopServerWithCustody(
|
|
|
790
629
|
detail: `pid ${pid} generation is ${generation}; left the server record in place`,
|
|
791
630
|
});
|
|
792
631
|
const finish = (result: StopResult): StopResult => {
|
|
793
|
-
if (result.stopped || result.method === "not-running")
|
|
632
|
+
if (result.stopped || result.method === "not-running")
|
|
633
|
+
unregisterServer(record.id, { coordRoot, pid: record.pid });
|
|
794
634
|
return result;
|
|
795
635
|
};
|
|
796
636
|
if (record.host !== hostname())
|
|
@@ -812,16 +652,16 @@ async function stopServerWithCustody(
|
|
|
812
652
|
if (targets.every((target) => processPresence(target.pid, dependencies.probePid) === "absent"))
|
|
813
653
|
return finish({ id: record.id, stopped: false, method: "not-running" });
|
|
814
654
|
|
|
815
|
-
const current = readServer(record.id, { coordRoot });
|
|
816
|
-
if (!current || !sameProcessCustody(current, record)) return refuse(record.pid, "unknown");
|
|
817
655
|
for (const name of ["SIGTERM", "SIGKILL"] as const) {
|
|
818
656
|
for (const target of targets) {
|
|
819
657
|
const presence = processPresence(target.pid, dependencies.probePid);
|
|
820
658
|
if (presence === "absent") continue;
|
|
821
659
|
if (presence === "unknown") return refuse(target.pid, "unknown");
|
|
822
|
-
const result =
|
|
823
|
-
|
|
824
|
-
|
|
660
|
+
const result =
|
|
661
|
+
target.pid === record.pid
|
|
662
|
+
? signalServerLeader(target.pid, target.token, name, dependencies)
|
|
663
|
+
: signalIfSameGeneration(target.pid, target.token, name, dependencies);
|
|
664
|
+
if (result.verdict !== "same") return refuse(target.pid, result.verdict);
|
|
825
665
|
}
|
|
826
666
|
const observed = await waitForExit(targets, name === "SIGTERM" ? graceMs : 2_000, dependencies);
|
|
827
667
|
if (observed.refused) return refuse(observed.refused.pid, observed.refused.verdict);
|
|
@@ -837,6 +677,24 @@ async function stopServerWithCustody(
|
|
|
837
677
|
};
|
|
838
678
|
}
|
|
839
679
|
|
|
680
|
+
function signalServerLeader(
|
|
681
|
+
pid: number,
|
|
682
|
+
token: string | undefined,
|
|
683
|
+
name: NodeJS.Signals,
|
|
684
|
+
dependencies: ServerProcessDependencies,
|
|
685
|
+
): { sent: boolean; verdict: ProcessGenerationVerdict } {
|
|
686
|
+
// The leader's generation authorizes its group. A failed group signal must
|
|
687
|
+
// not reuse that observation for the direct-PID fallback.
|
|
688
|
+
const kill = dependencies.kill ?? process.kill;
|
|
689
|
+
const group = signalIfSameGeneration(pid, token, name, {
|
|
690
|
+
readToken: dependencies.readToken,
|
|
691
|
+
kill: (leader, signal) => kill(-leader, signal),
|
|
692
|
+
});
|
|
693
|
+
return group.sent || group.verdict !== "same"
|
|
694
|
+
? group
|
|
695
|
+
: signalIfSameGeneration(pid, token, name, dependencies);
|
|
696
|
+
}
|
|
697
|
+
|
|
840
698
|
async function waitForExit(
|
|
841
699
|
targets: Array<{ pid: number; token: string | undefined }>,
|
|
842
700
|
waitMs: number,
|
|
@@ -2,59 +2,12 @@
|
|
|
2
2
|
* Tunnels as registry services.
|
|
3
3
|
*
|
|
4
4
|
* `tunnel up` and `tunnel reload` register the new gate; `tunnel down` removes
|
|
5
|
-
* its record.
|
|
5
|
+
* its record. Older tunnels need an explicit adoption of an observed listener.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { hostname } from "node:os";
|
|
9
8
|
import { resolve } from "node:path";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
type TunnelReadinessVerifier,
|
|
13
|
-
type TunnelState,
|
|
14
|
-
} from "../../lib/tunnel/state.ts";
|
|
15
|
-
import {
|
|
16
|
-
type RegisterServerInput,
|
|
17
|
-
readServer,
|
|
18
|
-
type ServerOptions,
|
|
19
|
-
verifyProcessGeneration,
|
|
20
|
-
withServerRegistrationCustody,
|
|
21
|
-
} from "./index.ts";
|
|
22
|
-
|
|
23
|
-
/** Verify only witnesses saved by the original tunnel registration. */
|
|
24
|
-
export function createTunnelReadinessVerifier(options: ServerOptions): TunnelReadinessVerifier {
|
|
25
|
-
return {
|
|
26
|
-
observeProcess(pid, originalToken) {
|
|
27
|
-
try {
|
|
28
|
-
process.kill(pid, 0);
|
|
29
|
-
} catch (error) {
|
|
30
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
31
|
-
if (code === "ESRCH") return "absent";
|
|
32
|
-
return "unknown";
|
|
33
|
-
}
|
|
34
|
-
return verifyProcessGeneration(pid, originalToken);
|
|
35
|
-
},
|
|
36
|
-
withRegistration(state, inspect) {
|
|
37
|
-
try {
|
|
38
|
-
const original = readServer(tunnelServerId(state.name), options);
|
|
39
|
-
if (
|
|
40
|
-
!original ||
|
|
41
|
-
original.host !== hostname() ||
|
|
42
|
-
original.kind !== "service" ||
|
|
43
|
-
original.type !== "tunnel" ||
|
|
44
|
-
original.registration_id !== state.registration_id ||
|
|
45
|
-
original.pid !== state.gate_pid ||
|
|
46
|
-
original.start_token !== state.gate_start_token ||
|
|
47
|
-
original.port !== state.gate_port ||
|
|
48
|
-
original.url !== state.url
|
|
49
|
-
)
|
|
50
|
-
return "unknown";
|
|
51
|
-
return withServerRegistrationCustody(original, options, inspect);
|
|
52
|
-
} catch {
|
|
53
|
-
return "unknown";
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|
|
9
|
+
import { gateLogFile, type TunnelState } from "../../lib/tunnel/state.ts";
|
|
10
|
+
import type { RegisterServerInput } from "./index.ts";
|
|
58
11
|
|
|
59
12
|
export function tunnelServerId(name: string): string {
|
|
60
13
|
return `tunnel-${name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-")}`;
|
|
@@ -62,10 +15,6 @@ export function tunnelServerId(name: string): string {
|
|
|
62
15
|
|
|
63
16
|
/** Register the new gate; saved helper PIDs have no generation witnesses. */
|
|
64
17
|
export function tunnelServerInput(state: TunnelState, coordRoot: string): RegisterServerInput {
|
|
65
|
-
if (Object.hasOwn(state, "provider_pid") || Object.hasOwn(state, "provider_start_token"))
|
|
66
|
-
throw new Error("obsolete tunnel provider authority is unknown; state retained");
|
|
67
|
-
if (!state.gate_start_token)
|
|
68
|
-
throw new Error("tunnel gate generation witness is unavailable; state retained");
|
|
69
18
|
return {
|
|
70
19
|
id: tunnelServerId(state.name),
|
|
71
20
|
kind: "service",
|
|
@@ -74,7 +23,6 @@ export function tunnelServerInput(state: TunnelState, coordRoot: string): Regist
|
|
|
74
23
|
url: state.url,
|
|
75
24
|
port: state.gate_port,
|
|
76
25
|
pid: state.gate_pid,
|
|
77
|
-
start_token: state.gate_start_token,
|
|
78
26
|
log: resolve(coordRoot, ".cache", "tunnel", gateLogFile(state.name)),
|
|
79
27
|
cwd: coordRoot,
|
|
80
28
|
started_at: state.started_at,
|
|
@@ -521,28 +521,49 @@ function acquireLease(lease: string): LeaseOwner {
|
|
|
521
521
|
pid: process.pid,
|
|
522
522
|
acquired_at: new Date().toISOString(),
|
|
523
523
|
};
|
|
524
|
-
|
|
524
|
+
if (claimLease(lease, parent, owner)) return owner;
|
|
525
|
+
if (recoverStaleLease(lease, DURABLE_HISTORY_LEASE_STALE_MS)) continue;
|
|
526
|
+
throw new Error(`durable history lease busy: ${lease}`);
|
|
527
|
+
}
|
|
528
|
+
throw new Error(`durable history lease busy: ${lease}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Publish a complete lease in one rename. The owner record is written and synced
|
|
533
|
+
* in a private claim directory first, so a writer stopped at any point leaves
|
|
534
|
+
* either no lease or one with an owner. A held lease always has an owner record,
|
|
535
|
+
* here and in the earlier create-then-write protocol, so a rename that replaces
|
|
536
|
+
* an empty lease directory never takes a held lease. A lease with any entry makes
|
|
537
|
+
* the rename fail and the caller reports it busy.
|
|
538
|
+
*/
|
|
539
|
+
function claimLease(lease: string, parent: DirectoryIdentity, owner: LeaseOwner): boolean {
|
|
540
|
+
const claim = `${lease}.claim-${process.pid}-${owner.owner_id}`;
|
|
541
|
+
try {
|
|
542
|
+
mkdirSync(claim, { mode: stateDirMode() });
|
|
543
|
+
assertDirectoryWithinParent(claim, parent);
|
|
544
|
+
const claimed = captureDirectoryIdentity(claim);
|
|
545
|
+
writeOwner(join(claim, "owner.json"), owner);
|
|
546
|
+
syncDirectory(claimed);
|
|
547
|
+
renameSync(claim, lease);
|
|
548
|
+
} catch {
|
|
549
|
+
return false;
|
|
550
|
+
} finally {
|
|
525
551
|
try {
|
|
526
|
-
|
|
527
|
-
assertDirectoryWithinParent(lease, parent);
|
|
528
|
-
claimed = captureDirectoryIdentity(lease);
|
|
529
|
-
writeOwner(join(lease, "owner.json"), owner);
|
|
530
|
-
syncDirectory(claimed);
|
|
531
|
-
return owner;
|
|
552
|
+
rmSync(claim, { recursive: true, force: true });
|
|
532
553
|
} catch {
|
|
533
|
-
|
|
534
|
-
try {
|
|
535
|
-
assertDirectoryIdentity(claimed);
|
|
536
|
-
rmSync(lease, { recursive: true, force: false });
|
|
537
|
-
} catch {
|
|
538
|
-
// Never remove a lease after its directory identity changes.
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
if (recoverStaleLease(lease, DURABLE_HISTORY_LEASE_STALE_MS)) continue;
|
|
542
|
-
throw new Error(`durable history lease busy: ${lease}`);
|
|
554
|
+
// A leftover claim directory never blocks a lease.
|
|
543
555
|
}
|
|
544
556
|
}
|
|
545
|
-
|
|
557
|
+
try {
|
|
558
|
+
assertDirectoryWithinParent(lease, parent);
|
|
559
|
+
const current = JSON.parse(completeJsonl(join(lease, "owner.json"))) as LeaseOwner;
|
|
560
|
+
if (current.owner_id !== owner.owner_id) return false;
|
|
561
|
+
syncDirectory(parent);
|
|
562
|
+
return true;
|
|
563
|
+
} catch {
|
|
564
|
+
releaseOwnedLease(lease, owner);
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
546
567
|
}
|
|
547
568
|
|
|
548
569
|
function writeOwner(path: string, owner: LeaseOwner): void {
|
|
@@ -595,7 +616,7 @@ function recoverStaleLease(lease: string, staleMs: number): boolean {
|
|
|
595
616
|
try {
|
|
596
617
|
assertDirectoryIdentity(leaseIdentity);
|
|
597
618
|
if (completeJsonl(ownerPath) !== raw) return false;
|
|
598
|
-
|
|
619
|
+
retireLease(lease, randomUUID());
|
|
599
620
|
return true;
|
|
600
621
|
} catch {
|
|
601
622
|
return false;
|
|
@@ -606,12 +627,23 @@ function releaseOwnedLease(lease: string, owner: LeaseOwner): void {
|
|
|
606
627
|
try {
|
|
607
628
|
const current = JSON.parse(completeJsonl(join(lease, "owner.json"))) as LeaseOwner;
|
|
608
629
|
if (current.owner_id !== owner.owner_id) return;
|
|
609
|
-
|
|
630
|
+
retireLease(lease, owner.owner_id);
|
|
610
631
|
} catch {
|
|
611
632
|
// A missing or replaced lease does not belong to this operation.
|
|
612
633
|
}
|
|
613
634
|
}
|
|
614
635
|
|
|
636
|
+
/**
|
|
637
|
+
* Move a lease aside before deleting it. Its path goes from held to absent in
|
|
638
|
+
* one rename and is never an empty directory that a concurrent claim could
|
|
639
|
+
* replace while a recursive removal is still running.
|
|
640
|
+
*/
|
|
641
|
+
function retireLease(lease: string, tag: string): void {
|
|
642
|
+
const retired = `${lease}.retired-${process.pid}-${tag}`;
|
|
643
|
+
renameSync(lease, retired);
|
|
644
|
+
rmSync(retired, { recursive: true, force: true });
|
|
645
|
+
}
|
|
646
|
+
|
|
615
647
|
function processIsAlive(pid: number): boolean {
|
|
616
648
|
try {
|
|
617
649
|
process.kill(pid, 0);
|