use super::{
config::ConfigFile,
process::{ExitStatus, Process},
};
use serde::Serialize;
use std::{
ffi::OsString,
io::{self, Write},
process::{Command, Stdio},
sync::{Arc, Mutex},
time::Duration,
};
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
const CARGO_CMD: &str = "cargo";
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30 * 60);
#[derive(Clone, Debug)]
pub struct CmdRunner {
program: OsString,
target_bin: Option<OsString>,
args: Vec<OsString>,
capture_stdout: bool,
capture_stderr: bool,
config: Option<Arc<ConfigFile>>,
print_info: bool,
mutex: Option<Arc<Mutex<()>>>,
timeout: Option<Duration>,
}
impl Default for CmdRunner {
fn default() -> Self {
let mut cmd = Self::new(CARGO_CMD);
cmd.exclusive();
cmd.arg("run");
cmd.arg("--");
cmd
}
}
impl CmdRunner {
pub fn target_bin<S>(bin: S) -> Self
where
S: Into<OsString>,
{
let mut cmd = Self::default();
let bin = bin.into();
cmd.target_bin = Some(bin.clone());
cmd.args.clear();
cmd.arg("run");
cmd.arg("--bin");
cmd.arg(bin);
cmd.arg("--");
cmd
}
pub fn new<S>(program: S) -> Self
where
S: Into<OsString>,
{
Self {
program: program.into(),
target_bin: None,
args: vec![],
capture_stdout: false,
capture_stderr: false,
config: None,
print_info: true,
mutex: None,
timeout: None,
}
}
pub fn arg<S>(&mut self, arg: S) -> &mut Self
where
S: Into<OsString>,
{
self.args.push(arg.into());
self
}
pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
self.args.extend(args.into_iter().map(|a| a.into()));
self
}
pub fn capture_stdout(&mut self) -> &mut Self {
self.capture_stdout = true;
self
}
pub fn capture_stderr(&mut self) -> &mut Self {
self.capture_stderr = true;
self
}
pub fn config<C>(&mut self, config: &C) -> &mut Self
where
C: Serialize,
{
if self.config.is_some() {
panic!("config file already added");
}
let target_bin = self
.target_bin
.as_ref()
.cloned()
.unwrap_or_else(|| "app".into());
let config_file = ConfigFile::create(&target_bin, config);
self.arg("-c");
self.arg(config_file.path());
self.config = Some(Arc::new(config_file));
self
}
pub fn exclusive(&mut self) -> &mut Self {
if self.mutex.is_none() {
self.mutex = Some(Arc::new(Mutex::new(())))
}
self
}
pub fn quiet(&mut self) -> &mut Self {
self.print_info = false;
self
}
pub fn timeout(&mut self, duration: Duration) -> &mut Self {
self.timeout = Some(duration);
self
}
pub fn run(&self) -> Process {
let guard = self
.mutex
.as_ref()
.map(|mutex| mutex.lock().expect("poisoned cmd mutex!"));
if self.print_info {
self.print_command().unwrap();
}
let stdout = if self.capture_stdout {
Stdio::piped()
} else {
Stdio::inherit()
};
let stderr = if self.capture_stderr {
Stdio::piped()
} else {
Stdio::inherit()
};
let child = Command::new(&self.program)
.args(&self.args)
.stdin(Stdio::piped())
.stdout(stdout)
.stderr(stderr)
.spawn()
.unwrap_or_else(|e| {
panic!("error running command: {}", e);
});
Process::new(child, self.timeout.unwrap_or(DEFAULT_TIMEOUT), guard)
}
pub fn status(&self) -> ExitStatus {
self.run().wait().unwrap_or_else(|e| {
panic!("error waiting for subprocess to terminate: {}", e);
})
}
fn print_command(&self) -> Result<(), io::Error> {
let stdout = BufferWriter::stdout(ColorChoice::Auto);
let mut buffer = stdout.buffer();
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
write!(&mut buffer, "+ ")?;
buffer.set_color(ColorSpec::new().set_fg(Some(Color::White)).set_bold(true))?;
write!(&mut buffer, "run")?;
buffer.reset()?;
let cmd = self.program.to_string_lossy();
let args: Vec<_> = self.args.iter().map(|arg| arg.to_string_lossy()).collect();
writeln!(&mut buffer, ": {} {}", cmd, args.join(" "))?;
stdout.print(&buffer)
}
}