use std::ffi::CStr;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::path::PathBuf;
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::platform::process::{ProcessInspectError, ProcessInspectErrorKind};
pub struct ProcessLiveness {
pid: u32,
exit_kqueue: OwnedFd,
exited: AtomicBool,
}
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> {
Ok(Self {
pid,
exit_kqueue: open_exit_kqueue(pid)?,
exited: AtomicBool::new(false),
})
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn is_alive(&self) -> bool {
!self.exited.load(Ordering::Relaxed)
&& kqueue_process_is_alive(&self.exit_kqueue, &self.exited)
}
}
pub fn process_executable_path(pid: u32) -> Result<PathBuf, io::Error> {
let mut buffer = [0_u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
let written = unsafe {
libc::proc_pidpath(
pid as libc::c_int,
buffer.as_mut_ptr().cast(),
buffer.len() as u32,
)
};
if written <= 0 {
return Err(io::Error::last_os_error());
}
let path = unsafe { CStr::from_ptr(buffer.as_ptr().cast()) };
Ok(PathBuf::from(path.to_str().map_err(|_| {
io::Error::other("executable path is not valid UTF-8")
})?))
}
pub fn process_signal_terminate(pid: u32) -> Result<(), ProcessInspectError> {
signal(pid, libc::SIGTERM)
}
pub fn process_force_kill(pid: u32) -> Result<(), ProcessInspectError> {
signal(pid, libc::SIGKILL)
}
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 validate_pid(pid: u32) -> Result<libc::pid_t, ProcessInspectError> {
if pid == 0 || pid > libc::pid_t::MAX as u32 {
Err(ProcessInspectError::stated(
ProcessInspectErrorKind::InvalidPid,
"pid outside the range this host issues",
))
} else {
Ok(pid as libc::pid_t)
}
}
fn open_exit_kqueue(pid: u32) -> Result<OwnedFd, ProcessInspectError> {
let native_pid = validate_pid(pid)?;
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) };
let change = libc::kevent {
ident: native_pid as libc::uintptr_t,
filter: libc::EVFILT_PROC,
flags: libc::EV_ADD | libc::EV_CLEAR,
fflags: libc::NOTE_EXIT,
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(kqueue_fd);
}
let source = io::Error::last_os_error();
if matches!(source.raw_os_error(), Some(libc::ESRCH)) {
Err(ProcessInspectError::stated(
ProcessInspectErrorKind::NotFound,
"no such process",
))
} else {
Err(ProcessInspectError {
kind: ProcessInspectErrorKind::Host,
source,
})
}
}
fn kqueue_process_is_alive(kqueue_fd: &OwnedFd, exited: &AtomicBool) -> bool {
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 true;
}
exited.store(true, Ordering::Relaxed);
false
}
#[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);
}
#[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_stays_dead_when_asked_twice() {
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.is_alive(), "and must still report dead");
}
}
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));
}
}