2026-09-14 10:26:11 -07:00
|
|
|
import { spawn } from "node:child_process";
|
|
|
|
|
|
2026-10-11 03:35:58 -07:00
|
|
|
export class FileLockBusyError extends Error {}
|
|
|
|
|
|
2026-09-14 10:26:11 -07:00
|
|
|
/** 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. */
|
2026-10-11 03:35:58 -07:00
|
|
|
export async function withFileLock<T>(
|
|
|
|
|
filename: string,
|
|
|
|
|
work: () => Promise<T>,
|
|
|
|
|
{ nonblocking = false }: { nonblocking?: boolean } = {},
|
|
|
|
|
): Promise<T> {
|
2026-09-15 15:23:24 -07:00
|
|
|
const child = spawn(
|
|
|
|
|
"flock",
|
|
|
|
|
[
|
|
|
|
|
"--exclusive",
|
2026-10-11 03:35:58 -07:00
|
|
|
...(nonblocking ? ["--nonblock"] : ["--timeout", "120"]),
|
2026-09-15 15:23:24 -07:00
|
|
|
"--conflict-exit-code",
|
|
|
|
|
"75",
|
|
|
|
|
filename,
|
|
|
|
|
process.execPath,
|
|
|
|
|
"-e",
|
|
|
|
|
'process.stdout.write("locked\\n"); process.stdin.resume();',
|
|
|
|
|
],
|
|
|
|
|
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
|
|
|
);
|
2026-09-14 10:26:11 -07:00
|
|
|
let diagnostics = "";
|
2026-09-15 15:23:24 -07:00
|
|
|
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<void>((resolve) => {
|
|
|
|
|
child.once("close", () => resolve());
|
|
|
|
|
});
|
2026-09-14 10:26:11 -07:00
|
|
|
try {
|
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
|
let output = "";
|
|
|
|
|
child.once("error", reject);
|
2026-10-11 03:35:58 -07:00
|
|
|
child.once("exit", (code) => {
|
2026-09-15 15:23:24 -07:00
|
|
|
reject(
|
2026-10-11 03:35:58 -07:00
|
|
|
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}`),
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-09-15 15:23:24 -07:00
|
|
|
child.stdout.on("data", (chunk) => {
|
|
|
|
|
output += chunk;
|
|
|
|
|
if (output.includes("locked\n")) resolve();
|
|
|
|
|
});
|
2026-09-14 10:26:11 -07:00
|
|
|
});
|
|
|
|
|
return await work();
|
2026-09-15 15:23:24 -07:00
|
|
|
} finally {
|
|
|
|
|
child.stdin.end();
|
|
|
|
|
await closed;
|
|
|
|
|
}
|
2026-09-14 10:26:11 -07:00
|
|
|
}
|