limit_cli/tools/browser/
executor.rs1use super::config::BrowserConfig;
6use async_trait::async_trait;
7use limit_agent::error::AgentError;
8use std::process::Command;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11use tokio::process::Command as AsyncCommand;
12use tokio::time::{timeout, Duration};
13
14#[derive(Debug, Clone)]
16pub struct BrowserOutput {
17 pub stdout: String,
19 pub stderr: String,
21 pub exit_code: Option<i32>,
23 pub success: bool,
25}
26
27impl BrowserOutput {
28 pub fn success(stdout: String) -> Self {
30 Self {
31 stdout,
32 stderr: String::new(),
33 exit_code: Some(0),
34 success: true,
35 }
36 }
37
38 pub fn failure(stderr: String, exit_code: Option<i32>) -> Self {
40 Self {
41 stdout: String::new(),
42 stderr,
43 exit_code,
44 success: false,
45 }
46 }
47}
48
49#[derive(Debug, thiserror::Error)]
51pub enum BrowserError {
52 #[error("Failed to execute browser command: {0}")]
54 ExecutionFailed(String),
55
56 #[error("Browser operation timed out after {0}ms")]
58 Timeout(u64),
59
60 #[error("Invalid arguments: {0}")]
62 InvalidArguments(String),
63
64 #[error("Browser binary not found: {0}")]
66 NotFound(String),
67
68 #[error("Failed to parse output: {0}")]
70 ParseError(String),
71
72 #[error("{0}")]
74 Other(String),
75}
76
77impl From<BrowserError> for AgentError {
78 fn from(err: BrowserError) -> Self {
79 AgentError::ToolError(err.to_string())
80 }
81}
82
83#[async_trait]
85pub trait BrowserExecutor: Send + Sync {
86 async fn execute(&self, args: &[&str]) -> Result<BrowserOutput, BrowserError>;
88
89 fn is_daemon_running(&self) -> bool;
91
92 fn config(&self) -> &BrowserConfig;
94}
95
96pub struct CliExecutor {
98 config: BrowserConfig,
99 is_open: Arc<AtomicBool>,
101}
102
103impl CliExecutor {
104 pub fn new(config: BrowserConfig) -> Self {
106 Self {
107 config,
108 is_open: Arc::new(AtomicBool::new(false)),
109 }
110 }
111
112 fn build_command(&self, args: &[&str]) -> AsyncCommand {
114 let mut cmd: AsyncCommand = AsyncCommand::new(self.config.binary());
115
116 if self.config.engine.as_arg() != "chrome" {
118 cmd.arg("--engine").arg(self.config.engine.as_arg());
119 }
120
121 if !self.config.headless {
124 cmd.arg("--headed");
125 }
126
127 cmd.args(args);
128 cmd
129 }
130
131 pub fn is_installed(&self) -> bool {
133 Command::new(self.config.binary())
134 .arg("--version")
135 .output()
136 .map(|o| o.status.success())
137 .unwrap_or(false)
138 }
139}
140
141#[async_trait]
142impl BrowserExecutor for CliExecutor {
143 async fn execute(&self, args: &[&str]) -> Result<BrowserOutput, BrowserError> {
144 let mut cmd = self.build_command(args);
145
146 let result = timeout(Duration::from_millis(self.config.timeout_ms), cmd.output())
147 .await
148 .map_err(|_| BrowserError::Timeout(self.config.timeout_ms))?
149 .map_err(|e| BrowserError::ExecutionFailed(e.to_string()))?;
150
151 let stdout = String::from_utf8_lossy(&result.stdout).to_string();
152 let stderr = String::from_utf8_lossy(&result.stderr).to_string();
153 let success = result.status.success();
154 let exit_code = result.status.code();
155
156 if !args.is_empty() {
158 match args[0] {
159 "open" => {
160 self.is_open.store(true, Ordering::SeqCst);
161 }
162 "close" => {
163 self.is_open.store(false, Ordering::SeqCst);
164 }
165 _ => {}
166 }
167 }
168
169 Ok(BrowserOutput {
170 stdout,
171 stderr,
172 exit_code,
173 success,
174 })
175 }
176
177 fn is_daemon_running(&self) -> bool {
178 self.is_open.load(Ordering::SeqCst)
179 }
180
181 fn config(&self) -> &BrowserConfig {
182 &self.config
183 }
184}
185
186impl Drop for CliExecutor {
187 fn drop(&mut self) {
188 if self.is_open.load(Ordering::SeqCst) {
190 let _ = Command::new(self.config.binary()).arg("close").output();
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_browser_output_success() {
201 let output = BrowserOutput::success("test output".to_string());
202 assert!(output.success);
203 assert_eq!(output.stdout, "test output");
204 assert_eq!(output.exit_code, Some(0));
205 }
206
207 #[test]
208 fn test_browser_output_failure() {
209 let output = BrowserOutput::failure("error message".to_string(), Some(1));
210 assert!(!output.success);
211 assert_eq!(output.stderr, "error message");
212 assert_eq!(output.exit_code, Some(1));
213 }
214
215 #[test]
216 fn test_browser_error_display() {
217 let err = BrowserError::Timeout(5000);
218 assert!(err.to_string().contains("5000ms"));
219
220 let err = BrowserError::NotFound("agent-browser".to_string());
221 assert!(err.to_string().contains("not found"));
222 }
223
224 #[test]
225 fn test_cli_executor_creation() {
226 let config = BrowserConfig::default();
227 let executor = CliExecutor::new(config);
228 assert!(!executor.is_daemon_running());
229 }
230}