use std::io;
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
use crate::platform::process::{
ProcessExitObservation, ProcessId, ProcessInspectError, ProcessInspectErrorKind,
ProcessSessionExit,
};
pub struct ProcessExitWatch {
pid: ProcessId,
pid_fd: OwnedFd,
}
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> {
Ok(Self {
pid,
pid_fd: pidfd_open(pid)?,
})
}
#[must_use]
pub fn pid(&self) -> ProcessId {
self.pid
}
pub async fn exited(&self) -> Result<ProcessExitObservation, ProcessInspectError> {
let readable = tokio::io::unix::AsyncFd::with_interest(
self.pid_fd.as_fd(),
tokio::io::Interest::READABLE,
)
.map_err(|source| ProcessInspectError {
kind: ProcessInspectErrorKind::Host,
source,
})?;
let _exited = readable
.readable()
.await
.map_err(|source| ProcessInspectError {
kind: ProcessInspectErrorKind::Host,
source,
})?;
peek_exit_status(&self.pid_fd)
}
}
fn pidfd_open(pid: ProcessId) -> Result<OwnedFd, ProcessInspectError> {
let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid.native_signed(), 0_u32) };
if raw >= 0 {
return Ok(unsafe { OwnedFd::from_raw_fd(raw as i32) });
}
let source = io::Error::last_os_error();
let kind = match source.raw_os_error() {
Some(libc::ESRCH) => ProcessInspectErrorKind::NotFound,
Some(libc::ENOSYS) | Some(libc::EPERM) => ProcessInspectErrorKind::Unsupported,
_ => ProcessInspectErrorKind::Host,
};
Err(ProcessInspectError { kind, source })
}
fn peek_exit_status(pid_fd: &OwnedFd) -> Result<ProcessExitObservation, ProcessInspectError> {
let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
let rc = unsafe {
libc::waitid(
libc::P_PIDFD,
pid_fd.as_raw_fd() as libc::id_t,
&mut info,
libc::WEXITED | libc::WNOWAIT,
)
};
if rc == -1 {
let source = io::Error::last_os_error();
return match source.raw_os_error() {
Some(libc::ECHILD) | Some(libc::EINVAL) => Ok(ProcessExitObservation::Unreported),
_ => Err(ProcessInspectError {
kind: ProcessInspectErrorKind::Host,
source,
}),
};
}
let status = unsafe { info.si_status() };
Ok(match info.si_code {
libc::CLD_EXITED => ProcessExitObservation::Reported(ProcessSessionExit::from_native(
Some(status),
None,
((status & 0xff) << 8) as u32,
)),
libc::CLD_KILLED => ProcessExitObservation::Reported(ProcessSessionExit::from_native(
None,
Some(status),
(status & 0x7f) as u32,
)),
libc::CLD_DUMPED => ProcessExitObservation::Reported(ProcessSessionExit::from_native(
None,
Some(status),
((status & 0x7f) | 0x80) as u32,
)),
_ => ProcessExitObservation::Unreported,
})
}
#[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_reconstructs_its_wait_status_word() {
let exit = ProcessSessionExit::from_native(Some(7), None, ((7 & 0xff) << 8) as u32);
assert_eq!(exit.exit_code(), Some(7));
assert_eq!(exit.native_status(), 0x0700);
}
}