Option C: Rewrite gyandoot-browser for camoufox-browser@0.1.0 daemon IPC

Complete rewrite removing old HTTP REST client. The latest camoufox-browser
npm package (v0.1.0) uses Unix socket JSON-line IPC, not HTTP REST on :9377.

Changes:
- gyandoot-browser/client.rs: Full daemon IPC via Unix socket (~/.camoufox-browser/*.sock)
  - JSON-line protocol matching ensureDaemonRunning + daemon.js
  - Auto-launches browser on first command (daemon manages lifecycle)
  - Supports navigate, click, type, scroll, snapshot, search macros, tab management
  - Proper HeathResponse, NavigationResult, PageSnapshot types
- gyandoot-browser/lib.rs: Exports new client API (no SearchMacro enum, no SessionManager)
- gyandoot-browser/Cargo.toml: reqwest -> tokio Unix socket deps
- Removed: macros.rs, session.rs, snapshot.rs (all merged into client.rs)
- browser_manager.rs: Triggers daemon via npx, waits for Unix socket
- browser_tools.rs: Simplified API (no tab_id, no create_tab/close_tab)

Fix: camoufox-browser process exited with status 1 error on fresh install
This commit is contained in:
2026-06-12 11:12:51 +05:30
parent b2650a569c
commit e06094c0a2
9 changed files with 733 additions and 1102 deletions

View File

@@ -1,174 +1,145 @@
//! BrowserManager — Auto-start, health-check, and lifecycle for Camoufox.
//! BrowserManager — Start, connect, and manage lifecycle of the camoufox-browser daemon.
//!
//! The `camoufox-browser@0.1.0` CLI auto-starts a background daemon
//! (Node.js process) on any command. The daemon listens on a Unix socket at
//! `~/.camoufox-browser/<session>.sock` and auto-launches the camoufox browser
//! on the first browser-dependent command.
//!
//! On `gyandoot run`, the BrowserManager:
//! 1. Checks if Camoufox is already running (health endpoint)
//! 2. If not, starts it as a background child process
//! 3. Waits for it to become healthy (up to 30 seconds)
//! 1. Checks if the daemon socket already exists and is connected
//! 2. If not, runs a quick CLI command to trigger daemon auto-start
//! 3. Waits for the Unix socket to appear (up to 30 seconds)
//! 4. Returns a ready-to-use `BrowserClient`
//!
//! Camoufox is installed globally via `npx camoufox-browser` by `install.sh`.
use std::process::Stdio;
use std::time::Duration;
use tokio::process::Command;
use tracing::{info, warn, error};
use tracing::{info, warn};
use gyandoot_browser::BrowserClient;
use gyandoot_browser::{check_daemon_socket, default_socket_path, BrowserClient};
/// Default Camoufox API endpoint.
const CAMOFOX_URL: &str = "http://localhost:9377";
/// Default session name.
const DEFAULT_SESSION: &str = "default";
/// Maximum time to wait for Camoufox to become healthy.
const STARTUP_TIMEOUT_SECS: u64 = 30;
/// Maximum time to wait for the daemon socket to appear.
const DAEMON_STARTUP_TIMEOUT_SECS: u64 = 30;
/// Health-check retry interval.
const HEALTH_RETRY_MS: u64 = 500;
/// Poll interval while waiting for daemon to start.
const DAEMON_POLL_MS: u64 = 500;
/// Result of browser initialization.
#[derive(Debug)]
pub enum BrowserInitResult {
/// Browser is running and ready.
/// Browser daemon is running and client is ready.
Ready(BrowserClient),
/// Could not start browser (user needs to install/start manually).
/// Could not start daemon.
Unavailable(String),
}
/// Initialize the Camoufox browser: check health, start if needed.
///
/// Returns a `BrowserInitResult` — either a ready client or an explanation.
/// Initialize the camoufox-browser daemon and return a ready client.
pub async fn init_browser() -> BrowserInitResult {
let client = BrowserClient::new();
let session = DEFAULT_SESSION;
let socket_path = default_socket_path(session);
// 1. Quick health check — is it already running?
match try_health_check(&client).await {
Ok(true) => {
info!("Camoufox already running on localhost:9377");
return BrowserInitResult::Ready(client);
}
Ok(false) => {
info!("Camoufox health check returned not-ready, attempting to start...");
}
Err(_) => {
info!("Camoufox not running, attempting to start...");
}
// 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);
return BrowserInitResult::Ready(client);
}
// 2. Try to start Camoufox as a background process
match start_camoufox().await {
info!("Camoufox daemon not running, triggering startup...");
// 2. Trigger daemon auto-start via CLI
// The CLI calls `ensureDaemonRunning()` internally which spawns the daemon
match trigger_daemon_start(session).await {
Ok(()) => {
// 3. Wait for it to become healthy
info!("Waiting for Camoufox to become healthy...");
match wait_for_health(&client).await {
// 3. Wait for socket to appear
info!("Waiting for daemon socket at {}...", socket_path.display());
match wait_for_socket(session).await {
Ok(true) => {
info!("Camoufox started successfully");
return BrowserInitResult::Ready(client);
info!("Camoufox daemon started successfully");
let client = BrowserClient::new().with_session(session);
// Brief pause for daemon to fully initialize
tokio::time::sleep(Duration::from_millis(1000)).await;
BrowserInitResult::Ready(client)
}
Ok(false) => {
let msg = format!(
"Camoufox process started but did not become healthy within {}s. \
Check the logs at ~/.gyandoot/camoufox.log",
STARTUP_TIMEOUT_SECS
"Daemon did not start within {}s — socket not found at {}",
DAEMON_STARTUP_TIMEOUT_SECS,
socket_path.display()
);
warn!("{}", msg);
return BrowserInitResult::Unavailable(msg);
BrowserInitResult::Unavailable(msg)
}
Err(e) => {
let msg = format!("Camoufox health-check error after startup: {}", e);
let msg = format!("Daemon socket error: {}", e);
warn!("{}", msg);
return BrowserInitResult::Unavailable(msg);
BrowserInitResult::Unavailable(msg)
}
}
}
Err(e) => {
let msg = format!(
"Could not start Camoufox: {}\n\n\
Install it manually:\n npm install -g camoufox-browser\n \
"Could not start camoufox daemon: {}\n\n\
Make sure Node.js v20+ and the package are installed:\n \
npm install -g camoufox-browser\n \
npx camoufox-browser@latest install\n\n\
Then start before running gyandoot:\n npx camoufox-browser start &",
Then try again.",
e
);
warn!("{}", msg);
return BrowserInitResult::Unavailable(msg);
BrowserInitResult::Unavailable(msg)
}
}
}
/// Quick health check — returns true if browser is connected and running.
async fn try_health_check(client: &BrowserClient) -> Result<bool, String> {
match tokio::time::timeout(Duration::from_secs(3), client.health()).await {
Ok(Ok(health)) => {
Ok(health.browser_connected && health.browser_running)
}
Ok(Err(e)) => Err(e.to_string()),
Err(_) => Err("timeout".to_string()),
}
}
/// Start Camoufox as a background child process.
async fn start_camoufox() -> Result<(), String> {
// Create log directory
/// Trigger daemon startup by running a quick CLI command.
///
/// The `camoufox-browser@latest` CLI auto-starts the daemon via
/// `ensureDaemonRunning()` on any command. We run a harmless
/// `tab_list` which spawns the daemon in the background and
/// returns once complete.
async fn trigger_daemon_start(session: &str) -> Result<(), String> {
// Create log directory for any stderr output
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.log");
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");
let log = std::fs::File::create(&log_file)
.map_err(|e| format!("Cannot create log file {}: {}", log_file.display(), e))?;
.map_err(|e| format!("Cannot create log file: {}", e))?;
// Try npx first (npm global install), then npx direct
let result = Command::new("npx")
// Run a command — the CLI auto-starts the daemon as a side effect
let _output = Command::new("npx")
.args(&[
"camoufox-browser@latest",
"start",
"--port", "9377",
"tab_list",
"--session", session,
])
.stdout(Stdio::from(log.try_clone().map_err(|e| e.to_string())?))
.stderr(Stdio::from(log))
.kill_on_drop(true)
.spawn();
.output()
.await
.map_err(|e| format!("npx failed: {}. Is Node.js installed?", e))?;
match result {
Ok(mut child) => {
// Detach — let it run in background
tokio::spawn(async move {
let status = child.wait().await;
if let Ok(s) = status {
if !s.success() {
warn!("Camoufox process exited with status: {}", s);
}
}
});
// Give it a moment to bind
tokio::time::sleep(Duration::from_millis(1500)).await;
Ok(())
}
Err(e) => {
// npx not available — try directly via node_modules
Err(format!("npx failed: {}. Is Node.js installed?", e))
}
}
// Return immediately — socket polling in the caller will detect the daemon
Ok(())
}
/// Poll the health endpoint until the browser is ready or timeout.
async fn wait_for_health(client: &BrowserClient) -> Result<bool, String> {
let deadline = tokio::time::Instant::now() + Duration::from_secs(STARTUP_TIMEOUT_SECS);
let mut last_error = String::new();
/// Poll for the daemon Unix socket to appear.
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 {
match try_health_check(client).await {
Ok(true) => return Ok(true),
Ok(false) => {
last_error = "browser not connected".to_string();
}
Err(e) => {
last_error = e;
}
if check_daemon_socket(session).await {
return Ok(true);
}
tokio::time::sleep(Duration::from_millis(HEALTH_RETRY_MS)).await;
tokio::time::sleep(Duration::from_millis(DAEMON_POLL_MS)).await;
}
Err(format!(
"Timed out after {}s. Last status: {}",
STARTUP_TIMEOUT_SECS, last_error
))
Ok(false)
}