aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-06 23:15:08 +0530
committeramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-06 23:15:08 +0530
commit2ba584869909363705b9f1b7232cf2bd89ba9cff (patch)
tree7d29ac75c9c32e8cb0bfc62aff2cf844c2988654
parent26daf87896d6645fe9be71460d2868755a4f939f (diff)
fix: stale socket detection prevents daemon spawn after crash
isDaemonRunning() only checked file existence of the Unix socket. A dead daemon leaves a stale socket, causing ensureDaemon() to skip spawning and client.start() to fail with 'Chrome not connected'. Replace with a liveness probe: connect, register, verify ack, then disconnect. On failure, clean up the stale socket. Also makes bridge handleRequest async to poll for Chrome connection for up to 15s before rejecting.
-rw-r--r--package.json2
-rw-r--r--src/daemon/bridge.ts16
-rw-r--r--src/daemon/spawn.ts64
3 files changed, 72 insertions, 10 deletions
diff --git a/package.json b/package.json
index ad081da..c82d38b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pi-browser-harness",
- "version": "0.8.1",
+ "version": "0.8.2",
"description": "Browser control extension for pi — navigate, click, type, screenshot, and extract data from real Chrome via CDP",
"keywords": [
"pi-package",
diff --git a/src/daemon/bridge.ts b/src/daemon/bridge.ts
index c2c1355..33ef63b 100644
--- a/src/daemon/bridge.ts
+++ b/src/daemon/bridge.ts
@@ -29,7 +29,7 @@ export type CdpBridge = {
* are initialized (not when Chrome is connected — that may take retries). */
start(): Promise<void>;
stop(): Promise<void>;
- handleRequest(req: WireRequest, clientId: string, send: SendToClient): 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;
@@ -339,11 +339,21 @@ export const createCdpBridge = (): CdpBridge => {
wsUrl = null;
};
- const handleRequest = (
+ const handleRequest = async (
req: WireRequest,
clientId: string,
send: SendToClient,
- ): void => {
+ ): 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) {
+ await new Promise((r) => setTimeout(r, 250));
+ if (ws && ws.readyState === WebSocket.OPEN) break;
+ }
+ }
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
send(clientId, {
type: "response",
diff --git a/src/daemon/spawn.ts b/src/daemon/spawn.ts
index e9f4d51..0a2670c 100644
--- a/src/daemon/spawn.ts
+++ b/src/daemon/spawn.ts
@@ -10,25 +10,77 @@
*/
import { spawn, type ChildProcess } from "node:child_process";
-import { access } from "node:fs/promises";
+import { access, unlink } from "node:fs/promises";
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
-import { DAEMON_SOCKET_PATH } from "./protocol";
+import { createConnection } from "node:net";
+import { DAEMON_SOCKET_PATH, serialize, type WireControl } from "./protocol";
const moduleDir = dirname(fileURLToPath(import.meta.url));
/**
- * Check whether the daemon is running by testing if its Unix socket exists.
- * The socket file is created by the daemon on startup and removed on shutdown.
+ * 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 (): Promise<boolean> => {
+export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => {
+ // If the socket file doesn't exist, the daemon definitely isn't running.
try {
await access(DAEMON_SOCKET_PATH);
- return true;
} catch {
return false;
}
+
+ return new Promise((resolve) => {
+ let settled = false;
+ const done = (alive: boolean): void => {
+ if (settled) return;
+ settled = true;
+ resolve(alive);
+ };
+
+ const sock = createConnection(DAEMON_SOCKET_PATH);
+ const timer = setTimeout(() => {
+ sock.destroy();
+ // ponytail: stale socket cleanup; ENOENT = already gone, fine
+ unlink(DAEMON_SOCKET_PATH).catch(() => {});
+ done(false);
+ }, timeoutMs);
+
+ sock.on("connect", () => {
+ const regMsg: WireControl = { type: "control", action: "register", clientId: "_liveness_probe" };
+ sock.write(serialize(regMsg) + "\n");
+ });
+
+ sock.on("data", (chunk: Buffer) => {
+ 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();
+ done(true);
+ }
+ });
+
+ sock.on("error", () => {
+ clearTimeout(timer);
+ sock.destroy();
+ // ponytail: connection refused = daemon dead, clean up stale socket
+ unlink(DAEMON_SOCKET_PATH).catch(() => {});
+ 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.
+ sock.on("close", () => {
+ clearTimeout(timer);
+ done(false);
+ });
+ });
};
/**