use anyhow::{Context, Result, anyhow};
use std::path::Path;
use std::time::{Duration, Instant};
use tokio::process::Command;
use crate::config::Step;
const MAX_CAPTURE_BYTES: usize = 100 * 1024;
#[derive(Debug, Clone)]
pub struct StepResult {
pub name: String,
pub success: bool,
pub exit_code: Option<i32>,
pub stdout: String,
pub stderr: String,
pub duration: Duration,
pub stdout_truncated: bool,
pub stderr_truncated: bool,
}
pub async fn run(step: &Step, cwd: &Path) -> Result<StepResult> {
let parts = shell_words::split(&step.cmd)
.with_context(|| format!("parsing cmd `{}` for step `{}`", step.cmd, step.name))?;
let (program, args) = parts
.split_first()
.ok_or_else(|| anyhow!("empty cmd for step `{}`", step.name))?;
let start = Instant::now();
let output = Command::new(program)
.args(args)
.current_dir(cwd)
.kill_on_drop(true)
.output()
.await;
let duration = start.elapsed();
let result = match output {
Ok(out) => {
let (stdout, stdout_truncated) = truncate_capture(&out.stdout);
let (stderr, stderr_truncated) = truncate_capture(&out.stderr);
StepResult {
name: step.name.clone(),
success: out.status.success(),
exit_code: out.status.code(),
stdout,
stderr,
duration,
stdout_truncated,
stderr_truncated,
}
}
Err(e) => StepResult {
name: step.name.clone(),
success: false,
exit_code: None,
stdout: String::new(),
stderr: format!("failed to launch `{program}`: {e}"),
duration,
stdout_truncated: false,
stderr_truncated: false,
},
};
Ok(result)
}
fn truncate_capture(bytes: &[u8]) -> (String, bool) {
if bytes.len() <= MAX_CAPTURE_BYTES {
(String::from_utf8_lossy(bytes).into_owned(), false)
} else {
let mut s = String::from_utf8_lossy(&bytes[..MAX_CAPTURE_BYTES]).into_owned();
s.push_str("\n... [output truncated at 100 KiB] ...\n");
(s, true)
}
}