use std::{
convert::TryInto,
io::{Error, Result},
ops::ControlFlow,
os::unix::process::ExitStatusExt,
process::ExitStatus,
};
use nix::{
errno::Errno,
libc,
sys::{
signal::{killpg, Signal},
wait::WaitPidFlag,
},
unistd::Pid,
};
use tokio::{
process::{Child, ChildStderr, ChildStdin, ChildStdout},
task::spawn_blocking,
};
pub(super) struct ChildImp {
pgid: Pid,
inner: Child,
}
impl ChildImp {
pub(super) fn new(inner: Child) -> Self {
let pid = inner
.id()
.expect("Command was reaped before we could read its PID")
.try_into()
.expect("Command PID > i32::MAX");
Self {
pgid: Pid::from_raw(pid),
inner,
}
}
pub(super) fn take_stdin(&mut self) -> Option<ChildStdin> {
self.inner.stdin.take()
}
pub(super) fn take_stdout(&mut self) -> Option<ChildStdout> {
self.inner.stdout.take()
}
pub(super) fn take_stderr(&mut self) -> Option<ChildStderr> {
self.inner.stderr.take()
}
pub fn inner(&mut self) -> &mut Child {
&mut self.inner
}
pub fn into_inner(self) -> Child {
self.inner
}
pub(super) fn signal_imp(&self, sig: Signal) -> Result<()> {
killpg(self.pgid, sig).map_err(Error::from)
}
pub fn start_kill(&mut self) -> Result<()> {
self.signal_imp(Signal::SIGKILL)
}
pub fn id(&self) -> Option<u32> {
self.inner.id()
}
fn wait_imp(pgid: i32, flag: WaitPidFlag) -> Result<ControlFlow<Option<ExitStatus>>> {
let mut parent_exit_status: Option<ExitStatus> = None;
loop {
let mut status: i32 = 0;
match unsafe { libc::waitpid(-pgid, &mut status as *mut libc::c_int, flag.bits()) } {
0 => {
return Ok(ControlFlow::Continue(()));
}
-1 => {
match Errno::last() {
Errno::ECHILD => {
return Ok(ControlFlow::Break(parent_exit_status));
}
errno => {
return Err(Error::from(errno));
}
}
}
pid => {
if pgid == pid {
parent_exit_status = Some(ExitStatus::from_raw(status));
} else {
}
}
};
}
}
pub async fn wait(&mut self) -> Result<ExitStatus> {
const MAX_RETRY_ATTEMPT: usize = 10;
let status = self.inner.wait().await?;
let pgid = self.pgid.as_raw();
for retry_attempt in 1..=MAX_RETRY_ATTEMPT {
if Self::wait_imp(pgid, WaitPidFlag::WNOHANG)?.is_break() {
break;
} else if retry_attempt == MAX_RETRY_ATTEMPT {
spawn_blocking(move || Self::wait_imp(pgid, WaitPidFlag::empty())).await??;
}
}
Ok(status)
}
pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
match Self::wait_imp(self.pgid.as_raw(), WaitPidFlag::WNOHANG)? {
ControlFlow::Break(res) => Ok(res),
ControlFlow::Continue(()) => self.inner.try_wait(),
}
}
}
impl crate::UnixChildExt for ChildImp {
fn signal(&self, sig: Signal) -> Result<()> {
self.signal_imp(sig)
}
}