Files
pi-gui/src/bridge/instance-lock.js
T
alex b7fea83ed6 feat(bridge): establish local pi status bridge
Deliver the initial local bridge, Noctalia v4/v5 adapters, desktop client, service unit, tests, and implementation documentation for persistent Pi status and control.
2026-07-27 14:51:45 +02:00

127 lines
3.1 KiB
JavaScript

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;