diff options
Diffstat (limited to 'src/daemon/server.ts')
| -rw-r--r-- | src/daemon/server.ts | 58 |
1 files changed, 4 insertions, 54 deletions
diff --git a/src/daemon/server.ts b/src/daemon/server.ts index fea9bb7..b694fc0 100644 --- a/src/daemon/server.ts +++ b/src/daemon/server.ts @@ -1,15 +1,3 @@ -/** - * IpcServer — Unix socket server for daemon ↔ pi client communication. - * - * Accepts multiple concurrent pi client connections over a Unix domain - * stream socket. Uses newline-delimited JSON framing (one WireMessage per - * line). Clients are anonymous until they send a control.register message. - * - * References: - * - net.createServer: https://nodejs.org/docs/latest-v22.x/api/net.html#netcreateserveroptions-connectionlistener - * - server.listen(path): https://nodejs.org/docs/latest-v22.x/api/net.html#serverlistenpath-backlog-callback - * - readline.createInterface: https://nodejs.org/docs/latest-v22.x/api/readline.html#readlinecreateinterfaceoptions - */ import { createServer, type Server, type Socket } from "node:net"; import { createInterface, type Interface } from "node:readline"; @@ -23,54 +11,35 @@ import { DAEMON_STALE_SOCKET_CLEANUP, } from "./protocol"; -// ── Types ────────────────────────────────────────────────────────────────────── -/** A connected pi client with its socket and readline interface. */ export type ClientSocket = { - /** Client namespace (set after registration). */ id: string; readonly socket: Socket; readonly rl: Interface; - /** True once a valid control.register message has been received. */ registered: boolean; }; -/** Handler called for every valid WireMessage from any client. */ export type MessageHandler = (msg: WireMessage, client: ClientSocket) => void; -/** Handler called when a client connects or disconnects. */ export type ConnectionHandler = (client: ClientSocket) => void; export type IpcServer = { - /** Start listening on the Unix socket. Resolves when the server is ready. */ start(): Promise<void>; - /** Close the server and all client connections. Resolves when cleaned up. */ stop(): Promise<void>; - /** Register a handler for incoming WireMessages from any client. */ onMessage(handler: MessageHandler): void; - /** Register a handler for client connect events. */ onConnect(handler: ConnectionHandler): void; - /** Register a handler for client disconnect events. */ onDisconnect(handler: ConnectionHandler): void; - /** Send a WireMessage to a specific client by clientId. No-op if client not found. */ send(clientId: string, msg: WireMessage): void; - /** Send a WireMessage to all connected (registered) clients. */ broadcast(msg: WireMessage): void; - /** Force-disconnect a client by clientId. */ disconnectClient(clientId: string): void; - /** All currently connected clients (by clientId). */ clients(): ReadonlyMap<string, ClientSocket>; - /** Number of connected clients. */ clientCount(): number; }; -// ── Implementation ───────────────────────────────────────────────────────────── export const createIpcServer = (): IpcServer => { let server: Server | null = null; - // Anonymous temp-id → ClientSocket. Temp ids are assigned before registration. const sockets = new Map<string, ClientSocket>(); - // clientId → ClientSocket (only registered clients). const clients = new Map<string, ClientSocket>(); let messageHandler: MessageHandler | null = null; let connectHandler: ConnectionHandler | null = null; @@ -79,17 +48,14 @@ export const createIpcServer = (): IpcServer => { const start = (): Promise<void> => new Promise((resolve, reject) => { - // Clean up stale socket file from a previous crashed daemon. if (DAEMON_STALE_SOCKET_CLEANUP) { try { unlinkSync(DAEMON_SOCKET_PATH); } catch { - // ENOENT is expected — no stale socket to clean. } } server = createServer({ pauseOnConnect: false }, (socket: Socket) => { - // Reject if at capacity (check registered clients only). if (clients.size >= DAEMON_MAX_CLIENTS) { socket.destroy(); return; @@ -101,8 +67,6 @@ export const createIpcServer = (): IpcServer => { sockets.set(tempId, currentClient); - // Buffer for partial lines. readline handles line splitting, but - // we store a reference in case we need it for diagnostics. let lineCount = 0; rl.on("line", (line: string) => { @@ -113,16 +77,13 @@ export const createIpcServer = (): IpcServer => { if (!isWireMessage(parsed)) return; msg = parsed; } catch { - // Malformed JSON — ignore silently, don't crash the daemon. return; } - // Handle registration before forwarding to the general handler. if (msg.type === "control" && msg.action === "register" && !currentClient.registered) { const clientId = msg.clientId; if (!clientId) return; - // If a client with this id is already connected, reject. if (clients.has(clientId)) { const reply: WireControl = { type: "control", @@ -134,14 +95,12 @@ export const createIpcServer = (): IpcServer => { return; } - // Promote from anonymous to registered — mutate in place so the - // closure captures the updated client reference. + // Mutate in place so the already-captured closure sees the promoted client. sockets.delete(tempId); currentClient.id = clientId; currentClient.registered = true; clients.set(clientId, currentClient); - // Acknowledge registration. const ack: WireControl = { type: "control", action: "registered", clientId }; socket.write(JSON.stringify(ack) + "\n"); @@ -149,29 +108,23 @@ export const createIpcServer = (): IpcServer => { return; } - // Require registration for non-control messages. if (!currentClient.registered) return; - // Forward to the general handler. messageHandler?.(msg, currentClient); }); socket.on("error", () => { - // Socket errors (ECONNRESET, etc.) are handled by the 'close' event. }); socket.on("close", () => { - // Clean up whichever map the client is in. const found = clients.get(currentClient.id) ?? sockets.get(tempId); if (found) { clients.delete(found.id); sockets.delete(tempId); - // Notify if this was a registered client. if (found.registered) { disconnectHandler?.(found); } } - // Clean up readline to prevent memory leaks. rl.close(); }); }); @@ -187,20 +140,18 @@ export const createIpcServer = (): IpcServer => { const stop = (): Promise<void> => new Promise((resolve) => { - // Close all client sockets. for (const [, client] of clients) { - try { client.socket.destroy(); } catch { /* best-effort */ } + try { client.socket.destroy(); } catch {} } for (const [, client] of sockets) { - try { client.socket.destroy(); } catch { /* best-effort */ } + try { client.socket.destroy(); } catch {} } clients.clear(); sockets.clear(); if (server) { server.close(() => { - // Clean up the socket file. - try { unlinkSync(DAEMON_SOCKET_PATH); } catch { /* ENOENT ok */ } + try { unlinkSync(DAEMON_SOCKET_PATH); } catch {} resolve(); }); } else { @@ -226,7 +177,6 @@ export const createIpcServer = (): IpcServer => { try { client.socket.write(JSON.stringify(msg) + "\n"); } catch { - // Socket write error — the close handler will clean up. } }, broadcast(msg) { |
