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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user