Skip to main content

zag_agent/
process.rs

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/// Read stderr from a child process handle into a trimmed String.
12async fn read_stderr(handle: Option<tokio::process::ChildStderr>) -> String {
13    if let Some(stderr) = handle {
14        let mut buf = Vec::new();
15        let mut reader = tokio::io::BufReader::new(stderr);
16        let _ = reader.read_to_end(&mut buf).await;
17        String::from_utf8_lossy(&buf).trim().to_string()
18    } else {
19        String::new()
20    }
21}
22
23/// Log non-empty stderr text.
24pub fn log_stderr_text(stderr: &str) {
25    if !stderr.is_empty() {
26        for line in stderr.lines() {
27            debug!("[STDERR] {}", line);
28        }
29    }
30}
31
32/// Log stderr and bail on non-zero exit status.
33///
34/// Returns `Ok(())` on success. On failure, logs stderr to file and
35/// returns an error containing the stderr text (or the exit status if stderr is empty).
36pub fn check_exit_status(
37    status: std::process::ExitStatus,
38    stderr: &str,
39    agent_name: &str,
40) -> Result<()> {
41    debug!("{} process exited with status: {}", agent_name, status);
42    if status.success() {
43        return Ok(());
44    }
45    if stderr.is_empty() {
46        anyhow::bail!("{} command failed with status: {}", agent_name, status);
47    } else {
48        anyhow::bail!("{}", stderr);
49    }
50}
51
52/// Handle stderr logging and exit status checking for a completed `Output`.
53///
54/// Logs any stderr to file, then bails if exit status is non-zero.
55pub fn handle_output(output: &std::process::Output, agent_name: &str) -> Result<()> {
56    let stderr_text = String::from_utf8_lossy(&output.stderr);
57    let stderr_text = stderr_text.trim();
58    log_stderr_text(stderr_text);
59    check_exit_status(output.status, stderr_text, agent_name)
60}
61
62/// Run a command capturing stdout and stderr, returning stdout text on success.
63///
64/// Stdin is inherited. On failure, stderr is included in the error message.
65pub async fn run_captured(cmd: &mut Command, agent_name: &str) -> Result<String> {
66    debug!("{}: running with captured stdout/stderr", agent_name);
67    cmd.stdin(Stdio::inherit())
68        .stdout(Stdio::piped())
69        .stderr(Stdio::piped());
70
71    let output = cmd.output().await?;
72    debug!(
73        "{}: captured {} bytes stdout, {} bytes stderr",
74        agent_name,
75        output.stdout.len(),
76        output.stderr.len()
77    );
78    handle_output(&output, agent_name)?;
79    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
80}
81
82/// Run a command with stderr captured.
83///
84/// - On success (exit code 0): captured stderr is logged to file only
85/// - On failure (exit code != 0): captured stderr is logged to file AND returned in the error
86///
87/// Stdout and stdin should be configured by the caller before calling this function.
88/// This function only sets stderr to piped.
89pub async fn run_with_captured_stderr(cmd: &mut Command) -> Result<()> {
90    debug!("Running command with captured stderr");
91    cmd.stderr(Stdio::piped());
92
93    let mut child = cmd.spawn()?;
94    let stderr_handle = child.stderr.take();
95    let status = child.wait().await?;
96    let stderr_text = read_stderr(stderr_handle).await;
97
98    log_stderr_text(&stderr_text);
99    check_exit_status(status, &stderr_text, "Command")
100}
101
102/// Spawn a command with stderr captured, but stdout piped for reading.
103///
104/// Returns the child process. The caller is responsible for reading stdout
105/// and calling `wait_with_stderr()` when done.
106pub async fn spawn_with_captured_stderr(cmd: &mut Command) -> Result<tokio::process::Child> {
107    debug!("Spawning command with captured stderr");
108    cmd.stderr(Stdio::piped());
109    let child = cmd.spawn()?;
110    Ok(child)
111}
112
113/// Wait for a child process and handle its captured stderr.
114///
115/// - On success: stderr logged to file only
116/// - On failure: stderr logged to file AND returned in the error
117pub async fn wait_with_stderr(mut child: tokio::process::Child) -> Result<()> {
118    let stderr_handle = child.stderr.take();
119    let status = child.wait().await?;
120    let stderr_text = read_stderr(stderr_handle).await;
121
122    log_stderr_text(&stderr_text);
123    check_exit_status(status, &stderr_text, "Command")
124}