agent_commander/
executor.rs1use std::process::Stdio;
4use tokio::io::{AsyncBufReadExt, BufReader};
5use tokio::process::{Child, Command};
6
7#[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
16pub 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 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 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
86pub 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 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 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 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 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 pub fn get_output(&self) -> (&str, &str, Option<i32>) {
141 (&self.stdout, &self.stderr, self.exit_code)
142 }
143
144 pub fn has_exited(&self) -> bool {
146 self.exit_code.is_some()
147 }
148}
149
150pub 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
172pub 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
191pub type CleanupFn = Box<dyn Fn() + Send + Sync>;
193
194pub fn setup_signal_handler<F>(cleanup_fn: F) -> impl Fn()
202where
203 F: Fn() + Send + Sync + 'static,
204{
205 let cleanup = std::sync::Arc::new(cleanup_fn);
210 let cleanup_clone = cleanup.clone();
211
212 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 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}