use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::path::PathBuf;
use crate::platform::process::{ProcessId, ProcessInspectError, ProcessInspectErrorKind};
pub struct ProcessLiveness {
pid: u32,
pid_fd: Option<OwnedFd>,
start_ticks: Option<u64>,
}
impl std::fmt::Debug for ProcessLiveness {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProcessLiveness")
.field("pid", &self.pid)
.finish_non_exhaustive()
}
}
impl ProcessLiveness {
pub fn open(pid: u32) -> Result<Self, ProcessInspectError> {
validate_pid(pid)?;
if !process_exists(pid) {
return Err(not_found());
}
let pid_fd = try_pidfd_open(pid)?;
let start_ticks = match pid_fd {
Some(_) => None,
None => read_proc_stat(pid).ok().flatten().map(|stat| stat.start_ticks),
};
Ok(Self {
pid,
pid_fd,
start_ticks,
})
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn is_alive(&self) -> bool {
match self.pid_fd.as_ref() {
Some(pid_fd) => pidfd_is_alive(pid_fd),
None => process_exists(self.pid),
}
}
pub fn has_exited(&self) -> io::Result<bool> {
match self.pid_fd.as_ref() {
Some(pid_fd) => pidfd_has_exited(pid_fd),
None => self.pid_has_exited(),
}
}
fn pid_has_exited(&self) -> io::Result<bool> {
let native_pid = validate_pid(self.pid).map_err(|error| error.source)?;
if unsafe { libc::kill(native_pid, 0) } != 0 {
let error = io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::ESRCH) => return Ok(true),
Some(libc::EPERM) => {}
_ => return Err(error),
}
}
let Some(start_ticks) = self.start_ticks else {
return Ok(false);
};
match read_proc_stat(self.pid)? {
None => Ok(true),
Some(stat) => Ok(stat.is_zombie || stat.start_ticks != start_ticks),
}
}
}
struct ProcStat {
is_zombie: bool,
start_ticks: u64,
}
fn read_proc_stat(pid: u32) -> io::Result<Option<ProcStat>> {
let stat = match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
Ok(stat) => stat,
Err(error)
if error.kind() == io::ErrorKind::NotFound
|| error.raw_os_error() == Some(libc::ESRCH) =>
{
return Ok(None)
}
Err(error) => return Err(error),
};
parse_proc_stat(&stat)
.map(Some)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "malformed /proc process stat"))
}
fn parse_proc_stat(stat: &str) -> Option<ProcStat> {
let suffix = stat.get(stat.rfind(')')? + 1..)?;
let mut fields = suffix.split_ascii_whitespace();
let state = fields.next()?; let start_ticks = fields.nth(18)?.parse().ok()?; Some(ProcStat {
is_zombie: matches!(state, "Z" | "X" | "x"),
start_ticks,
})
}
pub fn process_executable_path(pid: u32) -> Result<PathBuf, io::Error> {
std::fs::read_link(format!("/proc/{pid}/exe"))
}
#[allow(dead_code)] pub fn process_signal_terminate(pid: u32) -> Result<(), ProcessInspectError> {
signal(pid, libc::SIGTERM)
}
#[allow(dead_code)] pub fn process_force_kill(pid: u32) -> Result<(), ProcessInspectError> {
signal(pid, libc::SIGKILL)
}
#[allow(dead_code)]
fn signal(pid: u32, signal: libc::c_int) -> Result<(), ProcessInspectError> {
let native_pid = validate_pid(pid)?;
let rc = unsafe { libc::kill(native_pid, signal) };
if rc == 0 {
Ok(())
} else {
Err(ProcessInspectError::last_os_error(
ProcessInspectErrorKind::Host,
))
}
}
fn process_exists(pid: u32) -> bool {
let Ok(native_pid) = validate_pid(pid) else {
return false;
};
let rc = unsafe { libc::kill(native_pid, 0) };
if rc == 0 {
return true;
}
matches!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM))
}
fn validate_pid(pid: u32) -> Result<libc::pid_t, ProcessInspectError> {
ProcessId::new(pid).map(ProcessId::native_signed)
}
fn try_pidfd_open(pid: u32) -> Result<Option<OwnedFd>, ProcessInspectError> {
let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0_u32) };
if raw >= 0 {
return Ok(Some(unsafe { OwnedFd::from_raw_fd(raw as i32) }));
}
match io::Error::last_os_error().raw_os_error() {
Some(libc::ESRCH) => Err(not_found()),
_ => Ok(None),
}
}
fn pidfd_has_exited(pid_fd: &OwnedFd) -> io::Result<bool> {
loop {
let mut poll_fd = libc::pollfd {
fd: pid_fd.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let rc = unsafe { libc::poll(&mut poll_fd, 1, 0) };
if rc < 0 {
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(error);
}
if rc == 0 {
return Ok(false);
}
if poll_fd.revents & (libc::POLLNVAL | libc::POLLERR) != 0 {
return Err(io::Error::other(format!(
"pidfd poll reported an error condition (revents {:#x})",
poll_fd.revents
)));
}
return Ok(poll_fd.revents & (libc::POLLIN | libc::POLLHUP) != 0);
}
}
fn pidfd_is_alive(pid_fd: &OwnedFd) -> bool {
let mut poll_fd = libc::pollfd {
fd: pid_fd.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let rc = unsafe { libc::poll(&mut poll_fd, 1, 0) };
rc == 0
}
fn not_found() -> ProcessInspectError {
ProcessInspectError::stated(ProcessInspectErrorKind::NotFound, "no such process")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pid_zero_is_never_valid() {
let error = ProcessLiveness::open(0).expect_err("pid 0");
assert_eq!(error.kind, ProcessInspectErrorKind::InvalidPid);
assert!(!process_exists(0));
}
#[test]
fn this_process_is_alive_and_locatable() {
let me = std::process::id();
let handle = ProcessLiveness::open(me).expect("open self");
assert_eq!(handle.pid(), me);
assert!(handle.is_alive());
assert_eq!(
process_executable_path(me).expect("exe"),
std::env::current_exe().expect("current_exe")
);
}
#[test]
fn a_dead_process_reports_dead() {
let child = std::process::Command::new("/bin/sh")
.args(["-c", "exit 0"])
.spawn()
.expect("spawn");
let pid = child.id();
let handle = ProcessLiveness::open(pid).expect("open child");
let mut child = child;
child.wait().expect("reap");
assert!(!handle.is_alive(), "a reaped child must report dead");
assert!(handle.has_exited().expect("observe exit"));
}
#[test]
fn the_pid_fallback_detects_exit_and_a_successor() {
let me = std::process::id();
let start_ticks = read_proc_stat(me).expect("stat").expect("self").start_ticks;
let live = ProcessLiveness {
pid: me,
pid_fd: None,
start_ticks: Some(start_ticks),
};
assert!(!live.has_exited().expect("observe self"));
let successor = ProcessLiveness {
pid: me,
pid_fd: None,
start_ticks: Some(start_ticks.wrapping_add(1)),
};
assert!(successor.has_exited().expect("observe successor"));
let mut child = std::process::Command::new("/bin/sh")
.args(["-c", "exit 0"])
.spawn()
.expect("spawn");
let pid = child.id();
let start_ticks = read_proc_stat(pid).expect("stat").map(|stat| stat.start_ticks);
let handle = ProcessLiveness {
pid,
pid_fd: None,
start_ticks,
};
child.wait().expect("reap");
assert!(handle.has_exited().expect("observe reaped child"));
}
#[test]
fn proc_stat_parsing_survives_a_hostile_comm() {
let fields: Vec<String> = (4..=22).map(|field| field.to_string()).collect();
let stat = format!("42 (a) b) c) Z {}", fields.join(" "));
let parsed = parse_proc_stat(&stat).expect("parse");
assert!(parsed.is_zombie);
assert_eq!(parsed.start_ticks, 22);
}
}
pub fn process_same_executable_path(actual: &std::path::Path, expected: &std::path::Path) -> bool {
let resolve =
|path: &std::path::Path| std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
resolve(actual) == resolve(expected)
}
#[cfg(test)]
mod path_tests {
use super::*;
use std::path::Path;
#[test]
fn case_distinguishes_two_images() {
assert!(!process_same_executable_path(
Path::new("/tmp/Daemon"),
Path::new("/tmp/daemon"),
));
}
#[test]
fn a_path_matches_itself() {
assert!(process_same_executable_path(
Path::new("/tmp/rp-does-not-exist/daemon"),
Path::new("/tmp/rp-does-not-exist/daemon"),
));
let me = std::env::current_exe().expect("current_exe");
assert!(process_same_executable_path(&me, &me));
}
}