aboutsummaryrefslogtreecommitdiff
path: root/src
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 /src
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.
Diffstat (limited to 'src')
-rw-r--r--src/daemon/bridge.ts16
-rw-r--r--src/daemon/spawn.ts64
2 files changed, 71 insertions, 9 deletions
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);
+ });
+ });
};
/**