use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::time::Instant;
use super::{Opened, millis_until};
#[derive(Debug)]
pub(super) struct Event {
pidfd: OwnedFd,
}
pub(super) fn open(pid: u32) -> io::Result<Opened> {
let pid =
libc::pid_t::try_from(pid).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0 as libc::c_uint) };
if fd == -1 {
return opened_after(io::Error::last_os_error());
}
let pidfd = unsafe { OwnedFd::from_raw_fd(fd as RawFd) };
Ok(Opened::Watching(Event { pidfd }))
}
fn opened_after(error: io::Error) -> io::Result<Opened> {
match error.raw_os_error() {
Some(libc::ESRCH) => Ok(Opened::Gone),
Some(libc::ENOSYS | libc::EPERM) => Ok(Opened::Unsupported),
_ => Err(error),
}
}
impl Event {
pub(super) fn wait(&mut self, deadline: Option<Instant>) -> io::Result<bool> {
wait_readable(self.pidfd.as_raw_fd(), deadline)
}
#[cfg(feature = "async")]
pub(super) async fn exited(&mut self) -> io::Result<()> {
use std::os::fd::AsFd;
use tokio::io::{Interest, unix::AsyncFd};
if wait_readable(self.pidfd.as_raw_fd(), Some(Instant::now()))? {
return Ok(());
}
let registered = AsyncFd::with_interest(self.pidfd.as_fd(), Interest::READABLE)?;
loop {
let mut ready = registered.readable().await?;
if wait_readable(self.pidfd.as_raw_fd(), Some(Instant::now()))? {
return Ok(());
}
ready.clear_ready();
}
}
}
fn wait_readable(fd: RawFd, deadline: Option<Instant>) -> io::Result<bool> {
loop {
let revents = restarting(|| {
let mut entry = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let timeout = poll_timeout(deadline, Instant::now());
cvt(unsafe { libc::poll(&mut entry, 1, timeout) })?;
Ok(entry.revents)
})?;
if revents != 0 || !waits_again(deadline, Instant::now()) {
return exited_from(revents);
}
}
}
fn poll_timeout(deadline: Option<Instant>, now: Instant) -> libc::c_int {
deadline.map_or(-1, |deadline| {
libc::c_int::try_from(millis_until(deadline, now)).unwrap_or(libc::c_int::MAX)
})
}
fn waits_again(deadline: Option<Instant>, now: Instant) -> bool {
deadline.is_none_or(|deadline| now < deadline)
}
fn exited_from(revents: libc::c_short) -> io::Result<bool> {
if revents & libc::POLLERR != 0 || revents & libc::POLLNVAL != 0 {
return Err(io::Error::other(format!(
"polling the pidfd failed with events {revents:#x}"
)));
}
Ok(revents & libc::POLLIN != 0 || revents & libc::POLLHUP != 0)
}
fn cvt(result: libc::c_int) -> io::Result<libc::c_int> {
if result == -1 {
Err(io::Error::last_os_error())
} else {
Ok(result)
}
}
fn restarting<T>(mut call: impl FnMut() -> io::Result<T>) -> io::Result<T> {
loop {
match call() {
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
result => return result,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn a_missing_process_is_gone_and_a_missing_call_falls_back() {
let from = |errno| opened_after(io::Error::from_raw_os_error(errno));
assert!(matches!(from(libc::ESRCH), Ok(Opened::Gone)));
assert!(matches!(from(libc::ENOSYS), Ok(Opened::Unsupported)));
assert!(matches!(from(libc::EPERM), Ok(Opened::Unsupported)));
let error = from(libc::EMFILE).err().unwrap();
assert_eq!(error.raw_os_error(), Some(libc::EMFILE));
}
#[test]
fn only_readable_or_hung_up_reports_an_exit() {
assert!(!exited_from(0).unwrap());
assert!(exited_from(libc::POLLIN).unwrap());
assert!(exited_from(libc::POLLHUP).unwrap());
assert!(exited_from(libc::POLLIN | libc::POLLHUP).unwrap());
assert!(exited_from(libc::POLLNVAL).is_err());
assert!(exited_from(libc::POLLIN | libc::POLLERR).is_err());
}
#[test]
fn the_poll_timeout_covers_the_whole_deadline() {
let now = Instant::now();
assert_eq!(poll_timeout(None, now), -1);
assert_eq!(poll_timeout(Some(now), now), 0);
assert_eq!(poll_timeout(Some(now), now + Duration::from_secs(1)), 0);
assert_eq!(poll_timeout(Some(now + Duration::from_nanos(1)), now), 1);
assert_eq!(poll_timeout(Some(now + Duration::from_secs(5)), now), 5_000);
let far = now + Duration::from_secs(u64::from(u32::MAX));
assert_eq!(poll_timeout(Some(far), now), libc::c_int::MAX);
}
#[test]
fn a_poll_cut_short_of_its_deadline_polls_again() {
let now = Instant::now();
assert!(waits_again(Some(now + Duration::from_millis(1)), now));
let far = now + Duration::from_secs(u64::from(u32::MAX));
assert!(waits_again(
Some(far),
now + Duration::from_millis(i32::MAX as u64)
));
assert!(!waits_again(Some(now), now));
assert!(!waits_again(Some(now), now + Duration::from_millis(1)));
assert!(waits_again(None, now));
}
#[test]
fn only_minus_one_is_a_failed_call() {
assert_eq!(cvt(0).unwrap(), 0);
assert_eq!(cvt(1).unwrap(), 1);
assert!(cvt(-1).is_err());
}
#[test]
fn an_interrupted_call_is_restarted_and_other_errors_are_not() {
let mut calls = 0;
let result = restarting(|| {
calls += 1;
if calls == 1 {
Err(io::ErrorKind::Interrupted.into())
} else {
Ok(calls)
}
});
assert_eq!(result.unwrap(), 2);
let mut calls = 0;
let result = restarting(|| {
calls += 1;
if calls == 1 {
Err(io::Error::from(io::ErrorKind::WouldBlock))
} else {
Ok(calls)
}
});
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
assert_eq!(calls, 1);
}
}