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"))
|
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> {
|
async fn trigger_daemon_start(npx: &str, session: &str) -> Result<(), String> {
|
||||||
let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
|
let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
|
||||||
let log_dir = home.join(".gyandoot");
|
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))?;
|
.map_err(|e| format!("Cannot create {}: {}", log_dir.display(), e))?;
|
||||||
let log_file = log_dir.join("camoufox-daemon-spawn.log");
|
let log_file = log_dir.join("camoufox-daemon-spawn.log");
|
||||||
|
|
||||||
let output = Command::new(npx)
|
// Find the daemon.js path in the globally installed camoufox-browser package
|
||||||
.args(&[
|
let daemon_path = find_daemon_js(npx).await?;
|
||||||
"--yes",
|
|
||||||
&format!("camoufox-browser@{}", CAMOUFOX_BROWSER_VERSION),
|
|
||||||
"tab_list",
|
|
||||||
"--session", session,
|
|
||||||
])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("{} not found: {}", npx, e))?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
info!("Starting daemon: node {} (session={})", daemon_path.display(), session);
|
||||||
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 };
|
|
||||||
|
|
||||||
|
// 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(
|
let _ = std::fs::write(
|
||||||
&log_file,
|
&log_file,
|
||||||
format!(
|
format!(
|
||||||
"exit_code: {}\nstderr:\n{}\nstdout:\n{}\n",
|
"daemon_pid: {}\ndaemon_path: {}\nsession: {}\nstderr_log: {}\n",
|
||||||
output.status.code().unwrap_or(-1),
|
pid,
|
||||||
String::from_utf8_lossy(&output.stderr),
|
daemon_path.display(),
|
||||||
String::from_utf8_lossy(&output.stdout),
|
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(())
|
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> {
|
async fn wait_for_socket(session: &str) -> Result<bool, String> {
|
||||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(DAEMON_STARTUP_TIMEOUT_SECS);
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(DAEMON_STARTUP_TIMEOUT_SECS);
|
||||||
while tokio::time::Instant::now() < deadline {
|
while tokio::time::Instant::now() < deadline {
|
||||||
|
|||||||
Reference in New Issue
Block a user