fix(browser): auto-install camoufox browser binary + WSL system deps guide

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
This commit is contained in:
2026-06-12 18:17:08 +05:30
parent b5a5784707
commit ff2e6b45e1

View File

@@ -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/<session>.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/<session>.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<String, String> {
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(())
}