aboutsummaryrefslogtreecommitdiff
path: root/src/daemon/index.ts
blob: d2c43a8b8259fb8980c4d3c43efe41b44de9e1d0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
 * 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";
import { DAEMON_IDLE_TIMEOUT_MS } from "./protocol";

async function main() {
  console.log("[pi-browser-daemon] Starting...");

  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;

    const send: SendToClient = (cid, resp) => {
      ipcServer.send(cid, resp);
    };

    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({
      type: "control",
      action: "shutdown",
      reason: "chrome_disconnected",
    });
  });

  // ── 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 => {
    if (idleTimer) clearTimeout(idleTimer);
    if (ipcServer.clientCount() === 0) {
      idleTimer = setTimeout(() => {
        console.log("[pi-browser-daemon] Idle timeout — no clients for " +
          `${DAEMON_IDLE_TIMEOUT_MS / 60000} minutes. Shutting down.`);
        shutdown();
      }, DAEMON_IDLE_TIMEOUT_MS);
    }
  };

  const cancelIdleTimer = (): void => {
    if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
  };

  ipcServer.onConnect(() => { cancelIdleTimer(); });
  ipcServer.onDisconnect(() => { resetIdleTimer(); });

  // ── Start ──────────────────────────────────────────────────────────────

  try {
    await ipcServer.start();
    console.log("[pi-browser-daemon] IPC server listening");
  } catch (e) {
    console.error("[pi-browser-daemon] Failed to start IPC server:", e);
    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...");
    await cdpBridge.stop();
    await ipcServer.stop();
    process.exit(0);
  };

  process.on("SIGINT", shutdown);
  process.on("SIGTERM", shutdown);
}

main().catch((e) => {
  console.error("[pi-browser-daemon] Fatal:", e);
  process.exit(1);
});