use std::fs;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use crate::Pid;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessExitState {
Running,
Exited,
}
#[derive(Debug)]
pub struct ProcessExitWatcher {
pidfd: OwnedFd,
exited: bool,
}
impl ProcessExitWatcher {
pub fn try_new(pid: Pid) -> crate::Result<Self> {
let raw_fd = unsafe { libc::syscall(libc::SYS_pidfd_open as libc::c_long, pid.get(), 0) };
if raw_fd < 0 {
return Err(crate::Error::target(io::Error::last_os_error()));
}
Ok(Self {
pidfd: unsafe { OwnedFd::from_raw_fd(raw_fd as i32) },
exited: false,
})
}
pub fn poll(&mut self) -> crate::Result<ProcessExitState> {
if self.exited {
return Ok(ProcessExitState::Exited);
}
let mut fds = libc::pollfd {
fd: self.pidfd.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let rc = poll_retry(std::slice::from_mut(&mut fds), 0)?;
if rc > 0 {
return Ok(self.observe_revents(fds.revents)?);
}
Ok(ProcessExitState::Running)
}
pub(crate) fn poll_fd(&self) -> Option<i32> {
(!self.exited).then(|| self.pidfd.as_raw_fd())
}
pub(crate) fn observe_revents(&mut self, revents: i16) -> io::Result<ProcessExitState> {
if (revents & (libc::POLLNVAL | libc::POLLERR)) != 0 {
return Err(io::Error::from_raw_os_error(libc::EBADF));
}
if (revents & (libc::POLLIN | libc::POLLHUP)) != 0 {
self.exited = true;
return Ok(ProcessExitState::Exited);
}
Ok(ProcessExitState::Running)
}
pub(crate) fn is_exited(&self) -> bool {
self.exited
}
}
pub fn process_is_alive(watcher: &mut Option<ProcessExitWatcher>, pid: Pid) -> crate::Result<bool> {
if let Some(active) = watcher.as_mut() {
match active.poll() {
Ok(ProcessExitState::Exited) => return Ok(false),
Ok(ProcessExitState::Running) => return Ok(true),
Err(error) => return Err(error),
}
}
process_exists(pid)
}
pub fn process_exists(pid: Pid) -> crate::Result<bool> {
try_process_exists(pid).map_err(crate::Error::target)
}
pub(crate) fn try_process_exists(pid: Pid) -> io::Result<bool> {
let pid = pid.get();
let mut tasks = match fs::read_dir(format!("/proc/{pid}/task")) {
Ok(tasks) => tasks,
Err(err) if crate::error::is_target_gone_io(&err) => return Ok(false),
Err(err) => return Err(err),
};
let mut saw_leader = false;
for entry in &mut tasks {
let entry = match entry {
Ok(entry) => entry,
Err(err) if crate::error::is_target_gone_io(&err) => return Ok(false),
Err(err) => return Err(err),
};
let file_name = entry.file_name();
let Some(file_name) = file_name.to_str() else {
continue;
};
let Ok(tid) = file_name.parse::<i32>() else {
continue;
};
if tid != pid {
return Ok(true);
}
saw_leader = true;
}
Ok(saw_leader)
}
pub(crate) fn poll_retry(fds: &mut [libc::pollfd], timeout: libc::c_int) -> io::Result<i32> {
loop {
let result = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as libc::nfds_t, timeout) };
if result >= 0 {
return Ok(result);
}
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(libc::EINTR) {
return Err(error);
}
}
}
pub fn interrupt_process(pid: Pid) -> crate::Result<()> {
send_signal(pid, libc::SIGINT).map_err(crate::Error::target)
}
pub fn kill_process(pid: Pid) -> crate::Result<()> {
send_signal(pid, libc::SIGKILL).map_err(crate::Error::target)
}
fn send_signal(pid: Pid, signal: libc::c_int) -> io::Result<()> {
let rc = unsafe { libc::kill(pid.get(), signal) };
if rc == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::SleepChild;
use std::os::unix::process::ExitStatusExt;
use std::time::Duration;
fn pid(raw: i32) -> Pid {
Pid::new(raw).expect("positive test pid")
}
#[test]
fn process_exists_reports_current_and_missing_processes() {
assert!(process_exists(pid(std::process::id() as i32)).unwrap());
assert!(!process_exists(pid(i32::MAX)).unwrap());
}
#[test]
fn process_is_alive_uses_proc_fallback_without_watcher() {
let mut watcher = None;
assert!(process_is_alive(&mut watcher, pid(std::process::id() as i32)).unwrap());
assert!(!process_is_alive(&mut watcher, pid(i32::MAX)).unwrap());
}
#[test]
fn process_is_alive_uses_pidfd_watcher_when_available() {
let pid = pid(std::process::id() as i32);
let Ok(watcher) = ProcessExitWatcher::try_new(pid) else {
return;
};
let mut watcher = Some(watcher);
assert!(process_is_alive(&mut watcher, pid).unwrap());
assert!(watcher.is_some());
}
#[test]
fn process_is_alive_propagates_pidfd_poll_failure() {
let pid = pid(std::process::id() as i32);
let Ok(watcher) = ProcessExitWatcher::try_new(pid) else {
return;
};
let fd = watcher.pidfd.as_raw_fd();
assert_eq!(unsafe { libc::close(fd) }, 0);
let mut watcher = Some(watcher);
let error = process_is_alive(&mut watcher, pid).expect_err("closed pidfd must fail");
let watcher = watcher.take().expect("poll failure retains the watcher");
std::mem::forget(watcher);
assert_eq!(error.kind(), crate::ErrorKind::Io);
assert_eq!(error.raw_os_error(), Some(libc::EBADF));
}
#[test]
fn pidfd_watcher_observes_child_exit_when_available() {
let mut child = SleepChild::spawn();
let pid = pid(child.pid_i32());
let Ok(mut watcher) = ProcessExitWatcher::try_new(pid) else {
return;
};
assert_eq!(
watcher.poll().expect("poll live child"),
ProcessExitState::Running
);
kill_process(pid).expect("kill child");
let _ = child
.wait_timeout(Duration::from_secs(2))
.expect("wait child")
.expect("child exited after kill");
assert_eq!(
watcher.poll().expect("poll exited child"),
ProcessExitState::Exited
);
assert_eq!(
watcher.poll().expect("poll cached exited child"),
ProcessExitState::Exited
);
let mut watcher = Some(watcher);
assert!(!process_is_alive(&mut watcher, pid).unwrap());
}
#[test]
fn pidfd_revents_decode_exit_and_error_states() {
let pid = pid(std::process::id() as i32);
let Ok(mut watcher) = ProcessExitWatcher::try_new(pid) else {
return;
};
assert_eq!(
watcher.observe_revents(0).unwrap(),
ProcessExitState::Running
);
assert!(watcher.observe_revents(libc::POLLERR).is_err());
assert_eq!(
watcher.observe_revents(libc::POLLHUP).unwrap(),
ProcessExitState::Exited
);
assert!(watcher.is_exited());
}
#[test]
fn interrupt_process_sends_sigint() {
let mut child = SleepChild::spawn();
interrupt_process(pid(child.pid_i32())).expect("interrupt child");
let status = child
.wait_timeout(Duration::from_secs(2))
.expect("wait child")
.expect("child exited after interrupt");
assert_eq!(status.signal(), Some(libc::SIGINT));
}
#[test]
fn kill_process_sends_sigkill() {
let mut child = SleepChild::spawn();
kill_process(pid(child.pid_i32())).expect("kill child");
let status = child
.wait_timeout(Duration::from_secs(2))
.expect("wait child")
.expect("child exited after kill");
assert_eq!(status.signal(), Some(libc::SIGKILL));
}
#[test]
fn missing_signal_target_has_target_gone_error_kind() {
let error = kill_process(pid(i32::MAX)).expect_err("test PID should not exist");
assert_eq!(error.kind(), crate::ErrorKind::TargetGone);
assert_eq!(error.raw_os_error(), Some(libc::ESRCH));
}
#[test]
fn missing_pidfd_target_has_target_gone_error_kind() {
let error =
ProcessExitWatcher::try_new(pid(i32::MAX)).expect_err("test PID should not exist");
assert_eq!(error.kind(), crate::ErrorKind::TargetGone);
assert!(matches!(
error.raw_os_error(),
Some(libc::ENOENT | libc::ESRCH)
));
}
}