@openclaw/fs-safe 0.6.0 → 0.7.1
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 +79 -0
- package/README.md +7 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +24 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +69 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +104 -35
- package/dist/archive-tar-runtime.d.ts +3 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-names.js +2 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +38 -27
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/directory-guard.d.ts +2 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +10 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +81 -30
- package/dist/file-observation.d.ts +9 -0
- package/dist/file-observation.d.ts.map +1 -0
- package/dist/file-observation.js +22 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +32 -13
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +12 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +1 -1
- package/dist/opened-file-failure.d.ts +7 -0
- package/dist/opened-file-failure.d.ts.map +1 -0
- package/dist/opened-file-failure.js +41 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +8 -2
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +10 -10
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +113 -82
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -3
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +6 -5
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +17 -13
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +91 -37
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +16 -5
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +60 -28
- package/dist/sidecar-lock-root.d.ts +3 -0
- package/dist/sidecar-lock-root.d.ts.map +1 -0
- package/dist/sidecar-lock-root.js +76 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +4 -1
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/docs/advanced.md +8 -1
- package/docs/archive.md +202 -26
- package/docs/atomic.md +11 -2
- package/docs/durability.md +1 -1
- package/docs/file-store.md +2 -0
- package/docs/install.md +8 -4
- package/docs/json.md +11 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +16 -3
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +2 -0
- package/docs/secret-file.md +5 -1
- package/docs/secure-file.md +4 -2
- package/docs/security-model.md +18 -0
- package/docs/sidecar-lock.md +60 -4
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/docs/writing.md +6 -0
- package/package.json +10 -10
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { readFileHandleBounded } from "./bounded-read.js";
|
|
5
|
+
import { openSidecarRoot } from "./sidecar-lock-root.js";
|
|
4
6
|
import { createNativeExclusiveFile } from "./native-operations.js";
|
|
5
|
-
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, } from "./sidecar-lock-policy.js";
|
|
6
|
-
import {
|
|
7
|
+
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
8
|
+
import { readSidecarLockRawSnapshot, parseSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
|
|
9
|
+
import { createSuppressedError } from "./suppressed-error.js";
|
|
7
10
|
async function resolveNormalizedTargetPath(targetPath) {
|
|
8
11
|
const resolved = path.resolve(targetPath);
|
|
9
12
|
const dir = path.dirname(resolved);
|
|
@@ -16,19 +19,34 @@ async function resolveNormalizedTargetPath(targetPath) {
|
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
export async function acquireSidecarLock(options, context) {
|
|
22
|
+
const retry = options.retry ?? {};
|
|
23
|
+
validateSidecarLockRetryOptions(retry);
|
|
24
|
+
validateSidecarLockTimeoutMs(options.timeoutMs);
|
|
19
25
|
context.ensureExitCleanupRegistered();
|
|
20
26
|
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
|
|
21
27
|
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
22
|
-
|
|
28
|
+
let held = context.held.get(normalizedTargetPath);
|
|
23
29
|
if (held &&
|
|
24
30
|
options.reentrantOwner !== undefined &&
|
|
25
31
|
held.reentrantOwner !== undefined &&
|
|
26
32
|
options.reentrantOwner === held.reentrantOwner) {
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
// A final release may already have decremented the count to zero and be
|
|
34
|
+
// removing the sidecar. Do not admit a new reentrant handle until that
|
|
35
|
+
// cleanup settles: successful cleanup requires a fresh acquisition, while
|
|
36
|
+
// failed cleanup leaves the existing sidecar held for a retry.
|
|
37
|
+
if (held.releasePromise) {
|
|
38
|
+
await held.releasePromise.catch(() => undefined);
|
|
39
|
+
held = context.held.get(normalizedTargetPath);
|
|
40
|
+
}
|
|
41
|
+
if (held &&
|
|
42
|
+
options.reentrantOwner !== undefined &&
|
|
43
|
+
held.reentrantOwner !== undefined &&
|
|
44
|
+
options.reentrantOwner === held.reentrantOwner) {
|
|
45
|
+
held.refCount += 1;
|
|
46
|
+
return context.handleForHeldLock(normalizedTargetPath, held);
|
|
47
|
+
}
|
|
29
48
|
}
|
|
30
49
|
const startedAt = Date.now();
|
|
31
|
-
const retry = options.retry ?? {};
|
|
32
50
|
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
33
51
|
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
34
52
|
let ownsReclaimGuard = false;
|
|
@@ -77,13 +95,14 @@ export async function acquireSidecarLock(options, context) {
|
|
|
77
95
|
let handle = null;
|
|
78
96
|
let createdSnapshot = null;
|
|
79
97
|
let lockFileCreateDenied = false;
|
|
98
|
+
const payload = await options.payload();
|
|
99
|
+
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
80
100
|
try {
|
|
81
|
-
const payload = await options.payload();
|
|
82
|
-
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
83
101
|
if (options.lockRoot) {
|
|
84
|
-
const
|
|
102
|
+
const lockRoot = options.lockRoot;
|
|
103
|
+
const relativeLockPath = relativeSidecarLockPath(lockRoot, lockPath);
|
|
85
104
|
try {
|
|
86
|
-
await
|
|
105
|
+
await lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
|
|
87
106
|
}
|
|
88
107
|
catch (error) {
|
|
89
108
|
if (error instanceof FsSafeError && error.code === "already-exists") {
|
|
@@ -92,7 +111,23 @@ export async function acquireSidecarLock(options, context) {
|
|
|
92
111
|
throw error;
|
|
93
112
|
}
|
|
94
113
|
createdSnapshot = { raw, payload, ownershipToken };
|
|
95
|
-
|
|
114
|
+
const opened = await openSidecarRoot(lockRoot, relativeLockPath, "unlinked");
|
|
115
|
+
if (!opened) {
|
|
116
|
+
await waitForRetry();
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
// Root.open admits the current file, not necessarily the one we created.
|
|
121
|
+
const currentRaw = await readFileHandleBounded(opened.handle, Buffer.byteLength(raw));
|
|
122
|
+
if (!currentRaw.equals(Buffer.from(raw))) {
|
|
123
|
+
throw new FsSafeError("path-mismatch", "created sidecar lock changed before admission");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
await opened.handle.close().catch(() => undefined);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
handle = opened.handle;
|
|
96
131
|
}
|
|
97
132
|
else {
|
|
98
133
|
try {
|
|
@@ -107,6 +142,12 @@ export async function acquireSidecarLock(options, context) {
|
|
|
107
142
|
await handle.writeFile(raw, "utf8");
|
|
108
143
|
}
|
|
109
144
|
const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
|
|
145
|
+
if (snapshot.stat.nlink === 0) {
|
|
146
|
+
await handle.close();
|
|
147
|
+
handle = null;
|
|
148
|
+
await waitForRetry();
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
110
151
|
const createdHeld = {
|
|
111
152
|
refCount: 1,
|
|
112
153
|
reentrantOwner: options.reentrantOwner,
|
|
@@ -149,31 +190,36 @@ export async function acquireSidecarLock(options, context) {
|
|
|
149
190
|
return returnedHandle;
|
|
150
191
|
}
|
|
151
192
|
catch (err) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
193
|
+
try {
|
|
194
|
+
if (handle) {
|
|
195
|
+
const failedSnapshot = createdSnapshot ?? { payload: null };
|
|
196
|
+
try {
|
|
197
|
+
failedSnapshot.stat = await handle.stat();
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Best-effort cleanup of a failed exclusive create.
|
|
201
|
+
}
|
|
202
|
+
const current = context.held.get(normalizedTargetPath);
|
|
203
|
+
if (current?.handle === handle) {
|
|
204
|
+
context.held.delete(normalizedTargetPath);
|
|
205
|
+
}
|
|
206
|
+
await handle.close().catch(() => undefined);
|
|
207
|
+
// Root-created records retain the creator's byte/token receipt;
|
|
208
|
+
// partial direct writes use the exclusive descriptor's identity.
|
|
209
|
+
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
210
|
+
lockRoot: options.lockRoot,
|
|
211
|
+
parsePayload: options.parsePayload,
|
|
212
|
+
});
|
|
159
213
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
214
|
+
else if (createdSnapshot) {
|
|
215
|
+
await removeSidecarLockIfUnchanged(lockPath, createdSnapshot, {
|
|
216
|
+
lockRoot: options.lockRoot,
|
|
217
|
+
parsePayload: options.parsePayload,
|
|
218
|
+
});
|
|
163
219
|
}
|
|
164
|
-
await handle.close().catch(() => undefined);
|
|
165
|
-
// The file may be empty or partial JSON, so remove by the identity
|
|
166
|
-
// captured from our exclusive handle rather than by pathname alone.
|
|
167
|
-
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
168
|
-
lockRoot: options.lockRoot,
|
|
169
|
-
parsePayload: options.parsePayload,
|
|
170
|
-
});
|
|
171
220
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
lockRoot: options.lockRoot,
|
|
175
|
-
parsePayload: options.parsePayload,
|
|
176
|
-
});
|
|
221
|
+
catch (cleanupError) {
|
|
222
|
+
throw createSuppressedError(cleanupError, err, "file lock acquisition and cleanup both failed");
|
|
177
223
|
}
|
|
178
224
|
if (lockFileCreateDenied && withinDenialBudget()) {
|
|
179
225
|
await retryOrRethrowDenial(err);
|
|
@@ -185,24 +231,29 @@ export async function acquireSidecarLock(options, context) {
|
|
|
185
231
|
if (ownsReclaimGuard) {
|
|
186
232
|
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
187
233
|
ownsReclaimGuard = false;
|
|
234
|
+
await waitForRetry();
|
|
188
235
|
continue;
|
|
189
236
|
}
|
|
190
237
|
const nowMs = Date.now();
|
|
191
|
-
let
|
|
238
|
+
let rawSnapshot;
|
|
239
|
+
let lockFileOpenDenied = false;
|
|
192
240
|
try {
|
|
193
|
-
|
|
241
|
+
rawSnapshot = await readSidecarLockRawSnapshot(lockPath, {
|
|
194
242
|
lockRoot: options.lockRoot,
|
|
195
|
-
parsePayload: options.parsePayload,
|
|
196
243
|
rejectNonFile: true,
|
|
244
|
+
discardObservation: "changed",
|
|
245
|
+
onOpenFailure: (error) => { lockFileOpenDenied = isTransientLockFileDenial(error, lockPath); },
|
|
197
246
|
});
|
|
198
247
|
}
|
|
199
248
|
catch (readErr) {
|
|
200
|
-
if (!
|
|
249
|
+
if (!lockFileOpenDenied || !withinDenialBudget())
|
|
201
250
|
throw readErr;
|
|
202
251
|
await retryOrRethrowDenial(readErr);
|
|
203
252
|
continue;
|
|
204
253
|
}
|
|
254
|
+
const snapshot = parseSidecarLockSnapshot(rawSnapshot, options.parsePayload);
|
|
205
255
|
if (!snapshot) {
|
|
256
|
+
await waitForRetry();
|
|
206
257
|
continue;
|
|
207
258
|
}
|
|
208
259
|
if (context.held.has(normalizedTargetPath)) {
|
|
@@ -222,6 +273,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
222
273
|
lockRoot: options.lockRoot,
|
|
223
274
|
parsePayload: options.parsePayload,
|
|
224
275
|
}))) {
|
|
276
|
+
await waitForRetry();
|
|
225
277
|
continue;
|
|
226
278
|
}
|
|
227
279
|
const staleRecovery = options.staleRecovery ?? "fail-closed";
|
|
@@ -240,6 +292,8 @@ export async function acquireSidecarLock(options, context) {
|
|
|
240
292
|
parsePayload: options.parsePayload,
|
|
241
293
|
});
|
|
242
294
|
if (removal === "removed" || removal === "changed") {
|
|
295
|
+
if (removal === "changed")
|
|
296
|
+
await waitForRetry();
|
|
243
297
|
continue;
|
|
244
298
|
}
|
|
245
299
|
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
@@ -5,7 +5,9 @@ export declare function createSidecarLockHandle(params: {
|
|
|
5
5
|
lockPath: string;
|
|
6
6
|
normalizedTargetPath: string;
|
|
7
7
|
verifyStillHeld: () => Promise<boolean>;
|
|
8
|
-
release: (
|
|
8
|
+
release: (options?: {
|
|
9
|
+
retry?: boolean;
|
|
10
|
+
}) => Promise<unknown>;
|
|
9
11
|
}): SidecarLockHandle;
|
|
10
12
|
export declare function createHeldSidecarLockHandle(params: {
|
|
11
13
|
normalizedTargetPath: string;
|
|
@@ -15,6 +17,8 @@ export declare function createHeldSidecarLockHandle(params: {
|
|
|
15
17
|
lockRoot?: Root;
|
|
16
18
|
parsePayload?: (raw: string) => unknown;
|
|
17
19
|
};
|
|
18
|
-
release: (
|
|
20
|
+
release: (options?: {
|
|
21
|
+
retry?: boolean;
|
|
22
|
+
}) => Promise<unknown>;
|
|
19
23
|
}): SidecarLockHandle;
|
|
20
24
|
//# sourceMappingURL=sidecar-lock-handle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-handle.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"sidecar-lock-handle.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D,GAAG,iBAAiB,CA2BpB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,oBAAoB,EAAE,MAAM,CAAC;IAC7B,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,mBAAmB,CAAC;QAC9B,QAAQ,CAAC,EAAE,IAAI,CAAC;QAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;KACzC,CAAC;IACF,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D,GAAG,iBAAiB,CAWpB"}
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { sidecarLockSnapshotStillPresent, } from "./sidecar-lock-reclaim.js";
|
|
2
2
|
export function createSidecarLockHandle(params) {
|
|
3
3
|
let released = false;
|
|
4
|
+
let releasePromise;
|
|
5
|
+
let releaseStarted = false;
|
|
4
6
|
const release = async () => {
|
|
5
7
|
if (released)
|
|
6
8
|
return;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
if (!releasePromise) {
|
|
10
|
+
const retry = releaseStarted;
|
|
11
|
+
releaseStarted = true;
|
|
12
|
+
releasePromise = (async () => {
|
|
13
|
+
await params.release({ retry });
|
|
14
|
+
released = true;
|
|
15
|
+
})();
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
await releasePromise;
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
releasePromise = undefined;
|
|
22
|
+
}
|
|
9
23
|
};
|
|
10
24
|
return {
|
|
11
25
|
lockPath: params.lockPath,
|
|
@@ -23,6 +37,6 @@ export function createHeldSidecarLockHandle(params) {
|
|
|
23
37
|
lockRoot: params.held.lockRoot,
|
|
24
38
|
parsePayload: params.held.parsePayload,
|
|
25
39
|
}),
|
|
26
|
-
release: params.release,
|
|
40
|
+
release: (options) => params.release(options),
|
|
27
41
|
});
|
|
28
42
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
2
|
+
export declare function validateSidecarLockRetryOptions(retry: SidecarLockRetryOptions): void;
|
|
3
|
+
export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefined): void;
|
|
2
4
|
export declare function computeSidecarLockDelayMs(retry: SidecarLockRetryOptions, attempt: number): number;
|
|
3
5
|
export declare const maxTransientLockDenials = 8;
|
|
4
6
|
export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAQvE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAkBpF;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGhF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQjG;AASD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGnF;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAGT;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU7E;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnB"}
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
function assertFiniteNonNegative(value, label) {
|
|
3
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
4
|
+
throw new RangeError(`${label} must be a finite non-negative number`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function validateSidecarLockRetryOptions(retry) {
|
|
8
|
+
if (retry.retries !== undefined && (!Number.isSafeInteger(retry.retries) || retry.retries < 0)) {
|
|
9
|
+
throw new RangeError("lock retry.retries must be a non-negative safe integer");
|
|
10
|
+
}
|
|
11
|
+
if (retry.factor !== undefined)
|
|
12
|
+
assertFiniteNonNegative(retry.factor, "lock retry.factor");
|
|
13
|
+
if (retry.minTimeout !== undefined) {
|
|
14
|
+
assertFiniteNonNegative(retry.minTimeout, "lock retry.minTimeout");
|
|
15
|
+
}
|
|
16
|
+
if (retry.maxTimeout !== undefined) {
|
|
17
|
+
assertFiniteNonNegative(retry.maxTimeout, "lock retry.maxTimeout");
|
|
18
|
+
}
|
|
19
|
+
if (retry.minTimeout !== undefined &&
|
|
20
|
+
retry.maxTimeout !== undefined &&
|
|
21
|
+
retry.minTimeout > retry.maxTimeout) {
|
|
22
|
+
throw new RangeError("lock retry.minTimeout must not exceed retry.maxTimeout");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function validateSidecarLockTimeoutMs(timeoutMs) {
|
|
26
|
+
if (timeoutMs === undefined || timeoutMs === Number.POSITIVE_INFINITY)
|
|
27
|
+
return;
|
|
28
|
+
assertFiniteNonNegative(timeoutMs, "lock timeoutMs");
|
|
29
|
+
}
|
|
2
30
|
export function computeSidecarLockDelayMs(retry, attempt) {
|
|
31
|
+
validateSidecarLockRetryOptions(retry);
|
|
3
32
|
const minTimeout = retry.minTimeout ?? 50;
|
|
4
33
|
const maxTimeout = retry.maxTimeout ?? 1000;
|
|
5
34
|
const factor = retry.factor ?? 1;
|
|
@@ -12,6 +12,10 @@ export type SidecarLockSnapshot = {
|
|
|
12
12
|
stat?: Stats;
|
|
13
13
|
ownershipToken?: string;
|
|
14
14
|
};
|
|
15
|
+
type SidecarLockRawSnapshot = Omit<SidecarLockSnapshot, "payload"> & {
|
|
16
|
+
raw: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function parseSidecarLockSnapshot(snapshot: SidecarLockRawSnapshot | null, parser?: (raw: string) => unknown): SidecarLockSnapshot | null;
|
|
15
19
|
export declare function readSidecarLockOwnershipToken(raw: string): string | undefined;
|
|
16
20
|
export declare function serializeSidecarLockPayload(payload: Record<string, unknown>): {
|
|
17
21
|
raw: string;
|
|
@@ -19,15 +23,21 @@ export declare function serializeSidecarLockPayload(payload: Record<string, unkn
|
|
|
19
23
|
};
|
|
20
24
|
export declare function relativeSidecarLockPath(lockRoot: Root, lockPath: string): string;
|
|
21
25
|
export declare function parseSidecarLockPayload(raw: string, parser?: (raw: string) => unknown): unknown;
|
|
22
|
-
export declare function readSidecarLockSnapshot(lockPath: string, options?: {
|
|
23
|
-
lockRoot?: Root;
|
|
26
|
+
export declare function readSidecarLockSnapshot(lockPath: string, options?: Parameters<typeof readSidecarLockRawSnapshot>[1] & {
|
|
24
27
|
parsePayload?: (raw: string) => unknown;
|
|
28
|
+
}): Promise<SidecarLockSnapshot | null>;
|
|
29
|
+
export declare function readSidecarLockRawSnapshot(lockPath: string, options?: {
|
|
30
|
+
lockRoot?: Root;
|
|
25
31
|
rejectNonFile?: boolean;
|
|
26
32
|
allowDescriptorIdentityDrift?: boolean;
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
discardObservation?: "unlinked" | "changed";
|
|
34
|
+
onOpenFailure?: (error: unknown) => void;
|
|
35
|
+
}): Promise<SidecarLockRawSnapshot | null>;
|
|
36
|
+
export declare function readSidecarLockSnapshotSync(lockPath: string, parsePayload?: (raw: string) => unknown, options?: Parameters<typeof readSidecarLockRawSnapshotSync>[1]): SidecarLockSnapshot | null;
|
|
37
|
+
export declare function readSidecarLockRawSnapshotSync(lockPath: string, options?: {
|
|
29
38
|
rejectNonFile?: boolean;
|
|
30
|
-
|
|
39
|
+
onOpenFailure?: (error: unknown) => void;
|
|
40
|
+
}): SidecarLockRawSnapshot | null;
|
|
31
41
|
export declare function removeSidecarLockIfUnchangedSync(lockPath: string, observed: SidecarLockSnapshot): boolean;
|
|
32
42
|
export declare function sidecarLockSnapshotMatches(current: SidecarLockSnapshot, observed: SidecarLockSnapshot): boolean;
|
|
33
43
|
export declare function removeSidecarLockIfUnchanged(lockPath: string, observed: SidecarLockSnapshot | null, options?: {
|
|
@@ -49,4 +59,5 @@ export declare function removeStaleSidecarLockIfAllowed(params: {
|
|
|
49
59
|
lockRoot?: Root;
|
|
50
60
|
parsePayload?: (raw: string) => unknown;
|
|
51
61
|
}): Promise<"removed" | "changed" | "not-approved">;
|
|
62
|
+
export {};
|
|
52
63
|
//# sourceMappingURL=sidecar-lock-reclaim.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,12 +5,17 @@ import path from "node:path";
|
|
|
5
5
|
import { readFileDescriptorBoundedSync, readFileHandleBounded } from "./bounded-read.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
7
|
import { sameFileIdentity } from "./file-identity.js";
|
|
8
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
|
+
import { openSidecarRoot } from "./sidecar-lock-root.js";
|
|
8
10
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
9
11
|
const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
|
|
10
12
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
|
|
11
13
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
|
|
12
14
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX = "\t".repeat(8);
|
|
13
15
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_PATTERN = new RegExp(`\\n(${SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX}[ \\t]{${SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS}})\\n$`);
|
|
16
|
+
export function parseSidecarLockSnapshot(snapshot, parser) {
|
|
17
|
+
return snapshot && { ...snapshot, payload: parseSidecarLockPayload(snapshot.raw, parser) };
|
|
18
|
+
}
|
|
14
19
|
function createSidecarLockOwnershipToken() {
|
|
15
20
|
let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
|
|
16
21
|
for (const byte of randomBytes(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
|
|
@@ -55,16 +60,27 @@ export function parseSidecarLockPayload(raw, parser) {
|
|
|
55
60
|
return null;
|
|
56
61
|
}
|
|
57
62
|
}
|
|
63
|
+
function missingSnapshotPath(error) {
|
|
64
|
+
if (error.code === "ENOENT")
|
|
65
|
+
return null;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
58
68
|
export async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
69
|
+
return parseSidecarLockSnapshot(await readSidecarLockRawSnapshot(lockPath, options), options.parsePayload);
|
|
70
|
+
}
|
|
71
|
+
export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
|
|
59
72
|
let handle;
|
|
60
73
|
try {
|
|
61
74
|
if (options.lockRoot) {
|
|
62
|
-
const
|
|
75
|
+
const lockRoot = options.lockRoot;
|
|
76
|
+
const relative = relativeSidecarLockPath(lockRoot, lockPath);
|
|
77
|
+
const opened = await openSidecarRoot(lockRoot, relative, options.discardObservation, options.onOpenFailure);
|
|
78
|
+
if (!opened)
|
|
79
|
+
return null;
|
|
63
80
|
try {
|
|
64
81
|
const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
65
82
|
return {
|
|
66
83
|
raw,
|
|
67
|
-
payload: parseSidecarLockPayload(raw, options.parsePayload),
|
|
68
84
|
stat: opened.stat,
|
|
69
85
|
};
|
|
70
86
|
}
|
|
@@ -72,7 +88,9 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
|
72
88
|
await opened.handle.close().catch(() => undefined);
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
|
-
const before = await fs.lstat(lockPath);
|
|
91
|
+
const before = await fs.lstat(lockPath).catch(missingSnapshotPath);
|
|
92
|
+
if (!before)
|
|
93
|
+
return null;
|
|
76
94
|
if (!before.isFile() || before.isSymbolicLink()) {
|
|
77
95
|
if (options.rejectNonFile) {
|
|
78
96
|
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
@@ -89,11 +107,14 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
|
89
107
|
(typeof fsSync.constants.O_NONBLOCK === "number" ? fsSync.constants.O_NONBLOCK : 0));
|
|
90
108
|
}
|
|
91
109
|
catch (error) {
|
|
110
|
+
if (error.code === "ENOENT")
|
|
111
|
+
return null;
|
|
92
112
|
if (options.rejectNonFile && error.code === "ELOOP") {
|
|
93
113
|
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`, {
|
|
94
114
|
cause: error,
|
|
95
115
|
});
|
|
96
116
|
}
|
|
117
|
+
options.onOpenFailure?.(error);
|
|
97
118
|
throw error;
|
|
98
119
|
}
|
|
99
120
|
const opened = await handle.stat();
|
|
@@ -106,53 +127,64 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
|
106
127
|
if (!options.allowDescriptorIdentityDrift && !sameFileIdentity(before, opened))
|
|
107
128
|
return null;
|
|
108
129
|
const raw = (await readFileHandleBounded(handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
109
|
-
const after = await fs.lstat(lockPath);
|
|
110
|
-
if (!after.isFile() || !sameFileIdentity(before, after))
|
|
130
|
+
const after = await fs.lstat(lockPath).catch(missingSnapshotPath);
|
|
131
|
+
if (!after || !after.isFile() || !sameFileIdentity(before, after))
|
|
111
132
|
return null;
|
|
112
|
-
return { raw,
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
if (err.code === "ENOENT" ||
|
|
116
|
-
(err instanceof FsSafeError && err.code === "not-found")) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
throw err;
|
|
133
|
+
return { raw, stat: after };
|
|
120
134
|
}
|
|
121
135
|
finally {
|
|
122
136
|
await handle?.close().catch(() => undefined);
|
|
123
137
|
}
|
|
124
138
|
}
|
|
139
|
+
function lstatSidecarLockSync(lockPath) {
|
|
140
|
+
try {
|
|
141
|
+
return fsSync.lstatSync(lockPath);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return missingSnapshotPath(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
125
147
|
export function readSidecarLockSnapshotSync(lockPath, parsePayload, options = {}) {
|
|
148
|
+
return parseSidecarLockSnapshot(readSidecarLockRawSnapshotSync(lockPath, options), parsePayload);
|
|
149
|
+
}
|
|
150
|
+
export function readSidecarLockRawSnapshotSync(lockPath, options = {}) {
|
|
126
151
|
let fd;
|
|
127
152
|
try {
|
|
128
|
-
const before =
|
|
153
|
+
const before = lstatSidecarLockSync(lockPath);
|
|
154
|
+
if (!before)
|
|
155
|
+
return null;
|
|
129
156
|
if (!before.isFile() || before.isSymbolicLink()) {
|
|
130
157
|
if (options.rejectNonFile) {
|
|
131
158
|
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
132
159
|
}
|
|
133
160
|
return null;
|
|
134
161
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
162
|
+
try {
|
|
163
|
+
fd = fsSync.openSync(lockPath, resolveReadOpenFlags());
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error.code === "ENOENT")
|
|
167
|
+
return null;
|
|
168
|
+
options.onOpenFailure?.(error);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
139
171
|
const opened = fsSync.fstatSync(fd);
|
|
172
|
+
if (!opened.isFile()) {
|
|
173
|
+
if (options.rejectNonFile) {
|
|
174
|
+
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
140
178
|
const raw = readFileDescriptorBoundedSync(fd, MAX_LOCK_PAYLOAD_BYTES).toString("utf8");
|
|
141
|
-
const after =
|
|
142
|
-
if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
|
|
179
|
+
const after = lstatSidecarLockSync(lockPath);
|
|
180
|
+
if (!after || !sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
|
|
143
181
|
return null;
|
|
144
182
|
return {
|
|
145
183
|
raw,
|
|
146
|
-
payload: parseSidecarLockPayload(raw, parsePayload),
|
|
147
184
|
stat: after,
|
|
148
185
|
ownershipToken: readSidecarLockOwnershipToken(raw),
|
|
149
186
|
};
|
|
150
187
|
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
if (error.code === "ENOENT")
|
|
153
|
-
return null;
|
|
154
|
-
throw error;
|
|
155
|
-
}
|
|
156
188
|
finally {
|
|
157
189
|
if (fd !== undefined)
|
|
158
190
|
fsSync.closeSync(fd);
|
|
@@ -191,10 +223,10 @@ export async function removeSidecarLockIfUnchanged(lockPath, observed, options =
|
|
|
191
223
|
return false;
|
|
192
224
|
}
|
|
193
225
|
if (options.lockRoot) {
|
|
194
|
-
await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath))
|
|
226
|
+
await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath));
|
|
195
227
|
}
|
|
196
228
|
else {
|
|
197
|
-
await fs.rm(lockPath, { force: true })
|
|
229
|
+
await fs.rm(lockPath, { force: true });
|
|
198
230
|
}
|
|
199
231
|
return true;
|
|
200
232
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { OpenResult, Root } from "./root-impl.js";
|
|
2
|
+
export declare function openSidecarRoot(lockRoot: Root, relative: string, discardObservation?: "unlinked" | "changed", onOpenFailure?: (error: unknown) => void): Promise<OpenResult | null>;
|
|
3
|
+
//# sourceMappingURL=sidecar-lock-root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar-lock-root.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-root.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAGvD,wBAAsB,eAAe,CACnC,QAAQ,EAAE,IAAI,EACd,QAAQ,EAAE,MAAM,EAChB,kBAAkB,CAAC,EAAE,UAAU,GAAG,SAAS,EAC3C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACvC,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAuD5B"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { inspectDirectoryIdentity } from "./directory-guard.js";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { fileObservation } from "./file-observation.js";
|
|
5
|
+
import { isNotFoundPathError } from "./path.js";
|
|
6
|
+
import { resolveRootPath } from "./root-path.js";
|
|
7
|
+
export async function openSidecarRoot(lockRoot, relative, discardObservation, onOpenFailure) {
|
|
8
|
+
const resolved = await lockRoot.resolve(relative);
|
|
9
|
+
const canonicalPath = async () => (await resolveRootPath({
|
|
10
|
+
absolutePath: resolved,
|
|
11
|
+
rootPath: lockRoot.rootReal,
|
|
12
|
+
rootCanonicalPath: lockRoot.rootReal,
|
|
13
|
+
boundaryLabel: "sidecar lock root",
|
|
14
|
+
})).canonicalPath;
|
|
15
|
+
const expectedRealPath = await canonicalPath();
|
|
16
|
+
if (expectedRealPath === lockRoot.rootReal)
|
|
17
|
+
throw new FsSafeError("not-file", "sidecar lock is a directory");
|
|
18
|
+
const parents = [];
|
|
19
|
+
let completeParents = true;
|
|
20
|
+
if (discardObservation) {
|
|
21
|
+
// Follow permitted in-root aliases first; receipts cover canonical ancestry.
|
|
22
|
+
for (let dir = path.dirname(expectedRealPath);; dir = path.dirname(dir)) {
|
|
23
|
+
try {
|
|
24
|
+
parents.push({ dir, stat: await inspectDirectoryIdentity(dir) });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (!isNotFoundPathError(error))
|
|
28
|
+
throw error;
|
|
29
|
+
completeParents = false;
|
|
30
|
+
}
|
|
31
|
+
if (dir === lockRoot.rootReal)
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const observation = fileObservation();
|
|
36
|
+
try {
|
|
37
|
+
const opened = await observation.run(() => lockRoot.open(relative));
|
|
38
|
+
if (opened.realPath !== expectedRealPath) {
|
|
39
|
+
await opened.handle.close().catch(() => undefined);
|
|
40
|
+
throw new FsSafeError("path-mismatch", "sidecar lock path changed during open");
|
|
41
|
+
}
|
|
42
|
+
return opened;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const openFailed = observation.has(error, `open:${resolved}`);
|
|
46
|
+
if (openFailed)
|
|
47
|
+
onOpenFailure?.(error);
|
|
48
|
+
const missing = openFailed && error instanceof FsSafeError && error.code === "not-found";
|
|
49
|
+
if (missing && !discardObservation)
|
|
50
|
+
return null;
|
|
51
|
+
const discardable = observation.has(error, `unlinked:${resolved}`) ||
|
|
52
|
+
(discardObservation === "changed" && observation.has(error, `changed:${resolved}`));
|
|
53
|
+
if (!discardObservation || (!missing && (!completeParents || !discardable)))
|
|
54
|
+
throw error;
|
|
55
|
+
try {
|
|
56
|
+
try {
|
|
57
|
+
const current = await lockRoot.stat(relative);
|
|
58
|
+
if (!current.isFile || current.isSymbolicLink || current.nlink !== 1)
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
catch (probeError) {
|
|
62
|
+
if (!(probeError instanceof FsSafeError && probeError.code === "not-found"))
|
|
63
|
+
throw probeError;
|
|
64
|
+
}
|
|
65
|
+
for (const { dir, stat } of parents)
|
|
66
|
+
await inspectDirectoryIdentity(dir, stat);
|
|
67
|
+
await lockRoot.resolve(relative);
|
|
68
|
+
if (await canonicalPath() === expectedRealPath)
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Keep the original failure when directory or confinement proof fails.
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|