Files
openclaw/src/plugin-sdk/file-lock.ts
Peter Steinberger 538605ff44 [codex] Extract filesystem safety primitives (#77918)
* refactor: extract filesystem safety primitives

* refactor: use fs-safe for file access helpers

* refactor: reuse fs-safe for media reads

* refactor: use fs-safe for image reads

* refactor: reuse fs-safe in qqbot media opener

* refactor: reuse fs-safe for local media checks

* refactor: consume cleaner fs-safe api

* refactor: align fs-safe json option names

* fix: preserve fs-safe migration contracts

* refactor: use fs-safe primitive subpaths

* refactor: use grouped fs-safe subpaths

* refactor: align fs-safe api usage

* refactor: adapt private state store api

* chore: refresh proof gate

* refactor: follow fs-safe json api split

* refactor: follow reduced fs-safe surface

* build: default fs-safe python helper off

* fix: preserve fs-safe plugin sdk aliases

* refactor: consolidate fs-safe usage

* refactor: unify fs-safe store usage

* refactor: trim fs-safe temp workspace usage

* refactor: hide low-level fs-safe primitives

* build: use published fs-safe package

* fix: preserve outbound recovery durability after rebase

* chore: refresh pr checks
2026-05-06 02:15:17 +01:00

117 lines
3.2 KiB
TypeScript

import "../infra/fs-safe-defaults.js";
import {
acquireFileLock as acquireFsSafeFileLock,
drainFileLockManagerForTest,
resetFileLockManagerForTest,
} from "@openclaw/fs-safe/file-lock";
import { isPidAlive } from "../shared/pid-alive.js";
export type FileLockOptions = {
retries: {
retries: number;
factor: number;
minTimeout: number;
maxTimeout: number;
randomize?: boolean;
};
stale: number;
};
type LockFilePayload = {
pid?: number;
createdAt?: string;
};
export type FileLockHandle = {
lockPath: string;
release: () => Promise<void>;
};
export const FILE_LOCK_TIMEOUT_ERROR_CODE = "file_lock_timeout";
export type FileLockTimeoutError = Error & {
code: typeof FILE_LOCK_TIMEOUT_ERROR_CODE;
lockPath: string;
};
const FILE_LOCK_MANAGER_KEY = "openclaw.plugin-sdk.file-lock";
function readLockPayload(value: Record<string, unknown> | null): LockFilePayload | null {
if (!value) {
return null;
}
return {
pid: typeof value.pid === "number" ? value.pid : undefined,
createdAt: typeof value.createdAt === "string" ? value.createdAt : undefined,
};
}
async function shouldReclaimPluginLock(params: {
lockPath: string;
payload: Record<string, unknown> | null;
staleMs: number;
nowMs: number;
}): Promise<boolean> {
const payload = readLockPayload(params.payload);
if (payload?.pid && !isPidAlive(payload.pid)) {
return true;
}
if (payload?.createdAt) {
const createdAt = Date.parse(payload.createdAt);
return !Number.isFinite(createdAt) || params.nowMs - createdAt > params.staleMs;
}
return true;
}
function normalizeTimeoutError(err: unknown): never {
if ((err as { code?: unknown }).code === FILE_LOCK_TIMEOUT_ERROR_CODE) {
throw Object.assign(new Error((err as Error).message), {
code: FILE_LOCK_TIMEOUT_ERROR_CODE,
lockPath: (err as { lockPath?: string }).lockPath ?? "",
}) as FileLockTimeoutError;
}
throw err;
}
export function resetFileLockStateForTest(): void {
resetFileLockManagerForTest(FILE_LOCK_MANAGER_KEY, FILE_LOCK_MANAGER_KEY);
}
export async function drainFileLockStateForTest(): Promise<void> {
await drainFileLockManagerForTest(FILE_LOCK_MANAGER_KEY, FILE_LOCK_MANAGER_KEY);
}
/** Acquire a re-entrant process-local file lock backed by a `.lock` sidecar file. */
export async function acquireFileLock(
filePath: string,
options: FileLockOptions,
): Promise<FileLockHandle> {
try {
const lock = await acquireFsSafeFileLock(filePath, {
managerKey: FILE_LOCK_MANAGER_KEY,
staleMs: options.stale,
retry: options.retries,
allowReentrant: true,
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
shouldReclaim: shouldReclaimPluginLock,
});
return { lockPath: lock.lockPath, release: lock.release };
} catch (err) {
return normalizeTimeoutError(err);
}
}
/** Run an async callback while holding a file lock, always releasing the lock afterward. */
export async function withFileLock<T>(
filePath: string,
options: FileLockOptions,
fn: () => Promise<T>,
): Promise<T> {
const lock = await acquireFileLock(filePath, options);
try {
return await fn();
} finally {
await lock.release();
}
}