stitchkit 0.103.9 → 0.103.10
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/CHANGELOG.md +8 -0
- package/dist/agent-runtime/store-conformance-lifecycle.d.ts +10 -10
- package/dist/entrypoints/agent-runtime/sandbox.js +1 -1
- package/dist/entrypoints/application/diagnostic-journal.js +2 -1
- package/dist/entrypoints/files.js +8 -4
- package/dist/entrypoints/process.d.ts +2 -1
- package/dist/entrypoints/process.d.ts.map +1 -1
- package/dist/entrypoints/process.js +9 -2
- package/dist/{index-eeqmg7qm.js → index-6m6a9ty4.js} +92 -9
- package/dist/index-ga597cvy.js +184 -0
- package/dist/index-nybn2err.js +213 -0
- package/dist/internal/exclusive-lock.d.ts +2 -1
- package/dist/internal/exclusive-lock.d.ts.map +1 -1
- package/dist/internal/process-instance.d.ts +46 -9
- package/dist/internal/process-instance.d.ts.map +1 -1
- package/dist/internal/with-exclusive-lock.d.ts +4 -3
- package/dist/internal/with-exclusive-lock.d.ts.map +1 -1
- package/dist/process/command-owner.d.ts.map +1 -1
- package/dist/process/contract.d.ts +10 -0
- package/dist/process/contract.d.ts.map +1 -1
- package/dist/process/group.d.ts +1 -1
- package/dist/process/group.d.ts.map +1 -1
- package/dist/process/leader-settlement.d.ts +4 -0
- package/dist/process/leader-settlement.d.ts.map +1 -0
- package/llms/files.txt +1 -1
- package/llms/process.txt +76 -1
- package/llms-full.txt +77 -2
- package/llms.txt +2 -2
- package/native/darwin-arm64.node +0 -0
- package/native/darwin-x64.node +0 -0
- package/package.json +1 -1
- package/dist/index-7kxw1awn.js +0 -328
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,14 @@ additive**; the first breaking change landed in 0.10.0. Grep the file for
|
|
|
15
15
|
|
|
16
16
|
## [Unreleased]
|
|
17
17
|
|
|
18
|
+
## [0.103.10] - 2026-10-01
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `stitchkit/process`: `onLeaderSettled` observes launcher exit before inherited-pipe drain and settles resource scopes once on terminal failure; callbacks have bounded cancellation and preserve initial causes. Public process identity observation/probing preserves unavailable evidence and its cause from the existing Linux/Darwin owner.
|
|
23
|
+
- `stitchkit/files`: `ownerlessGraceMs: null` explicitly refuses age-based reclaim of unknown locks and guards; exact requested lock permissions survive restrictive umask, and failed descriptor setup cannot remove a replacement inode.
|
|
24
|
+
- Document and qualify neutral library bundling from public imports and published declarations with Zod as its only runtime peer; full-kernel install remains a build-time choice.
|
|
25
|
+
|
|
18
26
|
## [0.103.9] - 2026-10-01
|
|
19
27
|
|
|
20
28
|
### Added
|
|
@@ -133,6 +133,11 @@ export declare function fencingPhase(store: AgentRuntimeStore, conversationId: s
|
|
|
133
133
|
parts: ({
|
|
134
134
|
type: "text";
|
|
135
135
|
text: string;
|
|
136
|
+
} | {
|
|
137
|
+
type: "file";
|
|
138
|
+
mediaType: string;
|
|
139
|
+
reference: string;
|
|
140
|
+
filename?: string | undefined;
|
|
136
141
|
} | {
|
|
137
142
|
type: "reasoning";
|
|
138
143
|
text: string;
|
|
@@ -141,11 +146,6 @@ export declare function fencingPhase(store: AgentRuntimeStore, conversationId: s
|
|
|
141
146
|
provider: string;
|
|
142
147
|
data: Record<string, import("zod").JSONType>;
|
|
143
148
|
} | undefined;
|
|
144
|
-
} | {
|
|
145
|
-
type: "file";
|
|
146
|
-
mediaType: string;
|
|
147
|
-
reference: string;
|
|
148
|
-
filename?: string | undefined;
|
|
149
149
|
} | {
|
|
150
150
|
type: "source";
|
|
151
151
|
sourceId: string;
|
|
@@ -313,6 +313,11 @@ export declare function fencingPhase(store: AgentRuntimeStore, conversationId: s
|
|
|
313
313
|
parts: ({
|
|
314
314
|
type: "text";
|
|
315
315
|
text: string;
|
|
316
|
+
} | {
|
|
317
|
+
type: "file";
|
|
318
|
+
mediaType: string;
|
|
319
|
+
reference: string;
|
|
320
|
+
filename?: string | undefined;
|
|
316
321
|
} | {
|
|
317
322
|
type: "reasoning";
|
|
318
323
|
text: string;
|
|
@@ -321,11 +326,6 @@ export declare function fencingPhase(store: AgentRuntimeStore, conversationId: s
|
|
|
321
326
|
provider: string;
|
|
322
327
|
data: Record<string, import("zod").JSONType>;
|
|
323
328
|
} | undefined;
|
|
324
|
-
} | {
|
|
325
|
-
type: "file";
|
|
326
|
-
mediaType: string;
|
|
327
|
-
reference: string;
|
|
328
|
-
filename?: string | undefined;
|
|
329
329
|
} | {
|
|
330
330
|
type: "source";
|
|
331
331
|
sourceId: string;
|
|
@@ -14,9 +14,10 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
createBoundedChannel2
|
|
16
16
|
} from "../../index-w0eqdhyd.js";
|
|
17
|
+
import"../../index-nybn2err.js";
|
|
17
18
|
import {
|
|
18
19
|
attemptExclusiveLock
|
|
19
|
-
} from "../../index-
|
|
20
|
+
} from "../../index-ga597cvy.js";
|
|
20
21
|
import {
|
|
21
22
|
isRecord
|
|
22
23
|
} from "../../index-9jfdm5tg.js";
|
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
AppError2
|
|
11
11
|
} from "../index-n53mswt3.js";
|
|
12
|
+
import"../index-nybn2err.js";
|
|
12
13
|
import {
|
|
13
14
|
attemptExclusiveLock
|
|
14
|
-
} from "../index-
|
|
15
|
+
} from "../index-ga597cvy.js";
|
|
15
16
|
import {
|
|
16
17
|
writeFileAtomic2,
|
|
17
18
|
writeFileAtomicSync2
|
|
@@ -300,7 +301,10 @@ function pause(ms, signal) {
|
|
|
300
301
|
async function withExclusiveLock(path, run, options = {}) {
|
|
301
302
|
const label = options.label ?? path;
|
|
302
303
|
const timeoutMs = nonNegative("timeoutMs", options.timeoutMs, DEFAULT_TIMEOUT_MS);
|
|
303
|
-
const ownerlessGraceMs = nonNegative("ownerlessGraceMs", options.ownerlessGraceMs, DEFAULT_OWNERLESS_GRACE_MS);
|
|
304
|
+
const ownerlessGraceMs = options.ownerlessGraceMs === null ? null : nonNegative("ownerlessGraceMs", options.ownerlessGraceMs, DEFAULT_OWNERLESS_GRACE_MS);
|
|
305
|
+
const mode = options.mode ?? 384;
|
|
306
|
+
if (!Number.isInteger(mode) || mode < 0 || mode > 511)
|
|
307
|
+
throw new RangeError("[stitchkit] withExclusiveLock: mode must be permission bits from 0000 to 0777");
|
|
304
308
|
const { signal } = options;
|
|
305
309
|
const deadline = performance.now() + timeoutMs;
|
|
306
310
|
let delay = FIRST_RETRY_MS;
|
|
@@ -309,7 +313,7 @@ async function withExclusiveLock(path, run, options = {}) {
|
|
|
309
313
|
throw new ExclusiveLockError("LOCK_ABORTED", label, null, `[stitchkit] waiting for the lock on "${label}" was cancelled`, { cause: signal.reason });
|
|
310
314
|
}
|
|
311
315
|
const attempt = await attemptExclusiveLock(path, {
|
|
312
|
-
mode
|
|
316
|
+
mode,
|
|
313
317
|
reclaim: true,
|
|
314
318
|
ownerlessGraceMs,
|
|
315
319
|
...options.machineIdentity !== undefined && {
|
|
@@ -333,7 +337,7 @@ async function withExclusiveLock(path, run, options = {}) {
|
|
|
333
337
|
const remaining = deadline - performance.now();
|
|
334
338
|
if (remaining <= 0) {
|
|
335
339
|
const holder = attempt.diagnosis?.owner ?? null;
|
|
336
|
-
throw new ExclusiveLockError("LOCK_TIMEOUT", label, holder, `[stitchkit] the lock on "${label}" is held by ${describeHolder(holder)}${describeEvidence(attempt.diagnosis)}; gave up after ${timeoutMs} ms`, { cause: attempt.error });
|
|
340
|
+
throw new ExclusiveLockError("LOCK_TIMEOUT", label, holder, `[stitchkit] the lock on "${label}" is held by ${describeHolder(holder)}${describeEvidence(attempt.diagnosis)}; gave up after ${timeoutMs} ms`, { cause: attempt.diagnosis?.cause ?? attempt.error });
|
|
337
341
|
}
|
|
338
342
|
await pause(Math.min(delay * (0.5 + Math.random() / 2), remaining), signal);
|
|
339
343
|
delay = Math.min(delay * 2, MAX_RETRY_MS);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** POSIX Node/Bun finite native commands, independent of the agent runtime. */
|
|
2
|
+
export { observeProcessInstance, type ProcessInstance, type ProcessInstanceObservation, ProcessInstanceSchema, type ProcessOwnerEvidence, probeProcessOwner, } from '../internal/process-instance.js';
|
|
2
3
|
export { runNativeCommand } from '../process/command.js';
|
|
3
|
-
export { NativeCommandError, type NativeCommandOptions, type NativeCommandResult, } from '../process/contract.js';
|
|
4
|
+
export { NativeCommandError, type NativeCommandOptions, type NativeCommandResult, type NativeCommandSettlement, } from '../process/contract.js';
|
|
4
5
|
//# sourceMappingURL=process.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/entrypoints/process.ts"],"names":[],"mappings":"AAAA,+EAA+E;
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/entrypoints/process.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,OAAO,EACL,sBAAsB,EACtB,KAAK,eAAe,EACpB,KAAK,0BAA0B,EAC/B,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,iBAAiB,GAClB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,kBAAkB,EAClB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,GAC7B,MAAM,qBAAqB,CAAC"}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ProcessInstanceSchema2,
|
|
3
|
+
observeProcessInstance2,
|
|
4
|
+
probeProcessOwner2
|
|
5
|
+
} from "../index-nybn2err.js";
|
|
1
6
|
import {
|
|
2
7
|
NativeCommandError2,
|
|
3
8
|
startNativeCommand
|
|
4
|
-
} from "../index-
|
|
5
|
-
|
|
9
|
+
} from "../index-6m6a9ty4.js";
|
|
6
10
|
// src/process/command.ts
|
|
7
11
|
function runNativeCommand(input) {
|
|
8
12
|
return startNativeCommand(input).result;
|
|
9
13
|
}
|
|
10
14
|
export {
|
|
11
15
|
NativeCommandError2 as NativeCommandError,
|
|
16
|
+
ProcessInstanceSchema2 as ProcessInstanceSchema,
|
|
17
|
+
observeProcessInstance2 as observeProcessInstance,
|
|
18
|
+
probeProcessOwner2 as probeProcessOwner,
|
|
12
19
|
runNativeCommand
|
|
13
20
|
};
|
|
@@ -4,6 +4,14 @@ import {
|
|
|
4
4
|
|
|
5
5
|
// src/process/contract.ts
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
var NativeCommandSettlementSchema = z.discriminatedUnion("kind", [
|
|
8
|
+
z.object({
|
|
9
|
+
kind: z.literal("exit"),
|
|
10
|
+
exitCode: z.number().int().nullable(),
|
|
11
|
+
signal: z.string().nullable()
|
|
12
|
+
}),
|
|
13
|
+
z.object({ kind: z.literal("error"), cause: z.unknown() })
|
|
14
|
+
]);
|
|
7
15
|
var NativeCommandOptionsSchema = z.strictObject({
|
|
8
16
|
executable: z.string().min(1),
|
|
9
17
|
args: z.array(z.string()).default([]),
|
|
@@ -17,6 +25,7 @@ var NativeCommandOptionsSchema = z.strictObject({
|
|
|
17
25
|
stdin: z.custom((v) => v instanceof Uint8Array).optional(),
|
|
18
26
|
maxStdinBytes: z.number().int().positive().default(1024 * 1024),
|
|
19
27
|
onOutput: z.custom((v) => typeof v === "function").optional(),
|
|
28
|
+
onLeaderSettled: z.custom((v) => typeof v === "function").optional(),
|
|
20
29
|
killGraceMs: z.number().int().nonnegative().max(1e4).default(100),
|
|
21
30
|
cleanupTimeoutMs: z.number().int().positive().max(30000).default(2000)
|
|
22
31
|
}).superRefine((input, ctx) => {
|
|
@@ -50,6 +59,9 @@ class NativeCommandError2 extends Error {
|
|
|
50
59
|
import { spawn } from "node:child_process";
|
|
51
60
|
|
|
52
61
|
// src/process/group.ts
|
|
62
|
+
function deniedOnDarwin(error) {
|
|
63
|
+
return process.platform === "darwin" && error instanceof Error && "code" in error && error.code === "EPERM";
|
|
64
|
+
}
|
|
53
65
|
function signalGroup(pid, signal) {
|
|
54
66
|
if (pid === undefined)
|
|
55
67
|
return;
|
|
@@ -69,15 +81,30 @@ function exists(pid) {
|
|
|
69
81
|
} catch (error) {
|
|
70
82
|
if (error instanceof Error && "code" in error && error.code === "ESRCH")
|
|
71
83
|
return false;
|
|
84
|
+
if (deniedOnDarwin(error))
|
|
85
|
+
return true;
|
|
72
86
|
throw error;
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
|
-
async function
|
|
76
|
-
|
|
89
|
+
async function signalUntilSettled(pid, signal, deadline) {
|
|
90
|
+
for (;; ) {
|
|
91
|
+
try {
|
|
92
|
+
signalGroup(pid, signal);
|
|
93
|
+
return;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!deniedOnDarwin(error) || Date.now() >= deadline)
|
|
96
|
+
throw error;
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(10, deadline - Date.now())));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function stopCommandGroup(pid, graceMs, cleanupTimeoutMs = 2000) {
|
|
102
|
+
const deadline = Date.now() + graceMs + cleanupTimeoutMs;
|
|
103
|
+
await signalUntilSettled(pid, "SIGTERM", deadline);
|
|
77
104
|
const until = Date.now() + graceMs;
|
|
78
105
|
while (Date.now() < until && exists(pid))
|
|
79
106
|
await new Promise((resolve) => setTimeout(resolve, Math.min(10, until - Date.now())));
|
|
80
|
-
|
|
107
|
+
await signalUntilSettled(pid, "SIGKILL", deadline);
|
|
81
108
|
}
|
|
82
109
|
function commandCleanupError(cause) {
|
|
83
110
|
return new NativeCommandError2("COMMAND_CLEANUP", "Command cleanup did not complete", {
|
|
@@ -88,6 +115,39 @@ function waitForCommandClose(closed, timeoutMs) {
|
|
|
88
115
|
return raceAbort(closed, AbortSignal.timeout(timeoutMs));
|
|
89
116
|
}
|
|
90
117
|
|
|
118
|
+
// src/process/leader-settlement.ts
|
|
119
|
+
function createLeaderSettlement(options, signal) {
|
|
120
|
+
let pending;
|
|
121
|
+
return (event) => {
|
|
122
|
+
if (pending)
|
|
123
|
+
return pending;
|
|
124
|
+
pending = (async () => {
|
|
125
|
+
if (!options.onLeaderSettled)
|
|
126
|
+
return;
|
|
127
|
+
const controller = new AbortController;
|
|
128
|
+
const timer = setTimeout(() => controller.abort(new Error("Leader settlement deadline exceeded")), options.cleanupTimeoutMs);
|
|
129
|
+
const abort = () => controller.abort(signal.reason);
|
|
130
|
+
if (!signal.aborted)
|
|
131
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
132
|
+
try {
|
|
133
|
+
await raceAbort(Promise.resolve(options.onLeaderSettled(event, controller.signal)), controller.signal);
|
|
134
|
+
} catch (cause) {
|
|
135
|
+
if (signal.aborted && cause === signal.reason)
|
|
136
|
+
throw cause;
|
|
137
|
+
throw commandCleanupError(new AggregateError([event, cause], "Leader settlement failed"));
|
|
138
|
+
} finally {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
signal.removeEventListener("abort", abort);
|
|
141
|
+
controller.abort(new Error("Leader settlement completed"));
|
|
142
|
+
}
|
|
143
|
+
})();
|
|
144
|
+
pending.catch(() => {
|
|
145
|
+
return;
|
|
146
|
+
});
|
|
147
|
+
return pending;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
91
151
|
// src/process/command-owner.ts
|
|
92
152
|
function startNativeCommand(input, onSpawn) {
|
|
93
153
|
const options = NativeCommandOptionsSchema.parse(input);
|
|
@@ -119,6 +179,19 @@ function startNativeCommand(input, onSpawn) {
|
|
|
119
179
|
let total = 0;
|
|
120
180
|
let finished = false;
|
|
121
181
|
let spawnFailure;
|
|
182
|
+
const settle = createLeaderSettlement(options, controller.signal);
|
|
183
|
+
const leader = new Promise((resolve) => {
|
|
184
|
+
child.once("exit", (exitCode, signal) => resolve({ kind: "exit", exitCode, signal }));
|
|
185
|
+
child.once("error", (cause) => resolve({ kind: "error", cause }));
|
|
186
|
+
});
|
|
187
|
+
const pendingHandles = new Set(["leader", "stdin", "stdout", "stderr"]);
|
|
188
|
+
const released = Promise.all([
|
|
189
|
+
leader.then(() => pendingHandles.delete("leader")),
|
|
190
|
+
...["stdin", "stdout", "stderr"].map((name) => new Promise((resolve) => child[name].once("close", () => {
|
|
191
|
+
pendingHandles.delete(name);
|
|
192
|
+
resolve();
|
|
193
|
+
})))
|
|
194
|
+
]);
|
|
122
195
|
const closed = new Promise((resolve) => {
|
|
123
196
|
child.once("error", (error) => {
|
|
124
197
|
spawnFailure = new NativeCommandError2("COMMAND_UNAVAILABLE", "Command could not start", {
|
|
@@ -157,7 +230,7 @@ function startNativeCommand(input, onSpawn) {
|
|
|
157
230
|
drain(child.stdout, "stdout", stdout),
|
|
158
231
|
drain(child.stderr, "stderr", stderr)
|
|
159
232
|
]);
|
|
160
|
-
await raceAbort(drains, controller.signal);
|
|
233
|
+
await raceAbort(Promise.all([drains, leader.then(settle)]), controller.signal);
|
|
161
234
|
const status = await raceAbort(closed, controller.signal);
|
|
162
235
|
if (spawnFailure !== undefined)
|
|
163
236
|
throw spawnFailure;
|
|
@@ -167,11 +240,21 @@ function startNativeCommand(input, onSpawn) {
|
|
|
167
240
|
child.stdin.destroy();
|
|
168
241
|
child.stdout.destroy();
|
|
169
242
|
child.stderr.destroy();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
243
|
+
const cleanup = await Promise.allSettled([
|
|
244
|
+
settle({ kind: "error", cause: error }),
|
|
245
|
+
stopCommandGroup(child.pid, options.killGraceMs, options.cleanupTimeoutMs).then(async () => {
|
|
246
|
+
try {
|
|
247
|
+
await waitForCommandClose(released, options.cleanupTimeoutMs);
|
|
248
|
+
} catch (cause) {
|
|
249
|
+
throw new Error(`Command handles remained open: ${[...pendingHandles].join(", ")}`, {
|
|
250
|
+
cause
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
]);
|
|
255
|
+
const failures = cleanup.flatMap((item) => item.status === "rejected" && item.reason !== error ? [item.reason] : []);
|
|
256
|
+
if (failures.length) {
|
|
257
|
+
throw commandCleanupError(new AggregateError([error, ...failures], "Command failed and cleanup failed"));
|
|
175
258
|
}
|
|
176
259
|
throw error;
|
|
177
260
|
} finally {
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
machineIdentity,
|
|
3
|
+
ProcessInstanceSchema2,
|
|
4
|
+
readProcessInstance,
|
|
5
|
+
probeProcessOwner2
|
|
6
|
+
} from "./index-nybn2err.js";
|
|
7
|
+
import {
|
|
8
|
+
isRecord
|
|
9
|
+
} from "./index-9jfdm5tg.js";
|
|
10
|
+
|
|
11
|
+
// src/internal/exclusive-lock.ts
|
|
12
|
+
import { constants } from "node:fs";
|
|
13
|
+
import { lstat, open, unlink } from "node:fs/promises";
|
|
14
|
+
import { hostname } from "node:os";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
var ExclusiveLockOwnerSchema = z.object({
|
|
17
|
+
pid: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
18
|
+
host: z.string().min(1),
|
|
19
|
+
acquiredAt: z.string(),
|
|
20
|
+
machine: z.string().min(1).optional().catch(undefined),
|
|
21
|
+
process: ProcessInstanceSchema2.nullable().optional().catch(null)
|
|
22
|
+
});
|
|
23
|
+
function isCode(error, code) {
|
|
24
|
+
return isRecord(error) && error.code === code;
|
|
25
|
+
}
|
|
26
|
+
function readOwner(text) {
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(text);
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const owner = ExclusiveLockOwnerSchema.safeParse(parsed);
|
|
34
|
+
return owner.success ? owner.data : undefined;
|
|
35
|
+
}
|
|
36
|
+
function attribute(owner, identity) {
|
|
37
|
+
if (identity !== null && owner.machine !== undefined) {
|
|
38
|
+
return owner.machine === identity ? "this-machine" : "another-machine";
|
|
39
|
+
}
|
|
40
|
+
return owner.host === hostname() ? "this-machine" : "unattributable";
|
|
41
|
+
}
|
|
42
|
+
async function diagnose(owner, declaredIdentity) {
|
|
43
|
+
if (!owner)
|
|
44
|
+
return { attribution: "unattributable", liveness: "not-probed", owner: null };
|
|
45
|
+
const attribution = attribute(owner, await machineIdentity(declaredIdentity));
|
|
46
|
+
if (attribution !== "this-machine")
|
|
47
|
+
return { attribution, liveness: "not-probed", owner };
|
|
48
|
+
return { attribution, ...await probeProcessOwner2(owner.pid, owner.process), owner };
|
|
49
|
+
}
|
|
50
|
+
async function readLockFile(path, onFailure) {
|
|
51
|
+
let handle;
|
|
52
|
+
try {
|
|
53
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
54
|
+
const info = await handle.stat();
|
|
55
|
+
return {
|
|
56
|
+
owner: readOwner(await handle.readFile("utf8")),
|
|
57
|
+
ino: info.ino,
|
|
58
|
+
mtimeMs: info.mtimeMs
|
|
59
|
+
};
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
if (!isCode(cause, "ENOENT"))
|
|
62
|
+
onFailure?.(cause);
|
|
63
|
+
return;
|
|
64
|
+
} finally {
|
|
65
|
+
await handle?.close().catch(() => {
|
|
66
|
+
return;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function unlinkIfSame(path, ino) {
|
|
71
|
+
try {
|
|
72
|
+
if ((await lstat(path)).ino !== ino)
|
|
73
|
+
return;
|
|
74
|
+
await unlink(path);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (!isCode(error, "ENOENT"))
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function createOwned(path, options, reclaimed) {
|
|
81
|
+
const identity = await machineIdentity(options.machineIdentity);
|
|
82
|
+
const instance = await readProcessInstance(process.pid);
|
|
83
|
+
const handle = await open(path, "wx", options.mode);
|
|
84
|
+
const owner = {
|
|
85
|
+
pid: process.pid,
|
|
86
|
+
process: instance,
|
|
87
|
+
host: hostname(),
|
|
88
|
+
acquiredAt: new Date().toISOString(),
|
|
89
|
+
...identity !== null && { machine: identity }
|
|
90
|
+
};
|
|
91
|
+
let ino;
|
|
92
|
+
try {
|
|
93
|
+
ino = (await handle.stat()).ino;
|
|
94
|
+
await handle.chmod(options.mode);
|
|
95
|
+
await handle.writeFile(`${JSON.stringify(owner)}
|
|
96
|
+
`, "utf8");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
await handle.close().catch(() => {
|
|
99
|
+
return;
|
|
100
|
+
});
|
|
101
|
+
if (ino !== undefined)
|
|
102
|
+
await unlinkIfSame(path, ino).catch(() => {
|
|
103
|
+
return;
|
|
104
|
+
});
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
const ownedInode = ino;
|
|
108
|
+
let released = false;
|
|
109
|
+
return {
|
|
110
|
+
path,
|
|
111
|
+
owner,
|
|
112
|
+
reclaimed,
|
|
113
|
+
async release() {
|
|
114
|
+
if (released)
|
|
115
|
+
return;
|
|
116
|
+
released = true;
|
|
117
|
+
try {
|
|
118
|
+
await handle.close();
|
|
119
|
+
} finally {
|
|
120
|
+
await unlinkIfSame(path, ownedInode);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function reclaimable(state, diagnosis, graceMs) {
|
|
126
|
+
if (state.owner)
|
|
127
|
+
return diagnosis.liveness === "gone";
|
|
128
|
+
return typeof graceMs === "number" && Date.now() - state.mtimeMs >= graceMs;
|
|
129
|
+
}
|
|
130
|
+
async function reclaimUnderGuard(path, options) {
|
|
131
|
+
const guardPath = `${path}.reclaim`;
|
|
132
|
+
const acquired = await attemptExclusiveLock(guardPath, {
|
|
133
|
+
...options,
|
|
134
|
+
ownerlessGraceMs: options.ownerlessGraceMs === undefined ? 5000 : options.ownerlessGraceMs
|
|
135
|
+
});
|
|
136
|
+
if (!("held" in acquired))
|
|
137
|
+
return;
|
|
138
|
+
const guard = acquired.held;
|
|
139
|
+
try {
|
|
140
|
+
const state = await readLockFile(path);
|
|
141
|
+
if (state) {
|
|
142
|
+
const diagnosis = await diagnose(state.owner, options.machineIdentity);
|
|
143
|
+
if (!reclaimable(state, diagnosis, options.ownerlessGraceMs))
|
|
144
|
+
return "refused";
|
|
145
|
+
await unlinkIfSame(path, state.ino);
|
|
146
|
+
}
|
|
147
|
+
return await createOwned(path, options, state !== undefined).catch((error) => {
|
|
148
|
+
if (isCode(error, "EEXIST"))
|
|
149
|
+
return "refused";
|
|
150
|
+
throw error;
|
|
151
|
+
});
|
|
152
|
+
} finally {
|
|
153
|
+
await guard.release().catch(() => {
|
|
154
|
+
return;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function attemptExclusiveLock(path, options) {
|
|
159
|
+
try {
|
|
160
|
+
return { held: await createOwned(path, options, false) };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!isCode(error, "EEXIST"))
|
|
163
|
+
throw error;
|
|
164
|
+
if (!options.reclaim)
|
|
165
|
+
return { error };
|
|
166
|
+
let readFailure;
|
|
167
|
+
const state = await readLockFile(path, (cause) => {
|
|
168
|
+
readFailure = cause;
|
|
169
|
+
});
|
|
170
|
+
const diagnosis = {
|
|
171
|
+
...await diagnose(state?.owner, options.machineIdentity),
|
|
172
|
+
...readFailure !== undefined && { cause: readFailure }
|
|
173
|
+
};
|
|
174
|
+
if (!state || !reclaimable(state, diagnosis, options.ownerlessGraceMs)) {
|
|
175
|
+
return { error, diagnosis };
|
|
176
|
+
}
|
|
177
|
+
const reclaimed = await reclaimUnderGuard(path, options);
|
|
178
|
+
if (reclaimed !== undefined && reclaimed !== "refused")
|
|
179
|
+
return { held: reclaimed };
|
|
180
|
+
return { error, diagnosis };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { attemptExclusiveLock };
|