Skip to main content

ralph/lock/
pid.rs

1//! PID liveness detection.
2//!
3//! Responsibilities:
4//! - Provide tri-state PID liveness helpers.
5//! - Encapsulate platform-specific process existence checks.
6//!
7//! Not handled here:
8//! - Lock acquisition or cleanup policy.
9//! - Owner metadata parsing.
10//!
11//! Invariants/assumptions:
12//! - Indeterminate liveness is treated conservatively by callers.
13//! - `Running` means the numeric PID exists now; it does not prove that the
14//!   process is the same owner that originally wrote a lock file.
15
16/// Tri-state PID liveness result.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum PidLiveness {
19    Running,
20    NotRunning,
21    Indeterminate,
22}
23
24impl PidLiveness {
25    pub fn is_definitely_not_running(self) -> bool {
26        matches!(self, Self::NotRunning)
27    }
28
29    pub fn is_running_or_indeterminate(self) -> bool {
30        matches!(self, Self::Running | Self::Indeterminate)
31    }
32}
33
34pub fn pid_liveness(pid: u32) -> PidLiveness {
35    match pid_is_running(pid) {
36        Some(true) => PidLiveness::Running,
37        Some(false) => PidLiveness::NotRunning,
38        None => PidLiveness::Indeterminate,
39    }
40}
41
42#[cfg(windows)]
43fn pid_exists_via_toolhelp(pid: u32) -> Option<bool> {
44    use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
45    use windows_sys::Win32::System::Diagnostics::ToolHelp::{
46        CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, TH32CS_SNAPPROCESS,
47    };
48
49    // SAFETY: The ToolHelp snapshot APIs return OS-owned handles; we initialize
50    // the documented structure size, check each return value, and close the
51    // snapshot handle before returning.
52    unsafe {
53        let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
54        if snapshot == INVALID_HANDLE_VALUE {
55            log::debug!(
56                "CreateToolhelp32Snapshot failed for PID existence check, error: {}",
57                windows_sys::Win32::Foundation::GetLastError()
58            );
59            return None;
60        }
61
62        let result = {
63            let mut entry: PROCESSENTRY32 = std::mem::zeroed();
64            entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
65
66            if Process32First(snapshot, &mut entry) == 0 {
67                log::debug!(
68                    "Process32First failed, error: {}",
69                    windows_sys::Win32::Foundation::GetLastError()
70                );
71                None
72            } else {
73                let mut found = false;
74                loop {
75                    if entry.th32ProcessID == pid {
76                        found = true;
77                        break;
78                    }
79                    if Process32Next(snapshot, &mut entry) == 0 {
80                        break;
81                    }
82                }
83                Some(found)
84            }
85        };
86
87        CloseHandle(snapshot);
88        result
89    }
90}
91
92pub fn pid_is_running(pid: u32) -> Option<bool> {
93    #[cfg(unix)]
94    {
95        // SAFETY: `kill(pid, 0)` is a read-only liveness probe that does not
96        // dereference pointers or mutate Rust-managed memory.
97        let result = unsafe { libc::kill(pid as i32, 0) };
98        if result == 0 {
99            return Some(true);
100        }
101        let error = std::io::Error::last_os_error();
102        if error.raw_os_error() == Some(libc::ESRCH) {
103            return Some(false);
104        }
105        None
106    }
107
108    #[cfg(windows)]
109    {
110        use windows_sys::Win32::Foundation::{
111            CloseHandle, ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER,
112        };
113        use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION};
114
115        // SAFETY: `OpenProcess` returns an OS handle for the queried PID; we
116        // check the handle for zero and close it immediately on success.
117        unsafe {
118            let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
119            if handle != 0 {
120                CloseHandle(handle);
121                Some(true)
122            } else {
123                let error = windows_sys::Win32::Foundation::GetLastError();
124                if error == ERROR_INVALID_PARAMETER {
125                    Some(false)
126                } else if error == ERROR_ACCESS_DENIED {
127                    log::debug!(
128                        "OpenProcess({}) failed with ERROR_ACCESS_DENIED, falling back to ToolHelp enumeration",
129                        pid
130                    );
131                    pid_exists_via_toolhelp(pid)
132                } else {
133                    log::debug!(
134                        "OpenProcess({}) failed with unexpected error: {}",
135                        pid,
136                        error
137                    );
138                    None
139                }
140            }
141        }
142    }
143
144    #[cfg(not(any(unix, windows)))]
145    {
146        let _ = pid;
147        None
148    }
149}