


























@@ -1,4 +1,5 @@
11import "../infra/fs-safe-defaults.js";
2+import fs from "node:fs/promises";
23import {
34acquireFileLock as acquireFsSafeFileLock,
45drainFileLockManagerForTest,
@@ -28,14 +29,25 @@ export type FileLockHandle = {
2829};
29303031export const FILE_LOCK_TIMEOUT_ERROR_CODE = "file_lock_timeout";
32+export const FILE_LOCK_STALE_ERROR_CODE = "file_lock_stale";
31333234export type FileLockTimeoutError = Error & {
3335code: typeof FILE_LOCK_TIMEOUT_ERROR_CODE;
3436lockPath: string;
3537};
363839+export type FileLockStaleError = Error & {
40+code: typeof FILE_LOCK_STALE_ERROR_CODE;
41+lockPath: string;
42+};
43+3744const FILE_LOCK_MANAGER_KEY = "openclaw.plugin-sdk.file-lock";
384546+type LockFileSnapshot = {
47+raw: string;
48+payload: Record<string, unknown> | null;
49+};
50+3951function readLockPayload(value: Record<string, unknown> | null): LockFilePayload | null {
4052if (!value) {
4153return null;
@@ -63,13 +75,98 @@ async function shouldReclaimPluginLock(params: {
6375return true;
6476}
657766-function normalizeTimeoutError(err: unknown): never {
78+function isFileLockError(error: unknown, code: string): boolean {
79+return (error as { code?: unknown } | null)?.code === code;
80+}
81+82+async function readLockFileSnapshot(lockPath: string): Promise<LockFileSnapshot | null> {
83+let raw: string;
84+try {
85+raw = await fs.readFile(lockPath, "utf8");
86+} catch (err) {
87+if ((err as NodeJS.ErrnoException).code === "ENOENT") {
88+return null;
89+}
90+throw err;
91+}
92+93+try {
94+const parsed = JSON.parse(raw) as unknown;
95+return {
96+ raw,
97+payload:
98+parsed && typeof parsed === "object" && !Array.isArray(parsed)
99+ ? (parsed as Record<string, unknown>)
100+ : null,
101+};
102+} catch {
103+return { raw, payload: null };
104+}
105+}
106+107+function shouldRemoveReportedStalePluginLock(params: {
108+payload: Record<string, unknown> | null;
109+staleMs: number;
110+nowMs: number;
111+}): boolean {
112+const payload = readLockPayload(params.payload);
113+if (payload?.pid) {
114+return !isPidAlive(payload.pid);
115+}
116+if (payload?.createdAt) {
117+const createdAt = Date.parse(payload.createdAt);
118+return !Number.isFinite(createdAt) || params.nowMs - createdAt > params.staleMs;
119+}
120+return true;
121+}
122+123+async function removeReportedStaleLockIfStillStale(params: {
124+lockPath: string;
125+staleMs: number;
126+}): Promise<boolean> {
127+const snapshot = await readLockFileSnapshot(params.lockPath);
128+if (!snapshot) {
129+return true;
130+}
131+if (
132+!shouldRemoveReportedStalePluginLock({
133+payload: snapshot.payload,
134+staleMs: params.staleMs,
135+nowMs: Date.now(),
136+})
137+) {
138+return false;
139+}
140+141+const current = await readLockFileSnapshot(params.lockPath);
142+if (!current) {
143+return true;
144+}
145+if (current.raw !== snapshot.raw) {
146+return false;
147+}
148+149+try {
150+await fs.unlink(params.lockPath);
151+return true;
152+} catch (err) {
153+return (err as NodeJS.ErrnoException).code === "ENOENT";
154+}
155+}
156+157+function normalizeLockError(err: unknown): never {
67158if ((err as { code?: unknown }).code === FILE_LOCK_TIMEOUT_ERROR_CODE) {
68159throw Object.assign(new Error((err as Error).message), {
69160code: FILE_LOCK_TIMEOUT_ERROR_CODE,
70161lockPath: (err as { lockPath?: string }).lockPath ?? "",
71162}) as FileLockTimeoutError;
72163}
164+if ((err as { code?: unknown }).code === FILE_LOCK_STALE_ERROR_CODE) {
165+throw Object.assign(new Error((err as Error).message), {
166+code: FILE_LOCK_STALE_ERROR_CODE,
167+lockPath: (err as { lockPath?: string }).lockPath ?? "",
168+}) as FileLockStaleError;
169+}
73170throw err;
74171}
75172@@ -86,18 +183,32 @@ export async function acquireFileLock(
86183filePath: string,
87184options: FileLockOptions,
88185): Promise<FileLockHandle> {
89-try {
90-const lock = await acquireFsSafeFileLock(filePath, {
91-managerKey: FILE_LOCK_MANAGER_KEY,
92-staleMs: options.stale,
93-retry: options.retries,
94-allowReentrant: true,
95-payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
96-shouldReclaim: shouldReclaimPluginLock,
97-});
98-return { lockPath: lock.lockPath, release: lock.release };
99-} catch (err) {
100-return normalizeTimeoutError(err);
186+while (true) {
187+try {
188+const lock = await acquireFsSafeFileLock(filePath, {
189+managerKey: FILE_LOCK_MANAGER_KEY,
190+staleMs: options.stale,
191+retry: options.retries,
192+allowReentrant: true,
193+payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
194+shouldReclaim: shouldReclaimPluginLock,
195+});
196+return { lockPath: lock.lockPath, release: lock.release };
197+} catch (err) {
198+if (isFileLockError(err, FILE_LOCK_STALE_ERROR_CODE)) {
199+const lockPath = (err as { lockPath?: string }).lockPath;
200+if (
201+lockPath &&
202+(await removeReportedStaleLockIfStillStale({
203+ lockPath,
204+staleMs: options.stale,
205+}))
206+) {
207+continue;
208+}
209+}
210+return normalizeLockError(err);
211+}
101212}
102213}
103214此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。