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.
|
//! BrowserManager — Start, connect, and manage lifecycle of the camoufox-browser daemon.
|
||||||
//!
|
//!
|
||||||
//! The `camoufox-browser@0.1.0` CLI auto-starts a background daemon
|
//! Unlike the old `camoufox-browser` (v0.0.x) which exposed an HTTP REST API
|
||||||
//! (Node.js process) on any command. The daemon listens on a Unix socket at
|
//! on port 9377, the new `camoufox-browser@0.1.0` works via a Unix socket
|
||||||
//! `~/.camoufox-browser/<session>.sock` and auto-launches the camoufox browser
|
//! daemon. The CLI wrapper auto-starts the daemon, and commands communicate
|
||||||
//! on the first browser-dependent command.
|
//! via JSON-line protocol over `~/.camoufox-browser/<session>.sock`.
|
||||||
//!
|
//!
|
||||||
//! On `gyandoot run`, the BrowserManager:
|
//! On `gyandoot run`, the BrowserManager:
|
||||||
//! 1. Checks if the daemon socket already exists and is connected
|
//! 1. Checks if the daemon socket already exists and is connected
|
||||||
//! 2. If not, runs a quick CLI command to trigger daemon auto-start
|
//! 2. If not, runs `camoufox-browser install` to ensure the browser binary
|
||||||
//! 3. Waits for the Unix socket to appear (up to 30 seconds)
|
//! is downloaded (~662MB Camoufox browser)
|
||||||
//! 4. Returns a ready-to-use `BrowserClient`
|
//! 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
|
//! Cross-platform npx resolution
|
||||||
//!
|
//!
|
||||||
//! The daemon is started via Node.js's `npx`. Resolution order:
|
//! The daemon is started via Node.js's `npx`. Resolution order:
|
||||||
//! 1. `config.browser.node_path` — explicit path in config (e.g.
|
//! 1. `config.browser.node_path` — explicit path in config
|
||||||
//! `/home/user/.nvm/versions/node/v22/bin/npx`)
|
|
||||||
//! 2. `"npx"` on Linux/macOS, `"npx.cmd"` on Windows — PATH lookup
|
//! 2. `"npx"` on Linux/macOS, `"npx.cmd"` on Windows — PATH lookup
|
||||||
//!
|
//!
|
||||||
//! WSL Detection
|
//! WSL Detection
|
||||||
//!
|
//!
|
||||||
//! Windows Subsystem for Linux is detected by reading `/proc/version` for
|
//! Windows Subsystem for Linux is detected by reading `/proc/version`.
|
||||||
//! the presence of "microsoft" or "wsl" (case-insensitive). WSL-specific
|
//! WSL-specific error messages guide users to install Node.js inside WSL.
|
||||||
//! error messages guide users to ensure Node.js is installed via nvm or
|
|
||||||
//! fnm inside WSL (not via the Windows-side Node).
|
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -51,10 +51,6 @@ pub enum BrowserInitResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the npx executable name, respecting config and platform.
|
/// 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 {
|
fn resolve_npx(node_path: Option<&str>) -> &str {
|
||||||
if let Some(p) = node_path {
|
if let Some(p) = node_path {
|
||||||
if !p.is_empty() {
|
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).
|
/// 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 {
|
fn is_wsl() -> bool {
|
||||||
std::fs::read_to_string("/proc/version")
|
std::fs::read_to_string("/proc/version")
|
||||||
.map(|content| {
|
.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.
|
/// Initialize the camoufox-browser daemon and return a ready client.
|
||||||
///
|
///
|
||||||
/// `node_path` — optional explicit path to `npx`/`npx.cmd` (from
|
/// `node_path` — optional explicit path to `npx`/`npx.cmd`.
|
||||||
/// `config.browser.node_path`). Pass `None` for default PATH lookup.
|
/// Pass `None` for default PATH lookup.
|
||||||
pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
|
pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
|
||||||
let session = DEFAULT_SESSION;
|
let session = DEFAULT_SESSION;
|
||||||
let socket_path = default_socket_path(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...");
|
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 {
|
match trigger_daemon_start(session, node_path).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
// 3. Wait for socket to appear
|
// 4. Wait for socket to appear
|
||||||
info!("Waiting for daemon socket at {}...", socket_path.display());
|
info!("Waiting for daemon socket at {}...", socket_path.display());
|
||||||
match wait_for_socket(session).await {
|
match wait_for_socket(session).await {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
info!("Camoufox daemon started successfully");
|
info!("Camoufox daemon started successfully");
|
||||||
let client = BrowserClient::new().with_session(session);
|
let client = BrowserClient::new().with_session(session);
|
||||||
// Brief pause for daemon to fully initialize
|
|
||||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||||
BrowserInitResult::Ready(client)
|
BrowserInitResult::Ready(client)
|
||||||
}
|
}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
let msg = format!(
|
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,
|
DAEMON_STARTUP_TIMEOUT_SECS,
|
||||||
socket_path.display()
|
socket_path.display(),
|
||||||
|
resolve_npx(node_path),
|
||||||
);
|
);
|
||||||
warn!("{}", msg);
|
warn!("{}", msg);
|
||||||
BrowserInitResult::Unavailable(msg)
|
BrowserInitResult::Unavailable(msg)
|
||||||
@@ -138,39 +179,66 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let npx_name = resolve_npx(node_path);
|
let npx_name = resolve_npx(node_path);
|
||||||
let mut msg = format!(
|
let msg = format!(
|
||||||
"Could not start camoufox daemon: {}\n\n\
|
"Could not start camoufox daemon: {}\n\n\
|
||||||
Make sure Node.js v20+ and the package are installed:\n \
|
The npm package 'camoufox-browser' is installed, and the browser\n\
|
||||||
npm install -g camoufox-browser\n \
|
binary has been downloaded, but the daemon failed to start.\n\n\
|
||||||
{} camoufox-browser@latest install\n\n\
|
Try running this manually to see the real error:\n \
|
||||||
If Node is installed via nvm/fnm/asdf, set browser.node_path\n\
|
{} camoufox-browser@latest tab_list --session default\n\n\
|
||||||
in your config:\n \
|
Common fixes:\n \
|
||||||
[browser]\n \
|
1. Install missing system dependencies (Linux/WSL):\n \
|
||||||
node_path = {}\n\n\
|
sudo apt-get update && sudo apt-get install -y \\\n \
|
||||||
Then try again.",
|
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()
|
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);
|
warn!("{}", msg);
|
||||||
BrowserInitResult::Unavailable(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.
|
/// Trigger daemon startup by running a quick CLI command.
|
||||||
async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result<(), String> {
|
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 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 {
|
} else {
|
||||||
format!("\n \u{2192} npx at '{}' not found or not executable", npx)
|
format!("\n \u{2192} npx at '{}' not found or not executable", npx)
|
||||||
};
|
};
|
||||||
|
format!("{} not found: {}. Is Node.js installed?{}", npx, e, hint)
|
||||||
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
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
// Read the log for details
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout).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.clone() } else { stdout.clone() };
|
||||||
|
|
||||||
// Also save to log file for reference
|
// Save to log file
|
||||||
let _ = std::fs::write(&log_file, format!(
|
let _ = std::fs::write(&log_file, format!(
|
||||||
"exit_code: {}\nstdout:\n{}\nstderr:\n{}\n",
|
"exit_code: {}\nstdout:\n{}\nstderr:\n{}\n",
|
||||||
output.status.code().unwrap_or(-1),
|
output.status.code().unwrap_or(-1),
|
||||||
stdout, stderr
|
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!(
|
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),
|
output.status.code().unwrap_or(-1),
|
||||||
details,
|
details,
|
||||||
log_file.display(),
|
log_file.display(),
|
||||||
hint,
|
|
||||||
wsl_hint,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command succeeded — daemon should be starting
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user