#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessLiveness {
Alive,
Dead,
Unknown,
}
pub const PID_BIRTH_PROCFS_TAG: u64 = 1_u64 << 63;
pub const PID_BIRTH_SYSCTL_TAG: u64 = 1_u64 << 62;
pub const PID_BIRTH_FILETIME_TAG: u64 = 1_u64 << 61;
#[cfg(any(target_os = "macos", target_os = "windows", test))]
const PAYLOAD_MASK: u64 = !(PID_BIRTH_PROCFS_TAG | PID_BIRTH_SYSCTL_TAG | PID_BIRTH_FILETIME_TAG);
#[must_use]
pub fn current_process_birth_token() -> Option<u64> {
let pid = std::process::id();
#[cfg(target_os = "macos")]
{
macos::birth_token(pid)
}
#[cfg(windows)]
{
windows_impl::birth_token(pid)
}
#[cfg(not(any(target_os = "macos", windows)))]
{
let _ = pid;
None
}
}
#[must_use]
pub fn process_alive(pid: u32, pid_birth: u64) -> ProcessLiveness {
if pid == 0 {
return ProcessLiveness::Dead;
}
#[cfg(target_os = "macos")]
{
macos::alive(pid, pid_birth)
}
#[cfg(windows)]
{
windows_impl::alive(pid, pid_birth)
}
#[cfg(not(any(target_os = "macos", windows)))]
{
let _ = pid_birth;
ProcessLiveness::Unknown
}
}
#[cfg(target_os = "macos")]
mod macos {
use super::{PAYLOAD_MASK, PID_BIRTH_SYSCTL_TAG, ProcessLiveness};
enum StartTime {
Present(u64),
Absent,
Error,
}
fn read_start_time_usec(pid: u32) -> StartTime {
let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
let written = unsafe {
libc::proc_pidinfo(
pid as libc::c_int,
libc::PROC_PIDTBSDINFO,
0,
std::ptr::from_mut(&mut info).cast(),
size,
)
};
if written <= 0 {
return if written == 0
|| std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
{
StartTime::Absent
} else {
StartTime::Error
};
}
if written < size {
return StartTime::Error;
}
let usec = info
.pbi_start_tvsec
.wrapping_mul(1_000_000)
.wrapping_add(info.pbi_start_tvusec);
StartTime::Present(usec)
}
pub(super) fn birth_token(pid: u32) -> Option<u64> {
match read_start_time_usec(pid) {
StartTime::Present(usec) => Some(PID_BIRTH_SYSCTL_TAG | (usec & PAYLOAD_MASK)),
StartTime::Absent | StartTime::Error => None,
}
}
pub(super) fn alive(pid: u32, pid_birth: u64) -> ProcessLiveness {
match read_start_time_usec(pid) {
StartTime::Absent => ProcessLiveness::Dead,
StartTime::Error => ProcessLiveness::Unknown,
StartTime::Present(usec) => {
if pid_birth & PID_BIRTH_SYSCTL_TAG == 0 {
return ProcessLiveness::Alive;
}
if (usec & PAYLOAD_MASK) == (pid_birth & PAYLOAD_MASK) {
ProcessLiveness::Alive
} else {
ProcessLiveness::Dead
}
}
}
}
}
#[cfg(windows)]
mod windows_impl {
use super::{PAYLOAD_MASK, PID_BIRTH_FILETIME_TAG, ProcessLiveness};
use windows_sys::Win32::Foundation::{
CloseHandle, ERROR_ACCESS_DENIED, FILETIME, GetLastError,
};
use windows_sys::Win32::System::Threading::{
GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
};
enum Creation {
Present(u64),
Absent,
Error,
}
fn read_creation_100ns(pid: u32) -> Creation {
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
let last = unsafe { GetLastError() };
return if last == ERROR_ACCESS_DENIED {
Creation::Error
} else {
Creation::Absent
};
}
let mut creation = FILETIME {
dwLowDateTime: 0,
dwHighDateTime: 0,
};
let mut exit = creation;
let mut kernel = creation;
let mut user = creation;
let ok =
unsafe { GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user) };
unsafe {
CloseHandle(handle);
}
if ok == 0 {
return Creation::Error;
}
let ticks = (u64::from(creation.dwHighDateTime) << 32) | u64::from(creation.dwLowDateTime);
Creation::Present(ticks)
}
pub(super) fn birth_token(pid: u32) -> Option<u64> {
match read_creation_100ns(pid) {
Creation::Present(ticks) => Some(PID_BIRTH_FILETIME_TAG | (ticks & PAYLOAD_MASK)),
Creation::Absent | Creation::Error => None,
}
}
pub(super) fn alive(pid: u32, pid_birth: u64) -> ProcessLiveness {
match read_creation_100ns(pid) {
Creation::Absent => ProcessLiveness::Dead,
Creation::Error => ProcessLiveness::Unknown,
Creation::Present(ticks) => {
if pid_birth & PID_BIRTH_FILETIME_TAG == 0 {
return ProcessLiveness::Alive;
}
if (ticks & PAYLOAD_MASK) == (pid_birth & PAYLOAD_MASK) {
ProcessLiveness::Alive
} else {
ProcessLiveness::Dead
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn platform_birth_tags_are_distinct_and_high() {
assert_ne!(PID_BIRTH_PROCFS_TAG, PID_BIRTH_SYSCTL_TAG);
assert_ne!(PID_BIRTH_SYSCTL_TAG, PID_BIRTH_FILETIME_TAG);
assert_eq!(PID_BIRTH_PROCFS_TAG & PAYLOAD_MASK, 0);
assert_eq!(PID_BIRTH_SYSCTL_TAG & PAYLOAD_MASK, 0);
assert_eq!(PID_BIRTH_FILETIME_TAG & PAYLOAD_MASK, 0);
}
#[test]
fn pid_zero_is_dead() {
assert_eq!(process_alive(0, 0), ProcessLiveness::Dead);
}
#[cfg(not(any(target_os = "macos", windows)))]
#[test]
fn non_macos_non_windows_returns_unknown() {
assert_eq!(
process_alive(std::process::id(), 0),
ProcessLiveness::Unknown
);
assert!(current_process_birth_token().is_none());
}
#[cfg(any(target_os = "macos", windows))]
#[test]
fn current_process_is_alive_with_its_own_birth_token() {
let birth = current_process_birth_token().expect("own birth token available");
assert_eq!(
process_alive(std::process::id(), birth),
ProcessLiveness::Alive,
"the current process must read as alive with its own birth token"
);
}
#[cfg(any(target_os = "macos", windows))]
#[test]
fn recycled_pid_birth_mismatch_reads_dead() {
let birth = current_process_birth_token().expect("own birth token");
let tag = birth & !PAYLOAD_MASK;
let mismatched = tag | ((birth & PAYLOAD_MASK) ^ 0x5A5A);
assert_eq!(
process_alive(std::process::id(), mismatched),
ProcessLiveness::Dead,
"a start-time mismatch on a live PID must read as Dead (reuse-safe)"
);
}
#[cfg(any(target_os = "macos", windows))]
#[test]
fn almost_certainly_dead_pid_reads_dead() {
let verdict = process_alive(
0x7FFF_FFF0,
PID_BIRTH_SYSCTL_TAG | PID_BIRTH_FILETIME_TAG | 0x1234,
);
assert_ne!(verdict, ProcessLiveness::Alive);
}
}