diff --git a/src/capability-language/file-lock.ts b/src/capability-language/file-lock.ts index a1419ba..2a27ece 100644 --- a/src/capability-language/file-lock.ts +++ b/src/capability-language/file-lock.ts @@ -1,14 +1,19 @@ import { spawn } from "node:child_process"; +export class FileLockBusyError extends Error {} + /** Kernel-owned lock: a crashed coordinator cannot leave a stale ownership file. * The persistent file is just an inode; EOF releases the helper's lock. */ -export async function withFileLock(filename: string, work: () => Promise): Promise { +export async function withFileLock( + filename: string, + work: () => Promise, + { nonblocking = false }: { nonblocking?: boolean } = {}, +): Promise { const child = spawn( "flock", [ "--exclusive", - "--timeout", - "120", + ...(nonblocking ? ["--nonblock"] : ["--timeout", "120"]), "--conflict-exit-code", "75", filename, @@ -32,15 +37,17 @@ export async function withFileLock(filename: string, work: () => Promise): await new Promise((resolve, reject) => { let output = ""; child.once("error", reject); - child.once("exit", (code) => + child.once("exit", (code) => { reject( - new Error( - code === 75 - ? "Timed out after 120 seconds waiting for another authoring command; inspect that command before retrying" - : `Cannot acquire authoring lock: ${diagnostics}`, - ), - ), - ); + code === 75 + ? new FileLockBusyError( + nonblocking + ? "Another process owns this lock" + : "Timed out after 120 seconds waiting for another authoring command; inspect that command before retrying", + ) + : new Error(`Cannot acquire authoring lock: ${diagnostics}`), + ); + }); child.stdout.on("data", (chunk) => { output += chunk; if (output.includes("locked\n")) resolve();