Skip to main content

ubiquity_core/
command_local.rs

1//! Local command execution implementation using tokio::process
2
3use 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
20/// Local command executor using native OS processes
21pub 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        // Send start event
49        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        // Build the command
60        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        // Set environment variables
67        for (key, value) in &request.env {
68            cmd.env(key, value);
69        }
70
71        // Set working directory
72        if let Some(dir) = &request.working_dir {
73            cmd.current_dir(dir);
74        }
75
76        // Spawn the process
77        let mut child = cmd
78            .spawn()
79            .map_err(|e| UbiquityError::CommandExecution(format!("Failed to spawn process: {}", e)))?;
80
81        // Get handles to stdio
82        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        // Write stdin if provided
96        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        // Create readers for stdout and stderr
105        let stdout_reader = BufReader::new(stdout);
106        let stderr_reader = BufReader::new(stderr);
107
108        // Collect output
109        let collected_stdout;
110        let collected_stderr;
111
112        // Create tasks for reading output
113        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        // Create a task to handle the process
149        let process_task = tokio::spawn(async move {
150            child.wait().await
151        });
152
153        // Create cancellation and status handling
154        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        // Forward external cancel/status requests to internal channels
158        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        // Clone the handle for later use
173        let process_task_handle = process_task.abort_handle();
174        
175        // Wait for completion, cancellation, or timeout
176        let result = tokio::select! {
177            // Process completed normally
178            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                        // Wait for output collection
185                        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            // Cancellation requested
243            _ = cancel_rx_internal.recv() => {
244                let duration_ms = start_time.elapsed().as_millis() as u64;
245                
246                // Try to terminate the process
247                if let Ok(mut child) = cmd.spawn() {
248                    let _ = child.kill().await;
249                }
250                
251                // Cancel output tasks
252                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            // Timeout
276            _ = async {
277                if let Some(timeout_duration) = request.timeout {
278                    tokio::time::sleep(timeout_duration).await
279                } else {
280                    // No timeout, wait forever
281                    std::future::pending::<()>().await
282                }
283            } => {
284                let duration_ms = start_time.elapsed().as_millis() as u64;
285                
286                // Try to terminate the process
287                if let Ok(mut child) = cmd.spawn() {
288                    let _ = child.kill().await;
289                }
290                
291                // Cancel output tasks
292                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        // Clone result for status handler
312        let result_for_status = result.as_ref().ok().cloned();
313        
314        // Handle status requests
315        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        // Clean up forwarding tasks
324        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        // Register the command
351        self.context.register(command_id, handle).await;
352
353        // Spawn the execution task
354        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            // Unregister the command when done
360            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        // Should have at least Started and Completed events
403        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        // Wait a bit then cancel
419        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}