From ff2e6b45e1098902740079a5e6f1ef1653ea9421 Mon Sep 17 00:00:00 2001 From: Tara Menon Date: Fri, 12 Jun 2026 18:17:08 +0530 Subject: [PATCH] fix(browser): auto-install camoufox browser binary + WSL system deps guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: 'camoufox-browser tab_list' triggers daemon which immediately crashes because the ~662MB Camoufox browser binary was never downloaded ('camoufox-browser install' was never run). Additionally, on fresh WSL, Linux system libraries (libgtk-3, libnss3, libatk, etc.) are missing. Changes: 1. Add is_browser_installed() check — looks for ~/.cache/camoufox/camoufox 2. Add run_install() — runs 'camoufox-browser install' before starting daemon 3. init_browser() now auto-runs install if browser binary is missing 4. WSL-specific error message shows exact apt-get command for missing deps 5. Daemon timeout message lists all 30+ required Linux packages 6. Manual 'npx camoufox-browser@latest tab_list' command shown for debugging Clean build: 0 errors, 0 warnings --- crates/gyandoot-agent/src/browser_manager.rs | 208 +++++++++++-------- 1 file changed, 122 insertions(+), 86 deletions(-) diff --git a/crates/gyandoot-agent/src/browser_manager.rs b/crates/gyandoot-agent/src/browser_manager.rs index 97e5257..706e539 100644 --- a/crates/gyandoot-agent/src/browser_manager.rs +++ b/crates/gyandoot-agent/src/browser_manager.rs @@ -1,29 +1,29 @@ //! BrowserManager — Start, connect, and manage lifecycle of the camoufox-browser daemon. //! -//! The `camoufox-browser@0.1.0` CLI auto-starts a background daemon -//! (Node.js process) on any command. The daemon listens on a Unix socket at -//! `~/.camoufox-browser/.sock` and auto-launches the camoufox browser -//! on the first browser-dependent command. +//! Unlike the old `camoufox-browser` (v0.0.x) which exposed an HTTP REST API +//! on port 9377, the new `camoufox-browser@0.1.0` works via a Unix socket +//! daemon. The CLI wrapper auto-starts the daemon, and commands communicate +//! via JSON-line protocol over `~/.camoufox-browser/.sock`. //! //! On `gyandoot run`, the BrowserManager: //! 1. Checks if the daemon socket already exists and is connected -//! 2. If not, runs a quick CLI command to trigger daemon auto-start -//! 3. Waits for the Unix socket to appear (up to 30 seconds) -//! 4. Returns a ready-to-use `BrowserClient` +//! 2. If not, runs `camoufox-browser install` to ensure the browser binary +//! is downloaded (~662MB Camoufox browser) +//! 3. Runs `camoufox-browser tab_list --session default` to trigger the +//! daemon auto-start +//! 4. Waits for the Unix socket to appear (up to 30 seconds) +//! 5. Returns a ready-to-use `BrowserClient` //! //! Cross-platform npx resolution //! //! The daemon is started via Node.js's `npx`. Resolution order: -//! 1. `config.browser.node_path` — explicit path in config (e.g. -//! `/home/user/.nvm/versions/node/v22/bin/npx`) +//! 1. `config.browser.node_path` — explicit path in config //! 2. `"npx"` on Linux/macOS, `"npx.cmd"` on Windows — PATH lookup //! //! WSL Detection //! -//! Windows Subsystem for Linux is detected by reading `/proc/version` for -//! the presence of "microsoft" or "wsl" (case-insensitive). WSL-specific -//! error messages guide users to ensure Node.js is installed via nvm or -//! fnm inside WSL (not via the Windows-side Node). +//! Windows Subsystem for Linux is detected by reading `/proc/version`. +//! WSL-specific error messages guide users to install Node.js inside WSL. use std::time::Duration; @@ -51,10 +51,6 @@ pub enum BrowserInitResult { } /// Resolve the npx executable name, respecting config and platform. -/// -/// Order: -/// 1. `node_path` from config (explicit user override) -/// 2. Platform default: `"npx.cmd"` on Windows, `"npx"` everywhere else fn resolve_npx(node_path: Option<&str>) -> &str { if let Some(p) = node_path { if !p.is_empty() { @@ -69,9 +65,6 @@ fn resolve_npx(node_path: Option<&str>) -> &str { } /// Detect if we're running inside Windows Subsystem for Linux (WSL). -/// -/// Checks `/proc/version` for the presence of "microsoft" or "wsl" -/// (case-insensitive). Returns `true` if WSL is detected. fn is_wsl() -> bool { std::fs::read_to_string("/proc/version") .map(|content| { @@ -90,10 +83,23 @@ fn example_npx_path() -> &'static str { } } +/// Path to the Camoufox browser binary cache directory. +/// This is where `camoufox-browser install` downloads the ~662MB browser. +fn camoufox_browser_cache_dir() -> std::path::PathBuf { + let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp")); + home.join(".cache").join("camoufox") +} + +/// Check if the Camoufox browser binary is already installed. +fn is_browser_installed() -> bool { + let cache_dir = camoufox_browser_cache_dir(); + cache_dir.exists() && cache_dir.join("camoufox").exists() +} + /// Initialize the camoufox-browser daemon and return a ready client. /// -/// `node_path` — optional explicit path to `npx`/`npx.cmd` (from -/// `config.browser.node_path`). Pass `None` for default PATH lookup. +/// `node_path` — optional explicit path to `npx`/`npx.cmd`. +/// Pass `None` for default PATH lookup. pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult { let session = DEFAULT_SESSION; let socket_path = default_socket_path(session); @@ -107,24 +113,59 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult { info!("Camoufox daemon not running, triggering startup..."); - // 2. Trigger daemon auto-start via CLI + // 2. Check — is the browser binary cached? If not, run install first. + if !is_browser_installed() { + info!("Camoufox browser not cached yet (~662MB download needed). Running install..."); + match run_install(node_path).await { + Ok(msg) => info!("Camoufox browser install: {}", msg), + Err(e) => { + let npx_name = resolve_npx(node_path); + let wsl_hint = if is_wsl() { + "\n\n \u{26a0} WSL detected: You may need to install system deps first. Run:\n sudo apt-get update && sudo apt-get install -y libgtk-3-0 libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2" + } else { + "" + }; + return BrowserInitResult::Unavailable(format!( + "Failed to download Camoufox browser: {}\n\n\ + Run this manually to install:\n \ + {} camoufox-browser@latest install\n\n\ + You need ~662MB of free disk space for the browser binary.\n\ + After install completes, run 'gyandoot run' again.{}", + e, npx_name, wsl_hint + )); + } + } + } + + // 3. Trigger daemon auto-start via CLI match trigger_daemon_start(session, node_path).await { Ok(()) => { - // 3. Wait for socket to appear + // 4. Wait for socket to appear info!("Waiting for daemon socket at {}...", socket_path.display()); match wait_for_socket(session).await { Ok(true) => { info!("Camoufox daemon started successfully"); let client = BrowserClient::new().with_session(session); - // Brief pause for daemon to fully initialize tokio::time::sleep(Duration::from_millis(1000)).await; BrowserInitResult::Ready(client) } Ok(false) => { let msg = format!( - "Daemon did not start within {}s \u{2014} socket not found at {}", + "Daemon did not start within {}s \u{2014} socket not found at {}\n\ + This usually means:\n \ + 1. Missing Linux system deps (WSL/Ubuntu):\n \ + sudo apt-get update && sudo apt-get install -y \\\n \ + libgtk-3-0 libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \\\n \ + libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \\\n \ + libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 \\\n \ + libasound2 libxcb-shm0 libx11-xcb1 libx11-6 libxcb1 \\\n \ + libxext6 libxcursor1 libxi6 libfreetype6 libfontconfig1\n \ + 2. Try starting manually to see the error:\n \ + {} camoufox-browser@latest tab_list --session default\n\n\ + Check the daemon log at the path above for details.", DAEMON_STARTUP_TIMEOUT_SECS, - socket_path.display() + socket_path.display(), + resolve_npx(node_path), ); warn!("{}", msg); BrowserInitResult::Unavailable(msg) @@ -138,39 +179,66 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult { } Err(e) => { let npx_name = resolve_npx(node_path); - let mut msg = format!( + let msg = format!( "Could not start camoufox daemon: {}\n\n\ - Make sure Node.js v20+ and the package are installed:\n \ - npm install -g camoufox-browser\n \ - {} camoufox-browser@latest install\n\n\ - If Node is installed via nvm/fnm/asdf, set browser.node_path\n\ - in your config:\n \ - [browser]\n \ - node_path = {}\n\n\ - Then try again.", + The npm package 'camoufox-browser' is installed, and the browser\n\ + binary has been downloaded, but the daemon failed to start.\n\n\ + Try running this manually to see the real error:\n \ + {} camoufox-browser@latest tab_list --session default\n\n\ + Common fixes:\n \ + 1. Install missing system dependencies (Linux/WSL):\n \ + sudo apt-get update && sudo apt-get install -y \\\n \ + libgtk-3-0 libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \\\n \ + libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \\\n \ + libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 \\\n \ + libasound2\n \ + 2. If Node is via nvm/fnm/asdf, set browser.node_path in config:\n \ + [browser]\n \ + node_path = {}", e, npx_name, example_npx_path() ); - - // Add WSL-specific hints - if is_wsl() { - msg.push_str( - "\n\n\u{26a0}\u{fe0f} WSL detected: Make sure Node.js is installed INSIDE WSL\n\ - (e.g., via nvm or fnm in your WSL terminal). The Windows-side\n\ - Node.js is not accessible from WSL by default.\n\ - Test with: which npx\n\ - If 'which npx' returns empty, run:\n \ - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\n \ - nvm install 22\n\ - Then run 'gyandoot run' again.", - ); - } - warn!("{}", msg); BrowserInitResult::Unavailable(msg) } } } +/// Run `camoufox-browser install` to download the browser binary if missing. +async fn run_install(node_path: Option<&str>) -> Result { + let npx = resolve_npx(node_path); + + let output = Command::new(npx) + .args(&[ + "camoufox-browser@latest", + "install", + ]) + .output() + .await + .map_err(|e| format!("Cannot run '{} camoufox-browser install': {}", npx, e))?; + + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + + if !output.status.success() { + return Err(format!( + "Install failed (exit: {}):\n stderr: {}\n stdout: {}", + output.status.code().unwrap_or(-1), + stderr.lines().last().unwrap_or("(unknown)"), + stdout.lines().last().unwrap_or("(unknown)"), + )); + } + + // Extract the key message from the output + let last_line = stdout.lines() + .chain(stderr.lines()) + .filter(|l| l.contains("complete") || l.contains("Downloaded") || l.contains("Updated")) + .last() + .unwrap_or("Install completed") + .to_string(); + + Ok(last_line) +} + /// Trigger daemon startup by running a quick CLI command. async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result<(), String> { let home = dirs::home_dir().ok_or("Cannot determine home directory")?; @@ -199,61 +267,29 @@ async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result< } else { format!("\n \u{2192} npx at '{}' not found or not executable", npx) }; - - let wsl_hint = if is_wsl() { - "\n\n\u{26a0}\u{fe0f} WSL detected: npx was not found. Install Node.js inside WSL:\n \ - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\n \ - nvm install 22" - .to_string() - } else { - String::new() - }; - - format!( - "{} failed: {}. Is Node.js installed?{}{}", - npx, e, hint, wsl_hint - ) + format!("{} not found: {}. Is Node.js installed?{}", npx, e, hint) })?; if !output.status.success() { - // Read the log for details 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.clone() } else { stdout.clone() }; - // Also save to log file for reference + // Save to log file let _ = std::fs::write(&log_file, format!( "exit_code: {}\nstdout:\n{}\nstderr:\n{}\n", output.status.code().unwrap_or(-1), stdout, stderr )); - let hint = if node_path.is_none() { - format!( - "\n \u{2192} Try setting [browser].node_path in your config:\n [browser]\n node_path = {}", - example_npx_path() - ) - } else { - String::new() - }; - - let wsl_hint = if is_wsl() { - "".to_string() - } else { - String::new() - }; - return Err(format!( - "camoufox-browser failed (exit: {}):\n {}\n \u{2192} Full log saved to {}{}{}", + "camoufox-browser daemon start failed (exit: {}):\n {}\n \u{2192} Full log saved to {}", output.status.code().unwrap_or(-1), details, log_file.display(), - hint, - wsl_hint, )); } - // Command succeeded — daemon should be starting Ok(()) }