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