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.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user