use std::io;
use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus};
use std::sync::{Condvar, Mutex};
mod sys;
#[cfg(unix)]
pub mod unix;
#[derive(Debug)]
pub struct SharedChild {
child: Mutex<Child>,
state_lock: Mutex<ChildState>,
state_condvar: Condvar,
}
impl SharedChild {
pub fn spawn(command: &mut Command) -> io::Result<Self> {
let child = command.spawn()?;
Ok(Self {
child: Mutex::new(child),
state_lock: Mutex::new(NotWaiting),
state_condvar: Condvar::new(),
})
}
pub fn new(mut child: Child) -> io::Result<Self> {
let state = match child.try_wait()? {
Some(status) => Exited(status),
None => NotWaiting,
};
Ok(Self {
child: Mutex::new(child),
state_lock: Mutex::new(state),
state_condvar: Condvar::new(),
})
}
pub fn id(&self) -> u32 {
self.child.lock().unwrap().id()
}
fn get_handle(&self) -> sys::Handle {
sys::get_handle(&self.child.lock().unwrap())
}
pub fn wait(&self) -> io::Result<ExitStatus> {
let mut state = self.state_lock.lock().unwrap();
loop {
match *state {
NotWaiting => {
break;
}
Waiting => {
state = self.state_condvar.wait(state).unwrap();
}
Exited(exit_status) => return Ok(exit_status),
}
}
*state = Waiting;
drop(state);
let noreap_result = sys::wait_without_reaping(self.get_handle());
let mut state = self.state_lock.lock().unwrap();
let final_result = noreap_result.and_then(|_| self.child.lock().unwrap().wait());
*state = if let Ok(exit_status) = final_result {
Exited(exit_status)
} else {
NotWaiting
};
self.state_condvar.notify_all();
final_result
}
pub fn try_wait(&self) -> io::Result<Option<ExitStatus>> {
let mut status = self.state_lock.lock().unwrap();
match *status {
NotWaiting => {}
Waiting => return Ok(None),
Exited(exit_status) => return Ok(Some(exit_status)),
};
if sys::try_wait_without_reaping(self.get_handle())? {
let exit_status = self.child.lock().unwrap().wait()?;
*status = Exited(exit_status);
Ok(Some(exit_status))
} else {
Ok(None)
}
}
pub fn kill(&self) -> io::Result<()> {
let status = self.state_lock.lock().unwrap();
if let Exited(_) = *status {
return Ok(());
}
self.child.lock().unwrap().kill()
}
pub fn into_inner(self) -> Child {
self.child.into_inner().unwrap()
}
pub fn take_stdin(&self) -> Option<ChildStdin> {
self.child.lock().unwrap().stdin.take()
}
pub fn take_stdout(&self) -> Option<ChildStdout> {
self.child.lock().unwrap().stdout.take()
}
pub fn take_stderr(&self) -> Option<ChildStderr> {
self.child.lock().unwrap().stderr.take()
}
}
#[derive(Debug)]
enum ChildState {
NotWaiting,
Waiting,
Exited(ExitStatus),
}
use crate::ChildState::*;
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
use std::process::{Command, Stdio};
use std::sync::Arc;
#[cfg(unix)]
pub fn true_cmd() -> Command {
Command::new("true")
}
#[cfg(not(unix))]
pub fn true_cmd() -> Command {
let mut cmd = Command::new("python");
cmd.arg("-c").arg("");
cmd
}
#[cfg(unix)]
pub fn sleep_forever_cmd() -> Command {
let mut cmd = Command::new("sleep");
cmd.arg("1000000");
cmd
}
#[cfg(not(unix))]
pub fn sleep_forever_cmd() -> Command {
let mut cmd = Command::new("python");
cmd.arg("-c").arg("import time; time.sleep(1000000)");
cmd
}
#[cfg(unix)]
pub fn cat_cmd() -> Command {
Command::new("cat")
}
#[cfg(not(unix))]
pub fn cat_cmd() -> Command {
let mut cmd = Command::new("python");
cmd.arg("-c").arg("");
cmd
}
#[test]
fn test_wait() {
let child = SharedChild::spawn(&mut true_cmd()).unwrap();
let id = child.id();
assert!(id > 0);
let status = child.wait().unwrap();
assert_eq!(status.code().unwrap(), 0);
}
#[test]
fn test_kill() {
let child = SharedChild::spawn(&mut sleep_forever_cmd()).unwrap();
child.kill().unwrap();
let status = child.wait().unwrap();
assert!(!status.success());
}
#[test]
fn test_try_wait() {
let child = SharedChild::spawn(&mut sleep_forever_cmd()).unwrap();
let maybe_status = child.try_wait().unwrap();
assert_eq!(maybe_status, None);
child.kill().unwrap();
let mut maybe_status = None;
while let None = maybe_status {
maybe_status = child.try_wait().unwrap();
}
assert!(maybe_status.is_some());
assert!(!maybe_status.unwrap().success());
}
#[test]
fn test_many_waiters() {
let child = Arc::new(SharedChild::spawn(&mut sleep_forever_cmd()).unwrap());
let mut threads = Vec::new();
for _ in 0..10 {
let clone = child.clone();
threads.push(std::thread::spawn(move || clone.wait()));
}
child.kill().unwrap();
for thread in threads {
thread.join().unwrap().unwrap();
}
}
#[test]
fn test_waitid_after_exit_doesnt_hang() {
let child = true_cmd().spawn().unwrap();
sys::wait_without_reaping(sys::get_handle(&child)).unwrap();
sys::wait_without_reaping(sys::get_handle(&child)).unwrap();
}
#[test]
fn test_into_inner_before_wait() {
let shared_child = SharedChild::spawn(&mut sleep_forever_cmd()).unwrap();
let mut child = shared_child.into_inner();
child.kill().unwrap();
child.wait().unwrap();
}
#[test]
fn test_into_inner_after_wait() {
let shared_child = SharedChild::spawn(&mut sleep_forever_cmd()).unwrap();
shared_child.kill().unwrap();
shared_child.wait().unwrap();
let mut child = shared_child.into_inner();
let kill_err = child.kill().unwrap_err();
if cfg!(windows) {
assert_eq!(std::io::ErrorKind::PermissionDenied, kill_err.kind());
} else {
assert_eq!(std::io::ErrorKind::InvalidInput, kill_err.kind());
}
child.wait().unwrap();
}
#[test]
fn test_new() -> Result<(), Box<dyn Error>> {
let mut command = cat_cmd();
command.stdin(Stdio::piped());
command.stdout(Stdio::null());
let mut child = command.spawn()?;
let child_stdin = child.stdin.take().unwrap();
let mut shared_child = SharedChild::new(child).unwrap();
assert!(matches!(
*shared_child.state_lock.lock().unwrap(),
NotWaiting,
));
drop(child_stdin);
loop {
shared_child = SharedChild::new(shared_child.into_inner())?;
if let Exited(status) = &*shared_child.state_lock.lock().unwrap() {
assert!(status.success());
return Ok(());
}
}
}
#[test]
fn test_takes() -> Result<(), Box<dyn Error>> {
let mut command = true_cmd();
command.stdin(Stdio::piped());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
let shared_child = SharedChild::spawn(&mut command)?;
assert!(shared_child.take_stdin().is_some());
assert!(shared_child.take_stdout().is_some());
assert!(shared_child.take_stderr().is_some());
assert!(shared_child.take_stdin().is_none());
assert!(shared_child.take_stdout().is_none());
assert!(shared_child.take_stderr().is_none());
shared_child.wait()?;
Ok(())
}
}