fix(browser): platform-aware error messages + corrupted download detection

Root cause on macOS: 'camoufox-browser install' downloaded v150.0.2-alpha.26
instead of the stable browser binary. This alpha version is missing
libmozglue.dylib, causing dyld to crash immediately.

Changes:
1. detect_os() — returns Mac/Linux/Wsl/Windows enum for platform-aware logic
2. check_known_errors() — parses daemon stderr for known failures (missing
   libmozglue.dylib on macOS, missing libgtk/libnss on Linux, code-signing)
3. platform_fix_instructions() — returns OS-specific recovery steps:
   - macOS: rm -rf ~/Library/Caches/camoufox, reinstall, or Docker fallback
   - WSL/Linux: correct apt-get install commands (with t64 package variants)
4. Browser cache detection now checks Camoufox.app on macOS, camoufox on Linux
5. All error messages use detect_os() — no more Linux apt-get on macOS
6. Corrupted download detected automatically, user told to clear cache + retry

Clean build: 0 errors, 0 warnings
This commit is contained in:
2026-06-12 19:05:35 +05:30
parent cd0e58be82
commit 17f828f159

View File

@@ -19,11 +19,6 @@
//! 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 //! 1. `config.browser.node_path` — explicit path in config
//! 2. `"npx"` on Linux/macOS, `"npx.cmd"` on Windows — PATH lookup //! 2. `"npx"` on Linux/macOS, `"npx.cmd"` on Windows — PATH lookup
//!
//! WSL Detection
//!
//! 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; use std::time::Duration;
@@ -50,6 +45,37 @@ pub enum BrowserInitResult {
Unavailable(String), Unavailable(String),
} }
/// Platform category for platform-specific error messages.
#[derive(Debug, PartialEq)]
enum OsKind {
Mac,
Linux,
Windows,
Wsl,
}
/// Detect what operating system we're running on.
fn detect_os() -> OsKind {
if cfg!(target_os = "windows") {
return OsKind::Windows;
}
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();
l.contains("microsoft") || l.contains("wsl")
})
.unwrap_or(false)
{
OsKind::Wsl
} else {
OsKind::Linux
}
}
/// Resolve the npx executable name, respecting config and platform. /// Resolve the npx executable name, respecting config and platform.
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 {
@@ -64,36 +90,113 @@ fn resolve_npx(node_path: Option<&str>) -> &str {
} }
} }
/// Detect if we're running inside Windows Subsystem for Linux (WSL).
fn is_wsl() -> bool {
std::fs::read_to_string("/proc/version")
.map(|content| {
let lower = content.to_lowercase();
lower.contains("microsoft") || lower.contains("wsl")
})
.unwrap_or(false)
}
/// Get a platform-aware example npx path for error messages. /// Get a platform-aware example npx path for error messages.
fn example_npx_path() -> &'static str { fn example_npx_path() -> &'static str {
if cfg!(target_os = "macos") { match detect_os() {
"\"/Users/yourname/.nvm/versions/node/v22/bin/npx\"" OsKind::Mac => "\"/Users/yourname/.nvm/versions/node/v22/bin/npx\"",
} else { _ => "\"/home/user/.nvm/versions/node/v22/bin/npx\"",
"\"/home/user/.nvm/versions/node/v22/bin/npx\""
} }
} }
/// Path to the Camoufox browser binary cache directory. /// Path to the Camoufox browser binary cache directory.
/// This is where `camoufox-browser install` downloads the ~662MB browser. /// macOS: ~/Library/Caches/camoufox/
/// Linux/WSL: ~/.cache/camoufox/
fn camoufox_browser_cache_dir() -> std::path::PathBuf { fn camoufox_browser_cache_dir() -> std::path::PathBuf {
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp")); let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp"));
if cfg!(target_os = "macos") {
home.join("Library").join("Caches").join("camoufox")
} else {
home.join(".cache").join("camoufox") home.join(".cache").join("camoufox")
} }
}
/// Check if the Camoufox browser binary is already installed. /// Check if the Camoufox browser binary is already installed.
fn is_browser_installed() -> bool { fn is_browser_installed() -> bool {
let cache_dir = camoufox_browser_cache_dir(); let cache_dir = camoufox_browser_cache_dir();
cache_dir.exists() && cache_dir.join("camoufox").exists() let app_bundle = cache_dir.join("Camoufox.app");
let binary = cache_dir.join("camoufox");
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()
} else {
// Linux/WSL: binary at ~/.cache/camoufox/camoufox
binary.exists()
}
}
/// Check if the browser binary appears corrupted (missing critical dylib).
/// Looks for common indicators in the daemon log.
fn check_known_errors(log: &str) -> Option<String> {
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(),
);
}
// 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(),
);
}
}
// 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(),
);
}
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",
}
} }
/// Initialize the camoufox-browser daemon and return a ready client. /// Initialize the camoufox-browser daemon and return a ready client.
@@ -120,18 +223,26 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
Ok(msg) => info!("Camoufox browser install: {}", msg), Ok(msg) => info!("Camoufox browser install: {}", msg),
Err(e) => { Err(e) => {
let npx_name = resolve_npx(node_path); let npx_name = resolve_npx(node_path);
let wsl_hint = if is_wsl() { let os_type = detect_os();
"\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" let platform_hint = match os_type {
} else { 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(),
}; };
return BrowserInitResult::Unavailable(format!( return BrowserInitResult::Unavailable(format!(
"Failed to download Camoufox browser: {}\n\n\ "Failed to download Camoufox browser: {}\n\n\
Run this manually to install:\n \ Run this manually to install:\n \
{} camoufox-browser@latest install\n\n\ {} camoufox-browser@latest install\n\n\
You need ~662MB of free disk space for the browser binary.\n\ You need ~662MB of free disk space for the browser binary.{}",
After install completes, run 'gyandoot run' again.{}", e, npx_name, platform_hint
e, npx_name, wsl_hint
)); ));
} }
} }
@@ -151,21 +262,13 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
} }
Ok(false) => { Ok(false) => {
let msg = format!( let msg = format!(
"Daemon did not start within {}s \u{2014} socket not found at {}\n\ "Camoufox daemon did not start within {}s \u{2014} socket not found at {}\n\n\
This usually means:\n \ The browser binary may be corrupted. Try fixing it:\n\
1. Missing Linux system deps (WSL/Ubuntu):\n \ {}\n\n\
sudo apt-get update && sudo apt-get install -y \\\n \ Then run 'gyandoot run' 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 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), platform_fix_instructions(),
); );
warn!("{}", msg); warn!("{}", msg);
BrowserInitResult::Unavailable(msg) BrowserInitResult::Unavailable(msg)
@@ -178,25 +281,38 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
} }
} }
Err(e) => { Err(e) => {
let error_detail = e.to_string();
let npx_name = resolve_npx(node_path); let npx_name = resolve_npx(node_path);
let msg = format!(
"Could not start camoufox daemon: {}\n\n\ // Check for known errors in the daemon output
The npm package 'camoufox-browser' is installed, and the browser\n\ let known_error = check_known_errors(&error_detail);
binary has been downloaded, but the daemon failed to start.\n\n\
Try running this manually to see the real error:\n \ // Build the full message
let mut msg = format!(
"\u{26a0} Camoufox daemon failed to start.\n\n\
Error:\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());
} else {
msg.push_str(&format!(
"Try running this manually to see the full error:\n \
{} camoufox-browser@latest tab_list --session default\n\n\ {} camoufox-browser@latest tab_list --session default\n\n\
Common fixes:\n\ Common fixes:\n\
1. Install missing system dependencies (Linux/WSL):\n \ {}\n\n\
sudo apt-get update && sudo apt-get install -y \\\n \ 3. If Node is via nvm/fnm/asdf, set browser.node_path in config:\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 \ [browser]\n \
node_path = {}", node_path = {}",
e, npx_name, example_npx_path() npx_name,
); platform_fix_instructions(),
example_npx_path()
));
}
msg.push_str("\n\nThen run 'gyandoot run' again.");
warn!("{}", msg); warn!("{}", msg);
BrowserInitResult::Unavailable(msg) BrowserInitResult::Unavailable(msg)
} }
@@ -249,7 +365,6 @@ async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result<
let npx = resolve_npx(node_path); let npx = resolve_npx(node_path);
// Run a command — the CLI auto-starts the daemon as a side effect
let output = Command::new(npx) let output = Command::new(npx)
.args(&[ .args(&[
"camoufox-browser@latest", "camoufox-browser@latest",
@@ -283,8 +398,7 @@ async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result<
)); ));
return Err(format!( return Err(format!(
"camoufox-browser daemon start failed (exit: {}):\n {}\n \u{2192} Full log saved to {}", "{}\n \u{2192} Full log saved to {}",
output.status.code().unwrap_or(-1),
details, details,
log_file.display(), log_file.display(),
)); ));