use std::{io, ops::Deref, process::Stdio, time::Duration};
use once_cell::sync::Lazy;
use tokio::process::Command;
use crate::{Env, Location, Result, RunningProcess};
#[derive(Clone)]
pub struct Cmd<Loc> {
pub exe: String,
pub env: Env,
pub pwd: Loc,
pub msg: Option<String>,
}
impl<Loc> Cmd<Loc>
where
Loc: Location,
{
pub fn exe(&self) -> &str {
&self.exe
}
pub fn env(&self) -> &Env {
&self.env
}
pub fn pwd(&self) -> &Loc {
&self.pwd
}
pub fn msg(&self) -> Option<&String> {
self.msg.as_ref()
}
}
#[derive(Clone, Debug)]
pub struct KillTimeout(Duration);
impl KillTimeout {
pub fn new(duration: Duration) -> Self {
Self(duration)
}
pub fn duration(&self) -> Duration {
self.0
}
}
static DEFAULT_KILL_TIMEOUT: Lazy<Duration> = Lazy::new(|| {
let default = Duration::from_secs(10);
match std::env::var("PROCESS_TIMEOUT") {
Err(_) => default,
Ok(timeout) => match timeout.parse::<u64>() {
Ok(x) => Duration::from_secs(x),
Err(_) => {
eprintln!(
"⚠️ TIMEOUT variable is not a valid int: {}. Using default: {}",
timeout,
default.as_secs()
);
default
}
},
}
});
impl Default for KillTimeout {
fn default() -> Self {
Self(*DEFAULT_KILL_TIMEOUT)
}
}
impl Deref for KillTimeout {
type Target = Duration;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Duration> for KillTimeout {
fn from(value: Duration) -> Self {
Self(value)
}
}
pub struct SpawnOptions {
pub stdout: Stdio,
pub stderr: Stdio,
pub timeout: KillTimeout,
pub group: bool,
}
impl Default for SpawnOptions {
fn default() -> Self {
Self {
stdout: Stdio::inherit(),
stderr: Stdio::inherit(),
timeout: KillTimeout::default(),
group: false,
}
}
}
pub struct Output(Vec<u8>);
impl Output {
pub fn bytes(self) -> Vec<u8> {
self.0
}
pub fn as_string(self) -> Result<String> {
let bytes = self.bytes();
let string = String::from_utf8(bytes)?;
Ok(string)
}
}
impl<Loc> Cmd<Loc>
where
Loc: Location,
{
#[cfg(unix)]
pub(crate) const SHELL: &'static str = "/bin/sh";
#[cfg(windows)]
pub(crate) const SHELL: &'static str = "cmd";
#[cfg(unix)]
pub(crate) fn shelled(cmd: &str) -> Vec<&str> {
vec!["-c", cmd]
}
#[cfg(windows)]
pub(crate) fn shelled(cmd: &str) -> Vec<&str> {
vec!["/c", cmd]
}
pub async fn run(&self) -> Result<()> {
eprintln!("{}", crate::headline!(self));
let opts = SpawnOptions {
stdout: Stdio::inherit(),
stderr: Stdio::inherit(),
..Default::default()
};
self.spawn(opts)?.wait().await?;
Ok(())
}
pub async fn silent(&self) -> Result<()> {
let opts = SpawnOptions {
stdout: Stdio::null(),
stderr: Stdio::null(),
..Default::default()
};
self.spawn(opts)?.wait().await?;
Ok(())
}
pub async fn output(&self) -> Result<Output> {
let opts = SpawnOptions {
stdout: Stdio::piped(),
stderr: Stdio::piped(),
..Default::default()
};
let res = self.spawn(opts)?.wait().await?;
Ok(Output(res.stdout))
}
#[cfg(unix)]
pub fn spawn(&self, opts: SpawnOptions) -> io::Result<RunningProcess> {
let cmd = self;
let SpawnOptions {
stdout,
stderr,
timeout,
group,
} = opts;
let mut command = Command::new(Cmd::<Loc>::SHELL);
command
.args(Cmd::<Loc>::shelled(&cmd.exe))
.envs(cmd.env.to_owned())
.current_dir(cmd.pwd.as_path())
.stdout(stdout)
.stderr(stderr);
if group {
command.process_group(0);
}
let process = command.spawn()?;
Ok(RunningProcess {
process,
timeout,
group,
})
}
#[cfg(windows)]
pub fn spawn(&self, opts: SpawnOptions) -> io::Result<RunningProcess> {
let cmd = self;
let SpawnOptions {
stdout,
stderr,
timeout,
group,
} = opts;
let mut command = Command::new(Cmd::<Loc>::SHELL);
command
.args(Cmd::<Loc>::shelled(&cmd.exe))
.envs(cmd.env.to_owned())
.current_dir(cmd.pwd.as_path())
.stdout(stdout)
.stderr(stderr);
let process = command.spawn()?;
Ok(RunningProcess {
process,
timeout,
group,
})
}
}
#[macro_export]
macro_rules! cmd {
{
$exe:literal,
env: $env:expr,
pwd: $pwd:expr,
msg: $msg:literal$(,)?
} => {
$crate::Cmd {
exe: $exe.to_string(),
env: $env,
pwd: $pwd,
msg: Some($msg.to_string()),
}
};
{
exe: $exe:literal,
env: $env:expr,
pwd: $pwd:expr,
msg: $msg:literal$(,)?
} => {
$crate::Cmd {
exe: $exe.to_string(),
env: $env,
pwd: $pwd,
msg: Some($msg.to_string()),
}
};
{
$exe:literal,
env: $env:expr,
pwd: $pwd:expr,
msg: Some($msg:expr)$(,)?
} => {
$crate::Cmd {
exe: $exe.to_string(),
env: $env,
pwd: $pwd,
msg: Some($msg),
}
};
{
exe: $exe:literal,
env: $env:expr,
pwd: $pwd:expr,
msg: Some($msg:expr)$(,)?
} => {
$crate::Cmd {
exe: $exe.to_string(),
env: $env,
pwd: $pwd,
msg: Some($msg),
}
};
{
$exe:literal,
env: $env:expr,
pwd: $pwd:expr,
msg: None$(,)?
} => {
$crate::Cmd {
exe: $exe.to_string(),
env: $env,
pwd: $pwd,
msg: None,
}
};
{
exe: $exe:literal,
env: $env:expr,
pwd: $pwd:expr,
msg: None$(,)?
} => {
$crate::Cmd {
exe: $exe.to_string(),
env: $env,
pwd: $pwd,
msg: None,
}
};
{
$exe:literal,
env: $env:expr,
pwd: $pwd:expr,
msg: $msg:expr$(,)?
} => {
$crate::Cmd {
exe: $exe.to_string(),
env: $env,
pwd: $pwd,
msg: Some($msg),
}
};
{
exe: $exe:literal,
env: $env:expr,
pwd: $pwd:expr,
msg: $msg:expr$(,)?
} => {
$crate::Cmd {
exe: $exe.to_string(),
env: $env,
pwd: $pwd,
msg: Some($msg),
}
};
{
$exe:expr,
env: $env:expr,
pwd: $pwd:expr,
msg: $msg:literal$(,)?
} => {
$crate::Cmd {
exe: $exe,
env: $env,
pwd: $pwd,
msg: Some($msg.to_string()),
}
};
{
exe: $exe:expr,
env: $env:expr,
pwd: $pwd:expr,
msg: $msg:literal$(,)?
} => {
$crate::Cmd {
exe: $exe,
env: $env,
pwd: $pwd,
msg: Some($msg.to_string()),
}
};
{
$exe:expr,
env: $env:expr,
pwd: $pwd:expr,
msg: Some($msg:expr)$(,)?
} => {
$crate::Cmd {
exe: $exe,
env: $env,
pwd: $pwd,
msg: Some($msg),
}
};
{
exe: $exe:expr,
env: $env:expr,
pwd: $pwd:expr,
msg: Some($msg:expr)$(,)?
} => {
$crate::Cmd {
exe: $exe,
env: $env,
pwd: $pwd,
msg: Some($msg),
}
};
{
$exe:expr,
env: $env:expr,
pwd: $pwd:expr,
msg: None$(,)?
} => {
$crate::Cmd {
exe: $exe,
env: $env,
pwd: $pwd,
msg: None,
}
};
{
exe: $exe:expr,
env: $env:expr,
pwd: $pwd:expr,
msg: None$(,)?
} => {
$crate::Cmd {
exe: $exe,
env: $env,
pwd: $pwd,
msg: None,
}
};
{
$exe:expr,
env: $env:expr,
pwd: $pwd:expr,
msg: $msg:expr$(,)?
} => {
$crate::Cmd {
exe: $exe,
env: $env,
pwd: $pwd,
msg: Some($msg),
}
};
{
exe: $exe:expr,
env: $env:expr,
pwd: $pwd:expr,
msg: $msg:expr$(,)?
} => {
$crate::Cmd {
exe: $exe,
env: $env,
pwd: $pwd,
msg: Some($msg),
}
};
{
$exe:literal,
env: $env:expr,
pwd: $pwd:expr$(,)?
} => {
$crate::Cmd {
exe: $exe.to_string(),
env: $env,
pwd: $pwd,
msg: None,
}
};
{
exe: $exe:literal,
env: $env:expr,
pwd: $pwd:expr$(,)?
} => {
$crate::Cmd {
exe: $exe.to_string(),
env: $env,
pwd: $pwd,
msg: None,
}
};
{
$exe:expr,
env: $env:expr,
pwd: $pwd:expr$(,)?
} => {
$crate::Cmd {
exe: $exe,
env: $env,
pwd: $pwd,
msg: None,
}
};
{
exe: $exe:expr,
env: $env:expr,
pwd: $pwd:expr$(,)?
} => {
$crate::Cmd {
exe: $exe,
env: $env,
pwd: $pwd,
msg: None,
}
};
}
#[cfg(test)]
mod tests {
use crate::{Cmd, Env, Location};
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_literal_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
"ls",
env: env,
pwd: loc,
msg: "!",
}
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_literal_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
exe: "ls",
env: env,
pwd: loc,
msg: "!",
}
}
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_expr_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
format!("ls {}", "."),
env: env,
pwd: loc,
msg: "!",
}
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_expr_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
exe: format!("ls {}", "."),
env: env,
pwd: loc,
msg: "!",
}
}
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_expr_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
format!("ls {}", "."),
env: env,
pwd: loc,
msg: format!("!"),
}
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_expr_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
exe: format!("ls {}", "."),
env: env,
pwd: loc,
msg: format!("!"),
}
}
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_literal_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
"ls",
env: env,
pwd: loc,
msg: format!("!"),
}
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_literal_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
exe: "ls",
env: env,
pwd: loc,
msg: format!("!"),
}
}
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_literal_msg_some_expr<Loc: Location>(
env: Env,
loc: Loc,
) -> Cmd<Loc> {
cmd! {
"ls",
env: env,
pwd: loc,
msg: Some(format!("!")),
}
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_literal_msg_some_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
exe: "ls",
env: env,
pwd: loc,
msg: Some(format!("!")),
}
}
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_expr_msg_some_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
format!("ls {}", "."),
env: env,
pwd: loc,
msg: Some(format!("!")),
}
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_expr_msg_some_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
exe: format!("ls {}", "."),
env: env,
pwd: loc,
msg: Some(format!("!")),
}
}
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_literal_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
"ls",
env: env,
pwd: loc,
msg: None,
}
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_literal_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
exe: "ls",
env: env,
pwd: loc,
msg: None,
}
}
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_expr_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
format!("ls {}", "."),
env: env,
pwd: loc,
msg: None,
}
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_expr_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
exe: format!("ls {}", "."),
env: env,
pwd: loc,
msg: None,
}
}
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_literal_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
"ls",
env: env,
pwd: loc,
}
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_literal_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
exe: "ls",
env: env,
pwd: loc,
}
}
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_expr_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
format!("ls {}", "."),
env: env,
pwd: loc,
}
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_expr_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! {
exe: format!("ls {}", "."),
env: env,
pwd: loc,
}
}
#[allow(dead_code)]
fn cmd_macro_unlabeled_exe_no_trailing_comma<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! { "ls", env: env, pwd: loc }
}
#[allow(dead_code)]
fn cmd_macro_labeled_exe_no_trailing_comma<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
cmd! { exe: "ls", env: env, pwd: loc }
}
}