use std::io;
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::platform::process::{
ProcessExitObservation, ProcessId, ProcessInspectError, ProcessInspectErrorKind,
ProcessSessionExit,
};
pub struct ProcessExitWatch {
pid: ProcessId,
kqueue_fd: OwnedFd,
status_requested: bool,
exited: AtomicBool,
observation: std::sync::Mutex<Option<ProcessExitObservation>>,
}
impl std::fmt::Debug for ProcessExitWatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProcessExitWatch")
.field("pid", &self.pid)
.finish_non_exhaustive()
}
}
impl ProcessExitWatch {
pub fn open(pid: ProcessId) -> Result<Self, ProcessInspectError> {
let (kqueue_fd, status_requested) = open_exit_kqueue(pid)?;
Ok(Self {
pid,
kqueue_fd,
status_requested,
exited: AtomicBool::new(false),
observation: std::sync::Mutex::new(None),
})
}
#[must_use]
pub fn pid(&self) -> ProcessId {
self.pid
}
pub async fn exited(&self) -> Result<ProcessExitObservation, ProcessInspectError> {
if let Some(observation) = self.latched() {
return Ok(observation);
}
let readable = tokio::io::unix::AsyncFd::with_interest(
self.kqueue_fd.as_fd(),
tokio::io::Interest::READABLE,
)
.map_err(|source| ProcessInspectError {
kind: ProcessInspectErrorKind::Host,
source,
})?;
loop {
let mut guard = readable
.readable()
.await
.map_err(|source| ProcessInspectError {
kind: ProcessInspectErrorKind::Host,
source,
})?;
match collect_exit(&self.kqueue_fd, self.status_requested)? {
Some(observation) => {
self.latch(observation);
return Ok(observation);
}
None => match self.latched() {
Some(observation) => return Ok(observation),
None => guard.clear_ready(),
},
}
}
}
fn latched(&self) -> Option<ProcessExitObservation> {
if !self.exited.load(Ordering::Acquire) {
return None;
}
*self.observation.lock().expect("exit observation lock")
}
fn latch(&self, observation: ProcessExitObservation) {
*self.observation.lock().expect("exit observation lock") = Some(observation);
self.exited.store(true, Ordering::Release);
}
}
fn open_exit_kqueue(pid: ProcessId) -> Result<(OwnedFd, bool), ProcessInspectError> {
let raw_fd = unsafe { libc::kqueue() };
if raw_fd < 0 {
return Err(ProcessInspectError::last_os_error(
ProcessInspectErrorKind::Host,
));
}
let kqueue_fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
match register(&kqueue_fd, pid, libc::NOTE_EXIT | libc::NOTE_EXITSTATUS) {
Ok(()) => return Ok((kqueue_fd, true)),
Err(error) if error.kind == ProcessInspectErrorKind::NotFound => return Err(error),
Err(_) => {}
}
register(&kqueue_fd, pid, libc::NOTE_EXIT)?;
Ok((kqueue_fd, false))
}
fn register(kqueue_fd: &OwnedFd, pid: ProcessId, fflags: u32) -> Result<(), ProcessInspectError> {
let change = libc::kevent {
ident: pid.native_signed() as libc::uintptr_t,
filter: libc::EVFILT_PROC,
flags: libc::EV_ADD | libc::EV_CLEAR,
fflags,
data: 0,
udata: ptr::null_mut(),
};
let rc = unsafe {
libc::kevent(
kqueue_fd.as_raw_fd(),
&change,
1,
ptr::null_mut(),
0,
ptr::null(),
)
};
if rc == 0 {
return Ok(());
}
let source = io::Error::last_os_error();
let kind = match source.raw_os_error() {
Some(libc::ESRCH) => ProcessInspectErrorKind::NotFound,
_ => ProcessInspectErrorKind::Host,
};
Err(ProcessInspectError { kind, source })
}
fn collect_exit(
kqueue_fd: &OwnedFd,
status_requested: bool,
) -> Result<Option<ProcessExitObservation>, ProcessInspectError> {
let mut event = std::mem::MaybeUninit::<libc::kevent>::uninit();
let timeout = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
let rc = unsafe {
libc::kevent(
kqueue_fd.as_raw_fd(),
ptr::null(),
0,
event.as_mut_ptr(),
1,
&timeout,
)
};
if rc < 0 {
return Err(ProcessInspectError::last_os_error(
ProcessInspectErrorKind::Host,
));
}
if rc == 0 {
return Ok(None);
}
let event = unsafe { event.assume_init() };
if event.filter != libc::EVFILT_PROC || event.fflags & libc::NOTE_EXIT == 0 {
return Ok(None);
}
if !status_requested || event.fflags & libc::NOTE_EXITSTATUS == 0 {
return Ok(Some(ProcessExitObservation::Unreported));
}
Ok(Some(ProcessExitObservation::Reported(wait_status(
event.data as i32,
))))
}
fn wait_status(raw: i32) -> ProcessSessionExit {
if libc::WIFEXITED(raw) {
ProcessSessionExit::from_native(Some(libc::WEXITSTATUS(raw)), None, raw as u32)
} else if libc::WIFSIGNALED(raw) {
ProcessSessionExit::from_native(None, Some(libc::WTERMSIG(raw)), raw as u32)
} else {
ProcessSessionExit::from_native(None, None, raw as u32)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_reaped_process_cannot_be_watched() {
let mut child = std::process::Command::new("/bin/sh")
.args(["-c", "exit 0"])
.spawn()
.expect("spawn");
let pid = ProcessId::new(child.id()).expect("child pid is in range");
child.wait().expect("reap");
let error = ProcessExitWatch::open(pid).expect_err("a reaped pid has no process");
assert_eq!(error.kind, ProcessInspectErrorKind::NotFound);
}
#[test]
fn a_normal_exit_and_a_signal_decode_separately() {
let exited = wait_status(7 << 8);
assert_eq!(exited.exit_code(), Some(7));
assert_eq!(exited.signal(), None);
let signalled = wait_status(libc::SIGKILL);
assert_eq!(signalled.exit_code(), None);
assert_eq!(signalled.signal(), Some(libc::SIGKILL));
}
}