use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::path::PathBuf;
use crate::platform::process::{ProcessInspectError, ProcessInspectErrorKind};
pub struct ProcessLiveness {
pid: u32,
pid_fd: Option<OwnedFd>,
}
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 {
#[cfg(any(feature = "independent-spawn", test))]
pub(crate) fn open_pinned(pid: u32) -> io::Result<Self> {
if pid == 0 || pid > libc::pid_t::MAX as u32 {
return Err(io::Error::from(io::ErrorKind::InvalidInput));
}
let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0_u32) };
if raw < 0 {
let error = io::Error::last_os_error();
return Err(match error.raw_os_error() {
Some(libc::ENOSYS | libc::EINVAL) => io::Error::new(
io::ErrorKind::Unsupported,
"kernel-pinned process control is unavailable",
),
_ => error,
});
}
let pid_fd = unsafe { OwnedFd::from_raw_fd(raw as i32) };
Ok(Self {
pid,
pid_fd: Some(pid_fd),
})
}
#[cfg(any(feature = "independent-spawn", test))]
pub(crate) fn signal_pinned(&self, signal: i32) -> io::Result<()> {
let fd = self.pid_fd.as_ref().ok_or_else(|| {
io::Error::new(
io::ErrorKind::Unsupported,
"process identity is not kernel-pinned",
)
})?;
let result = unsafe {
libc::syscall(
libc::SYS_pidfd_send_signal,
fd.as_raw_fd(),
signal,
std::ptr::null::<libc::siginfo_t>(),
0_u32,
)
};
if result < 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
pub fn open(pid: u32) -> Result<Self, ProcessInspectError> {
validate_pid(pid)?;
if !process_exists(pid) {
return Err(not_found());
}
Ok(Self {
pid,
pid_fd: try_pidfd_open(pid)?,
})
}
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 process_executable_path(pid: u32) -> Result<PathBuf, io::Error> {
std::fs::read_link(format!("/proc/{pid}/exe"))
}
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 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> {
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 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_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 pinned_control_rejects_missing_handle_without_signalling_pid() {
let unpinned = ProcessLiveness {
pid: std::process::id(),
pid_fd: None,
};
assert_eq!(
unpinned.signal_pinned(libc::SIGKILL).unwrap_err().kind(),
io::ErrorKind::Unsupported
);
assert_eq!(
ProcessLiveness::open_pinned(0).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
}
#[test]
fn pinned_control_keeps_identity_after_exit() {
let mut command = std::process::Command::new("/bin/sh");
command.args(["-c", "exec sleep 60"]);
let mut child = crate::spawn_sync(
&mut command,
crate::platform::process::SpawnStdio::default(),
crate::platform::process::SyncEnvironment::Inherit,
)
.unwrap();
let pinned = ProcessLiveness::open_pinned(child.id()).unwrap();
pinned.signal_pinned(libc::SIGKILL).unwrap();
child.wait().unwrap();
assert!(!pinned.is_alive());
assert_eq!(
pinned.signal_pinned(0).unwrap_err().raw_os_error(),
Some(libc::ESRCH)
);
}
#[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");
}
}
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));
}
}