@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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/README.md +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
package/src/refresh-lock.test.ts
CHANGED
|
@@ -9,119 +9,119 @@ const DEFAULT_LOCK_TIMEOUT_MS = 15_000;
|
|
|
9
9
|
const DEFAULT_STALE_LOCK_MS = 90_000;
|
|
10
10
|
|
|
11
11
|
vi.mock("./storage.js", () => ({
|
|
12
|
-
|
|
12
|
+
getStoragePath: () => storagePath,
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
15
|
import { acquireRefreshLock, releaseRefreshLock } from "./refresh-lock.js";
|
|
16
16
|
|
|
17
17
|
describe("refresh-lock", () => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
await fs.rm(baseDir, { recursive: true, force: true });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("does not release lock with mismatched owner", async () => {
|
|
28
|
-
const lock = await acquireRefreshLock("acc-1");
|
|
29
|
-
expect(lock.acquired).toBe(true);
|
|
30
|
-
expect(lock.lockPath).toBeTruthy();
|
|
31
|
-
const lockPath = lock.lockPath!;
|
|
32
|
-
|
|
33
|
-
await releaseRefreshLock({ lockPath, owner: "wrong-owner" });
|
|
34
|
-
|
|
35
|
-
await expect(fs.stat(lockPath)).resolves.toBeTruthy();
|
|
36
|
-
|
|
37
|
-
await releaseRefreshLock(lock);
|
|
38
|
-
await expect(fs.stat(lockPath)).rejects.toMatchObject({
|
|
39
|
-
code: "ENOENT",
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
await fs.rm(baseDir, { recursive: true, force: true });
|
|
20
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
40
21
|
});
|
|
41
|
-
});
|
|
42
22
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
timeoutMs: 50,
|
|
46
|
-
staleMs: 10_000,
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await fs.rm(baseDir, { recursive: true, force: true });
|
|
47
25
|
});
|
|
48
|
-
expect(first.acquired).toBe(true);
|
|
49
|
-
const firstLockPath = first.lockPath!;
|
|
50
|
-
|
|
51
|
-
const old = Date.now() / 1000 - 120;
|
|
52
|
-
await fs.utimes(firstLockPath, old, old);
|
|
53
26
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(second.acquired).toBe(true);
|
|
60
|
-
expect(second.owner).not.toBe(first.owner);
|
|
27
|
+
it("does not release lock with mismatched owner", async () => {
|
|
28
|
+
const lock = await acquireRefreshLock("acc-1");
|
|
29
|
+
expect(lock.acquired).toBe(true);
|
|
30
|
+
expect(lock.lockPath).toBeTruthy();
|
|
31
|
+
const lockPath = lock.lockPath!;
|
|
61
32
|
|
|
62
|
-
|
|
63
|
-
});
|
|
33
|
+
await releaseRefreshLock({ lockPath, owner: "wrong-owner" });
|
|
64
34
|
|
|
65
|
-
|
|
66
|
-
const first = await acquireRefreshLock("acc-3", { timeoutMs: 50 });
|
|
67
|
-
expect(first.acquired).toBe(true);
|
|
35
|
+
await expect(fs.stat(lockPath)).resolves.toBeTruthy();
|
|
68
36
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
37
|
+
await releaseRefreshLock(lock);
|
|
38
|
+
await expect(fs.stat(lockPath)).rejects.toMatchObject({
|
|
39
|
+
code: "ENOENT",
|
|
40
|
+
});
|
|
73
41
|
});
|
|
74
|
-
expect(second.acquired).toBe(false);
|
|
75
|
-
|
|
76
|
-
await releaseRefreshLock(first);
|
|
77
|
-
});
|
|
78
42
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
43
|
+
it("acquires a new lock after stale lock timeout", async () => {
|
|
44
|
+
const first = await acquireRefreshLock("acc-2", {
|
|
45
|
+
timeoutMs: 50,
|
|
46
|
+
staleMs: 10_000,
|
|
47
|
+
});
|
|
48
|
+
expect(first.acquired).toBe(true);
|
|
49
|
+
const firstLockPath = first.lockPath!;
|
|
50
|
+
|
|
51
|
+
const old = Date.now() / 1000 - 120;
|
|
52
|
+
await fs.utimes(firstLockPath, old, old);
|
|
53
|
+
|
|
54
|
+
const second = await acquireRefreshLock("acc-2", {
|
|
55
|
+
timeoutMs: 200,
|
|
56
|
+
backoffMs: 5,
|
|
57
|
+
staleMs: 20,
|
|
58
|
+
});
|
|
59
|
+
expect(second.acquired).toBe(true);
|
|
60
|
+
expect(second.owner).not.toBe(first.owner);
|
|
61
|
+
|
|
62
|
+
await releaseRefreshLock(second);
|
|
82
63
|
});
|
|
83
|
-
expect(first.acquired).toBe(true);
|
|
84
|
-
expect(first.lockPath).toBeTruthy();
|
|
85
|
-
const firstLockPath = first.lockPath!;
|
|
86
64
|
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
it("returns not acquired when lock remains busy", async () => {
|
|
66
|
+
const first = await acquireRefreshLock("acc-3", { timeoutMs: 50 });
|
|
67
|
+
expect(first.acquired).toBe(true);
|
|
89
68
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
await expect(fs.stat(firstLockPath)).resolves.toBeTruthy();
|
|
98
|
-
await releaseRefreshLock(first);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("does not release when inode changed even if owner matches", async () => {
|
|
102
|
-
const first = await acquireRefreshLock("acc-4");
|
|
103
|
-
expect(first.acquired).toBe(true);
|
|
104
|
-
const firstLockPath = first.lockPath!;
|
|
105
|
-
const originalLockInode = first.lockInode;
|
|
106
|
-
expect(originalLockInode).not.toBeNull();
|
|
107
|
-
|
|
108
|
-
// Keep the same owner text but pass a deliberately mismatched inode. This
|
|
109
|
-
// tests the safety check directly without relying on filesystem-specific
|
|
110
|
-
// inode allocation behavior, which can aggressively recycle inode numbers
|
|
111
|
-
// on Linux CI runners.
|
|
112
|
-
await fs.writeFile(firstLockPath, JSON.stringify({ owner: first.owner, createdAt: Date.now() }), {
|
|
113
|
-
encoding: "utf-8",
|
|
114
|
-
mode: 0o600,
|
|
115
|
-
});
|
|
69
|
+
const second = await acquireRefreshLock("acc-3", {
|
|
70
|
+
timeoutMs: 30,
|
|
71
|
+
backoffMs: 5,
|
|
72
|
+
staleMs: DEFAULT_STALE_LOCK_MS,
|
|
73
|
+
});
|
|
74
|
+
expect(second.acquired).toBe(false);
|
|
116
75
|
|
|
117
|
-
|
|
118
|
-
lockPath: firstLockPath,
|
|
119
|
-
owner: first.owner,
|
|
120
|
-
lockInode: originalLockInode! + 1n,
|
|
76
|
+
await releaseRefreshLock(first);
|
|
121
77
|
});
|
|
122
78
|
|
|
123
|
-
|
|
79
|
+
it("stale reaper does NOT steal a lock held for 60s", async () => {
|
|
80
|
+
const first = await acquireRefreshLock("acc-stable-refresh", {
|
|
81
|
+
timeoutMs: DEFAULT_LOCK_TIMEOUT_MS,
|
|
82
|
+
});
|
|
83
|
+
expect(first.acquired).toBe(true);
|
|
84
|
+
expect(first.lockPath).toBeTruthy();
|
|
85
|
+
const firstLockPath = first.lockPath!;
|
|
86
|
+
|
|
87
|
+
const sixtySecondsAgo = Date.now() / 1000 - 60;
|
|
88
|
+
await fs.utimes(firstLockPath, sixtySecondsAgo, sixtySecondsAgo);
|
|
89
|
+
|
|
90
|
+
const second = await acquireRefreshLock("acc-stable-refresh", {
|
|
91
|
+
timeoutMs: 30,
|
|
92
|
+
backoffMs: 5,
|
|
93
|
+
staleMs: DEFAULT_STALE_LOCK_MS,
|
|
94
|
+
});
|
|
95
|
+
expect(second.acquired).toBe(false);
|
|
96
|
+
|
|
97
|
+
await expect(fs.stat(firstLockPath)).resolves.toBeTruthy();
|
|
98
|
+
await releaseRefreshLock(first);
|
|
99
|
+
});
|
|
124
100
|
|
|
125
|
-
|
|
126
|
-
|
|
101
|
+
it("does not release when inode changed even if owner matches", async () => {
|
|
102
|
+
const first = await acquireRefreshLock("acc-4");
|
|
103
|
+
expect(first.acquired).toBe(true);
|
|
104
|
+
const firstLockPath = first.lockPath!;
|
|
105
|
+
const originalLockInode = first.lockInode;
|
|
106
|
+
expect(originalLockInode).not.toBeNull();
|
|
107
|
+
|
|
108
|
+
// Keep the same owner text but pass a deliberately mismatched inode. This
|
|
109
|
+
// tests the safety check directly without relying on filesystem-specific
|
|
110
|
+
// inode allocation behavior, which can aggressively recycle inode numbers
|
|
111
|
+
// on Linux CI runners.
|
|
112
|
+
await fs.writeFile(firstLockPath, JSON.stringify({ owner: first.owner, createdAt: Date.now() }), {
|
|
113
|
+
encoding: "utf-8",
|
|
114
|
+
mode: 0o600,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await releaseRefreshLock({
|
|
118
|
+
lockPath: firstLockPath,
|
|
119
|
+
owner: first.owner,
|
|
120
|
+
lockInode: originalLockInode! + 1n,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await expect(fs.stat(firstLockPath)).resolves.toBeTruthy();
|
|
124
|
+
|
|
125
|
+
await fs.unlink(firstLockPath);
|
|
126
|
+
});
|
|
127
127
|
});
|
package/src/refresh-lock.ts
CHANGED
|
@@ -8,128 +8,128 @@ const DEFAULT_LOCK_BACKOFF_MS = 50;
|
|
|
8
8
|
const DEFAULT_STALE_LOCK_MS = 90_000;
|
|
9
9
|
|
|
10
10
|
function delay(ms: number): Promise<void> {
|
|
11
|
-
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function getLockPath(accountId: string): string {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const hash = createHash("sha1").update(accountId).digest("hex").slice(0, 24);
|
|
16
|
+
return join(dirname(getStoragePath()), "locks", `refresh-${hash}.lock`);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface RefreshLockResult {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
acquired: boolean;
|
|
21
|
+
lockPath: string | null;
|
|
22
|
+
owner: string | null;
|
|
23
|
+
lockInode: bigint | null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface AcquireLockOptions {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
backoffMs?: number;
|
|
29
|
+
staleMs?: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Try to acquire a per-account cross-process lock.
|
|
34
34
|
*/
|
|
35
35
|
export async function acquireRefreshLock(
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
accountId: string,
|
|
37
|
+
options: AcquireLockOptions = {},
|
|
38
38
|
): Promise<RefreshLockResult> {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
39
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
40
|
+
const backoffMs = options.backoffMs ?? DEFAULT_LOCK_BACKOFF_MS;
|
|
41
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
|
|
42
|
+
const lockPath = getLockPath(accountId);
|
|
43
|
+
const lockDir = dirname(lockPath);
|
|
44
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
45
|
+
const owner = randomBytes(12).toString("hex");
|
|
46
|
+
|
|
47
|
+
await fs.mkdir(lockDir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
while (Date.now() <= deadline) {
|
|
50
|
+
try {
|
|
51
|
+
const handle = await fs.open(lockPath, "wx", 0o600);
|
|
52
|
+
try {
|
|
53
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now(), owner }), "utf-8");
|
|
54
|
+
const stat = await handle.stat({ bigint: true });
|
|
55
|
+
return { acquired: true, lockPath, owner, lockInode: stat.ino };
|
|
56
|
+
} finally {
|
|
57
|
+
await handle.close();
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
61
|
+
if (code !== "EEXIST") {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const stat = await fs.stat(lockPath, { bigint: true });
|
|
67
|
+
if (Date.now() - Number(stat.mtimeMs) > staleMs) {
|
|
68
|
+
await fs.unlink(lockPath);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Lock may have been released concurrently; retry.
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const remaining = deadline - Date.now();
|
|
76
|
+
if (remaining <= 0) break;
|
|
77
|
+
const jitter = Math.floor(Math.random() * 25);
|
|
78
|
+
await delay(Math.min(remaining, backoffMs + jitter));
|
|
70
79
|
}
|
|
71
|
-
} catch {
|
|
72
|
-
// Lock may have been released concurrently; retry.
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const remaining = deadline - Date.now();
|
|
76
|
-
if (remaining <= 0) break;
|
|
77
|
-
const jitter = Math.floor(Math.random() * 25);
|
|
78
|
-
await delay(Math.min(remaining, backoffMs + jitter));
|
|
79
80
|
}
|
|
80
|
-
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
return { acquired: false, lockPath: null, owner: null, lockInode: null };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export type ReleaseLockInput =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
| {
|
|
87
|
+
lockPath: string | null;
|
|
88
|
+
owner?: string | null;
|
|
89
|
+
lockInode?: bigint | null;
|
|
90
|
+
}
|
|
91
|
+
| string
|
|
92
|
+
| null;
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* Release a lock acquired by acquireRefreshLock.
|
|
96
96
|
*/
|
|
97
97
|
export async function releaseRefreshLock(lock: ReleaseLockInput): Promise<void> {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
const lockPath = typeof lock === "string" || lock === null ? lock : lock.lockPath;
|
|
99
|
+
const owner = typeof lock === "object" && lock ? lock.owner || null : null;
|
|
100
|
+
const lockInode = typeof lock === "object" && lock ? (lock.lockInode ?? null) : null;
|
|
101
|
+
|
|
102
|
+
if (!lockPath) return;
|
|
103
|
+
|
|
104
|
+
// Ownership-safe release: avoid deleting a lock that another process
|
|
105
|
+
// acquired after ours became stale.
|
|
106
|
+
if (owner) {
|
|
107
|
+
try {
|
|
108
|
+
const content = await fs.readFile(lockPath, "utf-8");
|
|
109
|
+
const parsed = JSON.parse(content);
|
|
110
|
+
if (!parsed || typeof parsed !== "object" || parsed.owner !== owner) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (lockInode) {
|
|
115
|
+
const stat = await fs.stat(lockPath, { bigint: true });
|
|
116
|
+
if (stat.ino !== lockInode) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
122
|
+
if (code === "ENOENT") return;
|
|
123
|
+
// If unreadable/corrupt, fail closed to avoid deleting another
|
|
124
|
+
// process's lock when ownership cannot be verified.
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
103
128
|
|
|
104
|
-
// Ownership-safe release: avoid deleting a lock that another process
|
|
105
|
-
// acquired after ours became stale.
|
|
106
|
-
if (owner) {
|
|
107
129
|
try {
|
|
108
|
-
|
|
109
|
-
const parsed = JSON.parse(content);
|
|
110
|
-
if (!parsed || typeof parsed !== "object" || parsed.owner !== owner) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (lockInode) {
|
|
115
|
-
const stat = await fs.stat(lockPath, { bigint: true });
|
|
116
|
-
if (stat.ino !== lockInode) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
130
|
+
await fs.unlink(lockPath);
|
|
120
131
|
} catch (error) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// If unreadable/corrupt, fail closed to avoid deleting another
|
|
124
|
-
// process's lock when ownership cannot be verified.
|
|
125
|
-
return;
|
|
132
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
133
|
+
if (code !== "ENOENT") throw error;
|
|
126
134
|
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
await fs.unlink(lockPath);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
133
|
-
if (code !== "ENOENT") throw error;
|
|
134
|
-
}
|
|
135
135
|
}
|