diff --git a/crates/gyandoot-agent/src/browser_manager.rs b/crates/gyandoot-agent/src/browser_manager.rs index 880cff1..b39dcb0 100644 --- a/crates/gyandoot-agent/src/browser_manager.rs +++ b/crates/gyandoot-agent/src/browser_manager.rs @@ -62,7 +62,6 @@ fn detect_os() -> OsKind { if cfg!(target_os = "macos") { return OsKind::Mac; } - // Linux — check for WSL if std::fs::read_to_string("/proc/version") .map(|c| { let l = c.to_lowercase(); @@ -99,8 +98,6 @@ fn example_npx_path() -> &'static str { } /// Path to the Camoufox browser binary cache directory. -/// macOS: ~/Library/Caches/camoufox/ -/// Linux/WSL: ~/.cache/camoufox/ fn camoufox_browser_cache_dir() -> std::path::PathBuf { let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp")); if cfg!(target_os = "macos") { @@ -110,104 +107,95 @@ fn camoufox_browser_cache_dir() -> std::path::PathBuf { } } -/// Check if the Camoufox browser binary is already installed. +/// Check if the Camoufox browser binary is installed and uncorrupted. fn is_browser_installed() -> bool { let cache_dir = camoufox_browser_cache_dir(); - let app_bundle = cache_dir.join("Camoufox.app"); - let binary = cache_dir.join("camoufox"); - + if !cache_dir.exists() { + return false; + } if cfg!(target_os = "macos") { - // macOS: binary lives inside Camoufox.app/Contents/MacOS/camoufox - app_bundle.exists() && app_bundle.join("Contents").join("MacOS").join("camoufox").exists() + let macos_dir = cache_dir.join("Camoufox.app").join("Contents").join("MacOS"); + let binary = macos_dir.join("camoufox"); + let lib = macos_dir.join("libmozglue.dylib"); + binary.exists() && lib.exists() } else { - // Linux/WSL: binary at ~/.cache/camoufox/camoufox - binary.exists() + cache_dir.join("camoufox").exists() } } -/// Check if the browser binary appears corrupted (missing critical dylib). -/// Looks for common indicators in the daemon log. +/// Check for known error patterns in the daemon log and return a user-friendly hint. fn check_known_errors(log: &str) -> Option { let lower = log.to_lowercase(); - // macOS: missing libmozglue.dylib — corrupted download if lower.contains("libmozglue.dylib") && lower.contains("no such file") { - return Some( - "The Camoufox browser download appears corrupted (missing libmozglue.dylib). \ - This can happen with unstable alpha versions." - .to_string(), - ); + return Some("The Camoufox browser download is corrupted (missing libmozglue.dylib). \ + This is a known issue with the camoufox-browser@0.1.0 package on macOS." + .to_string()); } - // Linux: missing libgtk / libnss if lower.contains("error while loading shared libraries") { - if lower.contains("libgtk") || lower.contains("libnss") || lower.contains("libasound") { - return Some( - "Missing Linux system libraries needed by the Camoufox browser." - .to_string(), - ); - } + return Some("Missing Linux system libraries needed by the Camoufox browser.".to_string()); } - // macOS: code signing issue if lower.contains("killed") && lower.contains("code signature") { - return Some( - "macOS code signing issue. The downloaded browser binary may need manual approval." - .to_string(), - ); + return Some("macOS code signing issue. The downloaded browser binary may need manual approval." + .to_string()); } None } /// Get platform-specific dependency fix instructions. -fn platform_fix_instructions() -> &'static str { - match detect_os() { - OsKind::Mac => "\ - \u{2192} The Camoufox browser download was corrupted. Fix:\n \ - 1. Remove the corrupted cache:\n \ - rm -rf ~/Library/Caches/camoufox\n \ - 2. Reinstall the browser binary:\n \ - npx camoufox-browser@latest install\n \ - 3. The download is ~662MB. If it still fails, try:\n \ - npx camoufox-browser@latest install 2>&1 | tail -20\n \ - to see the real download error.\n \ - 4. Alternative: use Docker instead:\n \ - docker run -d --name camoufox -p 9377:9377 \\\n \ - ghcr.io/nousresearch/camoufox-browser:latest", - OsKind::Wsl => "\ - \u{2192} Missing Linux dependencies on WSL. Install them:\n \ - sudo apt-get update && sudo apt-get install -y \\\n \ - libgtk-3-0 libnss3 libnspr4 libatk1.0-0t64 \\\n \ - libatk-bridge2.0-0t64 libcups2t64 libdrm2 \\\n \ - libxkbcommon0 libxcomposite1 libxdamage1 \\\n \ - libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \\\n \ - libcairo2 libasound2t64 libxcb-shm0 \\\n \ - libx11-xcb1 libx11-6 libxcb1 libxext6 \\\n \ - libxcursor1 libxi6 libfreetype6 libfontconfig1", - OsKind::Linux => "\ - \u{2192} Missing Linux dependencies. Install them:\n \ - sudo apt-get update && sudo apt-get install -y \\\n \ - libgtk-3-0 libnss3 libnspr4 libatk1.0-0t64 \\\n \ - libatk-bridge2.0-0t64 libcups2t64 libdrm2 \\\n \ - libxkbcommon0 libxcomposite1 libxdamage1 \\\n \ - libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \\\n \ - libcairo2 libasound2t64 libxcb-shm0", - OsKind::Windows => "\ - \u{2192} Windows issue. Try reinstalling camoufox-browser:\n \ - npm install -g camoufox-browser", +fn platform_fix_instructions() -> String { + let os = detect_os(); + let mut msg = String::new(); + + let docker_ok = std::process::Command::new("docker") + .arg("info") + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + + match os { + OsKind::Mac => { + msg.push_str("The Camoufox browser download is corrupted on macOS.\n"); + msg.push_str("Fix:\n"); + msg.push_str(" 1. Remove the corrupted cache:\n"); + msg.push_str(" rm -rf ~/Library/Caches/camoufox\n"); + msg.push_str(" 2. Reinstall the browser binary:\n"); + msg.push_str(" npx camoufox-browser@latest install\n"); + msg.push_str(" (this downloads ~662MB, may take a few minutes)\n"); + if docker_ok { + msg.push_str(" 3. Or use Docker (already installed!):\n"); + } else { + msg.push_str(" 3. Or install Docker Desktop and use:\n"); + } + msg.push_str(" docker run -d --name camoufox -p 9377:9377 \\\n"); + msg.push_str(" ghcr.io/nousresearch/camoufox-browser:latest\n"); + } + OsKind::Wsl | OsKind::Linux => { + msg.push_str("Missing Linux system libraries. Install them:\n"); + msg.push_str(" sudo apt-get update && sudo apt-get install -y \\\n"); + msg.push_str(" libgtk-3-0 libnss3 libnspr4 libatk1.0-0t64 \\\n"); + msg.push_str(" libatk-bridge2.0-0t64 libcups2t64 libdrm2 \\\n"); + msg.push_str(" libxkbcommon0 libxcomposite1 libxdamage1 \\\n"); + msg.push_str(" libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \\\n"); + msg.push_str(" libcairo2 libasound2t64 libxcb-shm0\n"); + } + OsKind::Windows => { + msg.push_str("Windows issue. Try reinstalling:\n"); + msg.push_str(" npm install -g camoufox-browser\n"); + } } + + msg } /// Initialize the camoufox-browser daemon and return a ready client. -/// -/// `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); - // 1. Quick check — is the daemon already running? if check_daemon_socket(session).await { info!("Camoufox daemon already running on socket: {}", socket_path.display()); let client = BrowserClient::new().with_session(session); @@ -216,42 +204,31 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult { info!("Camoufox daemon not running, triggering startup..."); - // 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..."); + info!("Camoufox browser not cached. 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 os_type = detect_os(); - let platform_hint = match os_type { - OsKind::Wsl => "\n\n \u{26a0} WSL detected: Make sure you're not behind a proxy/firewall that blocks \ - the GitHub release download. The browser binary is ~662MB.\n \ - Try: curl -L https://github.com/msly/camoufox-browser/releases/download/v0.1.0/camoufox-browser-linux-x64 -o /dev/null" - .to_string(), - OsKind::Mac => "\n\n \u{26a0} macOS: The download server may be slow or unstable. Try:\n \ - npx camoufox-browser@latest install --verbose\n \ - If it keeps failing, use Docker instead:\n \ - docker run -d --name camoufox -p 9377:9377 \\\n \ - ghcr.io/nousresearch/camoufox-browser:latest" - .to_string(), - _ => String::new(), - }; + let mut hint = String::new(); + let os = detect_os(); + if os == OsKind::Wsl { + hint.push_str( + "\n\nWSL detected: Make sure you can download from GitHub releases.\n", + ); + hint.push_str("Try: curl -L --fail https://github.com/msly/camoufox-browser/releases\n"); + } 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.{}", - e, npx_name, platform_hint + Run manually:\n {} camoufox-browser@latest install\n{}", + e, npx_name, hint )); } } } - // 3. Trigger daemon auto-start via CLI match trigger_daemon_start(session, node_path).await { Ok(()) => { - // 4. Wait for socket to appear info!("Waiting for daemon socket at {}...", socket_path.display()); match wait_for_socket(session).await { Ok(true) => { @@ -262,10 +239,8 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult { } Ok(false) => { let msg = format!( - "Camoufox daemon did not start within {}s \u{2014} socket not found at {}\n\n\ - The browser binary may be corrupted. Try fixing it:\n\ - {}\n\n\ - Then run 'gyandoot run' again.", + "Camoufox daemon did not start within {}s.\n\ + Socket not found at {}\n\n{}", DAEMON_STARTUP_TIMEOUT_SECS, socket_path.display(), platform_fix_instructions(), @@ -282,33 +257,18 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult { } Err(e) => { let error_detail = e.to_string(); - let npx_name = resolve_npx(node_path); - - // Check for known errors in the daemon output let known_error = check_known_errors(&error_detail); - - // Build the full message - let mut msg = format!( - "\u{26a0} Camoufox daemon failed to start.\n\n\ - Error:\n {}\n\n", - error_detail, - ); + let mut msg = format!("Camoufox daemon failed to start.\n\n{}\n\n", error_detail); if let Some(hint) = known_error { - msg.push_str(&format!("Detected: {}\n\n", hint)); - msg.push_str(platform_fix_instructions()); + msg.push_str(&hint); + msg.push('\n'); + msg.push_str(&platform_fix_instructions()); } else { msg.push_str(&format!( - "Try running this manually to see the full error:\n \ - {} camoufox-browser@latest tab_list --session default\n\n\ - Common fixes:\n\ - {}\n\n\ - 3. If Node is via nvm/fnm/asdf, set browser.node_path in config:\n \ - [browser]\n \ - node_path = {}", - npx_name, + "Try running manually:\n {} camoufox-browser@latest tab_list --session default\n\n{}", + resolve_npx(node_path), platform_fix_instructions(), - example_npx_path() )); } @@ -319,15 +279,12 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult { } } -/// Run `camoufox-browser install` to download the browser binary if missing. +/// Run `camoufox-browser install` to download the browser binary. 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", - ]) + .args(&["camoufox-browser@latest", "install"]) .output() .await .map_err(|e| format!("Cannot run '{} camoufox-browser install': {}", npx, e))?; @@ -344,8 +301,8 @@ async fn run_install(node_path: Option<&str>) -> Result { )); } - // Extract the key message from the output - let last_line = stdout.lines() + let last_line = stdout + .lines() .chain(stderr.lines()) .filter(|l| l.contains("complete") || l.contains("Downloaded") || l.contains("Updated")) .last() @@ -355,7 +312,7 @@ async fn run_install(node_path: Option<&str>) -> Result { Ok(last_line) } -/// Trigger daemon startup by running a quick CLI command. +/// Trigger daemon startup by running `tab_list`, which auto-starts the daemon. async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result<(), String> { let home = dirs::home_dir().ok_or("Cannot determine home directory")?; let log_dir = home.join(".gyandoot"); @@ -366,21 +323,17 @@ async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result< let npx = resolve_npx(node_path); let output = Command::new(npx) - .args(&[ - "camoufox-browser@latest", - "tab_list", - "--session", session, - ]) + .args(&["camoufox-browser@latest", "tab_list", "--session", session]) .output() .await .map_err(|e| { let hint = if node_path.is_none() { format!( - "\n \u{2192} Set [browser].node_path in your config:\n [browser]\n node_path = {}", + "\nSet [browser].node_path in your config:\n [browser]\n node_path = {}", example_npx_path() ) } else { - format!("\n \u{2192} npx at '{}' not found or not executable", npx) + format!("\nnpx at '{}' not found or not executable", npx) }; format!("{} not found: {}. Is Node.js installed?{}", npx, e, hint) })?; @@ -388,17 +341,20 @@ async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result< 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.clone() } else { stdout.clone() }; + let details = if !stderr.is_empty() { stderr } else { stdout }; - // 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 _ = 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), + ), + ); return Err(format!( - "{}\n \u{2192} Full log saved to {}", + "{}\n Full log saved to {}", details, log_file.display(), ));