fix(browser): clean reimplementation — detect corrupted dl + Docker check

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

View File

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