From febc4b02c7a74fe0ffa3fb05712dd4dc1c65f17d Mon Sep 17 00:00:00 2001 From: Tara Menon Date: Sat, 13 Jun 2026 17:57:04 +0530 Subject: [PATCH] fix(browser): spawn daemon directly with node instead of npx wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: camoufox-browser@0.1.0's CLI wrapper spawns the daemon with stdio: 'ignore' and only waits 5 seconds. If the daemon crashes (silent better-sqlite3 native binding error, etc.), the error is swallowed and we see 'Failed to connect: No such file or directory'. Fix: 1. find_daemon_js() — locates daemon.js in global node_modules via 'npm root -g' with fallbacks for common nvm/homebrew paths 2. trigger_daemon_start() — spawns daemon.js directly with 'node', captures stderr to ~/.gyandoot/camoufox-daemon-stderr.log 3. Uses spawn() + std::mem::forget (background daemon) instead of .output() (which would wait forever since daemon never exits) 4. kill_on_drop(false) — prevents killing daemon when child drops 5. Poll loop in wait_for_socket() still works as before (30s timeout) --- crates/gyandoot-agent/src/browser_manager.rs | 122 +++++++++++++++---- 1 file changed, 96 insertions(+), 26 deletions(-) diff --git a/crates/gyandoot-agent/src/browser_manager.rs b/crates/gyandoot-agent/src/browser_manager.rs index d0a710d..5d88102 100644 --- a/crates/gyandoot-agent/src/browser_manager.rs +++ b/crates/gyandoot-agent/src/browser_manager.rs @@ -352,7 +352,11 @@ async fn install_npm_package(_npx: &str, package: &str, version: &str) -> Result Ok(format!("{package}@{version} installed")) } -/// Trigger daemon startup via `npx camoufox-browser@ tab_list`. +/// Trigger daemon startup by spawning the daemon.js directly with node. +/// This is better than going through `npx camoufox-browser tab_list` because: +/// 1. The npm CLI wrapper spawns the daemon with stdio: "ignore" (hides errors) +/// 2. We capture stderr directly to diagnose crashes +/// 3. We can wait longer than the 5s default in daemon-client.js async fn trigger_daemon_start(npx: &str, session: &str) -> Result<(), String> { let home = dirs::home_dir().ok_or("Cannot determine home directory")?; let log_dir = home.join(".gyandoot"); @@ -360,38 +364,104 @@ async fn trigger_daemon_start(npx: &str, session: &str) -> Result<(), String> { .map_err(|e| format!("Cannot create {}: {}", log_dir.display(), e))?; let log_file = log_dir.join("camoufox-daemon-spawn.log"); - let output = Command::new(npx) - .args(&[ - "--yes", - &format!("camoufox-browser@{}", CAMOUFOX_BROWSER_VERSION), - "tab_list", - "--session", session, - ]) - .output() - .await - .map_err(|e| format!("{} not found: {}", npx, e))?; + // Find the daemon.js path in the globally installed camoufox-browser package + let daemon_path = find_daemon_js(npx).await?; - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); - let details = if !stderr.is_empty() { stderr } else { stdout }; + info!("Starting daemon: node {} (session={})", daemon_path.display(), session); - let _ = std::fs::write( - &log_file, - format!( - "exit_code: {}\nstderr:\n{}\nstdout:\n{}\n", - output.status.code().unwrap_or(-1), - String::from_utf8_lossy(&output.stderr), - String::from_utf8_lossy(&output.stdout), - ), - ); + // Spawn the daemon in the background — it's a long-running server + // We capture stderr to a file for diagnostics + let stderr_file = log_dir.join("camoufox-daemon-stderr.log"); + let stderr_fd = std::fs::File::create(&stderr_file) + .map_err(|e| format!("Cannot create stderr log: {}", e))?; - return Err(format!("{}\n Log: {}", details, log_file.display())); - } + let child = Command::new("node") + .args(&[daemon_path.to_string_lossy().as_ref()]) + .env("CAMOUFOX_BROWSER_SESSION", session) + .stdout(Stdio::null()) + .stderr(stderr_fd) + .kill_on_drop(false) + .spawn() + .map_err(|e| format!("Cannot spawn daemon: {}. Is Node.js installed?", e))?; + + let pid = child.id().unwrap_or(0); + info!("Daemon spawned with PID {}", pid); + + // Write spawn info to log + let _ = std::fs::write( + &log_file, + format!( + "daemon_pid: {}\ndaemon_path: {}\nsession: {}\nstderr_log: {}\n", + pid, + daemon_path.display(), + session, + stderr_file.display(), + ), + ); + + // Don't await child — it runs forever. We'll poll for the socket. + // Detach the child handle so we don't hold it + std::mem::forget(child); Ok(()) } +/// Find the daemon.js entrypoint in the globally installed camoufox-browser package. +/// Tries: npm root -g → find dist/daemon.js +async fn find_daemon_js(npx: &str) -> Result { + // Try `npm root -g` to find global node_modules + let root_output = Command::new("npm") + .args(&["root", "-g"]) + .output() + .await + .map_err(|e| format!("Cannot find npm global root: {}", e))?; + + if !root_output.status.success() { + return Err("npm root -g failed".to_string()); + } + + let global_root = String::from_utf8_lossy(&root_output.stdout).trim().to_string(); + let daemon_path = std::path::Path::new(&global_root) + .join("camoufox-browser") + .join("dist") + .join("daemon.js"); + + if daemon_path.exists() { + info!("Found daemon at {}", daemon_path.display()); + return Ok(daemon_path); + } + + // Fallback: try common locations + let fallbacks = vec![ + home_dir().join(".nvm").join("versions").join("node").join("v22").join("lib").join("node_modules") + .join("camoufox-browser").join("dist").join("daemon.js"), + home_dir().join(".nvm").join("versions").join("node").join("v24").join("lib").join("node_modules") + .join("camoufox-browser").join("dist").join("daemon.js"), + std::path::PathBuf::from("/usr/local/lib/node_modules/camoufox-browser/dist/daemon.js"), + std::path::PathBuf::from("/opt/homebrew/lib/node_modules/camoufox-browser/dist/daemon.js"), + ]; + + for fb in &fallbacks { + if fb.exists() { + info!("Found daemon at {} (fallback)", fb.display()); + return Ok(fb.clone()); + } + } + + Err(format!( + "camoufox-browser daemon.js not found.\n\ + Tried: {}\n\ + And fallback paths.\n\ + Run: npm install -g camoufox-browser@{}", + daemon_path.display(), + CAMOUFOX_BROWSER_VERSION + )) +} + +fn home_dir() -> std::path::PathBuf { + dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp")) +} + async fn wait_for_socket(session: &str) -> Result { let deadline = tokio::time::Instant::now() + Duration::from_secs(DAEMON_STARTUP_TIMEOUT_SECS); while tokio::time::Instant::now() < deadline {