2021-08-19 10:44:02 +02:00
|
|
|
//! Command construction and configuration thereof.
|
|
|
|
|
2021-08-22 14:28:20 +02:00
|
|
|
use std::process::ExitStatus;
|
|
|
|
|
|
|
|
use command_group::{AsyncGroupChild, Signal};
|
|
|
|
use tokio::process::{Child, Command};
|
2021-08-22 16:34:44 +02:00
|
|
|
use tracing::{debug, trace};
|
2021-08-22 14:28:20 +02:00
|
|
|
|
|
|
|
use crate::error::RuntimeError;
|
2021-08-16 17:09:22 +02:00
|
|
|
|
|
|
|
/// Shell to use to run commands.
|
|
|
|
///
|
|
|
|
/// `Cmd` and `Powershell` are special-cased because they have different calling
|
|
|
|
/// conventions. Also `Cmd` is only available in Windows, while `Powershell` is
|
|
|
|
/// also available on unices (provided the end-user has it installed, of course).
|
|
|
|
///
|
2021-08-21 10:46:44 +02:00
|
|
|
/// See [`Config.cmd`] for the semantics of `None` vs the
|
2021-08-16 17:09:22 +02:00
|
|
|
/// other options.
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
|
|
pub enum Shell {
|
2021-08-18 15:12:50 +02:00
|
|
|
/// Use no shell, and execute the command directly.
|
|
|
|
None,
|
|
|
|
|
|
|
|
/// Use the given string as a unix shell invocation.
|
|
|
|
///
|
|
|
|
/// This means two things:
|
|
|
|
/// - the program is invoked with `-c` followed by the command, and
|
|
|
|
/// - the string will be split on space, and the resulting vec used as
|
|
|
|
/// execvp(3) arguments: first is the shell program, rest are additional
|
|
|
|
/// arguments (which come before the `-c` mentioned above). This is a very
|
|
|
|
/// simplistic approach deliberately: it will not support quoted
|
|
|
|
/// arguments, for example. Use [`Shell::None`] with a custom command vec
|
|
|
|
/// if you want that.
|
|
|
|
Unix(String),
|
|
|
|
|
|
|
|
/// Use the Windows CMD.EXE shell.
|
|
|
|
///
|
|
|
|
/// This is invoked with `/C` followed by the command.
|
|
|
|
#[cfg(windows)]
|
|
|
|
Cmd,
|
|
|
|
|
|
|
|
/// Use Powershell, on Windows or elsewhere.
|
|
|
|
///
|
|
|
|
/// This is invoked with `-Command` followed by the command.
|
|
|
|
///
|
|
|
|
/// This is preferred over `Unix("pwsh")`, though that will also work
|
|
|
|
/// on unices due to Powershell supporting the `-c` short option.
|
|
|
|
Powershell,
|
2021-08-16 17:09:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Shell {
|
2021-08-18 15:12:50 +02:00
|
|
|
#[cfg(windows)]
|
|
|
|
fn default() -> Self {
|
|
|
|
Self::Powershell
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(not(windows))]
|
|
|
|
fn default() -> Self {
|
|
|
|
Self::Unix("sh".into())
|
|
|
|
}
|
2021-08-16 17:09:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Shell {
|
2021-08-21 10:46:44 +02:00
|
|
|
/// Obtain a [`Command`] given a list of command parts.
|
2021-08-18 15:12:50 +02:00
|
|
|
///
|
|
|
|
/// Behaves as described in the enum documentation.
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// - Panics if `cmd` is empty.
|
|
|
|
/// - Panics if the string in the `Unix` variant is empty or only whitespace.
|
|
|
|
pub fn to_command(&self, cmd: &[String]) -> Command {
|
|
|
|
assert!(!cmd.is_empty(), "cmd was empty");
|
2021-08-22 17:11:58 +02:00
|
|
|
trace!(shell=?self, ?cmd, "constructing command");
|
2021-08-18 15:12:50 +02:00
|
|
|
|
|
|
|
match self {
|
|
|
|
Shell::None => {
|
|
|
|
// UNWRAP: checked by assert
|
|
|
|
#[allow(clippy::unwrap_used)]
|
|
|
|
let (first, rest) = cmd.split_first().unwrap();
|
|
|
|
let mut c = Command::new(first);
|
|
|
|
c.args(rest);
|
|
|
|
c
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
Shell::Cmd => {
|
|
|
|
let mut c = Command::new("cmd.exe");
|
|
|
|
c.arg("/C").arg(cmd.join(" "));
|
|
|
|
c
|
|
|
|
}
|
|
|
|
|
|
|
|
Shell::Powershell if cfg!(windows) => {
|
|
|
|
let mut c = Command::new("powershell.exe");
|
|
|
|
c.arg("-Command").arg(cmd.join(" "));
|
|
|
|
c
|
|
|
|
}
|
|
|
|
|
|
|
|
Shell::Powershell => {
|
|
|
|
let mut c = Command::new("pwsh");
|
|
|
|
c.arg("-Command").arg(cmd.join(" "));
|
|
|
|
c
|
|
|
|
}
|
|
|
|
|
|
|
|
Shell::Unix(name) => {
|
|
|
|
assert!(!name.is_empty(), "shell program was empty");
|
|
|
|
let sh = name.split_ascii_whitespace().collect::<Vec<_>>();
|
|
|
|
|
|
|
|
// UNWRAP: checked by assert
|
|
|
|
#[allow(clippy::unwrap_used)]
|
|
|
|
let (shprog, shopts) = sh.split_first().unwrap();
|
|
|
|
|
|
|
|
let mut c = Command::new(shprog);
|
|
|
|
c.args(shopts);
|
|
|
|
c.arg("-c").arg(cmd.join(" "));
|
|
|
|
c
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-08-16 17:09:22 +02:00
|
|
|
}
|
|
|
|
|
2021-08-22 14:28:20 +02:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum Process {
|
|
|
|
None,
|
|
|
|
Grouped(AsyncGroupChild),
|
|
|
|
Ungrouped(Child),
|
|
|
|
Done(ExitStatus),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Process {
|
|
|
|
fn default() -> Self {
|
|
|
|
Process::None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Process {
|
|
|
|
#[cfg(unix)]
|
|
|
|
pub fn signal(&mut self, sig: Signal) -> Result<(), RuntimeError> {
|
|
|
|
use command_group::UnixChildExt;
|
|
|
|
|
|
|
|
match self {
|
|
|
|
Self::None | Self::Done(_) => Ok(()),
|
|
|
|
Self::Grouped(c) => {
|
|
|
|
debug!(signal=%sig, pgid=?c.id(), "sending signal to process group");
|
|
|
|
c.signal(sig)
|
|
|
|
}
|
|
|
|
Self::Ungrouped(c) => {
|
|
|
|
debug!(signal=%sig, pid=?c.id(), "sending signal to process");
|
|
|
|
c.signal(sig)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.map_err(RuntimeError::Process)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn kill(&mut self) -> Result<(), RuntimeError> {
|
|
|
|
match self {
|
|
|
|
Self::None | Self::Done(_) => Ok(()),
|
|
|
|
Self::Grouped(c) => {
|
|
|
|
debug!(pgid=?c.id(), "killing process group");
|
|
|
|
c.kill()
|
|
|
|
}
|
|
|
|
Self::Ungrouped(c) => {
|
|
|
|
debug!(pid=?c.id(), "killing process");
|
|
|
|
c.kill().await
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.map_err(RuntimeError::Process)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_running(&mut self) -> Result<bool, RuntimeError> {
|
|
|
|
match self {
|
|
|
|
Self::None | Self::Done(_) => Ok(false),
|
|
|
|
Self::Grouped(c) => c.try_wait().map(|status| {
|
2021-08-22 16:34:44 +02:00
|
|
|
trace!("try-waiting on process group");
|
|
|
|
if let Some(status) = status {
|
|
|
|
trace!(?status, "converting to ::Done");
|
|
|
|
*self = Self::Done(status);
|
2021-08-22 14:28:20 +02:00
|
|
|
true
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
Self::Ungrouped(c) => c.try_wait().map(|status| {
|
2021-08-22 16:34:44 +02:00
|
|
|
trace!("try-waiting on process");
|
|
|
|
if let Some(status) = status {
|
|
|
|
trace!(?status, "converting to ::Done");
|
|
|
|
*self = Self::Done(status);
|
2021-08-22 14:28:20 +02:00
|
|
|
true
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
.map_err(RuntimeError::Process)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn wait(&mut self) -> Result<Option<ExitStatus>, RuntimeError> {
|
|
|
|
match self {
|
|
|
|
Self::None => Ok(None),
|
|
|
|
Self::Done(status) => Ok(Some(*status)),
|
|
|
|
Self::Grouped(c) => {
|
2021-08-22 16:34:44 +02:00
|
|
|
trace!("waiting on process group");
|
2021-08-22 14:28:20 +02:00
|
|
|
let status = c.wait().await?;
|
2021-08-22 16:34:44 +02:00
|
|
|
trace!(?status, "converting to ::Done");
|
2021-08-22 14:28:20 +02:00
|
|
|
*self = Self::Done(status);
|
|
|
|
Ok(Some(status))
|
|
|
|
}
|
|
|
|
Self::Ungrouped(c) => {
|
2021-08-22 16:34:44 +02:00
|
|
|
trace!("waiting on process");
|
2021-08-22 14:28:20 +02:00
|
|
|
let status = c.wait().await?;
|
2021-08-22 16:34:44 +02:00
|
|
|
trace!(?status, "converting to ::Done");
|
2021-08-22 14:28:20 +02:00
|
|
|
*self = Self::Done(status);
|
|
|
|
Ok(Some(status))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.map_err(RuntimeError::Process)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-16 17:09:22 +02:00
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
2021-08-18 15:12:50 +02:00
|
|
|
use super::Shell;
|
|
|
|
use command_group::AsyncCommandGroup;
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[cfg(unix)]
|
|
|
|
async fn unix_shell_default() -> Result<(), std::io::Error> {
|
|
|
|
assert!(Shell::default()
|
|
|
|
.to_command(&["echo".into(), "hi".into()])
|
|
|
|
.group_status()
|
|
|
|
.await?
|
|
|
|
.success());
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[cfg(unix)]
|
|
|
|
async fn unix_shell_none() -> Result<(), std::io::Error> {
|
|
|
|
assert!(Shell::None
|
|
|
|
.to_command(&["echo".into(), "hi".into()])
|
|
|
|
.group_status()
|
|
|
|
.await?
|
|
|
|
.success());
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[cfg(unix)]
|
|
|
|
async fn unix_shell_alternate() -> Result<(), std::io::Error> {
|
|
|
|
assert!(Shell::Unix("bash".into())
|
|
|
|
.to_command(&["echo".into(), "hi".into()])
|
|
|
|
.group_status()
|
|
|
|
.await?
|
|
|
|
.success());
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[cfg(unix)]
|
|
|
|
async fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> {
|
|
|
|
assert!(Shell::Unix("bash -o errexit".into())
|
|
|
|
.to_command(&["echo".into(), "hi".into()])
|
|
|
|
.group_status()
|
|
|
|
.await?
|
|
|
|
.success());
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[cfg(windows)]
|
|
|
|
async fn windows_shell_default() -> Result<(), std::io::Error> {
|
|
|
|
assert!(Shell::default()
|
|
|
|
.to_command(&["echo".into(), "hi".into()])
|
|
|
|
.group_status()
|
|
|
|
.await?
|
|
|
|
.success());
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[cfg(windows)]
|
|
|
|
async fn windows_shell_cmd() -> Result<(), std::io::Error> {
|
|
|
|
assert!(Shell::Cmd
|
|
|
|
.to_command(&["echo".into(), "hi".into()])
|
|
|
|
.group_status()
|
|
|
|
.await?
|
|
|
|
.success());
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[cfg(windows)]
|
|
|
|
async fn windows_shell_powershell() -> Result<(), std::io::Error> {
|
|
|
|
assert!(Shell::Powershell
|
|
|
|
.to_command(&["echo".into(), "hi".into()])
|
|
|
|
.group_status()
|
|
|
|
.await?
|
|
|
|
.success());
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[cfg(windows)]
|
|
|
|
async fn windows_shell_unix_style_powershell() -> Result<(), std::io::Error> {
|
|
|
|
assert!(Shell::Unix("powershell.exe".into())
|
|
|
|
.to_command(&["echo".into(), "hi".into()])
|
|
|
|
.group_status()
|
|
|
|
.await?
|
|
|
|
.success());
|
|
|
|
Ok(())
|
|
|
|
}
|
2021-08-16 17:09:22 +02:00
|
|
|
}
|