aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-28 20:27:07 +0530
committerAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-08-02 19:12:46 +0530
commita2588bb315a56a8dfcd3aa8a261cdc19b160d079 (patch)
tree2a65cfab8af322d076000026f9b42f98eecff42f
parent330767d862458386e804bde240a1f0d7835b9b8e (diff)
refactor(daemon): strip comment noise
Removes module banners, section dividers, reference lists, and comments restating the code. Keeps one-line notes for the Windows named-pipe endpoint, the Windows unlink no-op, the do-not-unlink-on-rejection rule, the `.cmd` shell requirement, shell-quoting of spaced paths, and the in-place client promotion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-rw-r--r--src/daemon/bridge.ts59
-rw-r--r--src/daemon/index.ts23
-rw-r--r--src/daemon/protocol.ts58
-rw-r--r--src/daemon/server.ts58
-rw-r--r--src/daemon/spawn.ts61
5 files changed, 13 insertions, 246 deletions
diff --git a/src/daemon/bridge.ts b/src/daemon/bridge.ts
index 3f8b604..bc162a7 100644
--- a/src/daemon/bridge.ts
+++ b/src/daemon/bridge.ts
@@ -1,14 +1,3 @@
-/**
- * CdpBridge — persistent WebSocket to Chrome, ID multiplexing, event routing.
- *
- * A single long-lived process connection to Chrome that proxies CDP traffic
- * for multiple pi clients connected via the IpcServer. One Allow dialog ever.
- *
- * References:
- * - CDP message format: https://github.com/aslushnikov/getting-started-with-cdp#protocol-fundamentals
- * - CDP sessions + IDs: https://github.com/aslushnikov/getting-started-with-cdp#targets--sessions
- * - Target domain: https://chromedevtools.github.io/devtools-protocol/tot/Target/
- */
import WebSocket from "ws";
import { discoverWsUrl } from "../cdp/discovery";
@@ -18,7 +7,6 @@ import type { WireRequest, WireResponse, WireEvent } from "./protocol";
import { CDP_CONNECT_TIMEOUT_MS, CDP_COMMAND_TIMEOUT_MS } from "./protocol";
import { asString, isRecord } from "../util/guards";
-// ── Types ──────────────────────────────────────────────────────────────────────
export type SendToClient = (clientId: string, msg: WireResponse | WireEvent) => void;
@@ -27,12 +15,9 @@ export type EventHandler = (event: WireEvent, targetClientIds: string[]) => void
export type CloseHandler = () => void;
export type CdpBridge = {
- /** Start the bridge. Resolves when the daemon IPC + Chrome discovery loop
- * are initialized (not when Chrome is connected — that may take retries). */
start(): Promise<void>;
stop(): Promise<void>;
handleRequest(req: WireRequest, clientId: string, send: SendToClient): Promise<void>;
- /** True when the Chrome WebSocket is open and ready. */
isAlive(): boolean;
recordSession(clientId: string, sessionId: string): void;
releaseSession(sessionId: string): void;
@@ -42,7 +27,6 @@ export type CdpBridge = {
onClose(handler: CloseHandler): void;
};
-// ── ID Multiplexer ────────────────────────────────────────────────────────────
interface PendingEntry {
clientId: string;
@@ -79,11 +63,10 @@ class IdMultiplexer {
}
}
-// ── Event Router ───────────────────────────────────────────────────────────────
class EventRouter {
- private owners = new Map<string, string>(); // sessionId → clientId
- private clientSessions = new Map<string, Set<string>>(); // clientId → sessions
+ private owners = new Map<string, string>();
+ private clientSessions = new Map<string, Set<string>>();
record(clientId: string, sessionId: string): void {
const prev = this.owners.get(sessionId);
@@ -112,11 +95,6 @@ class EventRouter {
this.clientSessions.delete(clientId);
}
- /**
- * Route a CDP event to client(s).
- * Session-scoped → only the owning client.
- * Browser-level (no sessionId) → all connected clients.
- */
route(event: WireEvent): string[] {
if (event.sessionId) {
const owner = this.owners.get(event.sessionId);
@@ -130,7 +108,6 @@ class EventRouter {
}
}
-// ── Implementation ─────────────────────────────────────────────────────────────
export const createCdpBridge = (): CdpBridge => {
let ws: WebSocket | null = null;
@@ -140,7 +117,6 @@ export const createCdpBridge = (): CdpBridge => {
let eventHandler: EventHandler | null = null;
let closeHandler: CloseHandler | null = null;
- // daemonId → { send, timer, isAttach }
const callbacks = new Map<number, {
send: SendToClient;
clientId: string;
@@ -149,7 +125,6 @@ export const createCdpBridge = (): CdpBridge => {
isAttach: boolean;
}>();
- // ── Handle raw CDP messages from Chrome ─────────────────────────────────
const onChromeMessage = (raw: string): void => {
let parsed: unknown;
@@ -157,12 +132,9 @@ export const createCdpBridge = (): CdpBridge => {
if (!isCdpRawMessage(parsed)) return;
const msg: CdpRawMessage = parsed;
- // ---- Response (has id) ----
if (msg.id !== undefined) {
const cb = callbacks.get(msg.id);
if (!cb) {
- // Stale response — client already timed out or disconnected.
- // Still clean up the multiplexer mapping.
mux.resolve(msg.id);
return;
}
@@ -178,7 +150,6 @@ export const createCdpBridge = (): CdpBridge => {
error: { code: msg.error.code ?? -1, message: msg.error.message },
});
} else {
- // Track session ownership for attach responses
if (cb.isAttach && msg.result) {
const sessionId = isRecord(msg.result) ? asString(msg.result["sessionId"]) : undefined;
if (sessionId) {
@@ -192,10 +163,8 @@ export const createCdpBridge = (): CdpBridge => {
return;
}
- // ---- Event (no id, has method) ----
if (!msg.method) return;
- // Inspector.detached — release session ownership
if (msg.method === "Inspector.detached" && msg.sessionId) {
router.release(msg.sessionId);
}
@@ -207,7 +176,6 @@ export const createCdpBridge = (): CdpBridge => {
...(msg.sessionId !== undefined ? { sessionId: msg.sessionId } : {}),
};
- // Route through the event handler for the IpcServer to broadcast
if (eventHandler) {
const targets = router.route(wireEvent);
if (targets.length > 0) {
@@ -216,29 +184,19 @@ export const createCdpBridge = (): CdpBridge => {
}
};
- // ── Connect to Chrome ───────────────────────────────────────────────────
- // ── Auto-reconnecting Chrome connection ───────────────────────────────
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let reconnectAttempt = 0;
let stopped = false;
- /**
- * Try to discover Chrome and open a WebSocket. On failure, schedule
- * an exponential backoff retry (capped at 60s). When Chrome finally
- * becomes available, the bridge connects automatically — no manual
- * daemon restart needed.
- */
const tryConnect = (): void => {
if (stopped) return;
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
const attempt = async (): Promise<void> => {
- // Already connected — nothing to do.
if (ws && ws.readyState === WebSocket.OPEN) return;
- // Resolve URL (discover if not cached, or clear cache on retry)
const cachedUrl = wsUrl;
let url: string;
if (cachedUrl === null || cachedUrl === "" || reconnectAttempt > 0) {
@@ -275,7 +233,6 @@ export const createCdpBridge = (): CdpBridge => {
clearTimeout(timer);
ws = sock;
reconnectAttempt = 0;
- // Enable target discovery so we see page events
ws.send(JSON.stringify({ id: 0, method: "Target.setDiscoverTargets", params: { discover: true } }));
console.log("[pi-browser-daemon] Connected to Chrome ✓");
settleOnce();
@@ -293,7 +250,6 @@ export const createCdpBridge = (): CdpBridge => {
sock.on("close", () => {
clearTimeout(timer);
ws = null;
- // Reject all pending callbacks
for (const [daemonId, cb] of callbacks) {
clearTimeout(cb.timer);
cb.send(cb.clientId, {
@@ -305,13 +261,11 @@ export const createCdpBridge = (): CdpBridge => {
mux.resolve(daemonId);
}
closeHandler?.();
- // Auto-reconnect on unexpected close
scheduleRetry();
});
});
await settledPromise;
- // If connection didn't succeed, schedule a retry
if (!ws || ws.readyState !== WebSocket.OPEN) {
scheduleRetry();
}
@@ -320,20 +274,16 @@ export const createCdpBridge = (): CdpBridge => {
attempt().catch(() => scheduleRetry());
};
- /** Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 60s (capped). */
const scheduleRetry = (): void => {
if (stopped) return;
const delay = Math.min(1000 * Math.pow(2, Math.min(reconnectAttempt, 6)), 60_000);
reconnectAttempt++;
- wsUrl = null; // force re-discovery on next attempt
+ wsUrl = null;
reconnectTimer = setTimeout(tryConnect, delay);
};
const start = async (): Promise<void> => {
stopped = false;
- // Don't await — the retry loop runs in the background.
- // The daemon is "started" when the IPC server is listening, not when
- // Chrome is connected.
tryConnect();
};
@@ -351,8 +301,6 @@ export const createCdpBridge = (): CdpBridge => {
clientId: string,
send: SendToClient,
): Promise<void> => {
- // ponytail: wait for Chrome to connect (user may still be clicking "Allow").
- // Bridge retries in background; this polls so the client doesn't fail first try.
if (!ws || ws.readyState !== WebSocket.OPEN) {
const deadline = Date.now() + 15_000;
while (Date.now() < deadline) {
@@ -372,7 +320,6 @@ export const createCdpBridge = (): CdpBridge => {
const daemonId = mux.allocate(clientId, req.id);
- // Store callback for the response
const timer = setTimeout(() => {
callbacks.delete(daemonId);
mux.resolve(daemonId);
diff --git a/src/daemon/index.ts b/src/daemon/index.ts
index d2c43a8..30a6b6e 100644
--- a/src/daemon/index.ts
+++ b/src/daemon/index.ts
@@ -1,12 +1,3 @@
-/**
- * Browser daemon entry point — standalone long-lived process.
- *
- * Owns a single persistent WebSocket to Chrome. Exposes a Unix domain socket
- * for pi instances to connect through. One Allow dialog ever (per Chrome
- * lifetime), zero repeated prompts.
- *
- * Run: npx tsx src/daemon/index.ts
- */
import { createIpcServer } from "./server";
import { createCdpBridge, type SendToClient } from "./bridge";
@@ -18,9 +9,7 @@ async function main() {
const ipcServer = createIpcServer();
const cdpBridge = createCdpBridge();
- // ── Wire IpcServer → CdpBridge ─────────────────────────────────────────
- // When a pi client sends a CDP request, proxy it through the bridge to Chrome.
ipcServer.onMessage((msg, client) => {
if (msg.type !== "request") return;
@@ -31,19 +20,16 @@ async function main() {
cdpBridge.handleRequest(msg, client.id, send);
});
- // When Chrome sends an event, route it to the correct pi client(s).
cdpBridge.onEvent((event, targetClientIds) => {
for (const cid of targetClientIds) {
ipcServer.send(cid, event);
}
});
- // When a client disconnects, clean up its CDP state.
ipcServer.onDisconnect((client) => {
cdpBridge.removeClient(client.id);
});
- // When Chrome disconnects, notify all clients.
cdpBridge.onClose(() => {
console.log("[pi-browser-daemon] Chrome disconnected");
ipcServer.broadcast({
@@ -53,11 +39,7 @@ async function main() {
});
});
- // ── Idle timeout ───────────────────────────────────────────────────────
- // The daemon exits after DAEMON_IDLE_TIMEOUT_MS with zero connected clients.
- // This prevents zombie processes when pi sessions end and never reconnect.
- // The timeout resets whenever a client connects.
let idleTimer: ReturnType<typeof setTimeout> | null = null;
const resetIdleTimer = (): void => {
@@ -78,7 +60,6 @@ async function main() {
ipcServer.onConnect(() => { cancelIdleTimer(); });
ipcServer.onDisconnect(() => { resetIdleTimer(); });
- // ── Start ──────────────────────────────────────────────────────────────
try {
await ipcServer.start();
@@ -88,14 +69,10 @@ async function main() {
process.exit(1);
}
- // Connect to Chrome (auto-retries with backoff in the background).
- // The daemon is ready immediately — Chrome may connect later.
await cdpBridge.start();
- // Start the idle timer (no clients yet at startup).
resetIdleTimer();
- // ── Graceful shutdown ──────────────────────────────────────────────────
const shutdown = async () => {
console.log("[pi-browser-daemon] Shutting down...");
diff --git a/src/daemon/protocol.ts b/src/daemon/protocol.ts
index 997a6c8..d3d8297 100644
--- a/src/daemon/protocol.ts
+++ b/src/daemon/protocol.ts
@@ -1,34 +1,17 @@
-/**
- * Unix socket daemon protocol — wire message types, type guards, and constants.
- *
- * The daemon speaks JSON-line protocol over a Unix domain socket.
- * One JSON object per newline. Four message types: request, response, event, control.
- *
- * References:
- * - CDP message format: https://github.com/aslushnikov/getting-started-with-cdp#protocol-fundamentals
- * - net.createServer IPC: https://nodejs.org/docs/latest-v22.x/api/net.html#serverlistenpath-backlog-callback
- * - readline line-delimited: https://nodejs.org/docs/latest-v22.x/api/readline.html#event-line
- */
import { isRecord } from "../util/guards";
-// ── Message types ──────────────────────────────────────────────────────────────
-/** A CDP command sent from a pi client to the daemon. */
export type WireRequest = {
readonly type: "request";
- /** Client-local CDP id. The daemon remaps this to a globally-unique id for Chrome. */
readonly id: number;
readonly method: string;
readonly params?: Record<string, unknown>;
- /** Optional CDP session id. Omitted for browser-level commands. */
readonly sessionId?: string;
};
-/** A CDP response (result or error) sent from the daemon back to the pi client. */
export type WireResponse = {
readonly type: "response";
- /** Matches the id from the corresponding WireRequest. */
readonly id: number;
readonly result?: unknown;
readonly error?: {
@@ -38,34 +21,23 @@ export type WireResponse = {
};
};
-/** A CDP event forwarded from Chrome through the daemon to pi client(s). */
export type WireEvent = {
readonly type: "event";
readonly method: string;
readonly params?: Record<string, unknown>;
- /** Session-scoped events carry this; browser-level events omit it. */
readonly sessionId?: string;
};
-/** Daemon lifecycle control messages. Bidirectional. */
export type WireControl = {
readonly type: "control";
readonly action: "register" | "registered" | "deregister" | "shutdown";
- /** pi client namespace (required for register/registered/deregister). */
readonly clientId?: string;
- /** Human-readable reason for shutdown. */
readonly reason?: string;
};
-/** All messages that may appear on the Unix socket. */
export type WireMessage = WireRequest | WireResponse | WireEvent | WireControl;
-// ── Type guard ─────────────────────────────────────────────────────────────────
-/**
- * Validates that an unknown parsed-JSON value is a well-formed WireMessage.
- * Does NOT validate the full CDP semantics — only structural shape.
- */
export const isWireMessage = (v: unknown): v is WireMessage => {
if (!isRecord(v)) return false;
const t = v["type"];
@@ -113,12 +85,9 @@ export const isWireMessage = (v: unknown): v is WireMessage => {
}
};
-// ── Wire message constructors ──────────────────────────────────────────────────
-/** Serialize a WireMessage to a JSON line (no trailing newline — caller adds \\n). */
export const serialize = (msg: WireMessage): string => JSON.stringify(msg);
-/** Deserialize a single JSON-line string to a WireMessage. Returns null on parse failure. */
export const deserialize = (line: string): WireMessage | null => {
let parsed: unknown;
try {
@@ -130,42 +99,19 @@ export const deserialize = (line: string): WireMessage | null => {
return parsed;
};
-// ── Constants ──────────────────────────────────────────────────────────────────
-/**
- * IPC endpoint for the daemon.
- *
- * On POSIX platforms this is a Unix domain socket path. On Windows, Node's IPC
- * requires a named pipe of the form `\\.\pipe\<name>` — a Unix path like
- * `/tmp/…` is not a valid `net` listen/connect target there and causes setup
- * to fail. See https://nodejs.org/api/net.html#ipc-support.
- */
+// Windows requires a named pipe (`\\.\pipe\<name>`); a Unix path is not a valid `net` listen/connect target there.
export const DAEMON_SOCKET_PATH =
process.platform === "win32"
? "\\\\.\\pipe\\pi-browser-daemon"
: "/tmp/pi-browser-daemon.sock";
-/**
- * How long the daemon stays alive with zero connected clients before exiting
- * (milliseconds). After this period, the daemon cleans up and shuts down.
- */
-export const DAEMON_IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
+export const DAEMON_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
-/**
- * Hard cap on concurrent pi client connections. Extra connections are rejected
- * immediately to prevent resource exhaustion.
- */
export const DAEMON_MAX_CLIENTS = 16;
-/**
- * If true, the daemon unlinks (deletes) any stale socket file from a previous
- * daemon crash before calling server.listen(). Without this, a crashed daemon
- * leaves a socket inode that blocks the next listen() call with EADDRINUSE.
- */
export const DAEMON_STALE_SOCKET_CLEANUP = true;
-/** CDP WebSocket connection timeout (ms). */
export const CDP_CONNECT_TIMEOUT_MS = 10_000;
-/** CDP command timeout when proxying through daemon (ms). */
export const CDP_COMMAND_TIMEOUT_MS = 10_000;
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) {
diff --git a/src/daemon/spawn.ts b/src/daemon/spawn.ts
index 63c0dab..d14d234 100644
--- a/src/daemon/spawn.ts
+++ b/src/daemon/spawn.ts
@@ -1,13 +1,3 @@
-/**
- * Daemon lifecycle helpers — spawn detection and lazy launch.
- *
- * The daemon is a standalone long-lived process that owns the single WebSocket
- * to Chrome. pi instances connect to it via a Unix domain socket. This module
- * provides the plumbing to detect a running daemon and spawn one if needed.
- *
- * Reference:
- * - child_process.spawn detached: https://nodejs.org/docs/latest-v22.x/api/child_process.html#optionsdetached
- */
import { spawn, type ChildProcess } from "node:child_process";
import { access, unlink } from "node:fs/promises";
@@ -21,12 +11,7 @@ const moduleDir = dirname(fileURLToPath(import.meta.url));
const isWindows = process.platform === "win32";
-/**
- * Remove a stale POSIX socket file. On Windows the daemon endpoint is a named
- * pipe, which is not a filesystem entry and is torn down automatically when the
- * owning process exits — so there is nothing to unlink, and calling unlink on a
- * pipe path throws. This is a no-op on Windows.
- */
+// No-op on Windows: a named pipe is not a filesystem entry and unlinking a pipe path throws.
const cleanupStaleSocket = (): void => {
if (isWindows) return;
unlink(DAEMON_SOCKET_PATH).catch(() => {});
@@ -45,12 +30,6 @@ export const daemonSocketMayExist = async (
}
};
-/**
- * Check whether the daemon is running by probing its Unix socket.
- * A file-existence check is unreliable — a crashed daemon leaves a stale socket.
- * We connect, send a register, wait for the registered ack, then disconnect.
- * On failure, clean up the stale socket so ensureDaemon can spawn a fresh one.
- */
export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => {
if (!(await daemonSocketMayExist())) return false;
@@ -65,7 +44,6 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => {
const sock = createConnection(DAEMON_SOCKET_PATH);
const timer = setTimeout(() => {
sock.destroy();
- // ponytail: stale socket cleanup; ENOENT = already gone, fine
cleanupStaleSocket();
done(false);
}, timeoutMs);
@@ -79,7 +57,6 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => {
const text = chunk.toString();
if (text.includes('"registered"')) {
clearTimeout(timer);
- // Send deregister so the probe doesn't hog a slot
const deregMsg: WireControl = { type: "control", action: "deregister", clientId: "_liveness_probe" };
try { sock.write(serialize(deregMsg) + "\n"); } catch {}
sock.destroy();
@@ -90,13 +67,11 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => {
sock.on("error", () => {
clearTimeout(timer);
sock.destroy();
- // ponytail: connection refused = daemon dead, clean up stale socket
cleanupStaleSocket();
done(false);
});
- // close without 'registered' ack = daemon alive but rejected us (e.g. max clients);
- // don't unlink — the daemon is still using that socket.
+ // Closed without a 'registered' ack: the daemon is alive but rejected us, so do NOT unlink its socket.
sock.on("close", () => {
clearTimeout(timer);
done(false);
@@ -104,45 +79,24 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => {
});
};
-/**
- * Spawn the daemon as a detached child process. The daemon will continue
- * running after the parent pi process exits.
- *
- * The daemon entry point is resolved relative to this module. When running
- * from source (tsx), the entry is src/daemon/index.ts. When running compiled,
- * it's dist/daemon/index.js.
- *
- * Returns the child process handle (caller should call child.unref()).
- */
export const spawnDaemon = (): ChildProcess | null => {
- // Resolve the daemon entry point and tsx binary.
const daemonScript = join(moduleDir, "index.ts");
- // On Windows, npm installs binaries as `.cmd` shims, not bare executables,
- // and the npx fallback is `npx.cmd`. A `.cmd` file can only be launched
- // through a shell (spawning it directly yields EINVAL), so set shell: true
- // whenever we invoke one.
+ // Windows npm binaries are `.cmd` shims, which only launch through a shell — spawning one directly yields EINVAL.
const tsxBinBase = join(moduleDir, "..", "..", "node_modules", ".bin", "tsx");
const tsxBin = isWindows ? `${tsxBinBase}.cmd` : tsxBinBase;
const hasLocalTsx = existsSync(tsxBin);
- // Use the local tsx binary to run the daemon TypeScript source directly.
- // The spawned process inherits the parent's env so module resolution works.
- // Fall back to `npx tsx` if the local binary isn't available.
const cmd = hasLocalTsx ? tsxBin : isWindows ? "npx.cmd" : "npx";
const args = hasLocalTsx ? [daemonScript] : ["tsx", daemonScript];
- // With shell: true, Node passes the command line to the shell verbatim
- // without quoting, so any path containing spaces (e.g. C:\Users\Some Name)
- // would be split into multiple tokens. Wrap each token in double quotes on
- // Windows to keep space-containing paths intact.
+ // With shell: true Node passes the command line verbatim, so a path containing spaces must be quoted or it splits into tokens.
const quote = (s: string): string => (isWindows ? `"${s}"` : s);
const child = spawn(quote(cmd), args.map(quote), {
detached: true,
stdio: "ignore",
env: { ...process.env },
- // `.cmd` shims (local tsx or npx.cmd on Windows) must run via a shell.
shell: isWindows,
windowsVerbatimArguments: isWindows,
});
@@ -151,19 +105,12 @@ export const spawnDaemon = (): ChildProcess | null => {
return child;
};
-/**
- * Ensure the daemon is running. If not, spawn it and wait (up to timeoutMs)
- * for the socket file to appear.
- *
- * Returns true if the daemon is running (or was successfully spawned).
- */
export const ensureDaemon = async (timeoutMs = 10_000): Promise<boolean> => {
if (await isDaemonRunning()) return true;
const child = spawnDaemon();
if (!child) return false;
- // Poll for the socket file to appear
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await isDaemonRunning()) return true;