import { randomUUID } from "node:crypto"; import { chmod, open, readFile, unlink } from "node:fs/promises"; const LOCK_MODE = 0o600; export class BridgeAlreadyRunningError extends Error { constructor(metadata) { super(`bridge instance is already running with pid ${metadata.pid}`); this.name = "BridgeAlreadyRunningError"; this.metadata = metadata; } } export class BridgeLockError extends Error { constructor(message) { super(message); this.name = "BridgeLockError"; } } function parseLockMetadata(text, lockPath) { let metadata; try { metadata = JSON.parse(text); } catch { throw new BridgeLockError(`lock file is unreadable: ${lockPath}`); } if ( metadata === null || typeof metadata !== "object" || !Number.isSafeInteger(metadata.pid) || metadata.pid < 1 || typeof metadata.token !== "string" || metadata.token.length < 1 || typeof metadata.socketPath !== "string" || metadata.socketPath.length < 1 || typeof metadata.startedAt !== "string" || Number.isNaN(Date.parse(metadata.startedAt)) ) { throw new BridgeLockError(`lock file has an invalid format: ${lockPath}`); } return metadata; } export function isProcessAlive(pid) { try { process.kill(pid, 0); return true; } catch (error) { if (error?.code === "ESRCH") return false; if (error?.code === "EPERM") return true; throw error; } } async function readLockMetadata(lockPath) { try { return parseLockMetadata(await readFile(lockPath, "utf8"), lockPath); } catch (error) { if (error?.code === "ENOENT") return undefined; throw error; } } export async function acquireBridgeLock({ lockPath, socketPath, pid = process.pid, now = () => new Date(), processAlive = isProcessAlive, }) { if (!lockPath || !socketPath) throw new TypeError("lockPath and socketPath are required"); const metadata = { pid, socketPath, startedAt: now().toISOString(), token: randomUUID(), }; for (let attempt = 0; attempt < 2; attempt += 1) { let handle; let createdLock = false; try { handle = await open(lockPath, "wx", LOCK_MODE); createdLock = true; await handle.writeFile(`${JSON.stringify(metadata)}\n`, "utf8"); await handle.sync(); await chmod(lockPath, LOCK_MODE); await handle.close(); return { metadata, async release() { const current = await readLockMetadata(lockPath); if (current?.token !== metadata.token) return false; await unlink(lockPath); return true; }, }; } catch (error) { await handle?.close().catch(() => {}); if (createdLock) { await unlink(lockPath).catch(() => {}); throw error; } if (error?.code !== "EEXIST") throw error; const existing = await readLockMetadata(lockPath); if (!existing) continue; if (processAlive(existing.pid)) throw new BridgeAlreadyRunningError(existing); // Only a lock with a readable PID that has exited may be reclaimed. await unlink(lockPath).catch((unlinkError) => { if (unlinkError?.code !== "ENOENT") throw unlinkError; }); } } throw new BridgeLockError( `could not acquire lock after stale-owner recovery: ${lockPath}`, ); } export const lockMode = LOCK_MODE;