1use async_trait::async_trait;
4use futures::Stream;
5use std::pin::Pin;
6use std::process::Stdio;
7use std::sync::Arc;
8use std::time::Instant;
9use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
10use tokio::process::Command;
11use tokio::sync::{mpsc, oneshot};
12use tracing::error;
13use uuid::Uuid;
14
15use crate::command::{
16 CommandContext, CommandEvent, CommandExecutor, CommandHandle, CommandRequest, CommandResult,
17};
18use crate::error::UbiquityError;
19
20pub struct LocalCommandExecutor {
22 context: Arc<CommandContext>,
23 event_buffer_size: usize,
24}
25
26impl LocalCommandExecutor {
27 pub fn new() -> Self {
28 Self {
29 context: Arc::new(CommandContext::new()),
30 event_buffer_size: 1024,
31 }
32 }
33
34 pub fn with_event_buffer_size(mut self, size: usize) -> Self {
35 self.event_buffer_size = size;
36 self
37 }
38
39 async fn execute_process(
40 request: CommandRequest,
41 event_tx: mpsc::Sender<CommandEvent>,
42 cancel_rx: mpsc::Receiver<()>,
43 status_rx: mpsc::Receiver<oneshot::Sender<CommandResult>>,
44 ) -> Result<(), UbiquityError> {
45 let start_time = Instant::now();
46 let command_id = request.id;
47
48 event_tx
50 .send(CommandEvent::Started {
51 command_id,
52 command: request.command.clone(),
53 args: request.args.clone(),
54 timestamp: chrono::Utc::now(),
55 })
56 .await
57 .map_err(|_| UbiquityError::Internal("Failed to send start event".to_string()))?;
58
59 let mut cmd = Command::new(&request.command);
61 cmd.args(&request.args);
62 cmd.stdout(Stdio::piped());
63 cmd.stderr(Stdio::piped());
64 cmd.stdin(Stdio::piped());
65
66 for (key, value) in &request.env {
68 cmd.env(key, value);
69 }
70
71 if let Some(dir) = &request.working_dir {
73 cmd.current_dir(dir);
74 }
75
76 let mut child = cmd
78 .spawn()
79 .map_err(|e| UbiquityError::CommandExecution(format!("Failed to spawn process: {}", e)))?;
80
81 let stdout = child
83 .stdout
84 .take()
85 .ok_or_else(|| UbiquityError::Internal("Failed to capture stdout".to_string()))?;
86 let stderr = child
87 .stderr
88 .take()
89 .ok_or_else(|| UbiquityError::Internal("Failed to capture stderr".to_string()))?;
90 let mut stdin = child
91 .stdin
92 .take()
93 .ok_or_else(|| UbiquityError::Internal("Failed to capture stdin".to_string()))?;
94
95 if let Some(input) = &request.stdin {
97 stdin
98 .write_all(input.as_bytes())
99 .await
100 .map_err(|e| UbiquityError::CommandExecution(format!("Failed to write stdin: {}", e)))?;
101 stdin.shutdown().await.ok();
102 }
103
104 let stdout_reader = BufReader::new(stdout);
106 let stderr_reader = BufReader::new(stderr);
107
108 let collected_stdout;
110 let collected_stderr;
111
112 let event_tx_stdout = event_tx.clone();
114 let event_tx_stderr = event_tx.clone();
115
116 let stdout_task = tokio::spawn(async move {
117 let mut lines = stdout_reader.lines();
118 let mut output = Vec::new();
119 while let Ok(Some(line)) = lines.next_line().await {
120 output.push(line.clone());
121 let _ = event_tx_stdout
122 .send(CommandEvent::Stdout {
123 command_id,
124 data: line,
125 timestamp: chrono::Utc::now(),
126 })
127 .await;
128 }
129 output
130 });
131
132 let stderr_task = tokio::spawn(async move {
133 let mut lines = stderr_reader.lines();
134 let mut output = Vec::new();
135 while let Ok(Some(line)) = lines.next_line().await {
136 output.push(line.clone());
137 let _ = event_tx_stderr
138 .send(CommandEvent::Stderr {
139 command_id,
140 data: line,
141 timestamp: chrono::Utc::now(),
142 })
143 .await;
144 }
145 output
146 });
147
148 let process_task = tokio::spawn(async move {
150 child.wait().await
151 });
152
153 let (cancel_tx, mut cancel_rx_internal) = mpsc::channel::<()>(1);
155 let (status_tx_internal, mut status_rx_internal) = mpsc::channel::<oneshot::Sender<CommandResult>>(1);
156
157 let cancel_forward = tokio::spawn(async move {
159 let mut cancel_rx = cancel_rx;
160 while let Some(()) = cancel_rx.recv().await {
161 let _ = cancel_tx.send(()).await;
162 }
163 });
164
165 let status_forward = tokio::spawn(async move {
166 let mut status_rx = status_rx;
167 while let Some(tx) = status_rx.recv().await {
168 let _ = status_tx_internal.send(tx).await;
169 }
170 });
171
172 let process_task_handle = process_task.abort_handle();
174
175 let result = tokio::select! {
177 exit_status = process_task => {
179 match exit_status {
180 Ok(Ok(status)) => {
181 let exit_code = status.code().unwrap_or(-1);
182 let duration_ms = start_time.elapsed().as_millis() as u64;
183
184 collected_stdout = stdout_task.await.unwrap_or_default();
186 collected_stderr = stderr_task.await.unwrap_or_default();
187
188 event_tx
189 .send(CommandEvent::Completed {
190 command_id,
191 exit_code,
192 duration_ms,
193 timestamp: chrono::Utc::now(),
194 })
195 .await
196 .ok();
197
198 Ok(CommandResult {
199 id: command_id,
200 exit_code: Some(exit_code),
201 stdout: collected_stdout.join("\n"),
202 stderr: collected_stderr.join("\n"),
203 duration_ms,
204 cancelled: false,
205 })
206 }
207 Ok(Err(e)) => {
208 let duration_ms = start_time.elapsed().as_millis() as u64;
209 let error_msg = format!("Process error: {}", e);
210
211 event_tx
212 .send(CommandEvent::Failed {
213 command_id,
214 error: error_msg.clone(),
215 duration_ms,
216 timestamp: chrono::Utc::now(),
217 })
218 .await
219 .ok();
220
221 Err(UbiquityError::CommandExecution(error_msg))
222 }
223 Err(e) => {
224 let duration_ms = start_time.elapsed().as_millis() as u64;
225 let error_msg = format!("Task join error: {}", e);
226
227 event_tx
228 .send(CommandEvent::Failed {
229 command_id,
230 error: error_msg.clone(),
231 duration_ms,
232 timestamp: chrono::Utc::now(),
233 })
234 .await
235 .ok();
236
237 Err(UbiquityError::Internal(error_msg))
238 }
239 }
240 }
241
242 _ = cancel_rx_internal.recv() => {
244 let duration_ms = start_time.elapsed().as_millis() as u64;
245
246 if let Ok(mut child) = cmd.spawn() {
248 let _ = child.kill().await;
249 }
250
251 stdout_task.abort();
253 stderr_task.abort();
254 process_task_handle.abort();
255
256 event_tx
257 .send(CommandEvent::Cancelled {
258 command_id,
259 duration_ms,
260 timestamp: chrono::Utc::now(),
261 })
262 .await
263 .ok();
264
265 Ok(CommandResult {
266 id: command_id,
267 exit_code: None,
268 stdout: String::new(),
269 stderr: String::new(),
270 duration_ms,
271 cancelled: true,
272 })
273 }
274
275 _ = async {
277 if let Some(timeout_duration) = request.timeout {
278 tokio::time::sleep(timeout_duration).await
279 } else {
280 std::future::pending::<()>().await
282 }
283 } => {
284 let duration_ms = start_time.elapsed().as_millis() as u64;
285
286 if let Ok(mut child) = cmd.spawn() {
288 let _ = child.kill().await;
289 }
290
291 stdout_task.abort();
293 stderr_task.abort();
294 process_task_handle.abort();
295
296 let error_msg = format!("Command timed out after {:?}", request.timeout.unwrap());
297 event_tx
298 .send(CommandEvent::Failed {
299 command_id,
300 error: error_msg.clone(),
301 duration_ms,
302 timestamp: chrono::Utc::now(),
303 })
304 .await
305 .ok();
306
307 Err(UbiquityError::Timeout(error_msg))
308 }
309 };
310
311 let result_for_status = result.as_ref().ok().cloned();
313
314 tokio::spawn(async move {
316 while let Some(response_tx) = status_rx_internal.recv().await {
317 if let Some(ref cmd_result) = result_for_status {
318 let _ = response_tx.send(cmd_result.clone());
319 }
320 }
321 });
322
323 cancel_forward.abort();
325 status_forward.abort();
326
327 result.map(|_| ())
328 }
329}
330
331impl Default for LocalCommandExecutor {
332 fn default() -> Self {
333 Self::new()
334 }
335}
336
337#[async_trait]
338impl CommandExecutor for LocalCommandExecutor {
339 async fn execute(
340 &self,
341 request: CommandRequest,
342 ) -> Result<Pin<Box<dyn Stream<Item = CommandEvent> + Send>>, UbiquityError> {
343 let (event_tx, event_rx) = mpsc::channel(self.event_buffer_size);
344 let (cancel_tx, cancel_rx) = mpsc::channel(1);
345 let (status_tx, status_rx) = mpsc::channel(1);
346
347 let command_id = request.id;
348 let handle = CommandHandle::new(command_id, cancel_tx, status_tx);
349
350 self.context.register(command_id, handle).await;
352
353 let context = self.context.clone();
355 let event_tx_clone = event_tx.clone();
356 tokio::spawn(async move {
357 let result = Self::execute_process(request, event_tx_clone, cancel_rx, status_rx).await;
358
359 context.unregister(&command_id).await;
361
362 if let Err(e) = result {
363 error!("Command execution error: {}", e);
364 }
365 });
366
367 Ok(Box::pin(tokio_stream::wrappers::ReceiverStream::new(event_rx)))
368 }
369
370 async fn cancel(&self, command_id: Uuid) -> Result<(), UbiquityError> {
371 self.context.cancel(&command_id).await
372 }
373
374 async fn status(&self, command_id: Uuid) -> Result<Option<CommandResult>, UbiquityError> {
375 if let Some(handle) = self.context.get(&command_id).await {
376 Ok(Some(handle.status().await?))
377 } else {
378 Ok(None)
379 }
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use futures::StreamExt;
387
388 #[tokio::test]
389 async fn test_local_command_execution() {
390 let executor = LocalCommandExecutor::new();
391 let request = CommandRequest::new("echo").with_args(vec!["hello world".to_string()]);
392
393 let mut stream = executor.execute(request).await.unwrap();
394 let mut events = Vec::new();
395
396 while let Some(event) = stream.next().await {
397 events.push(event);
398 }
399
400 assert!(!events.is_empty());
401
402 let has_started = events.iter().any(|e| matches!(e, CommandEvent::Started { .. }));
404 let has_completed = events.iter().any(|e| matches!(e, CommandEvent::Completed { .. }));
405
406 assert!(has_started);
407 assert!(has_completed);
408 }
409
410 #[tokio::test]
411 async fn test_command_cancellation() {
412 let executor = LocalCommandExecutor::new();
413 let request = CommandRequest::new("sleep").with_args(vec!["10".to_string()]);
414 let command_id = request.id;
415
416 let mut stream = executor.execute(request).await.unwrap();
417
418 tokio::time::sleep(Duration::from_millis(100)).await;
420 executor.cancel(command_id).await.unwrap();
421
422 let mut cancelled = false;
423 while let Some(event) = stream.next().await {
424 if matches!(event, CommandEvent::Cancelled { .. }) {
425 cancelled = true;
426 break;
427 }
428 }
429
430 assert!(cancelled);
431 }
432
433 #[tokio::test]
434 async fn test_command_timeout() {
435 let executor = LocalCommandExecutor::new();
436 let request = CommandRequest::new("sleep")
437 .with_args(vec!["10".to_string()])
438 .with_timeout(Duration::from_millis(100));
439
440 let mut stream = executor.execute(request).await.unwrap();
441 let mut timed_out = false;
442
443 while let Some(event) = stream.next().await {
444 if let CommandEvent::Failed { error, .. } = event {
445 if error.contains("timed out") {
446 timed_out = true;
447 break;
448 }
449 }
450 }
451
452 assert!(timed_out);
453 }
454
455 #[tokio::test]
456 async fn test_command_with_stdin() {
457 let executor = LocalCommandExecutor::new();
458 let request = CommandRequest::new("cat").with_stdin("test input data");
459
460 let mut stream = executor.execute(request).await.unwrap();
461 let mut stdout_data = String::new();
462
463 while let Some(event) = stream.next().await {
464 if let CommandEvent::Stdout { data, .. } = event {
465 stdout_data.push_str(&data);
466 }
467 }
468
469 assert_eq!(stdout_data.trim(), "test input data");
470 }
471}