aboutsummaryrefslogtreecommitdiff
path: root/src/daemon
diff options
context:
space:
mode:
authorAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-07-12 22:03:43 +0530
committerGitHub <noreply@github.com>2026-07-12 22:03:43 +0530
commitcf92c10eda651cdad882aa050439c80d2f40b4b1 (patch)
tree789e211f90c8548bac7b149ddfecc4312a6ba037 /src/daemon
parentf6bab02b6b93c3804ec83b853e333fba417a0988 (diff)
fix: make browser daemon setup work on Windows (#7) (#12)
The Unix socket daemon introduced in v0.6.0 made several POSIX-only assumptions that break setup on Windows: - protocol.ts hardcoded a Unix socket path (/tmp/pi-browser-daemon.sock). Node IPC on Windows requires a named pipe (\\.\pipe\<name>); a /tmp path is not a valid net listen/connect target there. - spawn.ts spawned the bare tsx binary / `npx` and probed the non-Windows .bin/tsx shim. On Windows npm binaries are .cmd shims that must run via a shell, and spawning them directly fails with EINVAL / ENOENT. - isDaemonRunning() pre-checked the socket with fs.access(), which always fails for a named pipe and would report a live daemon as dead. - stale-socket unlink() would throw on a named pipe path. Changes: - DAEMON_SOCKET_PATH is now platform-aware: named pipe on win32, Unix socket elsewhere. - spawnDaemon() resolves tsx.cmd / npx.cmd on Windows, runs via shell with per-token quoting (paths with spaces) and windowsVerbatimArguments. - isDaemonRunning() skips the fs.access pre-check on Windows and probes the pipe directly. - stale-socket cleanup is a no-op on Windows (named pipes self-clean). - setup.ts daemon-failure message uses the actual DAEMON_SOCKET_PATH. Closes #7
Diffstat (limited to 'src/daemon')
-rw-r--r--src/daemon/protocol.ts14
-rw-r--r--src/daemon/spawn.ts59
2 files changed, 58 insertions, 15 deletions
diff --git a/src/daemon/protocol.ts b/src/daemon/protocol.ts
index 9d777bc..4fa4a2a 100644
--- a/src/daemon/protocol.ts
+++ b/src/daemon/protocol.ts
@@ -133,8 +133,18 @@ export const deserialize = (line: string): WireMessage | null => {
// ── Constants ──────────────────────────────────────────────────────────────────
-/** Filesystem path for the daemon's Unix domain socket. */
-export const DAEMON_SOCKET_PATH = "/tmp/pi-browser-daemon.sock";
+/**
+ * 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.
+ */
+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
diff --git a/src/daemon/spawn.ts b/src/daemon/spawn.ts
index 0a2670c..d051fcc 100644
--- a/src/daemon/spawn.ts
+++ b/src/daemon/spawn.ts
@@ -19,6 +19,19 @@ import { DAEMON_SOCKET_PATH, serialize, type WireControl } from "./protocol";
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.
+ */
+const cleanupStaleSocket = (): void => {
+ if (isWindows) return;
+ unlink(DAEMON_SOCKET_PATH).catch(() => {});
+};
+
/**
* Check whether the daemon is running by probing its Unix socket.
* A file-existence check is unreliable — a crashed daemon leaves a stale socket.
@@ -26,11 +39,15 @@ const moduleDir = dirname(fileURLToPath(import.meta.url));
* On failure, clean up the stale socket so ensureDaemon can spawn a fresh one.
*/
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);
- } catch {
- return false;
+ // On POSIX, a missing socket file means the daemon definitely isn't running,
+ // so we can short-circuit. Windows named pipes are not filesystem entries —
+ // access() always fails there — so skip the pre-check and probe directly.
+ if (!isWindows) {
+ try {
+ await access(DAEMON_SOCKET_PATH);
+ } catch {
+ return false;
+ }
}
return new Promise((resolve) => {
@@ -45,7 +62,7 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => {
const timer = setTimeout(() => {
sock.destroy();
// ponytail: stale socket cleanup; ENOENT = already gone, fine
- unlink(DAEMON_SOCKET_PATH).catch(() => {});
+ cleanupStaleSocket();
done(false);
}, timeoutMs);
@@ -70,7 +87,7 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => {
clearTimeout(timer);
sock.destroy();
// ponytail: connection refused = daemon dead, clean up stale socket
- unlink(DAEMON_SOCKET_PATH).catch(() => {});
+ cleanupStaleSocket();
done(false);
});
@@ -96,18 +113,34 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => {
export const spawnDaemon = (): ChildProcess | null => {
// Resolve the daemon entry point and tsx binary.
const daemonScript = join(moduleDir, "index.ts");
- const tsxBin = join(moduleDir, "..", "..", "node_modules", ".bin", "tsx");
- // Use tsx to run the daemon TypeScript source directly.
- // The spawed process inherits the parent's env so module resolution works.
+ // 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.
+ 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 = existsSync(tsxBin) ? tsxBin : "npx";
- const args = existsSync(tsxBin) ? [daemonScript] : ["tsx", daemonScript];
+ 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.
+ const quote = (s: string): string => (isWindows ? `"${s}"` : s);
- const child = spawn(cmd, args, {
+ 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,
});
child.unref();