use std::io;
use crate::{ProcessId, Signal};
const CHILD_EVENT_OPTIONS: libc::c_int = libc::WUNTRACED | libc::WCONTINUED;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChildEvent {
Exited { pid: ProcessId, code: u8 },
Signalled { pid: ProcessId, signal: Signal },
Stopped { pid: ProcessId, signal: Signal },
Continued { pid: ProcessId },
}
impl ChildEvent {
#[must_use]
pub const fn pid(self) -> ProcessId {
match self {
Self::Exited { pid, .. }
| Self::Signalled { pid, .. }
| Self::Stopped { pid, .. }
| Self::Continued { pid } => pid,
}
}
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Exited { .. } | Self::Signalled { .. })
}
}
pub fn wait_event(process: ProcessId) -> io::Result<ChildEvent> {
let (pid, status) = waitpid_reaped(process.get(), CHILD_EVENT_OPTIONS)?;
decode_child_event(pid, status)
}
pub fn wait_event_nohang(process: ProcessId) -> io::Result<Option<ChildEvent>> {
waitpid_reaped_nohang(process.get(), libc::WNOHANG | CHILD_EVENT_OPTIONS)?
.map(|(pid, status)| decode_child_event(pid, status))
.transpose()
}
pub fn wait_any_event() -> io::Result<ChildEvent> {
let (pid, status) = waitpid_reaped(-1, CHILD_EVENT_OPTIONS)?;
decode_child_event(pid, status)
}
pub fn wait_any_event_nohang() -> io::Result<Option<ChildEvent>> {
waitpid_reaped_nohang(-1, libc::WNOHANG | CHILD_EVENT_OPTIONS)?
.map(|(pid, status)| decode_child_event(pid, status))
.transpose()
}
pub(crate) fn waitpid_reaped(
pid: libc::pid_t,
options: libc::c_int,
) -> io::Result<(libc::pid_t, libc::c_int)> {
let mut status: libc::c_int = 0;
loop {
let result = unsafe { libc::waitpid(pid, &raw mut status, options) };
if result >= 0 {
return Ok((result, status));
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted {
return Err(error);
}
}
}
pub(crate) fn waitpid_reaped_nohang(
pid: libc::pid_t,
options: libc::c_int,
) -> io::Result<Option<(libc::pid_t, libc::c_int)>> {
let (reaped, status) = waitpid_reaped(pid, options)?;
if reaped == 0 {
Ok(None)
} else {
Ok(Some((reaped, status)))
}
}
fn decode_child_event(pid: libc::pid_t, status: libc::c_int) -> io::Result<ChildEvent> {
let pid = ProcessId::try_from(pid)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if libc::WIFEXITED(status) {
let code = u8::try_from(libc::WEXITSTATUS(status)).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"child exit code is out of range",
)
})?;
return Ok(ChildEvent::Exited { pid, code });
}
if libc::WIFSIGNALED(status) {
return Ok(ChildEvent::Signalled {
pid,
signal: decode_signal(libc::WTERMSIG(status))?,
});
}
if libc::WIFSTOPPED(status) {
return Ok(ChildEvent::Stopped {
pid,
signal: decode_signal(libc::WSTOPSIG(status))?,
});
}
if libc::WIFCONTINUED(status) {
return Ok(ChildEvent::Continued { pid });
}
Err(io::Error::new(
io::ErrorKind::InvalidData,
"waitpid returned an unknown child status",
))
}
fn decode_signal(raw: libc::c_int) -> io::Result<Signal> {
Signal::try_from(raw).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}
#[cfg(test)]
mod tests {
use super::ChildEvent;
use crate::{ProcessId, Signal};
#[test]
fn terminal_classification_is_explicit() {
let process = ProcessId::new(42);
assert!(process.is_some());
let Some(pid) = process else {
return;
};
assert!(ChildEvent::Exited { pid, code: 0 }.is_terminal());
assert!(
ChildEvent::Signalled {
pid,
signal: Signal::TERM
}
.is_terminal()
);
assert!(
!ChildEvent::Stopped {
pid,
signal: Signal::STOP
}
.is_terminal()
);
assert!(!ChildEvent::Continued { pid }.is_terminal());
}
}