@sentropic/h2a 0.97.11 → 0.98.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/dist/bin.js +5 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli-command-map.d.ts.map +1 -1
- package/dist/cli-command-map.js +2 -1
- package/dist/cli-command-map.js.map +1 -1
- package/dist/cli-contract.js +1 -1
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +35 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/cluster-mesh-messaging.d.ts +21 -0
- package/dist/runtime/cluster-mesh-messaging.d.ts.map +1 -0
- package/dist/runtime/cluster-mesh-messaging.js +129 -0
- package/dist/runtime/cluster-mesh-messaging.js.map +1 -0
- package/dist/runtime/identity/readers.d.ts.map +1 -1
- package/dist/runtime/identity/readers.js +25 -2
- package/dist/runtime/identity/readers.js.map +1 -1
- package/dist/runtime/local-files/succession-lock.d.ts +105 -17
- package/dist/runtime/local-files/succession-lock.d.ts.map +1 -1
- package/dist/runtime/local-files/succession-lock.js +320 -108
- package/dist/runtime/local-files/succession-lock.js.map +1 -1
- package/dist/runtime/mcp/agent-launch.d.ts +6 -3
- package/dist/runtime/mcp/agent-launch.d.ts.map +1 -1
- package/dist/runtime/mcp/agent-launch.js +146 -4
- package/dist/runtime/mcp/agent-launch.js.map +1 -1
- package/dist/runtime/mcp/handlers.d.ts +7 -2
- package/dist/runtime/mcp/handlers.d.ts.map +1 -1
- package/dist/runtime/mcp/handlers.js +9 -4
- package/dist/runtime/mcp/handlers.js.map +1 -1
- package/dist/runtime/mcp/identity-state.d.ts +5 -4
- package/dist/runtime/mcp/identity-state.d.ts.map +1 -1
- package/dist/runtime/mcp/identity-state.js +43 -4
- package/dist/runtime/mcp/identity-state.js.map +1 -1
- package/dist/runtime/mcp/server.d.ts +6 -2
- package/dist/runtime/mcp/server.d.ts.map +1 -1
- package/dist/runtime/mcp/server.js +6 -4
- package/dist/runtime/mcp/server.js.map +1 -1
- package/dist/runtime/mcp/stdio.d.ts +5 -1
- package/dist/runtime/mcp/stdio.d.ts.map +1 -1
- package/dist/runtime/mcp/stdio.js +117 -24
- package/dist/runtime/mcp/stdio.js.map +1 -1
- package/dist/runtime/mcp/tools.d.ts.map +1 -1
- package/dist/runtime/mcp/tools.js +4 -3
- package/dist/runtime/mcp/tools.js.map +1 -1
- package/dist/runtime/mcp-central.js +2 -2
- package/dist/runtime/mcp-central.js.map +1 -1
- package/dist/runtime/mcp-http/hosted-mcp-server.d.ts.map +1 -1
- package/dist/runtime/mcp-http/hosted-mcp-server.js +3 -0
- package/dist/runtime/mcp-http/hosted-mcp-server.js.map +1 -1
- package/dist/runtime/send.d.ts +11 -5
- package/dist/runtime/send.d.ts.map +1 -1
- package/dist/runtime/send.js +30 -4
- package/dist/runtime/send.js.map +1 -1
- package/dist/runtime/upgrade/index.d.ts +5 -5
- package/dist/runtime/upgrade/index.d.ts.map +1 -1
- package/dist/runtime/upgrade/index.js +16 -13
- package/dist/runtime/upgrade/index.js.map +1 -1
- package/focus-app/client/_app/immutable/chunks/zL3j6erO.js +2 -0
- package/focus-app/client/_app/immutable/chunks/zL3j6erO.js.br +0 -0
- package/focus-app/client/_app/immutable/chunks/zL3j6erO.js.gz +0 -0
- package/focus-app/client/_app/immutable/entry/{app.D_yfk55R.js → app.Dfo3Ymae.js} +2 -2
- package/focus-app/client/_app/immutable/entry/app.Dfo3Ymae.js.br +0 -0
- package/focus-app/client/_app/immutable/entry/app.Dfo3Ymae.js.gz +0 -0
- package/focus-app/client/_app/immutable/entry/start.BSQLtKUn.js +1 -0
- package/focus-app/client/_app/immutable/entry/start.BSQLtKUn.js.br +0 -0
- package/focus-app/client/_app/immutable/entry/start.BSQLtKUn.js.gz +0 -0
- package/focus-app/client/_app/immutable/nodes/{1.Jo1uWTw0.js → 1.B83XRsuv.js} +1 -1
- package/focus-app/client/_app/immutable/nodes/1.B83XRsuv.js.br +0 -0
- package/focus-app/client/_app/immutable/nodes/1.B83XRsuv.js.gz +0 -0
- package/focus-app/client/_app/version.json +1 -1
- package/focus-app/client/_app/version.json.br +1 -1
- package/focus-app/client/_app/version.json.gz +0 -0
- package/focus-app/h2a-focus.json +61 -61
- package/focus-app/handler.js +8 -8
- package/focus-app/index.js +8 -8
- package/focus-app/server/chunks/chunks/{AppShell.js-BoWxjmfr.js → AppShell.js-DT15_EBV.js} +2 -2
- package/focus-app/server/chunks/chunks/{AppShell.js-BoWxjmfr.js.map → AppShell.js-DT15_EBV.js.map} +1 -1
- package/focus-app/server/chunks/chunks/{exports.js-D45A8dcc.js → exports.js-BrfiJlPR.js} +3 -3
- package/focus-app/server/chunks/chunks/{exports.js-D45A8dcc.js.map → exports.js-BrfiJlPR.js.map} +1 -1
- package/focus-app/server/chunks/chunks/{index.js-e3L5MwfB.js → index.js-BY6_68Zf.js} +2 -2
- package/focus-app/server/chunks/chunks/{index.js-e3L5MwfB.js.map → index.js-BY6_68Zf.js.map} +1 -1
- package/focus-app/server/chunks/chunks/{internal.js-DAV-A6NO.js → internal.js-BDHE9Kn1.js} +3 -3
- package/focus-app/server/chunks/chunks/{internal.js-DAV-A6NO.js.map → internal.js-BDHE9Kn1.js.map} +1 -1
- package/focus-app/server/chunks/chunks/{root.js-CEypm32A.js → root.js-CZyc7gzj.js} +2 -2
- package/focus-app/server/chunks/chunks/{root.js-CEypm32A.js.map → root.js-CZyc7gzj.js.map} +1 -1
- package/focus-app/server/chunks/chunks/{utils.js-Bzpr6vQU.js → utils.js-DBwpgn00.js} +178 -51
- package/focus-app/server/chunks/chunks/utils.js-DBwpgn00.js.map +1 -0
- package/focus-app/server/chunks/entries/endpoints/api/actions/launch/{_server.ts.js-Cah_Ivgw.js → _server.ts.js-H2iyKDJx.js} +2 -2
- package/focus-app/server/chunks/entries/endpoints/api/actions/launch/{_server.ts.js-Cah_Ivgw.js.map → _server.ts.js-H2iyKDJx.js.map} +1 -1
- package/focus-app/server/chunks/entries/endpoints/api/decisions/inject/{_server.ts.js-DW5wvcgH.js → _server.ts.js-BuR515Qd.js} +2 -2
- package/focus-app/server/chunks/entries/endpoints/api/decisions/inject/{_server.ts.js-DW5wvcgH.js.map → _server.ts.js-BuR515Qd.js.map} +1 -1
- package/focus-app/server/chunks/entries/endpoints/api/dossiers/agent-memory/include/{_server.ts.js-C2jIx8Jr.js → _server.ts.js-Cs32vOYa.js} +2 -2
- package/focus-app/server/chunks/entries/endpoints/api/dossiers/agent-memory/include/{_server.ts.js-C2jIx8Jr.js.map → _server.ts.js-Cs32vOYa.js.map} +1 -1
- package/focus-app/server/chunks/entries/endpoints/api/h2a/targets/{_server.ts.js-BkAMLcHu.js → _server.ts.js-DbEqL92p.js} +2 -2
- package/focus-app/server/chunks/entries/endpoints/api/h2a/targets/{_server.ts.js-BkAMLcHu.js.map → _server.ts.js-DbEqL92p.js.map} +1 -1
- package/focus-app/server/chunks/entries/fallbacks/{error.svelte.js-BWuS92Wk.js → error.svelte.js-BzO57FOz.js} +5 -5
- package/focus-app/server/chunks/entries/fallbacks/{error.svelte.js-BWuS92Wk.js.map → error.svelte.js-BzO57FOz.js.map} +1 -1
- package/focus-app/server/chunks/entries/pages/{_layout.svelte.js-CKspI0Bw.js → _layout.svelte.js-BHXu1zZ0.js} +3 -3
- package/focus-app/server/chunks/entries/pages/{_layout.svelte.js-CKspI0Bw.js.map → _layout.svelte.js-BHXu1zZ0.js.map} +1 -1
- package/focus-app/server/chunks/entries/pages/{_page.svelte.js-B8EWZmZj.js → _page.svelte.js-DgcoCR_G.js} +4 -4
- package/focus-app/server/chunks/entries/pages/{_page.svelte.js-B8EWZmZj.js.map → _page.svelte.js-DgcoCR_G.js.map} +1 -1
- package/focus-app/server/chunks/entries/pages/dossier/agent-memory/{_page.svelte.js-BZvcKeya.js → _page.svelte.js-D8qUTYi2.js} +4 -4
- package/focus-app/server/chunks/entries/pages/dossier/agent-memory/{_page.svelte.js-BZvcKeya.js.map → _page.svelte.js-D8qUTYi2.js.map} +1 -1
- package/focus-app/server/chunks/{handler-BCBpURX0.js → handler-BidpBSFZ.js} +4 -4
- package/focus-app/server/chunks/{handler-BCBpURX0.js.map → handler-BidpBSFZ.js.map} +1 -1
- package/focus-app/server/chunks/{index.js-CZWB6FqQ.js → index.js-BNQ-AMZE.js} +5 -5
- package/focus-app/server/chunks/{index.js-CZWB6FqQ.js.map → index.js-BNQ-AMZE.js.map} +1 -1
- package/focus-app/server/chunks/{manifest.js-C2CyTiUe.js → manifest.js-CAtvBIjA.js} +10 -10
- package/focus-app/server/chunks/{manifest.js-C2CyTiUe.js.map → manifest.js-CAtvBIjA.js.map} +1 -1
- package/focus-app/server/chunks/nodes/{0.js-Cp9RMJ1x.js → 0.js-DyONiS9Y.js} +2 -2
- package/focus-app/server/chunks/nodes/{0.js-Cp9RMJ1x.js.map → 0.js-DyONiS9Y.js.map} +1 -1
- package/focus-app/server/chunks/nodes/1.js-C16cgDY2.js +9 -0
- package/focus-app/server/chunks/nodes/{1.js-DMWmTj0d.js.map → 1.js-C16cgDY2.js.map} +1 -1
- package/focus-app/server/chunks/nodes/{2.js-Dv8Q6LQc.js → 2.js-C16ONuI7.js} +2 -2
- package/focus-app/server/chunks/nodes/{2.js-Dv8Q6LQc.js.map → 2.js-C16ONuI7.js.map} +1 -1
- package/focus-app/server/chunks/nodes/{3.js-DJH-zOae.js → 3.js-DIZzznQP.js} +2 -2
- package/focus-app/server/chunks/nodes/{3.js-DJH-zOae.js.map → 3.js-DIZzznQP.js.map} +1 -1
- package/package.json +4 -4
- package/focus-app/client/_app/immutable/chunks/BgBtu3IQ.js +0 -2
- package/focus-app/client/_app/immutable/chunks/BgBtu3IQ.js.br +0 -0
- package/focus-app/client/_app/immutable/chunks/BgBtu3IQ.js.gz +0 -0
- package/focus-app/client/_app/immutable/entry/app.D_yfk55R.js.br +0 -0
- package/focus-app/client/_app/immutable/entry/app.D_yfk55R.js.gz +0 -0
- package/focus-app/client/_app/immutable/entry/start.CoQWYNTs.js +0 -1
- package/focus-app/client/_app/immutable/entry/start.CoQWYNTs.js.br +0 -2
- package/focus-app/client/_app/immutable/entry/start.CoQWYNTs.js.gz +0 -0
- package/focus-app/client/_app/immutable/nodes/1.Jo1uWTw0.js.br +0 -4
- package/focus-app/client/_app/immutable/nodes/1.Jo1uWTw0.js.gz +0 -0
- package/focus-app/server/chunks/chunks/utils.js-Bzpr6vQU.js.map +0 -1
- package/focus-app/server/chunks/nodes/1.js-DMWmTj0d.js +0 -9
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Succession lock (v4) — single-machine succession protocol.
|
|
3
3
|
*
|
|
4
4
|
* Extracted from `runtime/upgrade/index.ts` (Lot 4, step 1: neutral
|
|
5
|
-
* intra-package move
|
|
5
|
+
* intra-package move). Lot 4 §2 adds fail-closed host and boot provenance before
|
|
6
|
+
* any death proof. This is the shared lock primitive
|
|
6
7
|
* used by the auto-upgrade prefix lock today, and (later lots) by the identity
|
|
7
8
|
* binding lock. It is a LEAF: it imports only `node:fs/os/crypto/child_process`
|
|
8
9
|
* and nothing from the store, so `upgrade/index.ts` and `local-files/locks.ts`
|
|
@@ -11,11 +12,11 @@
|
|
|
11
12
|
* The full protocol proof-sketch (I1–I7, Lemmas A–C) lives with the code below,
|
|
12
13
|
* moved unchanged from its original location.
|
|
13
14
|
*/
|
|
14
|
-
import { randomBytes } from "node:crypto";
|
|
15
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
15
16
|
import { spawnSync } from "node:child_process";
|
|
16
17
|
import { closeSync, fsyncSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
17
18
|
import { hostname } from "node:os";
|
|
18
|
-
import { basename, join } from "node:path";
|
|
19
|
+
import { basename, dirname, join } from "node:path";
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Prefix lock (v4): single-machine succession protocol.
|
|
21
22
|
//
|
|
@@ -97,28 +98,87 @@ export function safeAtIso(at) {
|
|
|
97
98
|
}
|
|
98
99
|
return `epoch-ms:${at}`;
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
+
const MACHINE_ID_RE = /^[0-9a-f]{32}$/;
|
|
102
|
+
const IO_PLATFORM_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
103
|
+
const STATIC_COMMAND_ENV = { LC_ALL: "C", TZ: "UTC0" };
|
|
104
|
+
function isNullMachineId(value) {
|
|
105
|
+
return /^0+$/.test(value.replaceAll("-", ""));
|
|
106
|
+
}
|
|
107
|
+
function sysctlPath(platform) {
|
|
108
|
+
return platform === "darwin"
|
|
109
|
+
? "/usr/sbin/sysctl"
|
|
110
|
+
: platform === "freebsd" || platform === "openbsd"
|
|
111
|
+
? "/sbin/sysctl"
|
|
112
|
+
: "/usr/sbin/sysctl";
|
|
113
|
+
}
|
|
114
|
+
function weakHost(hostnameReader) {
|
|
101
115
|
try {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
return
|
|
116
|
+
const host = hostnameReader().trim();
|
|
117
|
+
if (host)
|
|
118
|
+
return { host, hostKind: "weak" };
|
|
105
119
|
}
|
|
106
120
|
catch {
|
|
107
|
-
// fall through to
|
|
121
|
+
// fall through to sentinel
|
|
108
122
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
return { host: "unknown-host", hostKind: "weak" };
|
|
124
|
+
}
|
|
125
|
+
function parseIoPlatformUuid(output) {
|
|
126
|
+
const matches = output
|
|
127
|
+
.split("\n")
|
|
128
|
+
.map((line) => line.trim().match(/^"IOPlatformUUID"\s*=\s*"([0-9a-f-]+)"$/i)?.[1])
|
|
129
|
+
.filter((value) => value !== undefined);
|
|
130
|
+
if (matches.length !== 1 || !IO_PLATFORM_UUID_RE.test(matches[0]) || isNullMachineId(matches[0]))
|
|
131
|
+
return undefined;
|
|
132
|
+
return matches[0].toLowerCase();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Reads a machine-scoped host identity. The injected readers make source selection
|
|
136
|
+
* deterministic in tests without consulting process environment variables.
|
|
137
|
+
*/
|
|
138
|
+
export function readHostId(deps = {}) {
|
|
139
|
+
const platform = deps.platform ?? process.platform;
|
|
140
|
+
const readFile = deps.readFile ?? ((path) => readFileSync(path, "utf8"));
|
|
141
|
+
const hostnameReader = deps.hostname ?? hostname;
|
|
142
|
+
if (platform === "linux") {
|
|
143
|
+
try {
|
|
144
|
+
const host = readFile("/etc/machine-id").trim();
|
|
145
|
+
if (MACHINE_ID_RE.test(host) && !isNullMachineId(host))
|
|
146
|
+
return { host, hostKind: "machine-id" };
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// fall through to a weak hostname
|
|
150
|
+
}
|
|
113
151
|
}
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
else if (platform === "darwin") {
|
|
153
|
+
try {
|
|
154
|
+
const ioreg = deps.ioreg ?? (() => {
|
|
155
|
+
const r = (deps.spawn ?? spawnSync)("/usr/sbin/ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
timeout: 2000,
|
|
158
|
+
env: STATIC_COMMAND_ENV
|
|
159
|
+
});
|
|
160
|
+
return { status: r.status, stdout: typeof r.stdout === "string" ? r.stdout : null };
|
|
161
|
+
});
|
|
162
|
+
const result = ioreg();
|
|
163
|
+
const host = result.status === 0 && typeof result.stdout === "string"
|
|
164
|
+
? parseIoPlatformUuid(result.stdout)
|
|
165
|
+
: undefined;
|
|
166
|
+
if (host !== undefined)
|
|
167
|
+
return { host, hostKind: "machine-id" };
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// fall through to a weak hostname
|
|
171
|
+
}
|
|
116
172
|
}
|
|
117
|
-
return
|
|
173
|
+
return weakHost(hostnameReader);
|
|
118
174
|
}
|
|
119
|
-
|
|
175
|
+
/** Reads the current boot identity; readers are injectable for platform simulations. */
|
|
176
|
+
export function readBootId(deps = {}) {
|
|
177
|
+
const platform = deps.platform ?? process.platform;
|
|
178
|
+
const readFile = deps.readFile ?? ((path) => readFileSync(path, "utf8"));
|
|
179
|
+
const spawn = deps.spawn ?? spawnSync;
|
|
120
180
|
try {
|
|
121
|
-
const v =
|
|
181
|
+
const v = readFile("/proc/sys/kernel/random/boot_id").trim();
|
|
122
182
|
if (v)
|
|
123
183
|
return v;
|
|
124
184
|
}
|
|
@@ -127,10 +187,10 @@ function readBootId() {
|
|
|
127
187
|
}
|
|
128
188
|
// Outside Linux prefer the stable session UUID (TZ-independent) when present.
|
|
129
189
|
try {
|
|
130
|
-
const r =
|
|
190
|
+
const r = spawn(sysctlPath(platform), ["-n", "kern.bootsessionuuid"], {
|
|
131
191
|
encoding: "utf8",
|
|
132
192
|
timeout: 2000,
|
|
133
|
-
env:
|
|
193
|
+
env: STATIC_COMMAND_ENV
|
|
134
194
|
});
|
|
135
195
|
const v = (r.stdout ?? "").trim();
|
|
136
196
|
if (r.status === 0 && v)
|
|
@@ -140,10 +200,10 @@ function readBootId() {
|
|
|
140
200
|
// best-effort
|
|
141
201
|
}
|
|
142
202
|
try {
|
|
143
|
-
const r =
|
|
203
|
+
const r = spawn(sysctlPath(platform), ["-n", "kern.boottime"], {
|
|
144
204
|
encoding: "utf8",
|
|
145
205
|
timeout: 2000,
|
|
146
|
-
env:
|
|
206
|
+
env: STATIC_COMMAND_ENV
|
|
147
207
|
});
|
|
148
208
|
const v = (r.stdout ?? "").trim();
|
|
149
209
|
if (r.status === 0 && v)
|
|
@@ -156,9 +216,10 @@ function readBootId() {
|
|
|
156
216
|
}
|
|
157
217
|
// B2 decomposition: distinguish "this platform has no namespaces" (a KNOWN fact —
|
|
158
218
|
// one space per host) from "the namespace exists but is unreadable" (a genuine
|
|
159
|
-
// unknown). Only the latter is null; the former is the sentinel "host"
|
|
160
|
-
// liveness
|
|
161
|
-
//
|
|
219
|
+
// unknown). Only the latter is null; the former is the sentinel "host". Lot 4 §2
|
|
220
|
+
// permits that no-namespace liveness proof only on darwin; Windows and BSD still fail
|
|
221
|
+
// closed as undecidable. `platform`/`readLink` are injected only by tests
|
|
222
|
+
// (macOS/Windows/no-/proc sims); production passes none.
|
|
162
223
|
//
|
|
163
224
|
// readPidNs is the CONSERVATIVE gate that decides reclaim at all: an unknown (null)
|
|
164
225
|
// pid namespace makes even a PID-absent holder undecidable, because "absent" in an
|
|
@@ -210,9 +271,9 @@ function procMapsToSelf() {
|
|
|
210
271
|
* so a reader using a different source never concludes "dead" from a
|
|
211
272
|
* format/TZ-fragile comparison (C1).
|
|
212
273
|
*/
|
|
213
|
-
// `platform`/`mapsToSelf` are injected only by tests (mis-mapped-/proc /
|
|
214
|
-
// sims); production passes none.
|
|
215
|
-
export function procStartInfo(pid, platform = process.platform, mapsToSelf = procMapsToSelf) {
|
|
274
|
+
// `platform`/`mapsToSelf`/`spawn` are injected only by tests (mis-mapped-/proc /
|
|
275
|
+
// non-Linux sims); production passes none.
|
|
276
|
+
export function procStartInfo(pid, platform = process.platform, mapsToSelf = procMapsToSelf, spawn = spawnSync) {
|
|
216
277
|
// A start time is only trusted from a source whose value is STABLE for the life of the
|
|
217
278
|
// process (never moving under a wall-clock step), else a clock jump while the lock is held
|
|
218
279
|
// could make a live holder's start "differ" ⇒ a false death ⇒ two holders (I3).
|
|
@@ -224,7 +285,8 @@ export function procStartInfo(pid, platform = process.platform, mapsToSelf = pro
|
|
|
224
285
|
// - darwin: `ps lstart` is the ABSOLUTE fork wall-clock time (p_starttime), stable ⇒ trusted.
|
|
225
286
|
// - R-BSD: on FreeBSD/OpenBSD `ps` start is boot-relative and its boot time is re-derived on
|
|
226
287
|
// a clock step, so it is NOT stable. Every other non-Linux platform (incl. Windows, no ps)
|
|
227
|
-
// ⇒ undefined
|
|
288
|
+
// ⇒ undefined. Lock liveness on those platforms is already undecidable under Lot 4 §2.
|
|
289
|
+
// In doubt, never dead.
|
|
228
290
|
if (platform === "linux") {
|
|
229
291
|
if (!mapsToSelf())
|
|
230
292
|
return undefined;
|
|
@@ -247,10 +309,10 @@ export function procStartInfo(pid, platform = process.platform, mapsToSelf = pro
|
|
|
247
309
|
if (platform !== "darwin")
|
|
248
310
|
return undefined; // only darwin ps is a stable source
|
|
249
311
|
try {
|
|
250
|
-
const r =
|
|
312
|
+
const r = spawn("/bin/ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
251
313
|
encoding: "utf8",
|
|
252
314
|
timeout: 5000,
|
|
253
|
-
env:
|
|
315
|
+
env: STATIC_COMMAND_ENV
|
|
254
316
|
});
|
|
255
317
|
const v = (r.stdout ?? "").trim();
|
|
256
318
|
if (r.status === 0 && v)
|
|
@@ -271,24 +333,28 @@ function startSource(s) {
|
|
|
271
333
|
let ME_CACHE;
|
|
272
334
|
/** This process's identity, computed once and memoized. */
|
|
273
335
|
export function me() {
|
|
274
|
-
ME_CACHE ??= {
|
|
275
|
-
host
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
336
|
+
ME_CACHE ??= (() => {
|
|
337
|
+
const host = readHostId();
|
|
338
|
+
return {
|
|
339
|
+
host: host.host,
|
|
340
|
+
hostKind: host.hostKind,
|
|
341
|
+
boot: readBootId(),
|
|
342
|
+
ns: readPidNs(),
|
|
343
|
+
timeNs: readTimeNs(),
|
|
344
|
+
pid: process.pid,
|
|
345
|
+
start: procStartInfo(process.pid)?.start ?? null
|
|
346
|
+
};
|
|
347
|
+
})();
|
|
282
348
|
return ME_CACHE;
|
|
283
349
|
}
|
|
284
350
|
/** Fresh random token, >= 96 bits, never republished (H3). */
|
|
285
351
|
function newToken() {
|
|
286
352
|
return randomBytes(16).toString("hex");
|
|
287
353
|
}
|
|
288
|
-
|
|
289
|
-
const self = me();
|
|
354
|
+
function makeLockRecFor(self, token, target) {
|
|
290
355
|
return {
|
|
291
356
|
host: self.host,
|
|
357
|
+
hostKind: self.hostKind,
|
|
292
358
|
boot: self.boot,
|
|
293
359
|
ns: self.ns,
|
|
294
360
|
timeNs: self.timeNs,
|
|
@@ -299,11 +365,14 @@ export function makeLockRec(token, target) {
|
|
|
299
365
|
at: Date.now()
|
|
300
366
|
};
|
|
301
367
|
}
|
|
368
|
+
export function makeLockRec(token, target) {
|
|
369
|
+
return makeLockRecFor(me(), token, target);
|
|
370
|
+
}
|
|
302
371
|
export function parseLockRec(raw) {
|
|
303
372
|
if (typeof raw !== "object" || raw === null)
|
|
304
373
|
throw new Error("bad lock record");
|
|
305
374
|
const o = raw;
|
|
306
|
-
const { host, boot, ns, timeNs, pid, start, token, target, at } = o;
|
|
375
|
+
const { host, hostKind, boot, ns, timeNs, pid, start, token, target, operator, at } = o;
|
|
307
376
|
// timeNs is a required field like host/boot/ns/pid/start: an absent field is a
|
|
308
377
|
// malformed record → corrupt → fail-closed. No back-compat exception is carved
|
|
309
378
|
// for a "v4 without timeNs" — the redesign was never published, so no such lock
|
|
@@ -312,6 +381,12 @@ export function parseLockRec(raw) {
|
|
|
312
381
|
// as undatable ⇒ "live" (never reclaim, never a false death), not undecidable.
|
|
313
382
|
if (typeof host !== "string" || host.length === 0)
|
|
314
383
|
throw new Error("bad host");
|
|
384
|
+
// A missing provenance was written by versions <= 0.97.9. It is valid legacy
|
|
385
|
+
// state, but classifyLiveness treats it as unknown and therefore undecidable.
|
|
386
|
+
// Unknown values are corrupt and fail closed. Lot 5 must carry an instance UUID in
|
|
387
|
+
// a separate field rather than extending this persisted hostKind enum.
|
|
388
|
+
if (hostKind !== undefined && hostKind !== "machine-id" && hostKind !== "weak")
|
|
389
|
+
throw new Error("bad hostKind");
|
|
315
390
|
if (boot !== null && typeof boot !== "string")
|
|
316
391
|
throw new Error("bad boot");
|
|
317
392
|
if (ns !== null && typeof ns !== "string")
|
|
@@ -322,14 +397,17 @@ export function parseLockRec(raw) {
|
|
|
322
397
|
throw new Error("bad pid");
|
|
323
398
|
if (start !== null && typeof start !== "string")
|
|
324
399
|
throw new Error("bad start");
|
|
325
|
-
if (typeof token !== "string" || !LOCK_TOKEN_RE.test(token))
|
|
400
|
+
if (typeof token !== "string" || token.startsWith("legacy-") || !LOCK_TOKEN_RE.test(token))
|
|
326
401
|
throw new Error("bad token");
|
|
327
402
|
if (target !== undefined && typeof target !== "string")
|
|
328
403
|
throw new Error("bad target");
|
|
404
|
+
if (operator !== undefined && (operator !== true || target === undefined))
|
|
405
|
+
throw new Error("bad operator");
|
|
329
406
|
if (typeof at !== "number" || !Number.isFinite(at))
|
|
330
407
|
throw new Error("bad at");
|
|
331
408
|
return {
|
|
332
409
|
host,
|
|
410
|
+
...(hostKind !== undefined ? { hostKind } : {}),
|
|
333
411
|
boot,
|
|
334
412
|
ns,
|
|
335
413
|
timeNs,
|
|
@@ -337,18 +415,81 @@ export function parseLockRec(raw) {
|
|
|
337
415
|
start,
|
|
338
416
|
token,
|
|
339
417
|
...(target !== undefined ? { target } : {}),
|
|
418
|
+
...(operator === true ? { operator } : {}),
|
|
340
419
|
at
|
|
341
420
|
};
|
|
342
421
|
}
|
|
343
|
-
|
|
344
|
-
|
|
422
|
+
function parseLegacyLockHolder(raw, bytes) {
|
|
423
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
424
|
+
throw new Error("bad legacy lock record");
|
|
425
|
+
const o = raw;
|
|
426
|
+
const keys = Object.keys(o);
|
|
427
|
+
if (!keys.includes("pid")
|
|
428
|
+
|| !keys.includes("hostname")
|
|
429
|
+
|| !keys.includes("startedAt")
|
|
430
|
+
|| !keys.every((key) => key === "pid" || key === "hostname" || key === "startedAt" || key === "protocol" || key === "fenceEpoch")) {
|
|
431
|
+
throw new Error("bad legacy lock fields");
|
|
432
|
+
}
|
|
433
|
+
if (typeof o.pid !== "number" || !Number.isInteger(o.pid) || o.pid <= 0)
|
|
434
|
+
throw new Error("bad legacy pid");
|
|
435
|
+
if (typeof o.hostname !== "string")
|
|
436
|
+
throw new Error("bad legacy hostname");
|
|
437
|
+
if (typeof o.startedAt !== "string")
|
|
438
|
+
throw new Error("bad legacy startedAt");
|
|
439
|
+
if (o.protocol !== undefined && typeof o.protocol !== "string")
|
|
440
|
+
throw new Error("bad legacy protocol");
|
|
441
|
+
if (o.fenceEpoch !== undefined && typeof o.fenceEpoch !== "string")
|
|
442
|
+
throw new Error("bad legacy fenceEpoch");
|
|
443
|
+
return {
|
|
444
|
+
kind: "legacy",
|
|
445
|
+
// Do not trim, decode/re-encode, or JSON.stringify the input: a final `\n`
|
|
446
|
+
// is part of this fencing token by design and is covered by T-legacy.
|
|
447
|
+
token: `legacy-${createHash("sha256").update(bytes).digest("hex")}`,
|
|
448
|
+
pid: o.pid,
|
|
449
|
+
hostname: o.hostname,
|
|
450
|
+
startedAt: o.startedAt,
|
|
451
|
+
...(o.protocol !== undefined ? { protocol: o.protocol } : {}),
|
|
452
|
+
...(o.fenceEpoch !== undefined ? { fenceEpoch: o.fenceEpoch } : {})
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/** Advisory reader: distinguish failed reads from malformed file content. */
|
|
456
|
+
function readLockHolderDetailed(path) {
|
|
457
|
+
let bytes;
|
|
345
458
|
try {
|
|
346
|
-
|
|
459
|
+
bytes = readFileSync(path);
|
|
347
460
|
}
|
|
348
461
|
catch (e) {
|
|
349
|
-
return e
|
|
462
|
+
return errnoOf(e) === "ENOENT" ? "absent" : { kind: "io-error", code: errnoOf(e) };
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
466
|
+
try {
|
|
467
|
+
return parseLockRec(parsed);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return parseLegacyLockHolder(parsed, bytes);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return "corrupt";
|
|
350
475
|
}
|
|
351
476
|
}
|
|
477
|
+
/** Legacy tokens fingerprint the exact raw bytes; read errors remain corrupt. */
|
|
478
|
+
export function readLockHolder(path) {
|
|
479
|
+
const holder = readLockHolderDetailed(path);
|
|
480
|
+
return typeof holder === "object" && holder.kind === "io-error" ? "corrupt" : holder;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* v4-only reader retained for existing protocol callers. A readable legacy holder
|
|
484
|
+
* remains `corrupt` here so a caller that was not explicitly upgraded to the
|
|
485
|
+
* distinct legacy view continues to fail closed.
|
|
486
|
+
*/
|
|
487
|
+
export function readLockRecord(path) {
|
|
488
|
+
const holder = readLockHolder(path);
|
|
489
|
+
return holder !== "absent" && holder !== "corrupt" && holder.kind === "legacy"
|
|
490
|
+
? "corrupt"
|
|
491
|
+
: holder;
|
|
492
|
+
}
|
|
352
493
|
/**
|
|
353
494
|
* opus `publish` (I1): a name appears only with complete, durable content.
|
|
354
495
|
* Write tmp (wx + byte-count-checked write + fsync + close), then `link(2)`;
|
|
@@ -427,19 +568,24 @@ export function publishLockRecord(path, rec) {
|
|
|
427
568
|
* safe "live" (undatable), and a corrupt "legacy" start yields "undecidable" (C1).
|
|
428
569
|
*/
|
|
429
570
|
export function classifyLiveness(r, self, deps = {}) {
|
|
571
|
+
// Legacy records have no machine/boot/namespace/start provenance. In particular,
|
|
572
|
+
// even a same-host ESRCH PID may have been reused, so never probe it and never
|
|
573
|
+
// promote the holder to dead. The operator escape hatch consumes this token later.
|
|
574
|
+
if (r.kind === "legacy")
|
|
575
|
+
return { verdict: "undecidable", datable: false };
|
|
430
576
|
const platform = deps.platform ?? process.platform;
|
|
431
577
|
const probe = deps.probe ?? procStartInfo;
|
|
578
|
+
// Lot 4 §2 permits a death proof only for a strong machine identity on Linux or
|
|
579
|
+
// darwin, with the same known host and boot. Check this before every PID/start probe:
|
|
580
|
+
// cloned images can share a machine-id and an initial pid namespace inode.
|
|
581
|
+
if (platform !== "linux" && platform !== "darwin")
|
|
582
|
+
return { verdict: "undecidable", datable: false };
|
|
583
|
+
if (r.hostKind !== "machine-id" || self.hostKind !== "machine-id")
|
|
584
|
+
return { verdict: "undecidable", datable: false };
|
|
432
585
|
if (r.host !== self.host)
|
|
433
|
-
return { verdict: "undecidable", datable: false };
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
// to undecidable (that wedged a Mac rebooted mid-lock forever); fall through to kill(0)
|
|
437
|
-
// (PID absent ⇒ dead, on every platform) then the start comparison (only conclusive
|
|
438
|
-
// where the start is datable — Linux /proc and darwin ps; on BSD/Windows it is undatable
|
|
439
|
-
// ⇒ live, so after a reboot a reused PID blocks until it exits, surfaced by R2).
|
|
440
|
-
if (r.boot && self.boot && r.boot !== self.boot && platform === "linux") {
|
|
441
|
-
return { verdict: "dead", datable: false };
|
|
442
|
-
}
|
|
586
|
+
return { verdict: "undecidable", datable: false };
|
|
587
|
+
if (r.boot === null || self.boot === null || r.boot !== self.boot)
|
|
588
|
+
return { verdict: "undecidable", datable: false };
|
|
443
589
|
// An unreadable namespace on EITHER side is a genuine unknown — two records with a
|
|
444
590
|
// null namespace must never be treated as co-located (null-equality).
|
|
445
591
|
if (r.ns === null || self.ns === null)
|
|
@@ -500,21 +646,94 @@ export function livenessOf(r, self, deps = {}) {
|
|
|
500
646
|
export function isCertainlyDead(r) {
|
|
501
647
|
return classifyLiveness(r, me()).verdict === "dead";
|
|
502
648
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
649
|
+
/**
|
|
650
|
+
* Token-fenced escape hatch, using the same one-shot SUCC election as acquisition.
|
|
651
|
+
* It never owns or republishes LOCK and never sends a destructive process signal.
|
|
652
|
+
* An automatic successor already elected for g excludes the operator. After g is
|
|
653
|
+
* removed, a pending automatic retire sees absence and may publish a fresh LOCK.
|
|
654
|
+
*/
|
|
655
|
+
export function breakLockAsOperator(lockPath, options) {
|
|
656
|
+
const g = options?.expectToken;
|
|
657
|
+
if (typeof g !== "string" || !LOCK_TOKEN_RE.test(g)
|
|
658
|
+
|| (g.startsWith("legacy-") && !/^legacy-[a-f0-9]{64}$/.test(g))) {
|
|
659
|
+
return { broken: false, reason: "invalid-token", legacyRecords: 0 };
|
|
660
|
+
}
|
|
661
|
+
const holder = readLockHolder(lockPath);
|
|
662
|
+
if (holder === "absent" || holder === "corrupt") {
|
|
663
|
+
return { broken: false, reason: holder, legacyRecords: 0 };
|
|
664
|
+
}
|
|
665
|
+
const legacyRecords = holder.kind === "legacy" ? 1 : 0;
|
|
666
|
+
const context = {
|
|
667
|
+
token: holder.token, pid: holder.pid, legacyRecords,
|
|
668
|
+
...(legacyRecords === 1 ? { diagnostic: {
|
|
669
|
+
code: "legacy-record",
|
|
670
|
+
command: `h2a identity unlock --token ${holder.token} --assert-dead`
|
|
671
|
+
} } : {})
|
|
672
|
+
};
|
|
673
|
+
const refused = (reason) => ({ broken: false, reason, ...context });
|
|
674
|
+
if (holder.token !== g)
|
|
675
|
+
return refused("token-mismatch");
|
|
676
|
+
const operatorSelf = me();
|
|
677
|
+
const verdict = classifyLiveness(holder, operatorSelf).verdict;
|
|
678
|
+
if (verdict === "live")
|
|
679
|
+
return refused("live");
|
|
680
|
+
if (verdict === "undecidable" && options.assertDead !== true)
|
|
681
|
+
return refused("assert-dead-required");
|
|
682
|
+
let t = g;
|
|
683
|
+
for (let depth = 0; depth < PREFIX_LOCK_MAX_CHAIN; depth++) {
|
|
684
|
+
// A retry never silently changes the requested target to a replacement owner.
|
|
685
|
+
const current = readLockHolder(lockPath);
|
|
686
|
+
if (current === "corrupt")
|
|
687
|
+
return refused("corrupt");
|
|
688
|
+
if (current === "absent" || current.token !== g)
|
|
689
|
+
return refused("token-mismatch");
|
|
690
|
+
const successor = {
|
|
691
|
+
...makeLockRecFor(operatorSelf, newToken(), g),
|
|
692
|
+
...(verdict === "undecidable" ? { operator: true } : {})
|
|
693
|
+
};
|
|
694
|
+
const published = publishLockRecord(succPathFor(lockPath, t), successor);
|
|
695
|
+
if (published.status === "error")
|
|
696
|
+
return refused(`error:${published.code}`);
|
|
697
|
+
if (published.status === "retry")
|
|
698
|
+
return refused("busy");
|
|
699
|
+
if (published.status === "exists") {
|
|
700
|
+
const link = readLockRecord(succPathFor(lockPath, t));
|
|
701
|
+
if (link === "absent")
|
|
702
|
+
return refused("busy");
|
|
703
|
+
if (link === "corrupt" || link.target !== g)
|
|
704
|
+
return refused("dead-undecidable");
|
|
705
|
+
const linkVerdict = classifyLiveness(link, operatorSelf).verdict;
|
|
706
|
+
if (linkVerdict !== "dead")
|
|
707
|
+
return refused(linkVerdict === "live" ? "busy" : "dead-undecidable");
|
|
708
|
+
t = link.token;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
// Election makes this the sole successor. Re-read as the last action before
|
|
712
|
+
// unlink, including the raw-byte legacy token, so a winning LOCK survives.
|
|
713
|
+
const confirmed = readLockHolder(lockPath);
|
|
714
|
+
if (confirmed === "corrupt")
|
|
715
|
+
return refused("corrupt");
|
|
716
|
+
if (confirmed === "absent" || confirmed.token !== g) {
|
|
717
|
+
purgeSuccession(dirname(lockPath), lockPath, g);
|
|
718
|
+
return refused("token-mismatch");
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
unlinkSync(lockPath);
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
if (errnoOf(error) !== "ENOENT")
|
|
725
|
+
return refused(`error:${errnoOf(error)}`);
|
|
726
|
+
}
|
|
727
|
+
purgeSuccession(dirname(lockPath), lockPath, g);
|
|
728
|
+
return { broken: true, ...context };
|
|
514
729
|
}
|
|
730
|
+
return refused("dead-undecidable");
|
|
515
731
|
}
|
|
516
|
-
|
|
517
|
-
|
|
732
|
+
function lockDenied(reason) {
|
|
733
|
+
return { acquired: false, stillHeld: () => false, release: () => { }, reason };
|
|
734
|
+
}
|
|
735
|
+
/** Production acquisition. Extra JavaScript arguments cannot inject dependencies. */
|
|
736
|
+
export function acquirePrefixLock(prefix, options = {}) {
|
|
518
737
|
const lockPath = lockPathFor(prefix);
|
|
519
738
|
try {
|
|
520
739
|
mkdirSync(prefix, { recursive: true });
|
|
@@ -524,26 +743,39 @@ export function acquirePrefixLock(prefix, hooks = {}) {
|
|
|
524
743
|
}
|
|
525
744
|
const self = me();
|
|
526
745
|
for (let round = 0; round < PREFIX_LOCK_MAX_ROUNDS; round++) {
|
|
527
|
-
if (
|
|
528
|
-
|
|
746
|
+
if (options?.readFirst) {
|
|
747
|
+
const first = readLockHolderDetailed(lockPath);
|
|
748
|
+
// An I/O failure says nothing about presence; retain ordinary errno handling.
|
|
749
|
+
if (typeof first === "object" && first.kind === "io-error") {
|
|
750
|
+
// Fall through to ordinary publication.
|
|
751
|
+
}
|
|
752
|
+
else if (first !== "absent") {
|
|
753
|
+
if (first === "corrupt")
|
|
754
|
+
return lockDenied("dead-undecidable");
|
|
755
|
+
const firstLiveness = classifyLiveness(first, self).verdict;
|
|
756
|
+
if (firstLiveness !== "dead") {
|
|
757
|
+
return lockDenied(firstLiveness === "live" ? "busy" : "dead-undecidable");
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// A preliminary death authorizes nothing: publish, then read/classify afresh.
|
|
529
761
|
}
|
|
530
762
|
const tok = newToken();
|
|
531
|
-
const pub = publishLockRecord(lockPath,
|
|
763
|
+
const pub = publishLockRecord(lockPath, makeLockRecFor(self, tok));
|
|
532
764
|
if (pub.status === "ok")
|
|
533
765
|
return makeLease(prefix, lockPath, tok);
|
|
534
766
|
if (pub.status === "error")
|
|
535
767
|
return lockDenied(`error:${pub.code}`);
|
|
536
768
|
if (pub.status === "retry")
|
|
537
769
|
continue;
|
|
538
|
-
const cur = readLockRecord(lockPath);
|
|
770
|
+
const cur = readLockRecord(lockPath); // fresh after EEXIST; sole entry to succession
|
|
539
771
|
if (cur === "absent")
|
|
540
772
|
continue; // released meanwhile
|
|
541
773
|
if (cur === "corrupt")
|
|
542
774
|
return lockDenied("dead-undecidable"); // fail closed
|
|
543
|
-
const live =
|
|
775
|
+
const live = classifyLiveness(cur, self).verdict;
|
|
544
776
|
if (live !== "dead")
|
|
545
777
|
return lockDenied(live === "live" ? "busy" : "dead-undecidable");
|
|
546
|
-
const next = succeedDeadToken(prefix, lockPath, cur.token,
|
|
778
|
+
const next = succeedDeadToken(prefix, lockPath, cur.token, self);
|
|
547
779
|
if (next !== "retry")
|
|
548
780
|
return next;
|
|
549
781
|
}
|
|
@@ -554,23 +786,13 @@ export function acquirePrefixLock(prefix, hooks = {}) {
|
|
|
554
786
|
* SUCC(g) -> SUCC(r0) -> ...; publishing a link elects the sole live
|
|
555
787
|
* successor (Lemma B). "retry" means the chain settled meanwhile (re-read).
|
|
556
788
|
*/
|
|
557
|
-
function succeedDeadToken(prefix, lockPath, g,
|
|
558
|
-
const self = me();
|
|
789
|
+
function succeedDeadToken(prefix, lockPath, g, self) {
|
|
559
790
|
let t = g;
|
|
560
791
|
for (let depth = 0; depth < PREFIX_LOCK_MAX_CHAIN; depth++) {
|
|
561
792
|
const tok = newToken();
|
|
562
|
-
const pub = publishLockRecord(succPathFor(lockPath, t),
|
|
793
|
+
const pub = publishLockRecord(succPathFor(lockPath, t), makeLockRecFor(self, tok, g));
|
|
563
794
|
if (pub.status === "ok") {
|
|
564
|
-
|
|
565
|
-
prefix,
|
|
566
|
-
lockPath,
|
|
567
|
-
path: succPathFor(lockPath, t),
|
|
568
|
-
token: tok,
|
|
569
|
-
target: g,
|
|
570
|
-
round,
|
|
571
|
-
depth
|
|
572
|
-
});
|
|
573
|
-
return retireDeadToken(prefix, lockPath, g, hooks, round, depth);
|
|
795
|
+
return retireDeadToken(prefix, lockPath, g, self);
|
|
574
796
|
}
|
|
575
797
|
if (pub.status === "error")
|
|
576
798
|
return lockDenied(`error:${pub.code}`);
|
|
@@ -581,7 +803,7 @@ function succeedDeadToken(prefix, lockPath, g, hooks, round) {
|
|
|
581
803
|
return "retry"; // chain already settled
|
|
582
804
|
if (s === "corrupt" || s.target !== g)
|
|
583
805
|
return lockDenied("dead-undecidable");
|
|
584
|
-
const live =
|
|
806
|
+
const live = classifyLiveness(s, self).verdict;
|
|
585
807
|
if (live !== "dead")
|
|
586
808
|
return lockDenied(live === "live" ? "busy" : "dead-undecidable");
|
|
587
809
|
t = s.token; // it died: succeed it
|
|
@@ -593,19 +815,11 @@ function succeedDeadToken(prefix, lockPath, g, hooks, round) {
|
|
|
593
815
|
* opus `retire`: targeted removal (S) of exactly g (Lemma C), then purge the
|
|
594
816
|
* now-inert SUCC files targeting g (I5), then publish a fresh token.
|
|
595
817
|
*/
|
|
596
|
-
function retireDeadToken(prefix, lockPath, g,
|
|
818
|
+
function retireDeadToken(prefix, lockPath, g, self) {
|
|
597
819
|
const cur = readLockRecord(lockPath);
|
|
598
820
|
if (cur === "corrupt")
|
|
599
821
|
return lockDenied("dead-undecidable"); // keep our SUCC: fail closed
|
|
600
822
|
if (cur !== "absent" && cur.token === g) {
|
|
601
|
-
invokeLockHook(hooks.beforeRetireUnlink, {
|
|
602
|
-
prefix,
|
|
603
|
-
lockPath,
|
|
604
|
-
path: lockPath,
|
|
605
|
-
target: g,
|
|
606
|
-
round,
|
|
607
|
-
depth
|
|
608
|
-
});
|
|
609
823
|
// Lemma C (targeted removal), hardened: re-read LOCK as the LAST step before the
|
|
610
824
|
// unlink and remove it ONLY while it is STILL exactly g. Successor uniqueness
|
|
611
825
|
// (Lemma B) + monotonicity (I2) prove LOCK cannot legally change from g under this
|
|
@@ -631,14 +845,6 @@ function retireDeadToken(prefix, lockPath, g, hooks, round, depth) {
|
|
|
631
845
|
catch (e) {
|
|
632
846
|
unlinkCode = errnoOf(e);
|
|
633
847
|
}
|
|
634
|
-
invokeLockHook(hooks.afterRetireUnlink, {
|
|
635
|
-
prefix,
|
|
636
|
-
lockPath,
|
|
637
|
-
path: lockPath,
|
|
638
|
-
target: g,
|
|
639
|
-
round,
|
|
640
|
-
depth
|
|
641
|
-
});
|
|
642
848
|
// LOCK != g not established: keep our SUCC file, fail closed.
|
|
643
849
|
if (unlinkCode !== undefined && unlinkCode !== "ENOENT") {
|
|
644
850
|
return lockDenied(`error:${unlinkCode}`);
|
|
@@ -649,7 +855,7 @@ function retireDeadToken(prefix, lockPath, g, hooks, round, depth) {
|
|
|
649
855
|
// From here LOCK != g forever (I2): every SUCC targeting g is inert.
|
|
650
856
|
purgeSuccession(prefix, lockPath, g); // unlink SUCC files whose target === g
|
|
651
857
|
const tok = newToken();
|
|
652
|
-
const pub = publishLockRecord(lockPath,
|
|
858
|
+
const pub = publishLockRecord(lockPath, makeLockRecFor(self, tok));
|
|
653
859
|
if (pub.status === "ok")
|
|
654
860
|
return makeLease(prefix, lockPath, tok);
|
|
655
861
|
if (pub.status === "exists" || pub.status === "retry")
|
|
@@ -662,6 +868,12 @@ function retireDeadToken(prefix, lockPath, g, hooks, round, depth) {
|
|
|
662
868
|
*/
|
|
663
869
|
function makeLease(prefix, lockPath, token) {
|
|
664
870
|
let done = false;
|
|
871
|
+
const stillHeld = () => {
|
|
872
|
+
if (done)
|
|
873
|
+
return false;
|
|
874
|
+
const cur = readLockRecord(lockPath);
|
|
875
|
+
return cur !== "absent" && cur !== "corrupt" && cur.token === token;
|
|
876
|
+
};
|
|
665
877
|
const release = () => {
|
|
666
878
|
if (done)
|
|
667
879
|
return;
|
|
@@ -690,7 +902,7 @@ function makeLease(prefix, lockPath, token) {
|
|
|
690
902
|
// best-effort
|
|
691
903
|
}
|
|
692
904
|
collectLockDebris(prefix, lockPath, token); // holder-only GC (I5-safe)
|
|
693
|
-
return { acquired: true, release, token };
|
|
905
|
+
return { acquired: true, stillHeld, release, token };
|
|
694
906
|
}
|
|
695
907
|
/** Unlink SUCC files whose target === g (called only when LOCK != g can hold). */
|
|
696
908
|
function purgeSuccession(prefix, lockPath, g) {
|