Protect active development Graphs from rejected launcher cleanup

This commit is contained in:
Timothy J. Aveni
2026-10-11 03:42:32 -07:00
parent 2139f138ca
commit fa698dd306
+18 -11
View File
@@ -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<T>(filename: string, work: () => Promise<T>): Promise<T> {
export async function withFileLock<T>(
filename: string,
work: () => Promise<T>,
{ nonblocking = false }: { nonblocking?: boolean } = {},
): Promise<T> {
const child = spawn(
"flock",
[
"--exclusive",
"--timeout",
"120",
...(nonblocking ? ["--nonblock"] : ["--timeout", "120"]),
"--conflict-exit-code",
"75",
filename,
@@ -32,15 +37,17 @@ export async function withFileLock<T>(filename: string, work: () => Promise<T>):
await new Promise<void>((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();