use std::{
ffi::OsStr,
io,
path::Path,
process::{Child, ChildStdin, Command, Stdio},
};
use crate::utils::is_recoverable_kill_error;
pub trait ConfigureCommand {
fn current_dir(&mut self, dir: &Path);
fn env(&mut self, name: &str, value: &OsStr);
}
impl ConfigureCommand for Command {
fn current_dir(&mut self, dir: &Path) {
self.current_dir(dir);
}
fn env(&mut self, name: &str, value: &OsStr) {
self.env(name, value);
}
}
pub trait SpawnShell: ConfigureCommand {
type ShellProcess: ShellProcess;
type Reader: io::Read + 'static + Send;
type Writer: io::Write + 'static + Send;
fn spawn_shell(&mut self) -> io::Result<SpawnedShell<Self>>;
}
pub trait ShellProcess {
fn check_is_alive(&mut self) -> io::Result<()>;
fn terminate(self) -> io::Result<()>;
fn is_echoing(&self) -> bool {
false
}
}
#[derive(Debug)]
pub struct SpawnedShell<S: SpawnShell + ?Sized> {
pub shell: S::ShellProcess,
pub reader: S::Reader,
pub writer: S::Writer,
}
impl SpawnShell for Command {
type ShellProcess = Child;
type Reader = os_pipe::PipeReader;
type Writer = ChildStdin;
#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
fn spawn_shell(&mut self) -> io::Result<SpawnedShell<Self>> {
let (pipe_reader, pipe_writer) = os_pipe::pipe()?;
#[cfg(feature = "tracing")]
tracing::debug!("created OS pipe");
let mut shell = self
.stdin(Stdio::piped())
.stdout(pipe_writer.try_clone()?)
.stderr(pipe_writer)
.spawn()?;
#[cfg(feature = "tracing")]
tracing::debug!("created child");
self.stdout(Stdio::null()).stderr(Stdio::null());
let stdin = shell.stdin.take().unwrap();
Ok(SpawnedShell {
shell,
reader: pipe_reader,
writer: stdin,
})
}
}
impl ShellProcess for Child {
#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
fn check_is_alive(&mut self) -> io::Result<()> {
if let Some(exit_status) = self.try_wait()? {
let message = format!("Shell process has prematurely exited: {exit_status}");
Err(io::Error::new(io::ErrorKind::BrokenPipe, message))
} else {
Ok(())
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
fn terminate(mut self) -> io::Result<()> {
if self.try_wait()?.is_none() {
self.kill().or_else(|err| {
if is_recoverable_kill_error(&err) {
Ok(())
} else {
Err(err)
}
})?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Echoing<S> {
inner: S,
is_echoing: bool,
}
impl<S> Echoing<S> {
pub fn new(inner: S, is_echoing: bool) -> Self {
Self { inner, is_echoing }
}
}
impl<S: ConfigureCommand> ConfigureCommand for Echoing<S> {
fn current_dir(&mut self, dir: &Path) {
self.inner.current_dir(dir);
}
fn env(&mut self, name: &str, value: &OsStr) {
self.inner.env(name, value);
}
}
impl<S: SpawnShell> SpawnShell for Echoing<S> {
type ShellProcess = Echoing<S::ShellProcess>;
type Reader = S::Reader;
type Writer = S::Writer;
fn spawn_shell(&mut self) -> io::Result<SpawnedShell<Self>> {
let spawned = self.inner.spawn_shell()?;
Ok(SpawnedShell {
shell: Echoing {
inner: spawned.shell,
is_echoing: self.is_echoing,
},
reader: spawned.reader,
writer: spawned.writer,
})
}
}
impl<S: ShellProcess> ShellProcess for Echoing<S> {
fn check_is_alive(&mut self) -> io::Result<()> {
self.inner.check_is_alive()
}
fn terminate(self) -> io::Result<()> {
self.inner.terminate()
}
fn is_echoing(&self) -> bool {
self.is_echoing
}
}