@openclaw/fs-safe 0.5.1 → 0.5.3
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 +86 -0
- package/README.md +19 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +20 -12
- package/dist/archive-entry.d.ts +1 -0
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +37 -6
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +4 -0
- package/dist/archive-input.d.ts +7 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +11 -1
- package/dist/archive-limits.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +12 -6
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +2 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +62 -37
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +3 -2
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +15 -1
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +11 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -4
- package/dist/archive-zip-entry.d.ts +7 -1
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-entry.js +11 -1
- package/dist/archive-zip-integrity.d.ts +5 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -0
- package/dist/archive-zip-integrity.js +54 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +12 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -5
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +13 -2
- package/dist/deny-mutations.d.ts +1 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +6 -39
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +6 -6
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +5 -4
- package/dist/error-detail.d.ts +2 -0
- package/dist/error-detail.d.ts.map +1 -0
- package/dist/error-detail.js +4 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +4 -1
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +88 -35
- package/dist/file-store-boundary.d.ts +6 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -5
- package/dist/file-store-sync-write.d.ts +9 -0
- package/dist/file-store-sync-write.d.ts.map +1 -0
- package/dist/file-store-sync-write.js +97 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +62 -139
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +27 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +5 -6
- package/dist/install-path.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +19 -13
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +1 -3
- package/dist/local-file-access.d.ts +3 -2
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +12 -6
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -30
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +51 -12
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +1 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +14 -1
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +44 -17
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +6 -5
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +1 -7
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +4 -0
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permission-exec.d.ts.map +1 -0
- package/dist/permission-exec.js +28 -0
- package/dist/permissions-windows.d.ts +56 -0
- package/dist/permissions-windows.d.ts.map +1 -0
- package/dist/permissions-windows.js +285 -0
- package/dist/permissions.d.ts +3 -46
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +17 -328
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +21 -4
- package/dist/pinned-write.js +2 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +45 -4
- package/dist/read-error.d.ts +2 -0
- package/dist/read-error.d.ts.map +1 -0
- package/dist/read-error.js +11 -0
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +21 -2
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +10 -1
- package/dist/replace-file-copy-fallback.d.ts +3 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +18 -11
- package/dist/replace-file-descriptor.d.ts +33 -0
- package/dist/replace-file-descriptor.d.ts.map +1 -0
- package/dist/replace-file-descriptor.js +96 -0
- package/dist/replace-file.d.ts +9 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +64 -72
- package/dist/root-context.d.ts +8 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +43 -2
- package/dist/root-errors.d.ts +7 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +46 -1
- package/dist/root-file.d.ts +1 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +2 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +216 -169
- package/dist/root-path-symlink.d.ts +3 -0
- package/dist/root-path-symlink.d.ts.map +1 -0
- package/dist/root-path-symlink.js +35 -0
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +129 -190
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +15 -0
- package/dist/safe-path-segment.d.ts +2 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +12 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +29 -6
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +5 -1
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +6 -1
- package/dist/short-path.d.ts +2 -0
- package/dist/short-path.d.ts.map +1 -0
- package/dist/short-path.js +7 -0
- package/dist/sidecar-lock-acquire.d.ts +30 -0
- package/dist/sidecar-lock-acquire.d.ts.map +1 -0
- package/dist/sidecar-lock-acquire.js +260 -0
- package/dist/sidecar-lock-policy.d.ts +3 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -3
- package/dist/sidecar-lock-reclaim.d.ts +5 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +59 -9
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +27 -226
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +9 -8
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -4
- package/dist/test-hooks.d.ts +1 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/timing.d.ts +2 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +9 -0
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +10 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +15 -0
- package/docs/advanced.md +7 -1
- package/docs/archive.md +27 -20
- package/docs/assets/readme-banner.jpg +0 -0
- package/docs/atomic.md +48 -19
- package/docs/errors.md +34 -8
- package/docs/file-store.md +14 -1
- package/docs/filename.md +17 -16
- package/docs/index.md +1 -0
- package/docs/install-path.md +9 -6
- package/docs/json.md +8 -2
- package/docs/local-roots.md +79 -103
- package/docs/native-helper.md +11 -5
- package/docs/native.md +2 -2
- package/docs/output.md +8 -3
- package/docs/path-scope.md +4 -1
- package/docs/path.md +7 -3
- package/docs/permissions.md +8 -0
- package/docs/public-api.md +109 -0
- package/docs/quickstart.md +6 -2
- package/docs/reading.md +13 -10
- package/docs/root.md +26 -4
- package/docs/secret-file.md +21 -4
- package/docs/security-model.md +21 -4
- package/docs/sidecar-lock.md +15 -5
- package/docs/temp.md +42 -5
- package/docs/test-hooks.md +2 -0
- package/docs/types.md +32 -15
- package/docs/writing.md +25 -12
- package/package.json +15 -4
- package/dist/archive-file-io.d.ts +0 -9
- package/dist/archive-file-io.d.ts.map +0 -1
- package/dist/archive-file-io.js +0 -11
- package/dist/file-store-source.d.ts +0 -5
- package/dist/file-store-source.d.ts.map +0 -1
- package/dist/file-store-source.js +0 -30
- package/dist/file-url.d.ts +0 -2
- package/dist/file-url.d.ts.map +0 -1
- package/dist/file-url.js +0 -1
- package/dist/path-stat.d.ts +0 -4
- package/dist/path-stat.d.ts.map +0 -1
- package/dist/path-stat.js +0 -15
- package/dist/windows-permissions-native.d.ts +0 -9
- package/dist/windows-permissions-native.d.ts.map +0 -1
- package/dist/windows-permissions-native.js +0 -31
package/dist/secret-file.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
4
5
|
import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
5
6
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
6
7
|
import { FsSafeError } from "./errors.js";
|
|
@@ -8,6 +9,7 @@ import { sameFileIdentity } from "./file-identity.js";
|
|
|
8
9
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
9
10
|
import { openPinnedFileSync } from "./pinned-open.js";
|
|
10
11
|
import { runPinnedWriteHelper } from "./pinned-write.js";
|
|
12
|
+
import { serializePathWrite } from "./write-queue.js";
|
|
11
13
|
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
12
14
|
export const PRIVATE_SECRET_DIR_MODE = 0o700;
|
|
13
15
|
export const PRIVATE_SECRET_FILE_MODE = 0o600;
|
|
@@ -89,7 +91,6 @@ function readSecretFileOutcomeSync(filePath, label, options = {}) {
|
|
|
89
91
|
filePath: resolvedPath,
|
|
90
92
|
rejectPathSymlink: options.rejectSymlink,
|
|
91
93
|
rejectHardlinks: options.rejectHardlinks !== false,
|
|
92
|
-
maxBytes,
|
|
93
94
|
});
|
|
94
95
|
if (!opened.ok) {
|
|
95
96
|
const error = normalizeSecretReadError(opened.reason === "validation" ? new Error("security validation failed") : opened.error);
|
|
@@ -101,6 +102,15 @@ function readSecretFileOutcomeSync(filePath, label, options = {}) {
|
|
|
101
102
|
};
|
|
102
103
|
}
|
|
103
104
|
try {
|
|
105
|
+
if (!sameFileIdentity(previewStat, opened.stat)) {
|
|
106
|
+
const error = new FsSafeError("path-mismatch", "security validation failed");
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
code: "path-mismatch",
|
|
110
|
+
error,
|
|
111
|
+
message: `Failed to read ${label} file at ${resolvedPath}: ${String(error)}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
104
114
|
const raw = readFileDescriptorBoundedSync(opened.fd, maxBytes).toString("utf8");
|
|
105
115
|
const secret = raw.trim();
|
|
106
116
|
if (!secret) {
|
|
@@ -116,9 +126,7 @@ function readSecretFileOutcomeSync(filePath, label, options = {}) {
|
|
|
116
126
|
const normalized = normalizeSecretReadError(error);
|
|
117
127
|
return {
|
|
118
128
|
ok: false,
|
|
119
|
-
code: error instanceof FsSafeError
|
|
120
|
-
? "too-large"
|
|
121
|
-
: "invalid-path",
|
|
129
|
+
code: error instanceof FsSafeError ? error.code : "read-failed",
|
|
122
130
|
error: normalized,
|
|
123
131
|
message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
124
132
|
};
|
|
@@ -257,6 +265,15 @@ async function ensurePrivateDirectory(rootDir, targetDir, mode) {
|
|
|
257
265
|
}
|
|
258
266
|
return { rootGuard, targetReal: await fsp.realpath(resolvedTarget) };
|
|
259
267
|
}
|
|
268
|
+
async function secretFileWriteQueueKey(filePath) {
|
|
269
|
+
try {
|
|
270
|
+
return await canonicalPathFromExistingAncestor(filePath);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Keep validation and its public error shape owned by the write path below.
|
|
274
|
+
return path.resolve(filePath);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
260
277
|
async function materializeSecretFileAtomic(params, createOnly) {
|
|
261
278
|
const mode = params.mode ?? PRIVATE_SECRET_FILE_MODE;
|
|
262
279
|
const dirMode = params.dirMode ?? PRIVATE_SECRET_DIR_MODE;
|
|
@@ -304,11 +321,17 @@ async function materializeSecretFileAtomic(params, createOnly) {
|
|
|
304
321
|
await enforcePrivateFileIdentityAndMode(finalFilePath, identity, mode);
|
|
305
322
|
}
|
|
306
323
|
export async function writeSecretFileAtomic(params) {
|
|
307
|
-
await
|
|
324
|
+
const canonicalPath = await secretFileWriteQueueKey(params.filePath);
|
|
325
|
+
await serializePathWrite(canonicalPath, async () => {
|
|
326
|
+
await materializeSecretFileAtomic(params, false);
|
|
327
|
+
});
|
|
308
328
|
}
|
|
309
329
|
export async function createSecretFileAtomic(params) {
|
|
310
330
|
try {
|
|
311
|
-
await
|
|
331
|
+
const canonicalPath = await secretFileWriteQueueKey(params.filePath);
|
|
332
|
+
await serializePathWrite(canonicalPath, async () => {
|
|
333
|
+
await materializeSecretFileAtomic(params, true);
|
|
334
|
+
});
|
|
312
335
|
}
|
|
313
336
|
catch (error) {
|
|
314
337
|
if ((error instanceof FsSafeError && error.code === "already-exists") ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;AA0F1B,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAM7B"}
|
|
@@ -68,7 +68,11 @@ async function readSecretFileOutcome(filePath, label, options) {
|
|
|
68
68
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
69
69
|
return {
|
|
70
70
|
ok: false,
|
|
71
|
-
code: error instanceof FsSafeError
|
|
71
|
+
code: error instanceof FsSafeError
|
|
72
|
+
? error.code
|
|
73
|
+
: pathErrorCode(error) === "not-found"
|
|
74
|
+
? "not-found"
|
|
75
|
+
: "read-failed",
|
|
72
76
|
error: normalized,
|
|
73
77
|
message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
74
78
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-temp-dir.d.ts","sourceRoot":"","sources":["../src/secure-temp-dir.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secure-temp-dir.d.ts","sourceRoot":"","sources":["../src/secure-temp-dir.ts"],"names":[],"mappings":"AAOA,KAAK,aAAa,GAAG;IACnB,WAAW,IAAI,OAAO,CAAC;IACvB,cAAc,IAAI,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,aAAa,CAAC;IAC5C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAWF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,CA8JnF"}
|
package/dist/secure-temp-dir.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { tmpdir as getOsTmpDir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { assertSafePathSegment } from "./safe-path-segment.js";
|
|
4
5
|
function isNodeErrorWithCode(err, code) {
|
|
5
6
|
return (typeof err === "object" &&
|
|
6
7
|
err !== null &&
|
|
@@ -8,6 +9,10 @@ function isNodeErrorWithCode(err, code) {
|
|
|
8
9
|
err.code === code);
|
|
9
10
|
}
|
|
10
11
|
export function resolveSecureTempRoot(options) {
|
|
12
|
+
const fallbackPrefix = assertSafePathSegment(options.fallbackPrefix, {
|
|
13
|
+
allowDotPrefix: true,
|
|
14
|
+
label: "fallback temp prefix",
|
|
15
|
+
});
|
|
11
16
|
const TMP_DIR_ACCESS_MODE = fs.constants.W_OK | fs.constants.X_OK;
|
|
12
17
|
const accessSync = options.accessSync ?? fs.accessSync;
|
|
13
18
|
const chmodSync = options.chmodSync ?? fs.chmodSync;
|
|
@@ -42,7 +47,7 @@ export function resolveSecureTempRoot(options) {
|
|
|
42
47
|
};
|
|
43
48
|
const fallback = () => {
|
|
44
49
|
const base = tmpdir();
|
|
45
|
-
const suffix = uid === undefined ?
|
|
50
|
+
const suffix = uid === undefined ? fallbackPrefix : `${fallbackPrefix}-${uid}`;
|
|
46
51
|
const joiner = platform === "win32" ? path.win32.join : path.join;
|
|
47
52
|
return joiner(base, suffix);
|
|
48
53
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"short-path.d.ts","sourceRoot":"","sources":["../src/short-path.ts"],"names":[],"mappings":"AAGA,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI/C"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { formatErrorDetail } from "./error-detail.js";
|
|
3
|
+
export function shortPath(value) {
|
|
4
|
+
const home = os.homedir();
|
|
5
|
+
const shortened = value.startsWith(home) ? `~${value.slice(home.length)}` : value;
|
|
6
|
+
return formatErrorDetail(shortened);
|
|
7
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type NativeFileHandle } from "./native-operations.js";
|
|
2
|
+
import type { Root } from "./root-impl.js";
|
|
3
|
+
import { type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
|
|
4
|
+
import type { SidecarLockAcquireOptions, SidecarLockHandle } from "./sidecar-lock-types.js";
|
|
5
|
+
type SidecarFileHandle = Pick<NativeFileHandle, "close" | "stat" | "writeFile">;
|
|
6
|
+
export type HeldSidecarLock = {
|
|
7
|
+
refCount: number;
|
|
8
|
+
reentrantOwner?: string;
|
|
9
|
+
handle: SidecarFileHandle;
|
|
10
|
+
lockPath: string;
|
|
11
|
+
snapshot: SidecarLockSnapshot;
|
|
12
|
+
acquiredAt: number;
|
|
13
|
+
metadata: Record<string, unknown>;
|
|
14
|
+
releasePromise?: Promise<void>;
|
|
15
|
+
lockRoot?: Root;
|
|
16
|
+
parsePayload?: (raw: string) => unknown;
|
|
17
|
+
compromiseTimer?: NodeJS.Timeout;
|
|
18
|
+
};
|
|
19
|
+
type SidecarLockAcquisitionContext = {
|
|
20
|
+
held: Map<string, HeldSidecarLock>;
|
|
21
|
+
reclaimGuards: Set<string>;
|
|
22
|
+
ensureExitCleanupRegistered(): void;
|
|
23
|
+
handleForHeldLock(normalizedTargetPath: string, held: HeldSidecarLock): SidecarLockHandle;
|
|
24
|
+
releaseHeldLock(normalizedTargetPath: string, held: HeldSidecarLock, options?: {
|
|
25
|
+
force?: boolean;
|
|
26
|
+
}): Promise<boolean>;
|
|
27
|
+
};
|
|
28
|
+
export declare function acquireSidecarLock<TPayload extends Record<string, unknown>>(options: SidecarLockAcquireOptions<TPayload>, context: SidecarLockAcquisitionContext): Promise<SidecarLockHandle>;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=sidecar-lock-acquire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAO3C,OAAO,EAUL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5F,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAEhF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;CAClC,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,2BAA2B,IAAI,IAAI,CAAC;IACpC,iBAAiB,CAAC,oBAAoB,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAC1F,eAAe,CACb,oBAAoB,EAAE,MAAM,EAC5B,IAAI,EAAE,eAAe,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB,CAAC;AAaF,wBAAsB,kBAAkB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,OAAO,EAAE,yBAAyB,CAAC,QAAQ,CAAC,EAC5C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CAmP5B"}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { createNativeExclusiveFile } from "./native-operations.js";
|
|
5
|
+
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, } from "./sidecar-lock-policy.js";
|
|
6
|
+
import { readSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
|
|
7
|
+
async function resolveNormalizedTargetPath(targetPath) {
|
|
8
|
+
const resolved = path.resolve(targetPath);
|
|
9
|
+
const dir = path.dirname(resolved);
|
|
10
|
+
await fs.mkdir(dir, { recursive: true });
|
|
11
|
+
try {
|
|
12
|
+
return path.join(await fs.realpath(dir), path.basename(resolved));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return resolved;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function acquireSidecarLock(options, context) {
|
|
19
|
+
context.ensureExitCleanupRegistered();
|
|
20
|
+
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
|
|
21
|
+
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
22
|
+
const held = context.held.get(normalizedTargetPath);
|
|
23
|
+
if (held &&
|
|
24
|
+
options.reentrantOwner !== undefined &&
|
|
25
|
+
held.reentrantOwner !== undefined &&
|
|
26
|
+
options.reentrantOwner === held.reentrantOwner) {
|
|
27
|
+
held.refCount += 1;
|
|
28
|
+
return context.handleForHeldLock(normalizedTargetPath, held);
|
|
29
|
+
}
|
|
30
|
+
const startedAt = Date.now();
|
|
31
|
+
const retry = options.retry ?? {};
|
|
32
|
+
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
33
|
+
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
34
|
+
let ownsReclaimGuard = false;
|
|
35
|
+
let attempt = 0;
|
|
36
|
+
// Bounded so a genuine denial still surfaces as EPERM, not a lock timeout.
|
|
37
|
+
let transientDenials = 0;
|
|
38
|
+
const withinDenialBudget = () => ++transientDenials <= maxTransientLockDenials;
|
|
39
|
+
const waitForRetry = async () => {
|
|
40
|
+
const elapsed = Date.now() - startedAt;
|
|
41
|
+
if ((options.timeoutMs !== undefined &&
|
|
42
|
+
options.timeoutMs !== Number.POSITIVE_INFINITY &&
|
|
43
|
+
elapsed >= options.timeoutMs) ||
|
|
44
|
+
(maxRetries !== undefined && attempt >= maxRetries)) {
|
|
45
|
+
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
46
|
+
code: "file_lock_timeout",
|
|
47
|
+
lockPath,
|
|
48
|
+
normalizedTargetPath,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
|
|
52
|
+
? Number.POSITIVE_INFINITY
|
|
53
|
+
: Math.max(0, options.timeoutMs - elapsed);
|
|
54
|
+
const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
|
|
55
|
+
attempt += 1;
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
57
|
+
};
|
|
58
|
+
// Waiting can fail on the caller's own retry or deadline limits. Classifying
|
|
59
|
+
// a denial as contention must not cost them the original diagnosis, so hand
|
|
60
|
+
// the denial back when no further attempt can be scheduled.
|
|
61
|
+
const retryOrRethrowDenial = async (denial) => {
|
|
62
|
+
try {
|
|
63
|
+
await waitForRetry();
|
|
64
|
+
}
|
|
65
|
+
catch (waitError) {
|
|
66
|
+
if (waitError.code === "file_lock_timeout")
|
|
67
|
+
throw denial;
|
|
68
|
+
throw waitError;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
try {
|
|
72
|
+
while (true) {
|
|
73
|
+
if (!ownsReclaimGuard && (await sidecarReclaimGuardExists(reclaimGuardPath))) {
|
|
74
|
+
await waitForRetry();
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let handle = null;
|
|
78
|
+
let createdSnapshot = null;
|
|
79
|
+
let lockFileCreateDenied = false;
|
|
80
|
+
try {
|
|
81
|
+
const payload = await options.payload();
|
|
82
|
+
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
83
|
+
if (options.lockRoot) {
|
|
84
|
+
const relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
|
|
85
|
+
try {
|
|
86
|
+
await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof FsSafeError && error.code === "already-exists") {
|
|
90
|
+
throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
createdSnapshot = { raw, payload, ownershipToken };
|
|
95
|
+
handle = (await options.lockRoot.open(relativeLockPath)).handle;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
try {
|
|
99
|
+
handle =
|
|
100
|
+
(await createNativeExclusiveFile(lockPath, 0o600)) ??
|
|
101
|
+
(await fs.open(lockPath, "wx"));
|
|
102
|
+
}
|
|
103
|
+
catch (createError) {
|
|
104
|
+
lockFileCreateDenied = isTransientLockFileDenial(createError, lockPath);
|
|
105
|
+
throw createError;
|
|
106
|
+
}
|
|
107
|
+
await handle.writeFile(raw, "utf8");
|
|
108
|
+
}
|
|
109
|
+
const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
|
|
110
|
+
const createdHeld = {
|
|
111
|
+
refCount: 1,
|
|
112
|
+
reentrantOwner: options.reentrantOwner,
|
|
113
|
+
handle,
|
|
114
|
+
lockPath,
|
|
115
|
+
snapshot,
|
|
116
|
+
acquiredAt: Date.now(),
|
|
117
|
+
metadata: options.metadata ?? {},
|
|
118
|
+
lockRoot: options.lockRoot,
|
|
119
|
+
parsePayload: options.parsePayload,
|
|
120
|
+
};
|
|
121
|
+
context.held.set(normalizedTargetPath, createdHeld);
|
|
122
|
+
if (ownsReclaimGuard) {
|
|
123
|
+
try {
|
|
124
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
125
|
+
ownsReclaimGuard = false;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
await context.releaseHeldLock(normalizedTargetPath, createdHeld, { force: true });
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const returnedHandle = context.handleForHeldLock(normalizedTargetPath, createdHeld);
|
|
133
|
+
const interval = options.compromiseCheckIntervalMs;
|
|
134
|
+
if (options.onCompromised && interval !== undefined && interval > 0) {
|
|
135
|
+
createdHeld.compromiseTimer = setInterval(() => {
|
|
136
|
+
void returnedHandle.verifyStillHeld().then((stillHeld) => {
|
|
137
|
+
if (!stillHeld && createdHeld.compromiseTimer) {
|
|
138
|
+
clearInterval(createdHeld.compromiseTimer);
|
|
139
|
+
createdHeld.compromiseTimer = undefined;
|
|
140
|
+
options.onCompromised?.({ lockPath, normalizedTargetPath });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}, interval);
|
|
144
|
+
createdHeld.compromiseTimer.unref();
|
|
145
|
+
}
|
|
146
|
+
return returnedHandle;
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (handle) {
|
|
150
|
+
const failedSnapshot = { payload: null };
|
|
151
|
+
try {
|
|
152
|
+
failedSnapshot.stat = await handle.stat();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Best-effort cleanup of a failed exclusive create.
|
|
156
|
+
}
|
|
157
|
+
const current = context.held.get(normalizedTargetPath);
|
|
158
|
+
if (current?.handle === handle) {
|
|
159
|
+
context.held.delete(normalizedTargetPath);
|
|
160
|
+
}
|
|
161
|
+
await handle.close().catch(() => undefined);
|
|
162
|
+
// The file may be empty or partial JSON, so remove by the identity
|
|
163
|
+
// captured from our exclusive handle rather than by pathname alone.
|
|
164
|
+
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
165
|
+
lockRoot: options.lockRoot,
|
|
166
|
+
parsePayload: options.parsePayload,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else if (createdSnapshot) {
|
|
170
|
+
await removeSidecarLockIfUnchanged(lockPath, createdSnapshot, {
|
|
171
|
+
lockRoot: options.lockRoot,
|
|
172
|
+
parsePayload: options.parsePayload,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (lockFileCreateDenied && withinDenialBudget()) {
|
|
176
|
+
await retryOrRethrowDenial(err);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (err.code !== "EEXIST") {
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
if (ownsReclaimGuard) {
|
|
183
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
184
|
+
ownsReclaimGuard = false;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const nowMs = Date.now();
|
|
188
|
+
let snapshot;
|
|
189
|
+
try {
|
|
190
|
+
snapshot = await readSidecarLockSnapshot(lockPath, {
|
|
191
|
+
lockRoot: options.lockRoot,
|
|
192
|
+
parsePayload: options.parsePayload,
|
|
193
|
+
rejectNonFile: true,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (readErr) {
|
|
197
|
+
if (!isTransientLockFileDenial(readErr, lockPath) || !withinDenialBudget())
|
|
198
|
+
throw readErr;
|
|
199
|
+
await retryOrRethrowDenial(readErr);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!snapshot) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (context.held.has(normalizedTargetPath)) {
|
|
206
|
+
await waitForRetry();
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
|
|
210
|
+
if (await shouldReclaim({
|
|
211
|
+
lockPath,
|
|
212
|
+
normalizedTargetPath,
|
|
213
|
+
payload: snapshot?.payload ?? null,
|
|
214
|
+
staleMs: options.staleMs,
|
|
215
|
+
nowMs,
|
|
216
|
+
heldByThisProcess: context.held.has(normalizedTargetPath),
|
|
217
|
+
})) {
|
|
218
|
+
if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
|
|
219
|
+
lockRoot: options.lockRoot,
|
|
220
|
+
parsePayload: options.parsePayload,
|
|
221
|
+
}))) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const staleRecovery = options.staleRecovery ?? "fail-closed";
|
|
225
|
+
if (staleRecovery === "remove-if-unchanged") {
|
|
226
|
+
if (!(await tryAcquireSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath))) {
|
|
227
|
+
await waitForRetry();
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
ownsReclaimGuard = true;
|
|
231
|
+
const removal = await removeStaleSidecarLockIfAllowed({
|
|
232
|
+
lockPath,
|
|
233
|
+
normalizedTargetPath,
|
|
234
|
+
snapshot,
|
|
235
|
+
shouldRemoveStaleLock: options.shouldRemoveStaleLock,
|
|
236
|
+
lockRoot: options.lockRoot,
|
|
237
|
+
parsePayload: options.parsePayload,
|
|
238
|
+
});
|
|
239
|
+
if (removal === "removed" || removal === "changed") {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
243
|
+
ownsReclaimGuard = false;
|
|
244
|
+
}
|
|
245
|
+
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
246
|
+
code: "file_lock_stale",
|
|
247
|
+
lockPath,
|
|
248
|
+
normalizedTargetPath,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
await waitForRetry();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
if (ownsReclaimGuard) {
|
|
257
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath).catch(() => undefined);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
2
2
|
export declare function computeSidecarLockDelayMs(retry: SidecarLockRetryOptions, attempt: number): number;
|
|
3
|
+
export declare const maxTransientLockDenials = 8;
|
|
4
|
+
export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
|
|
3
5
|
export declare function sidecarLockPayloadIsStale(payload: unknown, staleMs: number, nowMs: number): boolean;
|
|
6
|
+
export declare function sidecarLockPayloadCreatedAtMs(payload: unknown): number | null;
|
|
4
7
|
export declare function defaultSidecarLockShouldReclaim(params: {
|
|
5
8
|
lockPath: string;
|
|
6
9
|
payload: unknown;
|
|
@@ -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;AAEvE,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOjG;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,
|
|
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;AAEvE,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOjG;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"}
|
|
@@ -7,7 +7,23 @@ export function computeSidecarLockDelayMs(retry, attempt) {
|
|
|
7
7
|
const jitter = retry.randomize ? 1 + Math.random() : 1;
|
|
8
8
|
return Math.min(maxTimeout, Math.round(base * jitter));
|
|
9
9
|
}
|
|
10
|
+
// Windows denies access to a lock file while a just-unlinked directory entry
|
|
11
|
+
// is still being torn down, so a contended acquire sees EPERM on a name that is
|
|
12
|
+
// already gone -- both when creating it exclusively and when reading the
|
|
13
|
+
// holder's snapshot. The next attempt succeeds, so this is contention rather
|
|
14
|
+
// than a permission failure. The error must name the lock file itself: the
|
|
15
|
+
// exclusive-create helper opens the parent directory first, and a denial from
|
|
16
|
+
// that setup step carries no teardown evidence and has to reach the caller.
|
|
17
|
+
export const maxTransientLockDenials = 8;
|
|
18
|
+
export function isTransientLockFileDenial(error, lockPath) {
|
|
19
|
+
const denial = error;
|
|
20
|
+
return process.platform === "win32" && denial?.code === "EPERM" && denial.path === lockPath;
|
|
21
|
+
}
|
|
10
22
|
export function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
|
|
23
|
+
const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
|
|
24
|
+
return createdAtMs !== null && nowMs - createdAtMs > staleMs;
|
|
25
|
+
}
|
|
26
|
+
export function sidecarLockPayloadCreatedAtMs(payload) {
|
|
11
27
|
const createdAt = payload &&
|
|
12
28
|
typeof payload === "object" &&
|
|
13
29
|
"createdAt" in payload &&
|
|
@@ -15,11 +31,12 @@ export function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
|
|
|
15
31
|
? payload.createdAt
|
|
16
32
|
: "";
|
|
17
33
|
const createdAtMs = Date.parse(createdAt);
|
|
18
|
-
return Number.isFinite(createdAtMs)
|
|
34
|
+
return Number.isFinite(createdAtMs) ? createdAtMs : null;
|
|
19
35
|
}
|
|
20
36
|
export async function defaultSidecarLockShouldReclaim(params) {
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
const createdAtMs = sidecarLockPayloadCreatedAtMs(params.payload);
|
|
38
|
+
if (createdAtMs !== null)
|
|
39
|
+
return params.nowMs - createdAtMs > params.staleMs;
|
|
23
40
|
try {
|
|
24
41
|
return params.nowMs - (await fs.stat(params.lockPath)).mtimeMs > params.staleMs;
|
|
25
42
|
}
|
|
@@ -22,8 +22,12 @@ export declare function parseSidecarLockPayload(raw: string, parser?: (raw: stri
|
|
|
22
22
|
export declare function readSidecarLockSnapshot(lockPath: string, options?: {
|
|
23
23
|
lockRoot?: Root;
|
|
24
24
|
parsePayload?: (raw: string) => unknown;
|
|
25
|
+
rejectNonFile?: boolean;
|
|
26
|
+
allowDescriptorIdentityDrift?: boolean;
|
|
25
27
|
}): Promise<SidecarLockSnapshot | null>;
|
|
26
|
-
export declare function readSidecarLockSnapshotSync(lockPath: string, parsePayload?: (raw: string) => unknown
|
|
28
|
+
export declare function readSidecarLockSnapshotSync(lockPath: string, parsePayload?: (raw: string) => unknown, options?: {
|
|
29
|
+
rejectNonFile?: boolean;
|
|
30
|
+
}): SidecarLockSnapshot | null;
|
|
27
31
|
export declare function removeSidecarLockIfUnchangedSync(lockPath: string, observed: SidecarLockSnapshot): boolean;
|
|
28
32
|
export declare function sidecarLockSnapshotMatches(current: SidecarLockSnapshot, observed: SidecarLockSnapshot): boolean;
|
|
29
33
|
export declare function removeSidecarLockIfUnchanged(lockPath: string, observed: SidecarLockSnapshot | null, options?: {
|
|
@@ -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;AAM7C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"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"}
|