fix(browser): clean reimplementation — detect corrupted dl + Docker check
Clean build: 0 errors, 0 warnings
This commit is contained in:
@@ -62,7 +62,6 @@ fn detect_os() -> OsKind {
|
|||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
return OsKind::Mac;
|
return OsKind::Mac;
|
||||||
}
|
}
|
||||||
// Linux — check for WSL
|
|
||||||
if std::fs::read_to_string("/proc/version")
|
if std::fs::read_to_string("/proc/version")
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
let l = c.to_lowercase();
|
let l = c.to_lowercase();
|
||||||
@@ -99,8 +98,6 @@ fn example_npx_path() -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Path to the Camoufox browser binary cache directory.
|
/// Path to the Camoufox browser binary cache directory.
|
||||||
/// 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") {
|
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 {
|
fn is_browser_installed() -> bool {
|
||||||
let cache_dir = camoufox_browser_cache_dir();
|
let cache_dir = camoufox_browser_cache_dir();
|
||||||
let app_bundle = cache_dir.join("Camoufox.app");
|
if !cache_dir.exists() {
|
||||||
let binary = cache_dir.join("camoufox");
|
return false;
|
||||||
|
}
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
// macOS: binary lives inside Camoufox.app/Contents/MacOS/camoufox
|
let macos_dir = cache_dir.join("Camoufox.app").join("Contents").join("MacOS");
|
||||||
app_bundle.exists() && app_bundle.join("Contents").join("MacOS").join("camoufox").exists()
|
let binary = macos_dir.join("camoufox");
|
||||||
|
let lib = macos_dir.join("libmozglue.dylib");
|
||||||
|
binary.exists() && lib.exists()
|
||||||
} else {
|
} else {
|
||||||
// Linux/WSL: binary at ~/.cache/camoufox/camoufox
|
cache_dir.join("camoufox").exists()
|
||||||
binary.exists()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the browser binary appears corrupted (missing critical dylib).
|
/// Check for known error patterns in the daemon log and return a user-friendly hint.
|
||||||
/// Looks for common indicators in the daemon log.
|
|
||||||
fn check_known_errors(log: &str) -> Option<String> {
|
fn check_known_errors(log: &str) -> Option<String> {
|
||||||
let lower = log.to_lowercase();
|
let lower = log.to_lowercase();
|
||||||
|
|
||||||
// macOS: missing libmozglue.dylib — corrupted download
|
|
||||||
if lower.contains("libmozglue.dylib") && lower.contains("no such file") {
|
if lower.contains("libmozglue.dylib") && lower.contains("no such file") {
|
||||||
return Some(
|
return Some("The Camoufox browser download is corrupted (missing libmozglue.dylib). \
|
||||||
"The Camoufox browser download appears corrupted (missing libmozglue.dylib). \
|
This is a known issue with the camoufox-browser@0.1.0 package on macOS."
|
||||||
This can happen with unstable alpha versions."
|
.to_string());
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux: missing libgtk / libnss
|
|
||||||
if lower.contains("error while loading shared libraries") {
|
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") {
|
if lower.contains("killed") && lower.contains("code signature") {
|
||||||
return Some(
|
return Some("macOS code signing issue. The downloaded browser binary may need manual approval."
|
||||||
"macOS code signing issue. The downloaded browser binary may need manual approval."
|
.to_string());
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get platform-specific dependency fix instructions.
|
/// Get platform-specific dependency fix instructions.
|
||||||
fn platform_fix_instructions() -> &'static str {
|
fn platform_fix_instructions() -> String {
|
||||||
match detect_os() {
|
let os = detect_os();
|
||||||
OsKind::Mac => "\
|
let mut msg = String::new();
|
||||||
\u{2192} The Camoufox browser download was corrupted. Fix:\n \
|
|
||||||
1. Remove the corrupted cache:\n \
|
let docker_ok = std::process::Command::new("docker")
|
||||||
rm -rf ~/Library/Caches/camoufox\n \
|
.arg("info")
|
||||||
2. Reinstall the browser binary:\n \
|
.output()
|
||||||
npx camoufox-browser@latest install\n \
|
.map(|o| o.status.success())
|
||||||
3. The download is ~662MB. If it still fails, try:\n \
|
.unwrap_or(false);
|
||||||
npx camoufox-browser@latest install 2>&1 | tail -20\n \
|
|
||||||
to see the real download error.\n \
|
match os {
|
||||||
4. Alternative: use Docker instead:\n \
|
OsKind::Mac => {
|
||||||
docker run -d --name camoufox -p 9377:9377 \\\n \
|
msg.push_str("The Camoufox browser download is corrupted on macOS.\n");
|
||||||
ghcr.io/nousresearch/camoufox-browser:latest",
|
msg.push_str("Fix:\n");
|
||||||
OsKind::Wsl => "\
|
msg.push_str(" 1. Remove the corrupted cache:\n");
|
||||||
\u{2192} Missing Linux dependencies on WSL. Install them:\n \
|
msg.push_str(" rm -rf ~/Library/Caches/camoufox\n");
|
||||||
sudo apt-get update && sudo apt-get install -y \\\n \
|
msg.push_str(" 2. Reinstall the browser binary:\n");
|
||||||
libgtk-3-0 libnss3 libnspr4 libatk1.0-0t64 \\\n \
|
msg.push_str(" npx camoufox-browser@latest install\n");
|
||||||
libatk-bridge2.0-0t64 libcups2t64 libdrm2 \\\n \
|
msg.push_str(" (this downloads ~662MB, may take a few minutes)\n");
|
||||||
libxkbcommon0 libxcomposite1 libxdamage1 \\\n \
|
if docker_ok {
|
||||||
libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \\\n \
|
msg.push_str(" 3. Or use Docker (already installed!):\n");
|
||||||
libcairo2 libasound2t64 libxcb-shm0 \\\n \
|
} else {
|
||||||
libx11-xcb1 libx11-6 libxcb1 libxext6 \\\n \
|
msg.push_str(" 3. Or install Docker Desktop and use:\n");
|
||||||
libxcursor1 libxi6 libfreetype6 libfontconfig1",
|
}
|
||||||
OsKind::Linux => "\
|
msg.push_str(" docker run -d --name camoufox -p 9377:9377 \\\n");
|
||||||
\u{2192} Missing Linux dependencies. Install them:\n \
|
msg.push_str(" ghcr.io/nousresearch/camoufox-browser:latest\n");
|
||||||
sudo apt-get update && sudo apt-get install -y \\\n \
|
}
|
||||||
libgtk-3-0 libnss3 libnspr4 libatk1.0-0t64 \\\n \
|
OsKind::Wsl | OsKind::Linux => {
|
||||||
libatk-bridge2.0-0t64 libcups2t64 libdrm2 \\\n \
|
msg.push_str("Missing Linux system libraries. Install them:\n");
|
||||||
libxkbcommon0 libxcomposite1 libxdamage1 \\\n \
|
msg.push_str(" sudo apt-get update && sudo apt-get install -y \\\n");
|
||||||
libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \\\n \
|
msg.push_str(" libgtk-3-0 libnss3 libnspr4 libatk1.0-0t64 \\\n");
|
||||||
libcairo2 libasound2t64 libxcb-shm0",
|
msg.push_str(" libatk-bridge2.0-0t64 libcups2t64 libdrm2 \\\n");
|
||||||
OsKind::Windows => "\
|
msg.push_str(" libxkbcommon0 libxcomposite1 libxdamage1 \\\n");
|
||||||
\u{2192} Windows issue. Try reinstalling camoufox-browser:\n \
|
msg.push_str(" libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \\\n");
|
||||||
npm install -g camoufox-browser",
|
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.
|
/// 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 {
|
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);
|
||||||
|
|
||||||
// 1. Quick check — is the daemon already running?
|
|
||||||
if check_daemon_socket(session).await {
|
if check_daemon_socket(session).await {
|
||||||
info!("Camoufox daemon already running on socket: {}", socket_path.display());
|
info!("Camoufox daemon already running on socket: {}", socket_path.display());
|
||||||
let client = BrowserClient::new().with_session(session);
|
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...");
|
info!("Camoufox daemon not running, triggering startup...");
|
||||||
|
|
||||||
// 2. Check — is the browser binary cached? If not, run install first.
|
|
||||||
if !is_browser_installed() {
|
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 {
|
match run_install(node_path).await {
|
||||||
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 os_type = detect_os();
|
let mut hint = String::new();
|
||||||
let platform_hint = match os_type {
|
let os = detect_os();
|
||||||
OsKind::Wsl => "\n\n \u{26a0} WSL detected: Make sure you're not behind a proxy/firewall that blocks \
|
if os == OsKind::Wsl {
|
||||||
the GitHub release download. The browser binary is ~662MB.\n \
|
hint.push_str(
|
||||||
Try: curl -L https://github.com/msly/camoufox-browser/releases/download/v0.1.0/camoufox-browser-linux-x64 -o /dev/null"
|
"\n\nWSL detected: Make sure you can download from GitHub releases.\n",
|
||||||
.to_string(),
|
);
|
||||||
OsKind::Mac => "\n\n \u{26a0} macOS: The download server may be slow or unstable. Try:\n \
|
hint.push_str("Try: curl -L --fail https://github.com/msly/camoufox-browser/releases\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 manually:\n {} camoufox-browser@latest install\n{}",
|
||||||
{} camoufox-browser@latest install\n\n\
|
e, npx_name, hint
|
||||||
You need ~662MB of free disk space for the browser binary.{}",
|
|
||||||
e, npx_name, platform_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(()) => {
|
||||||
// 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) => {
|
||||||
@@ -262,10 +239,8 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
|
|||||||
}
|
}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"Camoufox daemon did not start within {}s \u{2014} socket not found at {}\n\n\
|
"Camoufox daemon did not start within {}s.\n\
|
||||||
The browser binary may be corrupted. Try fixing it:\n\
|
Socket not found at {}\n\n{}",
|
||||||
{}\n\n\
|
|
||||||
Then run 'gyandoot run' again.",
|
|
||||||
DAEMON_STARTUP_TIMEOUT_SECS,
|
DAEMON_STARTUP_TIMEOUT_SECS,
|
||||||
socket_path.display(),
|
socket_path.display(),
|
||||||
platform_fix_instructions(),
|
platform_fix_instructions(),
|
||||||
@@ -282,33 +257,18 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error_detail = e.to_string();
|
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);
|
let known_error = check_known_errors(&error_detail);
|
||||||
|
let mut msg = format!("Camoufox daemon failed to start.\n\n{}\n\n", 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,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(hint) = known_error {
|
if let Some(hint) = known_error {
|
||||||
msg.push_str(&format!("Detected: {}\n\n", hint));
|
msg.push_str(&hint);
|
||||||
msg.push_str(platform_fix_instructions());
|
msg.push('\n');
|
||||||
|
msg.push_str(&platform_fix_instructions());
|
||||||
} else {
|
} else {
|
||||||
msg.push_str(&format!(
|
msg.push_str(&format!(
|
||||||
"Try running this manually to see the full error:\n \
|
"Try running manually:\n {} camoufox-browser@latest tab_list --session default\n\n{}",
|
||||||
{} camoufox-browser@latest tab_list --session default\n\n\
|
resolve_npx(node_path),
|
||||||
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,
|
|
||||||
platform_fix_instructions(),
|
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> {
|
async fn run_install(node_path: Option<&str>) -> Result<String, String> {
|
||||||
let npx = resolve_npx(node_path);
|
let npx = resolve_npx(node_path);
|
||||||
|
|
||||||
let output = Command::new(npx)
|
let output = Command::new(npx)
|
||||||
.args(&[
|
.args(&["camoufox-browser@latest", "install"])
|
||||||
"camoufox-browser@latest",
|
|
||||||
"install",
|
|
||||||
])
|
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Cannot run '{} camoufox-browser install': {}", npx, e))?;
|
.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
|
||||||
let last_line = stdout.lines()
|
.lines()
|
||||||
.chain(stderr.lines())
|
.chain(stderr.lines())
|
||||||
.filter(|l| l.contains("complete") || l.contains("Downloaded") || l.contains("Updated"))
|
.filter(|l| l.contains("complete") || l.contains("Downloaded") || l.contains("Updated"))
|
||||||
.last()
|
.last()
|
||||||
@@ -355,7 +312,7 @@ async fn run_install(node_path: Option<&str>) -> Result<String, String> {
|
|||||||
Ok(last_line)
|
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> {
|
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")?;
|
||||||
let log_dir = home.join(".gyandoot");
|
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 npx = resolve_npx(node_path);
|
||||||
|
|
||||||
let output = Command::new(npx)
|
let output = Command::new(npx)
|
||||||
.args(&[
|
.args(&["camoufox-browser@latest", "tab_list", "--session", session])
|
||||||
"camoufox-browser@latest",
|
|
||||||
"tab_list",
|
|
||||||
"--session", session,
|
|
||||||
])
|
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
let hint = if node_path.is_none() {
|
let hint = if node_path.is_none() {
|
||||||
format!(
|
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()
|
example_npx_path()
|
||||||
)
|
)
|
||||||
} else {
|
} 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)
|
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() {
|
if !output.status.success() {
|
||||||
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 } else { stdout };
|
||||||
|
|
||||||
// Save to log file
|
let _ = std::fs::write(
|
||||||
let _ = std::fs::write(&log_file, format!(
|
&log_file,
|
||||||
"exit_code: {}\nstdout:\n{}\nstderr:\n{}\n",
|
format!(
|
||||||
output.status.code().unwrap_or(-1),
|
"exit_code: {}\nstderr:\n{}\nstdout:\n{}\n",
|
||||||
stdout, stderr
|
output.status.code().unwrap_or(-1),
|
||||||
));
|
String::from_utf8_lossy(&output.stderr),
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"{}\n \u{2192} Full log saved to {}",
|
"{}\n Full log saved to {}",
|
||||||
details,
|
details,
|
||||||
log_file.display(),
|
log_file.display(),
|
||||||
));
|
));
|
||||||
|
|||||||
Reference in New Issue
Block a user