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, { nonblocking = false }: { nonblocking?: boolean } = {}, ): Promise { const child = spawn( "flock", [ "--exclusive", ...(nonblocking ? ["--nonblock"] : ["--timeout", "120"]), "--conflict-exit-code", "75", filename, process.execPath, "-e", 'process.stdout.write("locked\\n"); process.stdin.resume();', ], { stdio: ["pipe", "pipe", "pipe"] }, ); let diagnostics = ""; child.stdin.on("error", () => { /* acquisition/exit handling reports helper failure */ }); child.stderr.on("data", (chunk) => { diagnostics = (diagnostics + String(chunk)).slice(-2000); }); const closed = new Promise((resolve) => { child.once("close", () => resolve()); }); try { await new Promise((resolve, reject) => { let output = ""; child.once("error", reject); child.once("exit", (code) => { reject( 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(); }); }); return await work(); } finally { child.stdin.end(); await closed; } }