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:
2026-07-27 14:51:45 +02:00
commit b7fea83ed6
91 changed files with 15029 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
import { chmod, lstat, mkdir, unlink } from "node:fs/promises";
import path from "node:path";
import { acquireBridgeLock } from "./instance-lock.js";
import { startUnixSocketServer } from "./unix-server.js";
const RUNTIME_DIRECTORY_MODE = 0o700;
export async function createRuntimePaths(
runtimeDir = process.env.XDG_RUNTIME_DIR,
) {
if (!runtimeDir || !path.isAbsolute(runtimeDir)) {
throw new Error("XDG_RUNTIME_DIR must be an absolute path");
}
const directory = path.join(runtimeDir, "pi-status-bridge");
await mkdir(directory, { recursive: true, mode: RUNTIME_DIRECTORY_MODE });
await chmod(directory, RUNTIME_DIRECTORY_MODE);
return {
directory,
socketPath: path.join(directory, "bridge.sock"),
lockPath: path.join(directory, "bridge.lock"),
};
}
async function removeStaleSocket(socketPath) {
try {
const socket = await lstat(socketPath);
if (!socket.isSocket())
throw new Error(`refusing to remove non-socket path: ${socketPath}`);
await unlink(socketPath);
} catch (error) {
if (error?.code !== "ENOENT") throw error;
}
}
export async function startBridgeServer({
handleRequest,
subscribe,
runtimeDir,
processAlive,
maxFrameBytes,
}) {
const paths = await createRuntimePaths(runtimeDir);
const lock = await acquireBridgeLock({
lockPath: paths.lockPath,
socketPath: paths.socketPath,
...(processAlive ? { processAlive } : {}),
});
try {
await removeStaleSocket(paths.socketPath);
const socketServer = await startUnixSocketServer({
socketPath: paths.socketPath,
handleRequest,
...(subscribe ? { subscribe } : {}),
...(maxFrameBytes ? { maxFrameBytes } : {}),
});
return {
...paths,
async close() {
await socketServer.close();
await lock.release();
},
};
} catch (error) {
await lock.release();
throw error;
}
}
export const runtimeDirectoryMode = RUNTIME_DIRECTORY_MODE;