🚀 Camoufox default web engine — auto-install + auto-start
Full integration:
- install.sh now installs camoufox-browser via npm (preferred) or Docker
- install.sh creates systemd/launchd auto-start service on Linux/macOS
- New browser_manager.rs — health-check, auto-start, wait-for-ready
- AgentLoop::run() auto-inits Camoufox and registers web_search/web_browse
- System prompt tells LLM to use web_search/web_browse as PRIMARY tools
- Graceful fallback: if Camoufox unavailable, logs warning, uses bash tools
- ToolExecutor is now Arc<Mutex<>> for runtime tool registration
GyanDoot v2 now: install → Camoufox auto-starts → web tools available →
"search NSE India" uses Camoufox with anti-detection 🎯
This commit is contained in:
174
crates/gyandoot-agent/src/browser_manager.rs
Normal file
174
crates/gyandoot-agent/src/browser_manager.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
//! BrowserManager — Auto-start, health-check, and lifecycle for Camoufox.
|
||||
//!
|
||||
//! 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)
|
||||
//! 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 gyandoot_browser::BrowserClient;
|
||||
|
||||
/// Default Camoufox API endpoint.
|
||||
const CAMOFOX_URL: &str = "http://localhost:9377";
|
||||
|
||||
/// Maximum time to wait for Camoufox to become healthy.
|
||||
const STARTUP_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
/// Health-check retry interval.
|
||||
const HEALTH_RETRY_MS: u64 = 500;
|
||||
|
||||
/// Result of browser initialization.
|
||||
#[derive(Debug)]
|
||||
pub enum BrowserInitResult {
|
||||
/// Browser is running and ready.
|
||||
Ready(BrowserClient),
|
||||
/// Could not start browser (user needs to install/start manually).
|
||||
Unavailable(String),
|
||||
}
|
||||
|
||||
/// Initialize the Camoufox browser: check health, start if needed.
|
||||
///
|
||||
/// Returns a `BrowserInitResult` — either a ready client or an explanation.
|
||||
pub async fn init_browser() -> BrowserInitResult {
|
||||
let client = BrowserClient::new();
|
||||
|
||||
// 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...");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try to start Camoufox as a background process
|
||||
match start_camoufox().await {
|
||||
Ok(()) => {
|
||||
// 3. Wait for it to become healthy
|
||||
info!("Waiting for Camoufox to become healthy...");
|
||||
match wait_for_health(&client).await {
|
||||
Ok(true) => {
|
||||
info!("Camoufox started successfully");
|
||||
return 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
|
||||
);
|
||||
warn!("{}", msg);
|
||||
return BrowserInitResult::Unavailable(msg);
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("Camoufox health-check error after startup: {}", e);
|
||||
warn!("{}", msg);
|
||||
return BrowserInitResult::Unavailable(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!(
|
||||
"Could not start Camoufox: {}\n\n\
|
||||
Install it manually:\n npm install -g camoufox-browser\n \
|
||||
npx camoufox-browser@latest install\n\n\
|
||||
Then start before running gyandoot:\n npx camoufox-browser start &",
|
||||
e
|
||||
);
|
||||
warn!("{}", msg);
|
||||
return 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
|
||||
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");
|
||||
|
||||
let log = std::fs::File::create(&log_file)
|
||||
.map_err(|e| format!("Cannot create log file {}: {}", log_file.display(), e))?;
|
||||
|
||||
// Try npx first (npm global install), then npx direct
|
||||
let result = Command::new("npx")
|
||||
.args(&[
|
||||
"camoufox-browser@latest",
|
||||
"start",
|
||||
"--port", "9377",
|
||||
])
|
||||
.stdout(Stdio::from(log.try_clone().map_err(|e| e.to_string())?))
|
||||
.stderr(Stdio::from(log))
|
||||
.kill_on_drop(true)
|
||||
.spawn();
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(HEALTH_RETRY_MS)).await;
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"Timed out after {}s. Last status: {}",
|
||||
STARTUP_TIMEOUT_SECS, last_error
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user