use crate::{
Result,
fast::{CompactString, SmallVec},
};
use std::{
ffi::OsStr,
path::PathBuf,
process::{Child, Stdio},
sync::Mutex,
thread,
time::{Duration, Instant},
};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct TsgoCommand {
executable: PathBuf,
cwd: PathBuf,
env: SmallVec<[(CompactString, CompactString); 4]>,
}
impl TsgoCommand {
pub fn new(executable: impl Into<PathBuf>) -> Self {
Self {
executable: executable.into(),
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
env: SmallVec::new(),
}
}
pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
self.cwd = cwd.into();
self
}
pub fn with_env(
mut self,
key: impl Into<CompactString>,
value: impl Into<CompactString>,
) -> Self {
let key = key.into();
let value = value.into();
if let Some((_, entry)) = self.env.iter_mut().find(|(existing, _)| existing == key) {
*entry = value;
} else {
self.env.push((key, value));
}
self
}
pub fn cwd(&self) -> &PathBuf {
&self.cwd
}
pub fn spawn_async<I, S>(&self, args: I) -> std::io::Result<Child>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.spawn(args)
}
pub fn spawn_blocking<I, S>(&self, args: I) -> std::io::Result<std::process::Child>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.spawn(args)
}
fn spawn<I, S>(&self, args: I) -> std::io::Result<std::process::Child>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut command = std::process::Command::new(&self.executable);
command
.args(args)
.current_dir(&self.cwd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.envs(
self.env
.iter()
.map(|(key, value)| (key.as_str(), value.as_str())),
);
command.spawn()
}
}
#[derive(Debug)]
pub struct AsyncChildGuard {
child: Mutex<Option<Child>>,
}
impl AsyncChildGuard {
pub fn new(child: Child) -> Self {
Self {
child: Mutex::new(Some(child)),
}
}
pub async fn shutdown(&self, wait_for: Duration) -> Result<()> {
let mut child = self.child.lock().unwrap();
let Some(mut child) = child.take() else {
return Ok(());
};
wait_for_child_exit(&mut child, wait_for)?;
Ok(())
}
}
impl Drop for AsyncChildGuard {
fn drop(&mut self) {
if let Ok(mut child) = self.child.try_lock()
&& let Some(child) = child.as_mut()
{
let _ = terminate_child_process(child);
}
}
}
pub fn wait_for_child_exit(child: &mut Child, wait_for: Duration) -> std::io::Result<()> {
let deadline = Instant::now() + wait_for;
loop {
if child.try_wait()?.is_some() {
return Ok(());
}
if Instant::now() >= deadline {
return terminate_child_process(child);
}
thread::sleep(Duration::from_millis(10));
}
}
pub fn terminate_child_process(child: &mut Child) -> std::io::Result<()> {
if child.try_wait()?.is_some() {
return Ok(());
}
match child.kill() {
Ok(()) => {}
Err(error) => {
if child.try_wait()?.is_none() {
return Err(error);
}
return Ok(());
}
}
let _ = child.wait()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{terminate_child_process, wait_for_child_exit};
use std::{process::Command, time::Duration};
#[cfg(unix)]
#[test]
fn terminate_child_process_reaps_running_child() {
let mut child = Command::new("sh")
.arg("-c")
.arg("sleep 30")
.spawn()
.expect("spawn sleeper");
terminate_child_process(&mut child).expect("terminate child");
assert!(child.try_wait().expect("try_wait").is_some());
}
#[cfg(unix)]
#[test]
fn wait_for_child_exit_times_out_and_reaps() {
let mut child = Command::new("sh")
.arg("-c")
.arg("sleep 30")
.spawn()
.expect("spawn sleeper");
wait_for_child_exit(&mut child, Duration::from_millis(10)).expect("wait with timeout");
assert!(child.try_wait().expect("try_wait").is_some());
}
#[cfg(unix)]
#[test]
fn terminate_child_process_is_ok_after_natural_exit() {
let mut child = Command::new("sh")
.arg("-c")
.arg("exit 0")
.spawn()
.expect("spawn exiting child");
let _ = child.wait().expect("wait");
terminate_child_process(&mut child).expect("terminate exited child");
}
}