use std::path::Path;
use std::process::Output;
use std::sync::Arc;
use anyhow::Context;
use async_trait::async_trait;
pub(crate) fn shell_program() -> &'static str {
if cfg!(windows) { "cmd.exe" } else { "/bin/sh" }
}
pub(crate) fn shell_flag() -> &'static str {
if cfg!(windows) { "/C" } else { "-c" }
}
#[async_trait]
pub trait CommandRunner: Send + Sync + std::fmt::Debug {
async fn run(&self, program: &str, args: &[&str], cwd: &Path) -> anyhow::Result<Output>;
async fn run_mut(&self, program: &str, args: &[&str], cwd: &Path) -> anyhow::Result<Output>;
async fn run_interactive(
&self,
program: &str,
args: &[&str],
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus>;
async fn run_shell_interactive(
&self,
command: &str,
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus>;
async fn run_streaming(
&self,
command: &str,
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus>;
}
fn make_success_output() -> Output {
#[cfg(unix)]
let status = {
use std::os::unix::process::ExitStatusExt;
std::process::ExitStatus::from_raw(0)
};
#[cfg(windows)]
let status = {
use std::os::windows::process::ExitStatusExt;
std::process::ExitStatus::from_raw(0)
};
Output {
status,
stdout: Vec::new(),
stderr: Vec::new(),
}
}
fn make_success_exit_status() -> std::process::ExitStatus {
make_success_output().status
}
#[derive(Debug)]
pub struct VerboseCommandRunner<R: CommandRunner> {
pub(crate) inner: R,
}
impl<R: CommandRunner> VerboseCommandRunner<R> {
pub fn new(inner: R) -> Self {
Self { inner }
}
}
#[async_trait]
impl<R: CommandRunner> CommandRunner for VerboseCommandRunner<R> {
async fn run(&self, program: &str, args: &[&str], cwd: &Path) -> anyhow::Result<Output> {
log::debug!("run: {program} {} (cwd: {})", args.join(" "), cwd.display());
self.inner.run(program, args, cwd).await
}
async fn run_mut(&self, program: &str, args: &[&str], cwd: &Path) -> anyhow::Result<Output> {
log::debug!(
"run_mut: {program} {} (cwd: {})",
args.join(" "),
cwd.display()
);
self.inner.run_mut(program, args, cwd).await
}
async fn run_interactive(
&self,
program: &str,
args: &[&str],
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus> {
log::debug!(
"run_interactive: {program} {} (cwd: {})",
args.join(" "),
cwd.display()
);
self.inner.run_interactive(program, args, cwd).await
}
async fn run_shell_interactive(
&self,
command: &str,
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus> {
log::debug!(
"run_shell_interactive: {command:?} (cwd: {})",
cwd.display()
);
self.inner.run_shell_interactive(command, cwd).await
}
async fn run_streaming(
&self,
command: &str,
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus> {
log::debug!("run_streaming: {command:?} (cwd: {})", cwd.display());
self.inner.run_streaming(command, cwd).await
}
}
#[derive(Debug)]
pub struct RealCommandRunner;
#[async_trait]
impl CommandRunner for RealCommandRunner {
async fn run(&self, program: &str, args: &[&str], cwd: &Path) -> anyhow::Result<Output> {
tokio::process::Command::new(program)
.args(args)
.current_dir(cwd)
.output()
.await
.with_context(|| format!("Failed to run '{program}'"))
}
async fn run_mut(&self, program: &str, args: &[&str], cwd: &Path) -> anyhow::Result<Output> {
self.run(program, args, cwd).await
}
async fn run_interactive(
&self,
program: &str,
args: &[&str],
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus> {
let program = program.to_string();
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let cwd = cwd.to_path_buf();
tokio::task::spawn_blocking(move || {
std::process::Command::new(&program)
.args(&args)
.current_dir(&cwd)
.status()
.with_context(|| format!("Failed to run '{program}'"))
})
.await
.context("spawn_blocking panicked")?
}
async fn run_shell_interactive(
&self,
command: &str,
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus> {
let command = command.to_string();
let cwd = cwd.to_path_buf();
tokio::task::spawn_blocking(move || {
std::process::Command::new(shell_program())
.args([shell_flag(), &command])
.current_dir(&cwd)
.status()
.with_context(|| format!("Failed to run shell command: '{command}'"))
})
.await
.context("spawn_blocking panicked")?
}
async fn run_streaming(
&self,
command: &str,
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus> {
use std::process::Stdio;
log::info!("Running: {command}");
tokio::process::Command::new(shell_program())
.args([shell_flag(), command])
.current_dir(cwd)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.await
.with_context(|| format!("Failed to run streaming command: '{command}'"))
}
}
#[derive(Debug)]
pub struct DryRunCommandRunner {
inner: Arc<dyn CommandRunner>,
}
impl DryRunCommandRunner {
pub fn new(inner: Arc<dyn CommandRunner>) -> Self {
Self { inner }
}
}
#[async_trait]
impl CommandRunner for DryRunCommandRunner {
async fn run(&self, program: &str, args: &[&str], cwd: &Path) -> anyhow::Result<Output> {
self.inner.run(program, args, cwd).await
}
async fn run_mut(&self, program: &str, args: &[&str], cwd: &Path) -> anyhow::Result<Output> {
log::info!(
"[dry-run] would run: {program} {} (cwd: {})",
args.join(" "),
cwd.display()
);
Ok(make_success_output())
}
async fn run_interactive(
&self,
program: &str,
args: &[&str],
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus> {
log::info!(
"[dry-run] would run (interactive): {program} {} (cwd: {})",
args.join(" "),
cwd.display()
);
Ok(make_success_exit_status())
}
async fn run_shell_interactive(
&self,
command: &str,
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus> {
log::info!(
"[dry-run] would run (shell interactive): {command:?} (cwd: {})",
cwd.display()
);
Ok(make_success_exit_status())
}
async fn run_streaming(
&self,
command: &str,
cwd: &Path,
) -> anyhow::Result<std::process::ExitStatus> {
log::info!(
"[dry-run] would run (streaming): {command:?} (cwd: {})",
cwd.display()
);
Ok(make_success_exit_status())
}
}
#[cfg(any(test, feature = "test-support"))]
pub mod test_support;
#[cfg(test)]
mod tests;