fix(browser): spawn daemon directly with node instead of npx wrapper

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)
This commit is contained in:
2026-06-13 17:57:04 +05:30
parent 9851e41182
commit febc4b02c7

View File

@@ -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@<pinned> 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);
// 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))?;
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!(
"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),
"daemon_pid: {}\ndaemon_path: {}\nsession: {}\nstderr_log: {}\n",
pid,
daemon_path.display(),
session,
stderr_file.display(),
),
);
return Err(format!("{}\n Log: {}", details, log_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<std::path::PathBuf, String> {
// 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<bool, String> {
let deadline = tokio::time::Instant::now() + Duration::from_secs(DAEMON_STARTUP_TIMEOUT_SECS);
while tokio::time::Instant::now() < deadline {