//! 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/.sock` and auto-launches the camoufox browser //! on the first browser-dependent command. //! //! On `gyandoot run`, the BrowserManager: //! 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` use std::process::Stdio; use std::time::Duration; use tokio::process::Command; use tracing::{info, warn}; use gyandoot_browser::{check_daemon_socket, default_socket_path, BrowserClient}; /// Default session name. const DEFAULT_SESSION: &str = "default"; /// Maximum time to wait for the daemon socket to appear. const DAEMON_STARTUP_TIMEOUT_SECS: u64 = 30; /// Poll interval while waiting for daemon to start. const DAEMON_POLL_MS: u64 = 500; /// Result of browser initialization. #[derive(Debug)] pub enum BrowserInitResult { /// Browser daemon is running and client is ready. Ready(BrowserClient), /// Could not start daemon. Unavailable(String), } /// Initialize the camoufox-browser daemon and return a ready client. pub async fn init_browser() -> 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); return BrowserInitResult::Ready(client); } 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 socket to appear info!("Waiting for daemon socket at {}...", socket_path.display()); match wait_for_socket(session).await { Ok(true) => { 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!( "Daemon did not start within {}s — socket not found at {}", DAEMON_STARTUP_TIMEOUT_SECS, socket_path.display() ); warn!("{}", msg); BrowserInitResult::Unavailable(msg) } Err(e) => { let msg = format!("Daemon socket error: {}", e); warn!("{}", msg); BrowserInitResult::Unavailable(msg) } } } Err(e) => { let msg = format!( "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 try again.", e ); warn!("{}", msg); BrowserInitResult::Unavailable(msg) } } } /// 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-daemon-spawn.log"); let log = std::fs::File::create(&log_file) .map_err(|e| format!("Cannot create log file: {}", e))?; // Run a command — the CLI auto-starts the daemon as a side effect let _output = Command::new("npx") .args(&[ "camoufox-browser@latest", "tab_list", "--session", session, ]) .stdout(Stdio::from(log.try_clone().map_err(|e| e.to_string())?)) .stderr(Stdio::from(log)) .output() .await .map_err(|e| format!("npx failed: {}. Is Node.js installed?", e))?; // Return immediately — socket polling in the caller will detect the daemon Ok(()) } /// Poll for the daemon Unix socket to appear. async fn wait_for_socket(session: &str) -> Result { let deadline = tokio::time::Instant::now() + Duration::from_secs(DAEMON_STARTUP_TIMEOUT_SECS); while tokio::time::Instant::now() < deadline { if check_daemon_socket(session).await { return Ok(true); } tokio::time::sleep(Duration::from_millis(DAEMON_POLL_MS)).await; } Ok(false) }