use anyhow::Result;
use log::debug;
use std::process::Stdio;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
#[cfg(test)]
#[path = "process_tests.rs"]
mod tests;
#[derive(Debug, Clone)]
pub struct ProcessError {
pub exit_code: Option<i32>,
pub stderr: String,
pub agent_name: String,
}
impl std::fmt::Display for ProcessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.stderr.is_empty() {
write!(
f,
"{} command failed with exit code {:?}",
self.agent_name, self.exit_code
)
} else {
write!(f, "{}", self.stderr)
}
}
}
impl std::error::Error for ProcessError {}
async fn read_stderr(handle: Option<tokio::process::ChildStderr>) -> String {
if let Some(stderr) = handle {
let mut buf = Vec::new();
let mut reader = tokio::io::BufReader::new(stderr);
let _ = reader.read_to_end(&mut buf).await;
String::from_utf8_lossy(&buf).trim().to_string()
} else {
String::new()
}
}
pub fn log_stderr_text(stderr: &str) {
if !stderr.is_empty() {
for line in stderr.lines() {
debug!("[STDERR] {}", line);
}
}
}
pub fn check_exit_status(
status: std::process::ExitStatus,
stderr: &str,
agent_name: &str,
) -> Result<()> {
debug!("{} process exited with status: {}", agent_name, status);
if status.success() {
return Ok(());
}
Err(ProcessError {
exit_code: status.code(),
stderr: stderr.to_string(),
agent_name: agent_name.to_string(),
}
.into())
}
pub fn handle_output(output: &std::process::Output, agent_name: &str) -> Result<()> {
let stderr_text = String::from_utf8_lossy(&output.stderr);
let stderr_text = stderr_text.trim();
log_stderr_text(stderr_text);
check_exit_status(output.status, stderr_text, agent_name)
}
pub async fn run_captured(cmd: &mut Command, agent_name: &str) -> Result<String> {
debug!("{}: running with captured stdout/stderr", agent_name);
cmd.stdin(Stdio::inherit())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = cmd.output().await?;
debug!(
"{}: captured {} bytes stdout, {} bytes stderr",
agent_name,
output.stdout.len(),
output.stderr.len()
);
handle_output(&output, agent_name)?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub async fn run_with_captured_stderr(cmd: &mut Command) -> Result<()> {
debug!("Running command with captured stderr");
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn()?;
let stderr_handle = child.stderr.take();
let status = child.wait().await?;
let stderr_text = read_stderr(stderr_handle).await;
log_stderr_text(&stderr_text);
check_exit_status(status, &stderr_text, "Command")
}
pub async fn spawn_with_captured_stderr(cmd: &mut Command) -> Result<tokio::process::Child> {
debug!("Spawning command with captured stderr");
cmd.stderr(Stdio::piped());
let child = cmd.spawn()?;
Ok(child)
}
pub async fn wait_with_stderr(mut child: tokio::process::Child) -> Result<()> {
let stderr_handle = child.stderr.take();
let status = child.wait().await?;
let stderr_text = read_stderr(stderr_handle).await;
log_stderr_text(&stderr_text);
check_exit_status(status, &stderr_text, "Command")
}