use super::config::BrowserConfig;
use async_trait::async_trait;
use limit_agent::error::AgentError;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::process::Command as AsyncCommand;
use tokio::time::{timeout, Duration};
#[derive(Debug, Clone)]
pub struct BrowserOutput {
pub stdout: String,
pub stderr: String,
pub exit_code: Option<i32>,
pub success: bool,
}
impl BrowserOutput {
pub fn success(stdout: String) -> Self {
Self {
stdout,
stderr: String::new(),
exit_code: Some(0),
success: true,
}
}
pub fn failure(stderr: String, exit_code: Option<i32>) -> Self {
Self {
stdout: String::new(),
stderr,
exit_code,
success: false,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum BrowserError {
#[error("Failed to execute browser command: {0}")]
ExecutionFailed(String),
#[error("Browser operation timed out after {0}ms")]
Timeout(u64),
#[error("Invalid arguments: {0}")]
InvalidArguments(String),
#[error("Browser binary not found: {0}")]
NotFound(String),
#[error("Failed to parse output: {0}")]
ParseError(String),
#[error("{0}")]
Other(String),
}
impl From<BrowserError> for AgentError {
fn from(err: BrowserError) -> Self {
AgentError::ToolError(err.to_string())
}
}
#[async_trait]
pub trait BrowserExecutor: Send + Sync {
async fn execute(&self, args: &[&str]) -> Result<BrowserOutput, BrowserError>;
fn is_daemon_running(&self) -> bool;
fn config(&self) -> &BrowserConfig;
}
pub struct CliExecutor {
config: BrowserConfig,
is_open: Arc<AtomicBool>,
}
impl CliExecutor {
pub fn new(config: BrowserConfig) -> Self {
Self {
config,
is_open: Arc::new(AtomicBool::new(false)),
}
}
fn build_command(&self, args: &[&str]) -> AsyncCommand {
let mut cmd: AsyncCommand = AsyncCommand::new(self.config.binary());
if self.config.engine.as_arg() != "chrome" {
cmd.arg("--engine").arg(self.config.engine.as_arg());
}
if !self.config.headless {
cmd.arg("--headed");
}
cmd.args(args);
cmd
}
pub fn is_installed(&self) -> bool {
Command::new(self.config.binary())
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
}
#[async_trait]
impl BrowserExecutor for CliExecutor {
async fn execute(&self, args: &[&str]) -> Result<BrowserOutput, BrowserError> {
let mut cmd = self.build_command(args);
let result = timeout(Duration::from_millis(self.config.timeout_ms), cmd.output())
.await
.map_err(|_| BrowserError::Timeout(self.config.timeout_ms))?
.map_err(|e| BrowserError::ExecutionFailed(e.to_string()))?;
let stdout = String::from_utf8_lossy(&result.stdout).to_string();
let stderr = String::from_utf8_lossy(&result.stderr).to_string();
let success = result.status.success();
let exit_code = result.status.code();
if !args.is_empty() {
match args[0] {
"open" => {
self.is_open.store(true, Ordering::SeqCst);
}
"close" => {
self.is_open.store(false, Ordering::SeqCst);
}
_ => {}
}
}
Ok(BrowserOutput {
stdout,
stderr,
exit_code,
success,
})
}
fn is_daemon_running(&self) -> bool {
self.is_open.load(Ordering::SeqCst)
}
fn config(&self) -> &BrowserConfig {
&self.config
}
}
impl Drop for CliExecutor {
fn drop(&mut self) {
if self.is_open.load(Ordering::SeqCst) {
let _ = Command::new(self.config.binary()).arg("close").output();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_browser_output_success() {
let output = BrowserOutput::success("test output".to_string());
assert!(output.success);
assert_eq!(output.stdout, "test output");
assert_eq!(output.exit_code, Some(0));
}
#[test]
fn test_browser_output_failure() {
let output = BrowserOutput::failure("error message".to_string(), Some(1));
assert!(!output.success);
assert_eq!(output.stderr, "error message");
assert_eq!(output.exit_code, Some(1));
}
#[test]
fn test_browser_error_display() {
let err = BrowserError::Timeout(5000);
assert!(err.to_string().contains("5000ms"));
let err = BrowserError::NotFound("agent-browser".to_string());
assert!(err.to_string().contains("not found"));
}
#[test]
fn test_cli_executor_creation() {
let config = BrowserConfig::default();
let executor = CliExecutor::new(config);
assert!(!executor.is_daemon_running());
}
}