use anyhow::Context;
use std::process::Output;
use std::process::Stdio;
use tokio::process::Command;
use tracing::debug;
use crate::sdk::{command_debug_log, command_failure_message, decode_output};
#[derive(Debug)]
pub struct CommandOutcome {
pub output: Output,
pub stdout: String,
pub stderr: String,
}
impl CommandOutcome {
pub fn is_success(&self) -> bool {
self.output.status.success()
}
}
#[derive(Clone, Debug)]
pub struct CommandRunner {
debug: bool,
}
impl CommandRunner {
pub fn new(debug: bool) -> Self {
Self { debug }
}
pub async fn run(&self, program: &str, args: &[&str]) -> anyhow::Result<CommandOutcome> {
let output = Command::new(program)
.args(args)
.output()
.await
.with_context(|| format!("Failed to execute {} {}", program, args.join(" ")))?;
command_debug_log(self.debug, program, args, &output, |msg| debug!("{}", msg));
let stdout = decode_output(&output.stdout);
let stderr = decode_output(&output.stderr);
Ok(CommandOutcome {
output,
stdout,
stderr,
})
}
pub async fn run_checked(
&self,
program: &str,
args: &[&str],
hint: Option<&str>,
) -> anyhow::Result<CommandOutcome> {
let outcome = self.run(program, args).await?;
if !outcome.output.status.success() {
let message = command_failure_message(program, args, &outcome.output, hint);
return Err(anyhow::anyhow!(message));
}
Ok(outcome)
}
pub async fn run_with_stdio(
&self,
program: &str,
args: &[&str],
) -> anyhow::Result<std::process::ExitStatus> {
let status = Command::new(program)
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.await
.with_context(|| format!("Failed to execute {} {}", program, args.join(" ")))?;
if self.debug {
debug!(
"{} {} -> status={}",
program,
args.join(" "),
status
.code()
.map(|c| c.to_string())
.unwrap_or_else(|| "SIG".into())
);
}
Ok(status)
}
pub async fn run_builder<F>(
&self,
program: &str,
args: &[&str],
configure: F,
) -> anyhow::Result<CommandOutcome>
where
F: FnOnce(&mut Command),
{
let mut cmd = Command::new(program);
cmd.args(args);
configure(&mut cmd);
let output = cmd
.output()
.await
.with_context(|| format!("Failed to execute {} {}", program, args.join(" ")))?;
command_debug_log(self.debug, program, args, &output, |msg| debug!("{}", msg));
let stdout = decode_output(&output.stdout);
let stderr = decode_output(&output.stderr);
Ok(CommandOutcome {
output,
stdout,
stderr,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn command_runner_success() {
let runner = CommandRunner::new(true);
#[cfg(windows)]
let outcome = runner
.run_checked("cmd", &["/C", "echo hello"], None)
.await
.unwrap();
#[cfg(not(windows))]
let outcome = runner
.run_checked("sh", &["-c", "echo hello"], None)
.await
.unwrap();
assert!(outcome.is_success());
assert_eq!(outcome.stdout, "hello");
}
#[tokio::test]
async fn command_runner_failure_returns_error() {
let runner = CommandRunner::new(false);
#[cfg(windows)]
let err = runner
.run_checked("cmd", &["/C", "exit /b 1"], Some("intentional failure"))
.await
.unwrap_err();
#[cfg(not(windows))]
let err = runner
.run_checked("sh", &["-c", "false"], Some("intentional failure"))
.await
.unwrap_err();
assert!(err.to_string().contains("failed with status"));
}
}