feat: embed daemon JS in binary, eliminate npm install of camoufox-browser
The daemon JS files (daemon.js, server.js, browser.js, protocol.js, snapshot.js, ipc.js) are now EMBEDDED in the compiled Rust binary via include_dir! and extracted to ~/.gyandoot/daemon/ on first run. This eliminates the need to npm install -g camoufox-browser@0.1.0, which bundles an incompatible camoufox-js@0.9.3 that caused: - 'Cannot open database' errors - Re-downloading 298MB browser on every navigation - Circular install/remove/install loops Now the daemon imports camoufox-js from the global npm install (v0.11.0) which is installed by install.sh. Clean, no bundled deps, no conflicts. Also removed: is_npm_package_installed(), install_npm_package(), find_daemon_js() — all replaced by gyandoot_browser::daemon::extract_daemon()
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
//! BrowserManager — Start, connect, and manage lifecycle of the camoufox-browser daemon.
|
||||
//!
|
||||
//! Uses `camoufox-js@0.11.0` (pinned, not @latest) for fetching the browser binary,
|
||||
//! and `camoufox-browser@0.1.0` (pinned) for the daemon Unix socket protocol.
|
||||
//! The daemon JS files (daemon.js, server.js, browser.js, protocol.js, snapshot.js, ipc.js)
|
||||
//! are EMBEDDED in the compiled binary at compile time via `include_dir!`.
|
||||
//! They are extracted to `~/.gyandoot/daemon/` on first run.
|
||||
//!
|
||||
//! Pinning rationale:
|
||||
//! - `camoufox-browser@0.1.0` bundles `camoufox-js@0.9.3` which downloads BROKEN
|
||||
//! alpha macOS builds (v150.0.2-alpha.26, missing libmozglue.dylib)
|
||||
//! - `camoufox-js@0.11.0` (latest, Jun 4 2026) downloads STABLE browser binaries
|
||||
//! - Both packages share the same cache directory (~/Library/Caches/camoufox/).
|
||||
//! We use the pinned `camoufox-js` for fetch, `camoufox-browser` for daemon.
|
||||
//! This eliminates the need to `npm install -g camoufox-browser@0.1.0`, which bundles
|
||||
//! an incompatible `camoufox-js@0.9.3`. The daemon imports `camoufox-js` from the
|
||||
//! global npm install (v0.11.0, installed by install.sh).
|
||||
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
@@ -26,10 +24,6 @@ use gyandoot_browser::{check_daemon_socket, default_socket_path, BrowserClient};
|
||||
/// v0.11.0 (Jun 4 2026) — latest stable, downloads correct macOS arm64/x64 builds.
|
||||
const CAMOUFOX_JS_VERSION: &str = "0.11.0";
|
||||
|
||||
/// Version of camoufox-browser for daemon IPC protocol.
|
||||
/// v0.1.0 (Mar 15 2026) — the only published version, daemon protocol is stable.
|
||||
const CAMOUFOX_BROWSER_VERSION: &str = "0.1.0";
|
||||
|
||||
/// Default session name.
|
||||
const DEFAULT_SESSION: &str = "default";
|
||||
|
||||
@@ -132,8 +126,8 @@ async fn remove_macos_quarantine() {
|
||||
///
|
||||
/// Steps:
|
||||
/// 1. Ensure browser binary is downloaded using pinned `camoufox-js@0.11.0`
|
||||
/// 2. Ensure npm package is available for daemon
|
||||
/// 3. Start daemon via pinned `camoufox-browser@0.1.0`
|
||||
/// 2. Extract embedded daemon JS files to `~/.gyandoot/daemon/`
|
||||
/// 3. Start daemon via `node ~/.gyandoot/daemon/daemon.js`
|
||||
/// 4. Connect to Unix socket
|
||||
pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
|
||||
let session = DEFAULT_SESSION;
|
||||
@@ -171,23 +165,20 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
|
||||
// (even if it was already installed — prior npx downloads may have set it)
|
||||
remove_macos_quarantine().await;
|
||||
|
||||
// 3. Ensure the `camoufox-browser` npm package is globally installed for daemon
|
||||
if !is_npm_package_installed(npx, "camoufox-browser", CAMOUFOX_BROWSER_VERSION).await {
|
||||
info!("Installing camoufox-browser@{} (daemon)...", CAMOUFOX_BROWSER_VERSION);
|
||||
match install_npm_package(npx, "camoufox-browser", CAMOUFOX_BROWSER_VERSION).await {
|
||||
Ok(_) => info!("camoufox-browser@{} installed", CAMOUFOX_BROWSER_VERSION),
|
||||
Err(e) => {
|
||||
return BrowserInitResult::Unavailable(format!(
|
||||
"Failed to install camoufox-browser: {}\n\n\
|
||||
Try manually: npm install -g camoufox-browser@{}",
|
||||
e, CAMOUFOX_BROWSER_VERSION
|
||||
));
|
||||
}
|
||||
// 3. Extract embedded daemon files to ~/.gyandoot/daemon/
|
||||
let daemon_path = match gyandoot_browser::daemon::extract_daemon() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
return BrowserInitResult::Unavailable(format!(
|
||||
"Failed to extract daemon files: {}\n\n\
|
||||
Make sure Node.js is installed and ~/.gyandoot/ is writable.",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Trigger daemon start
|
||||
match trigger_daemon_start(npx, session).await {
|
||||
match trigger_daemon_start(&daemon_path, session).await {
|
||||
Ok(()) => {
|
||||
info!("Waiting for daemon socket...");
|
||||
match wait_for_socket(session).await {
|
||||
@@ -207,10 +198,10 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
|
||||
libxkbcommon0 libxcomposite1 libxdamage1 \\\n \
|
||||
libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \\\n \
|
||||
libcairo2 libasound2t64 libxcb-shm0\n \
|
||||
2. Try debugging: {} camoufox-browser@{} tab_list --session default\n \
|
||||
2. Check daemon stderr log: ~/.gyandoot/camoufox-daemon-stderr.log\n \
|
||||
Log: {}",
|
||||
DAEMON_STARTUP_TIMEOUT_SECS,
|
||||
npx, CAMOUFOX_BROWSER_VERSION, socket_path.display()
|
||||
socket_path.display()
|
||||
);
|
||||
warn!("{}", msg);
|
||||
BrowserInitResult::Unavailable(msg)
|
||||
@@ -230,10 +221,7 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
|
||||
npx
|
||||
));
|
||||
} else {
|
||||
msg.push_str(&format!(
|
||||
"Debug: {} camoufox-browser@{} tab_list --session default",
|
||||
npx, CAMOUFOX_BROWSER_VERSION
|
||||
));
|
||||
msg.push_str("Check daemon stderr log: ~/.gyandoot/camoufox-daemon-stderr.log");
|
||||
}
|
||||
warn!("{}", msg);
|
||||
BrowserInitResult::Unavailable(msg)
|
||||
@@ -326,95 +314,15 @@ async fn run_fetch(npx: &str, js_version: &str) -> Result<String, String> {
|
||||
.to_string())
|
||||
}
|
||||
|
||||
/// Check if an npm package is already installed globally by checking for its daemon.js on disk.
|
||||
/// We do NOT run `npx --version` because that actually loads the package code,
|
||||
/// which fails if we've removed the bundled camoufox-js dependency.
|
||||
async fn is_npm_package_installed(_npx: &str, package: &str, _version: &str) -> bool {
|
||||
// Try `npm root -g` to find global node_modules
|
||||
let root_output = Command::new("npm")
|
||||
.args(&["root", "-g"])
|
||||
.output()
|
||||
.await;
|
||||
let global_root = match root_output {
|
||||
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
|
||||
_ => return false,
|
||||
};
|
||||
let daemon_path = std::path::Path::new(&global_root)
|
||||
.join(package)
|
||||
.join("dist")
|
||||
.join("daemon.js");
|
||||
if daemon_path.exists() {
|
||||
return true;
|
||||
}
|
||||
// Fallback: check common locations
|
||||
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp"));
|
||||
let fallbacks = vec![
|
||||
home.join(".nvm").join("versions").join("node").join("v22").join("lib").join("node_modules")
|
||||
.join(package).join("dist").join("daemon.js"),
|
||||
home.join(".nvm").join("versions").join("node").join("v24").join("lib").join("node_modules")
|
||||
.join(package).join("dist").join("daemon.js"),
|
||||
std::path::PathBuf::from("/usr/local/lib/node_modules").join(package).join("dist").join("daemon.js"),
|
||||
std::path::PathBuf::from("/opt/homebrew/lib/node_modules").join(package).join("dist").join("daemon.js"),
|
||||
];
|
||||
for fb in &fallbacks {
|
||||
if fb.exists() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Install an npm package globally, then remove the bundled camoufox-js
|
||||
/// so the daemon uses the globally installed version instead.
|
||||
async fn install_npm_package(_npx: &str, package: &str, version: &str) -> Result<String, String> {
|
||||
let output = Command::new("npm")
|
||||
.args(&["install", "-g", &format!("{package}@{version}")])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("npm not found: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
return Err(stderr.lines().last().unwrap_or("(unknown)").to_string());
|
||||
}
|
||||
|
||||
// After install, remove the bundled camoufox-js so Node resolves to the global version
|
||||
let root_output = Command::new("npm")
|
||||
.args(&["root", "-g"])
|
||||
.output()
|
||||
.await;
|
||||
if let Ok(o) = root_output {
|
||||
if o.status.success() {
|
||||
let global_root = String::from_utf8_lossy(&o.stdout).trim().to_string();
|
||||
let bundled = std::path::Path::new(&global_root)
|
||||
.join(package)
|
||||
.join("node_modules")
|
||||
.join("camoufox-js");
|
||||
if bundled.exists() {
|
||||
let _ = std::fs::remove_dir_all(&bundled);
|
||||
info!("Removed bundled camoufox-js from {} (daemon will use global version)", package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(format!("{package}@{version} installed"))
|
||||
}
|
||||
|
||||
/// Trigger daemon startup by spawning the daemon.js directly with node.
|
||||
/// This is better than going through `npx camoufox-browser tab_list` because:
|
||||
/// 1. The npm CLI wrapper spawns the daemon with stdio: "ignore" (hides errors)
|
||||
/// 2. We capture stderr directly to diagnose crashes
|
||||
/// 3. We can wait longer than the 5s default in daemon-client.js
|
||||
async fn trigger_daemon_start(npx: &str, session: &str) -> Result<(), String> {
|
||||
/// Trigger daemon startup by spawning the embedded daemon.js with node.
|
||||
/// The daemon files are extracted to `~/.gyandoot/daemon/` at compile time.
|
||||
async fn trigger_daemon_start(daemon_path: &std::path::Path, session: &str) -> Result<(), String> {
|
||||
let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
|
||||
let log_dir = home.join(".gyandoot");
|
||||
std::fs::create_dir_all(&log_dir)
|
||||
.map_err(|e| format!("Cannot create {}: {}", log_dir.display(), e))?;
|
||||
let log_file = log_dir.join("camoufox-daemon-spawn.log");
|
||||
|
||||
// Find the daemon.js path in the globally installed camoufox-browser package
|
||||
let daemon_path = find_daemon_js(npx).await?;
|
||||
|
||||
info!("Starting daemon: node {} (session={})", daemon_path.display(), session);
|
||||
|
||||
// Spawn the daemon in the background — it's a long-running server
|
||||
@@ -454,62 +362,6 @@ async fn trigger_daemon_start(npx: &str, session: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find the daemon.js entrypoint in the globally installed camoufox-browser package.
|
||||
/// Tries: npm root -g → find dist/daemon.js
|
||||
async fn find_daemon_js(_npx: &str) -> Result<std::path::PathBuf, String> {
|
||||
// Try `npm root -g` to find global node_modules
|
||||
let root_output = Command::new("npm")
|
||||
.args(&["root", "-g"])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Cannot find npm global root: {}", e))?;
|
||||
|
||||
if !root_output.status.success() {
|
||||
return Err("npm root -g failed".to_string());
|
||||
}
|
||||
|
||||
let global_root = String::from_utf8_lossy(&root_output.stdout).trim().to_string();
|
||||
let daemon_path = std::path::Path::new(&global_root)
|
||||
.join("camoufox-browser")
|
||||
.join("dist")
|
||||
.join("daemon.js");
|
||||
|
||||
if daemon_path.exists() {
|
||||
info!("Found daemon at {}", daemon_path.display());
|
||||
return Ok(daemon_path);
|
||||
}
|
||||
|
||||
// Fallback: try common locations
|
||||
let fallbacks = vec![
|
||||
home_dir().join(".nvm").join("versions").join("node").join("v22").join("lib").join("node_modules")
|
||||
.join("camoufox-browser").join("dist").join("daemon.js"),
|
||||
home_dir().join(".nvm").join("versions").join("node").join("v24").join("lib").join("node_modules")
|
||||
.join("camoufox-browser").join("dist").join("daemon.js"),
|
||||
std::path::PathBuf::from("/usr/local/lib/node_modules/camoufox-browser/dist/daemon.js"),
|
||||
std::path::PathBuf::from("/opt/homebrew/lib/node_modules/camoufox-browser/dist/daemon.js"),
|
||||
];
|
||||
|
||||
for fb in &fallbacks {
|
||||
if fb.exists() {
|
||||
info!("Found daemon at {} (fallback)", fb.display());
|
||||
return Ok(fb.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"camoufox-browser daemon.js not found.\n\
|
||||
Tried: {}\n\
|
||||
And fallback paths.\n\
|
||||
Run: npm install -g camoufox-browser@{}",
|
||||
daemon_path.display(),
|
||||
CAMOUFOX_BROWSER_VERSION
|
||||
))
|
||||
}
|
||||
|
||||
fn home_dir() -> std::path::PathBuf {
|
||||
dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
|
||||
}
|
||||
|
||||
async fn wait_for_socket(session: &str) -> Result<bool, String> {
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(DAEMON_STARTUP_TIMEOUT_SECS);
|
||||
while tokio::time::Instant::now() < deadline {
|
||||
@@ -519,4 +371,4 @@ async fn wait_for_socket(session: &str) -> Result<bool, String> {
|
||||
tokio::time::sleep(Duration::from_millis(DAEMON_POLL_MS)).await;
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user