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