1#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
15pub enum PidRefusal {
16 #[error("PID 0 would address this process's own process group")]
18 Zero,
19 #[error("PID {0} is above i32::MAX and would address a process group or every process")]
22 OutOfRange(u32),
23 #[error("PID 1 is init and is never signalled")]
25 Init,
26 #[error("PID {0} is this process")]
28 OwnProcess(u32),
29 #[error("process group {0} contains this process")]
31 OwnGroup(u32),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36pub enum PidFileError {
37 #[error("the PID file is empty")]
39 Empty,
40 #[error("the PID file does not hold a number: {0:?}")]
42 NotANumber(String),
43 #[error("the PID file holds a negative number: {0}")]
45 Negative(String),
46 #[error("the PID file holds an impossible PID: {0}")]
48 Impossible(String),
49}
50
51pub const MAX_PID: u32 = i32::MAX as u32;
53
54fn in_signal_range(pid: u32) -> Result<i32, PidRefusal> {
55 match pid {
56 0 => Err(PidRefusal::Zero),
57 1 => Err(PidRefusal::Init),
58 p => i32::try_from(p).map_err(|_| PidRefusal::OutOfRange(p)),
59 }
60}
61
62pub fn signal_target(pid: u32) -> Result<i32, PidRefusal> {
68 let raw = in_signal_range(pid)?;
69 if pid == std::process::id() {
70 return Err(PidRefusal::OwnProcess(pid));
71 }
72 Ok(raw)
73}
74
75pub fn group_target(pgid: u32) -> Result<i32, PidRefusal> {
79 let raw = signal_target(pgid)?;
80 #[cfg(unix)]
81 {
82 if unsafe { libc::getpgrp() } == raw {
84 return Err(PidRefusal::OwnGroup(pgid));
85 }
86 }
87 Ok(raw)
88}
89
90pub fn parse_pid_file(text: &str) -> Result<u32, PidFileError> {
95 let value = text.lines().next().unwrap_or("").trim();
96 if value.is_empty() {
97 return Err(PidFileError::Empty);
98 }
99 let (negative, digits) = match value.strip_prefix('-') {
100 Some(rest) => (true, rest),
101 None => (false, value),
102 };
103 if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
104 return Err(PidFileError::NotANumber(value.to_owned()));
105 }
106 if negative {
107 return Err(PidFileError::Negative(value.to_owned()));
108 }
109 match digits.parse::<u32>() {
110 Ok(pid) if in_signal_range(pid).is_ok() => Ok(pid),
111 _ => Err(PidFileError::Impossible(value.to_owned())),
112 }
113}
114
115pub fn is_alive(pid: u32) -> bool {
121 let raw = match signal_target(pid) {
122 Ok(raw) => raw,
123 Err(PidRefusal::OwnProcess(_) | PidRefusal::Init) => return true,
124 Err(_) => return false,
125 };
126 #[cfg(unix)]
127 {
128 unsafe { libc::kill(raw, 0) == 0 }
132 }
133 #[cfg(windows)]
134 {
135 use windows_sys::Win32::Foundation::{CloseHandle, STILL_ACTIVE, WAIT_TIMEOUT};
136 use windows_sys::Win32::System::Threading::{
137 GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, WaitForSingleObject,
138 };
139 let _ = raw;
140
141 unsafe {
145 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
146 if handle.is_null() {
147 return false;
148 }
149 let wait = WaitForSingleObject(handle, 0);
150 if wait == WAIT_TIMEOUT {
151 CloseHandle(handle);
152 return true;
153 }
154 let mut exit_code: u32 = 0;
155 GetExitCodeProcess(handle, &raw mut exit_code);
156 CloseHandle(handle);
157 exit_code == STILL_ACTIVE as u32
158 }
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn current_process_is_alive() {
168 assert!(is_alive(std::process::id()));
169 }
170
171 #[test]
172 fn bogus_pid_is_not_alive() {
173 assert!(!is_alive(u32::MAX - 42));
174 }
175
176 #[test]
177 fn wrapping_pids_are_never_probed_and_never_alive() {
178 for pid in [0, u32::MAX, MAX_PID + 1] {
181 assert!(!is_alive(pid), "{pid}");
182 }
183 }
184
185 #[test]
186 fn own_process_and_init_are_alive_without_a_probe() {
187 assert!(is_alive(1));
188 assert!(is_alive(std::process::id()));
189 }
190
191 #[test]
192 fn signal_target_accepts_only_other_pids_in_range() {
193 assert_eq!(signal_target(0), Err(PidRefusal::Zero));
194 assert_eq!(signal_target(1), Err(PidRefusal::Init));
195 assert_eq!(
196 signal_target(u32::MAX),
197 Err(PidRefusal::OutOfRange(u32::MAX))
198 );
199 assert_eq!(
200 signal_target(MAX_PID + 1),
201 Err(PidRefusal::OutOfRange(MAX_PID + 1))
202 );
203 let me = std::process::id();
204 assert_eq!(signal_target(me), Err(PidRefusal::OwnProcess(me)));
205 assert_eq!(signal_target(MAX_PID), Ok(i32::MAX));
206 assert_eq!(signal_target(2), Ok(2));
207 }
208
209 #[cfg(unix)]
210 #[test]
211 fn group_target_refuses_own_group_and_wrapping_ids() {
212 let own = unsafe { libc::getpgrp() } as u32;
214 assert!(group_target(own).is_err());
215 assert_eq!(group_target(0), Err(PidRefusal::Zero));
216 assert_eq!(
217 group_target(u32::MAX),
218 Err(PidRefusal::OutOfRange(u32::MAX))
219 );
220 }
221
222 #[test]
223 fn pid_file_garbage_is_refused() {
224 assert_eq!(parse_pid_file(""), Err(PidFileError::Empty));
225 assert_eq!(parse_pid_file(" \n"), Err(PidFileError::Empty));
226 for text in ["abc", "12abc", "-", "+5", "0x10", "1.5"] {
227 assert!(
228 matches!(parse_pid_file(text), Err(PidFileError::NotANumber(_))),
229 "{text}"
230 );
231 }
232 assert!(matches!(
233 parse_pid_file("-1"),
234 Err(PidFileError::Negative(_))
235 ));
236 for text in ["0", "1", "4294967295", "2147483648", "99999999999999999999"] {
237 assert!(
238 matches!(parse_pid_file(text), Err(PidFileError::Impossible(_))),
239 "{text}"
240 );
241 }
242 assert_eq!(parse_pid_file("2147483647"), Ok(MAX_PID));
243 assert_eq!(parse_pid_file(" 4242 \nextra"), Ok(4242));
244 }
245}