#[cfg(unix)]
pub fn is_process_alive(pid: u32) -> bool {
let Ok(pid) = i32::try_from(pid) else {
return false;
};
let result = unsafe { libc::kill(pid, 0) };
result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(windows)]
pub fn is_process_alive(pid: u32) -> bool {
use windows_sys::Win32::Foundation::{CloseHandle, STILL_ACTIVE};
use windows_sys::Win32::System::Threading::{
GetExitCodeProcess, OpenProcess, PROCESS_QUERY_INFORMATION,
};
unsafe {
let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if handle == 0 {
return false;
}
let mut exit_code = 0u32;
let ok = GetExitCodeProcess(handle, &mut exit_code);
CloseHandle(handle);
ok != 0 && exit_code == STILL_ACTIVE as u32
}
}
#[cfg(not(any(unix, windows)))]
pub fn is_process_alive(_pid: u32) -> bool {
false
}
#[cfg(target_os = "linux")]
pub fn pid_start_time(pid: u32) -> Option<u64> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
linux_process_identity_from_stat(&stat).map(|(_, start_time)| start_time)
}
#[cfg(not(target_os = "linux"))]
pub fn pid_start_time(_pid: u32) -> Option<u64> {
None
}
pub fn is_process_alive_with_identity(pid: u32, expected_start_time: Option<u64>) -> bool {
if !is_process_alive(pid) {
return false;
}
match expected_start_time {
Some(expected) => pid_start_time(pid) == Some(expected),
None => true,
}
}
#[cfg(target_os = "linux")]
pub fn is_process_running_with_identity(pid: u32, expected_start_time: Option<u64>) -> bool {
let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
return false;
};
linux_process_identity_from_stat(&stat).is_some_and(|(state, start_time)| {
state != 'Z'
&& expected_start_time
.map(|expected| expected == start_time)
.unwrap_or(true)
})
}
#[cfg(not(target_os = "linux"))]
pub fn is_process_running_with_identity(pid: u32, expected_start_time: Option<u64>) -> bool {
is_process_alive_with_identity(pid, expected_start_time)
}
#[cfg(target_os = "linux")]
pub(crate) fn wait_for_process_exit_with_identity(
pid: u32,
expected_start_time: u64,
timeout: std::time::Duration,
) -> bool {
let Ok(raw_pid) = i32::try_from(pid) else {
return false;
};
let deadline = std::time::Instant::now() + timeout;
loop {
if !is_process_alive_with_identity(pid, Some(expected_start_time)) {
return true;
}
if !is_process_running_with_identity(pid, Some(expected_start_time)) {
let mut status = 0;
let waited = unsafe { libc::waitpid(raw_pid, &mut status, libc::WNOHANG) };
if waited == raw_pid || !is_process_alive_with_identity(pid, Some(expected_start_time))
{
return true;
}
}
if std::time::Instant::now() >= deadline {
return false;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
#[cfg(target_os = "linux")]
fn linux_process_identity_from_stat(stat: &str) -> Option<(char, u64)> {
let fields: Vec<&str> = stat
.get(stat.rfind(')')? + 1..)?
.split_whitespace()
.collect();
let state = fields.first()?.chars().next()?;
let start_time = fields.get(19)?.parse().ok()?;
Some((state, start_time))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_process_is_alive() {
assert!(is_process_alive(std::process::id()));
}
#[test]
fn missing_process_is_not_alive() {
assert!(!is_process_alive(0x7fff_fffe));
}
#[cfg(target_os = "linux")]
#[test]
fn parses_start_time_after_complex_command_name() {
let stat =
"123 (command (with) spaces) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 4242";
assert_eq!(linux_process_identity_from_stat(stat), Some(('S', 4242)));
assert_eq!(linux_process_identity_from_stat("malformed"), None);
assert_eq!(linux_process_identity_from_stat("123 (short) S 1"), None);
}
#[cfg(target_os = "linux")]
#[test]
fn identity_rejects_a_reused_pid() {
let pid = std::process::id();
let start_time = pid_start_time(pid);
assert!(start_time.is_some());
assert!(is_process_alive_with_identity(pid, start_time));
assert!(!is_process_alive_with_identity(pid, Some(u64::MAX)));
assert!(is_process_alive_with_identity(pid, None));
assert!(!is_process_alive_with_identity(0x7fff_fffe, None));
assert!(is_process_running_with_identity(pid, start_time));
}
#[cfg(target_os = "linux")]
#[test]
fn parses_zombie_state_for_completion_waiters() {
let stat = "123 (completed worker) Z 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 4242";
assert_eq!(linux_process_identity_from_stat(stat), Some(('Z', 4242)));
}
#[cfg(target_os = "linux")]
#[test]
#[allow(clippy::zombie_processes)] fn recovered_identity_reaps_an_exited_child() {
let child = std::process::Command::new("true").spawn().unwrap();
let pid = child.id();
let start_time = pid_start_time(pid).unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
while is_process_running_with_identity(pid, Some(start_time))
&& std::time::Instant::now() < deadline
{
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(is_process_alive_with_identity(pid, Some(start_time)));
assert!(!is_process_running_with_identity(pid, Some(start_time)));
assert!(wait_for_process_exit_with_identity(
pid,
start_time,
std::time::Duration::from_secs(1),
));
assert!(!is_process_alive_with_identity(pid, Some(start_time)));
}
}