#![deny(rustdoc::missing_crate_level_docs)]
#![warn(missing_docs)]
#![doc(test(attr(deny(warnings))))]
#![deny(clippy::all)]
#![allow(clippy::collapsible_else_if)]
#![allow(clippy::result_large_err)]
pub mod fs {
use displaydoc::Display;
use std::path::PathBuf;
pub(crate) trait PathWrapper {
fn into_path_buf(self) -> PathBuf;
}
#[derive(Debug, Display, Clone)]
#[ignore_extra_doc_attributes]
pub struct File(pub PathBuf);
impl PathWrapper for File {
fn into_path_buf(self) -> PathBuf {
let Self(path) = self;
path
}
}
#[derive(Debug, Display, Clone)]
#[ignore_extra_doc_attributes]
pub struct Directory(pub PathBuf);
impl PathWrapper for Directory {
fn into_path_buf(self) -> PathBuf {
let Self(path) = self;
path
}
}
}
pub mod exe {
use super::fs::{self, PathWrapper};
use displaydoc::Display;
use indexmap::IndexMap;
use once_cell::sync::Lazy;
use signal_hook::consts::{signal::*, TERM_SIGNALS};
use thiserror::Error;
use std::{
collections::VecDeque,
ffi::{OsStr, OsString},
io, iter,
os::unix::process::ExitStatusExt,
path::{Path, PathBuf},
process, str,
};
#[derive(Debug, Display, Clone)]
#[ignore_extra_doc_attributes]
pub struct Exe(pub fs::File);
impl<R: AsRef<OsStr>> From<&R> for Exe {
fn from(value: &R) -> Self {
let p = Path::new(value);
let f = fs::File(p.to_path_buf());
Self(f)
}
}
impl Default for Exe {
fn default() -> Self { Self(fs::File(PathBuf::default())) }
}
impl Exe {
pub fn is_empty(&self) -> bool {
let Self(fs::File(exe)) = self;
exe.as_os_str().is_empty()
}
}
impl PathWrapper for Exe {
fn into_path_buf(self) -> PathBuf {
let Self(exe) = self;
exe.into_path_buf()
}
}
#[derive(Debug, Display, Clone, Default)]
#[ignore_extra_doc_attributes]
pub struct Argv(pub VecDeque<OsString>);
impl<R: AsRef<OsStr>, I: iter::IntoIterator<Item=R>> From<I> for Argv {
fn from(value: I) -> Self {
let argv: VecDeque<OsString> = value
.into_iter()
.map(|s| {
let s: &OsStr = s.as_ref();
s.to_os_string()
})
.collect();
Self(argv)
}
}
impl Argv {
pub fn trailing_args(mut self) -> Self {
if self.0.is_empty() {
Self(VecDeque::new())
} else {
self.unshift("--".into());
self
}
}
pub fn unshift(&mut self, leftmost_arg: OsString) {
let Self(ref mut argv) = self;
argv.push_front(leftmost_arg);
}
}
#[derive(Debug, Display, Clone, Default)]
#[ignore_extra_doc_attributes]
pub struct EnvModifications(pub IndexMap<OsString, OsString>);
impl<R: AsRef<OsStr>, I: iter::IntoIterator<Item=(R, R)>> From<I> for EnvModifications {
fn from(value: I) -> Self {
let env: IndexMap<OsString, OsString> = value
.into_iter()
.map(|(k, v)| {
let k: &OsStr = k.as_ref();
let v: &OsStr = v.as_ref();
(k.to_os_string(), v.to_os_string())
})
.collect();
Self(env)
}
}
#[derive(Debug, Display, Clone, Default)]
#[ignore_extra_doc_attributes]
pub struct Command {
pub exe: Exe,
pub wd: Option<fs::Directory>,
pub argv: Argv,
pub env: EnvModifications,
}
impl Command {
pub(crate) fn command(self) -> async_process::Command {
dbg!(&self);
let Self {
exe,
wd,
argv,
env: EnvModifications(env),
} = self;
if exe.is_empty() {
unreachable!(
"command was executed before .exe was set; this can only occur using ::default()"
);
}
let mut command = async_process::Command::new(exe.into_path_buf());
if let Some(wd) = wd {
command.current_dir(wd.into_path_buf());
}
command.args(argv.0);
for (var, val) in env.into_iter() {
command.env(&var, &val);
}
command
}
pub fn unshift_new_exe(&mut self, new_exe: Exe) {
if new_exe.is_empty() {
unreachable!("new_exe is an empty string!! self was: {:?}", self);
}
let mut argv = self.argv.clone();
if !self.exe.is_empty() {
argv.unshift(self.exe.clone().into_path_buf().as_os_str().to_os_string());
}
self.argv = argv;
self.exe = new_exe;
}
pub(crate) fn unshift_shell_script(&mut self, script_path: Exe) {
self.unshift_new_exe(script_path);
self.unshift_new_exe(Exe(fs::File(PathBuf::from("sh"))));
}
}
#[derive(Debug, Display, Error)]
pub enum CommandError {
NonZeroExit(i32),
ProcessTerminated(i32, &'static str),
ProcessKilled(i32, &'static str),
Io(#[from] io::Error),
Utf8(#[from] str::Utf8Error),
}
macro_rules! signal_pairs {
($($name:ident),+) => {
[$(($name, stringify!($name))),+]
}
}
static SIGNAL_NAMES: Lazy<IndexMap<i32, &'static str>> = Lazy::new(|| {
signal_pairs![
SIGABRT, SIGALRM, SIGBUS, SIGCHLD, SIGCONT, SIGFPE, SIGHUP, SIGILL, SIGINT, SIGIO, SIGKILL,
SIGPIPE, SIGPROF, SIGQUIT, SIGSEGV, SIGSTOP, SIGSYS, SIGTERM, SIGTRAP, SIGTSTP, SIGTTIN,
SIGTTOU, SIGURG, SIGUSR1, SIGUSR2, SIGVTALRM, SIGWINCH, SIGXCPU, SIGXFSZ
]
.into()
});
impl CommandError {
pub fn analyze_exit_status(status: process::ExitStatus) -> Result<(), Self> {
if let Some(code) = status.code() {
if code == 0 {
Ok(())
} else {
Err(Self::NonZeroExit(code))
}
} else if let Some(signal) = status.signal() {
let name = SIGNAL_NAMES.get(&signal).unwrap();
Err(if TERM_SIGNALS.contains(&signal) {
Self::ProcessTerminated(signal, name)
} else {
Self::ProcessKilled(signal, name)
})
} else {
unreachable!("status {:?} had no exit code or signal", status)
}
}
pub(crate) fn command_with_context(
self,
command: Command,
context: String,
) -> CommandErrorWrapper {
CommandErrorWrapper {
command,
context,
error: self,
}
}
}
#[derive(Debug, Display, Error)]
pub struct CommandErrorWrapper {
pub command: Command,
pub context: String,
#[source]
pub error: CommandError,
}
}
pub mod base {
use super::*;
use async_trait::async_trait;
use displaydoc::Display;
use thiserror::Error;
use std::io;
#[derive(Debug, Display, Error)]
pub enum SetupError {
Inner(#[source] Box<SetupErrorWrapper>),
Io(#[from] io::Error),
}
impl SetupError {
pub fn with_context(self, context: String) -> SetupErrorWrapper {
SetupErrorWrapper {
context,
error: self,
}
}
}
#[derive(Debug, Display, Error)]
pub struct SetupErrorWrapper {
pub context: String,
#[source]
pub error: SetupError,
}
#[async_trait]
pub trait CommandBase {
async fn setup_command(self) -> Result<exe::Command, SetupError>;
}
}
pub mod sync {
use super::exe;
use async_trait::async_trait;
use std::{process, str};
#[derive(Debug, Clone)]
#[allow(missing_docs)]
pub struct RawOutput {
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
impl RawOutput {
pub fn extract(
command: exe::Command,
output: process::Output,
) -> Result<Self, exe::CommandErrorWrapper> {
let process::Output {
status,
stdout,
stderr,
} = output;
let output = Self { stdout, stderr };
if let Err(e) = exe::CommandError::analyze_exit_status(status) {
let output_msg: String = match output.decode(command.clone()) {
Ok(decoded) => format!("(utf-8 decoded) {:?}", decoded),
Err(_) => format!("(could not decode) {:?}", &output),
};
return Err(e.command_with_context(
command,
format!("when analyzing exit status for output {}", output_msg),
));
}
Ok(output)
}
pub fn decode(
&self,
command: exe::Command,
) -> Result<DecodedOutput<'_>, exe::CommandErrorWrapper> {
let Self { stdout, stderr } = self;
let stdout =
str::from_utf8(stdout)
.map_err(|e| e.into())
.map_err(|e: exe::CommandError| {
e.command_with_context(
command.clone(),
format!("when decoding stdout from {:?}", &self),
)
})?;
let stderr =
str::from_utf8(stderr)
.map_err(|e| e.into())
.map_err(|e: exe::CommandError| {
e.command_with_context(command, format!("when decoding stderr from {:?}", &self))
})?;
Ok(DecodedOutput { stdout, stderr })
}
}
#[derive(Debug, Clone)]
#[allow(missing_docs)]
pub struct DecodedOutput<'a> {
pub stdout: &'a str,
pub stderr: &'a str,
}
#[async_trait]
pub trait SyncInvocable {
async fn invoke(self) -> Result<RawOutput, exe::CommandErrorWrapper>;
}
#[async_trait]
impl SyncInvocable for exe::Command {
async fn invoke(self) -> Result<RawOutput, exe::CommandErrorWrapper> {
let mut command = self.clone().command();
let output =
command
.output()
.await
.map_err(|e| e.into())
.map_err(|e: exe::CommandError| {
e.command_with_context(self.clone(), "waiting for output".to_string())
})?;
let output = RawOutput::extract(self, output)?;
Ok(output)
}
}
}
pub mod stream {
use super::exe;
use async_process::{self, Child, Stdio};
pub struct Streaming {
pub child: Child,
pub command: exe::Command,
}
impl Streaming {
pub async fn wait(self) -> Result<(), exe::CommandErrorWrapper> {
let Self { mut child, command } = self;
let status = child.status().await.map_err(|e| {
let e: exe::CommandError = e.into();
e.command_with_context(command.clone(), "merging async streams".to_string())
})?;
exe::CommandError::analyze_exit_status(status)
.map_err(|e| e.command_with_context(command, "checking async exit status".to_string()))?;
Ok(())
}
}
pub trait Streamable {
fn invoke_streaming(self) -> Result<Streaming, exe::CommandErrorWrapper>;
}
impl Streamable for exe::Command {
fn invoke_streaming(self) -> Result<Streaming, exe::CommandErrorWrapper> {
let mut command = self.clone().command();
let child = command
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| e.into())
.map_err(|e: exe::CommandError| {
e.command_with_context(self.clone(), "spawning async process".to_string())
})?;
Ok(Streaming {
child,
command: self,
})
}
}
}
pub mod sh {
use super::{
base::{self, CommandBase},
exe, fs,
sync::SyncInvocable,
};
use async_trait::async_trait;
use displaydoc::Display;
use indexmap::IndexMap;
use tempfile::{NamedTempFile, TempPath};
use thiserror::Error;
use std::{
ffi::OsString,
io::{self, BufRead, Write},
str,
};
#[derive(Debug, Display, Error)]
pub enum ShellError {
Setup(#[from] base::SetupErrorWrapper),
Command(#[from] exe::CommandErrorWrapper),
Io(#[from] io::Error),
Utf8(#[from] str::Utf8Error),
}
impl ShellError {
pub fn with_context(self, context: String) -> ShellErrorWrapper {
ShellErrorWrapper {
context,
error: self,
}
}
}
#[derive(Debug, Display, Error)]
pub struct ShellErrorWrapper {
pub context: String,
#[source]
pub error: ShellError,
}
#[derive(Debug, Clone)]
pub struct ShellSource {
pub contents: Vec<u8>,
}
impl ShellSource {
fn write_to_temp_path(self) -> io::Result<TempPath> {
let (mut script_file, script_path) = NamedTempFile::new()?.into_parts();
let Self { contents } = self;
script_file.write_all(&contents)?;
script_file.sync_all()?;
Ok(script_path)
}
pub async fn into_script(self) -> Result<ShellScript, ShellError> {
let script_path = self.write_to_temp_path()?;
let script_path = exe::Exe(fs::File(
script_path
.keep()
.expect("should never be any error keeping the shell script path"),
));
Ok(ShellScript { script_path })
}
}
#[derive(Debug, Clone)]
pub struct EnvAfterScript {
pub source: ShellSource,
}
impl EnvAfterScript {
fn into_source(self) -> ShellSource {
let Self {
source: ShellSource { mut contents },
} = self;
contents.extend_from_slice(b"\n\nexec env");
ShellSource { contents }
}
async fn into_command(self) -> Result<exe::Command, ShellErrorWrapper> {
let source = self.into_source();
let script = source
.into_script()
.await
.map_err(|e| e.with_context("when writing env script to file".to_string()))?;
let sh = script.with_command(exe::Command::default());
let command = sh
.setup_command()
.await
.map_err(|e| {
e.with_context("when setting up the shell command".to_string())
.into()
})
.map_err(|e: ShellError| {
e.with_context("when setting up the shell command, again".to_string())
})?;
Ok(command)
}
async fn extract_stdout(self) -> Result<Vec<u8>, ShellErrorWrapper> {
let command = self.into_command().await?;
let output = command
.invoke()
.await
.map_err(|e| e.into())
.map_err(|e: ShellError| e.with_context("when extracting env bindings".to_string()))?;
Ok(output.stdout)
}
pub async fn extract_env_bindings(self) -> Result<exe::EnvModifications, ShellErrorWrapper> {
let stdout = self.extract_stdout().await?;
let mut env_map: IndexMap<OsString, OsString> = IndexMap::new();
for line in stdout.lines() {
let line = line
.map_err(|e| e.into())
.map_err(|e: ShellError| e.with_context("when extracting stdout line".to_string()))?;
if let Some(equals_index) = line.find('=') {
let key = &line[..equals_index];
let value = &line[equals_index + 1..];
env_map.insert(key.into(), value.into());
}
}
Ok(exe::EnvModifications(env_map))
}
}
#[derive(Debug, Clone)]
pub struct ShellScript {
pub script_path: exe::Exe,
}
impl ShellScript {
pub fn with_command(self, base: exe::Command) -> ShellScriptInvocation {
ShellScriptInvocation { script: self, base }
}
}
#[derive(Debug, Clone)]
pub struct ShellScriptInvocation {
pub script: ShellScript,
pub base: exe::Command,
}
#[async_trait]
impl CommandBase for ShellScriptInvocation {
async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
let Self {
script: ShellScript { script_path },
mut base,
} = self;
base.unshift_shell_script(script_path);
Ok(base)
}
}
}