1use anyhow::Result;
2use log::debug;
3use std::process::Stdio;
4use tokio::io::AsyncReadExt;
5use tokio::process::Command;
6
7#[cfg(test)]
8#[path = "process_tests.rs"]
9mod tests;
10
11#[derive(Debug, Clone)]
16pub struct ProcessError {
17 pub exit_code: Option<i32>,
19 pub stderr: String,
21 pub agent_name: String,
23}
24
25impl std::fmt::Display for ProcessError {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 if self.stderr.is_empty() {
28 write!(
29 f,
30 "{} command failed with exit code {:?}",
31 self.agent_name, self.exit_code
32 )
33 } else {
34 write!(f, "{}", self.stderr)
35 }
36 }
37}
38
39impl std::error::Error for ProcessError {}
40
41async fn read_stderr(handle: Option<tokio::process::ChildStderr>) -> String {
43 if let Some(stderr) = handle {
44 let mut buf = Vec::new();
45 let mut reader = tokio::io::BufReader::new(stderr);
46 let _ = reader.read_to_end(&mut buf).await;
47 String::from_utf8_lossy(&buf).trim().to_string()
48 } else {
49 String::new()
50 }
51}
52
53pub fn log_stderr_text(stderr: &str) {
55 if !stderr.is_empty() {
56 for line in stderr.lines() {
57 debug!("[STDERR] {}", line);
58 }
59 }
60}
61
62pub fn check_exit_status(
67 status: std::process::ExitStatus,
68 stderr: &str,
69 agent_name: &str,
70) -> Result<()> {
71 debug!("{} process exited with status: {}", agent_name, status);
72 if status.success() {
73 return Ok(());
74 }
75 Err(ProcessError {
76 exit_code: status.code(),
77 stderr: stderr.to_string(),
78 agent_name: agent_name.to_string(),
79 }
80 .into())
81}
82
83pub fn handle_output(output: &std::process::Output, agent_name: &str) -> Result<()> {
87 let stderr_text = String::from_utf8_lossy(&output.stderr);
88 let stderr_text = stderr_text.trim();
89 log_stderr_text(stderr_text);
90 check_exit_status(output.status, stderr_text, agent_name)
91}
92
93pub async fn run_captured(cmd: &mut Command, agent_name: &str) -> Result<String> {
97 debug!("{}: running with captured stdout/stderr", agent_name);
98 cmd.stdin(Stdio::inherit())
99 .stdout(Stdio::piped())
100 .stderr(Stdio::piped());
101
102 let output = cmd.output().await?;
103 debug!(
104 "{}: captured {} bytes stdout, {} bytes stderr",
105 agent_name,
106 output.stdout.len(),
107 output.stderr.len()
108 );
109 handle_output(&output, agent_name)?;
110 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
111}
112
113pub async fn run_with_captured_stderr(cmd: &mut Command) -> Result<()> {
121 debug!("Running command with captured stderr");
122 cmd.stderr(Stdio::piped());
123
124 let mut child = cmd.spawn()?;
125 let stderr_handle = child.stderr.take();
126 let status = child.wait().await?;
127 let stderr_text = read_stderr(stderr_handle).await;
128
129 log_stderr_text(&stderr_text);
130 check_exit_status(status, &stderr_text, "Command")
131}
132
133pub async fn spawn_with_captured_stderr(cmd: &mut Command) -> Result<tokio::process::Child> {
138 debug!("Spawning command with captured stderr");
139 cmd.stderr(Stdio::piped());
140 let child = cmd.spawn()?;
141 Ok(child)
142}
143
144pub async fn wait_with_stderr(mut child: tokio::process::Child) -> Result<()> {
149 let stderr_handle = child.stderr.take();
150 let status = child.wait().await?;
151 let stderr_text = read_stderr(stderr_handle).await;
152
153 log_stderr_text(&stderr_text);
154 check_exit_status(status, &stderr_text, "Command")
155}