aboutsummaryrefslogtreecommitdiff
path: root/src/daemon/server.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/daemon/server.ts')
-rw-r--r--src/daemon/server.ts58
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) {