aboutsummaryrefslogtreecommitdiff
path: root/src/daemon/spawn.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/daemon/spawn.ts')
-rw-r--r--src/daemon/spawn.ts61
1 files changed, 4 insertions, 57 deletions
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;