Skip to main content

agent_commander/
executor.rs

1//! Execute commands using tokio
2
3use std::process::Stdio;
4use tokio::io::{AsyncBufReadExt, BufReader};
5use tokio::process::{Child, Command};
6
7/// Command execution result
8#[derive(Debug, Clone, Default)]
9pub struct ExecutionResult {
10    pub exit_code: i32,
11    pub stdout: String,
12    pub stderr: String,
13    pub command: String,
14}
15
16/// Execute a command and return the result
17///
18/// # Arguments
19/// * `command` - Command to execute
20/// * `dry_run` - If true, just return the command without executing
21/// * `attached` - If true, stream output to console
22///
23/// # Returns
24/// Execution result
25pub async fn execute_command(
26    command: &str,
27    dry_run: bool,
28    attached: bool,
29) -> Result<ExecutionResult, std::io::Error> {
30    if dry_run {
31        println!("Dry run - command that would be executed:");
32        println!("{}", command);
33        return Ok(ExecutionResult {
34            exit_code: 0,
35            stdout: String::new(),
36            stderr: String::new(),
37            command: command.to_string(),
38        });
39    }
40
41    let mut child = Command::new("bash")
42        .arg("-c")
43        .arg(command)
44        .stdout(Stdio::piped())
45        .stderr(Stdio::piped())
46        .spawn()?;
47
48    let mut stdout = String::new();
49    let mut stderr = String::new();
50
51    // Read stdout
52    if let Some(stdout_pipe) = child.stdout.take() {
53        let mut reader = BufReader::new(stdout_pipe).lines();
54        while let Some(line) = reader.next_line().await? {
55            stdout.push_str(&line);
56            stdout.push('\n');
57            if attached {
58                println!("{}", line);
59            }
60        }
61    }
62
63    // Read stderr
64    if let Some(stderr_pipe) = child.stderr.take() {
65        let mut reader = BufReader::new(stderr_pipe).lines();
66        while let Some(line) = reader.next_line().await? {
67            stderr.push_str(&line);
68            stderr.push('\n');
69            if attached {
70                eprintln!("{}", line);
71            }
72        }
73    }
74
75    let status = child.wait().await?;
76    let exit_code = status.code().unwrap_or(1);
77
78    Ok(ExecutionResult {
79        exit_code,
80        stdout,
81        stderr,
82        command: command.to_string(),
83    })
84}
85
86/// Process handle for non-blocking command execution
87pub struct ProcessHandle {
88    pub command: String,
89    child: Option<Child>,
90    stdout: String,
91    stderr: String,
92    exit_code: Option<i32>,
93}
94
95impl ProcessHandle {
96    /// Create a new process handle
97    fn new(command: String, child: Child) -> Self {
98        Self {
99            command,
100            child: Some(child),
101            stdout: String::new(),
102            stderr: String::new(),
103            exit_code: None,
104        }
105    }
106
107    /// Wait for the process to exit
108    pub async fn wait_for_exit(&mut self) -> Result<i32, std::io::Error> {
109        if let Some(exit_code) = self.exit_code {
110            return Ok(exit_code);
111        }
112
113        if let Some(mut child) = self.child.take() {
114            // Read remaining stdout
115            if let Some(stdout_pipe) = child.stdout.take() {
116                let mut reader = BufReader::new(stdout_pipe).lines();
117                while let Some(line) = reader.next_line().await? {
118                    self.stdout.push_str(&line);
119                    self.stdout.push('\n');
120                }
121            }
122
123            // Read remaining stderr
124            if let Some(stderr_pipe) = child.stderr.take() {
125                let mut reader = BufReader::new(stderr_pipe).lines();
126                while let Some(line) = reader.next_line().await? {
127                    self.stderr.push_str(&line);
128                    self.stderr.push('\n');
129                }
130            }
131
132            let status = child.wait().await?;
133            self.exit_code = Some(status.code().unwrap_or(1));
134        }
135
136        Ok(self.exit_code.unwrap_or(1))
137    }
138
139    /// Get collected output
140    pub fn get_output(&self) -> (&str, &str, Option<i32>) {
141        (&self.stdout, &self.stderr, self.exit_code)
142    }
143
144    /// Check if process has exited
145    pub fn has_exited(&self) -> bool {
146        self.exit_code.is_some()
147    }
148}
149
150/// Start a command execution without waiting for completion
151///
152/// # Arguments
153/// * `command` - Command to execute
154/// * `attached` - If true, stream output to console
155///
156/// # Returns
157/// Process handle
158pub async fn start_command(
159    command: &str,
160    _attached: bool,
161) -> Result<ProcessHandle, std::io::Error> {
162    let child = Command::new("bash")
163        .arg("-c")
164        .arg(command)
165        .stdout(Stdio::piped())
166        .stderr(Stdio::piped())
167        .spawn()?;
168
169    Ok(ProcessHandle::new(command.to_string(), child))
170}
171
172/// Execute a command in the background (detached)
173///
174/// # Arguments
175/// * `command` - Command to execute
176///
177/// # Returns
178/// Process ID if available
179pub async fn execute_detached(command: &str) -> Result<Option<u32>, std::io::Error> {
180    let child = Command::new("bash")
181        .arg("-c")
182        .arg(command)
183        .stdout(Stdio::null())
184        .stderr(Stdio::null())
185        .stdin(Stdio::null())
186        .spawn()?;
187
188    Ok(child.id())
189}
190
191/// Signal handler cleanup function type
192pub type CleanupFn = Box<dyn Fn() + Send + Sync>;
193
194/// Setup CTRL+C handler for graceful shutdown
195///
196/// # Arguments
197/// * `cleanup_fn` - Function to call on CTRL+C
198///
199/// # Returns
200/// Function to remove the handler
201pub fn setup_signal_handler<F>(cleanup_fn: F) -> impl Fn()
202where
203    F: Fn() + Send + Sync + 'static,
204{
205    // Note: In Rust with tokio, signal handling is typically done differently
206    // This is a simplified version that uses ctrlc crate pattern
207    // For production use, consider tokio::signal
208
209    let cleanup = std::sync::Arc::new(cleanup_fn);
210    let cleanup_clone = cleanup.clone();
211
212    // Set up a simple flag for shutdown
213    let shutdown = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
214    let shutdown_clone = shutdown.clone();
215
216    std::thread::spawn(move || {
217        // This is a simplified pattern - in real code use tokio::signal
218        loop {
219            if shutdown_clone.load(std::sync::atomic::Ordering::Relaxed) {
220                cleanup_clone();
221                break;
222            }
223            std::thread::sleep(std::time::Duration::from_millis(100));
224        }
225    });
226
227    move || {
228        shutdown.store(true, std::sync::atomic::Ordering::Relaxed);
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[tokio::test]
237    async fn test_execute_command_dry_run() {
238        let result = execute_command("echo hello", true, false).await.unwrap();
239        assert_eq!(result.exit_code, 0);
240        assert_eq!(result.command, "echo hello");
241    }
242
243    #[tokio::test]
244    #[cfg(not(target_os = "windows"))]
245    async fn test_execute_command_real() {
246        let result = execute_command("echo hello", false, false).await.unwrap();
247        assert_eq!(result.exit_code, 0);
248        assert!(result.stdout.contains("hello"));
249    }
250
251    #[tokio::test]
252    #[cfg(not(target_os = "windows"))]
253    async fn test_start_command_and_wait() {
254        let mut handle = start_command("echo hello", false).await.unwrap();
255        let exit_code = handle.wait_for_exit().await.unwrap();
256        assert_eq!(exit_code, 0);
257
258        let (stdout, _, _) = handle.get_output();
259        assert!(stdout.contains("hello"));
260    }
261
262    #[tokio::test]
263    #[cfg(not(target_os = "windows"))]
264    async fn test_execute_detached() {
265        let pid = execute_detached("sleep 0.1").await.unwrap();
266        assert!(pid.is_some());
267    }
268}