Skip to main content

upstream_rs/utils/platform/
process_id.rs

1use std::process;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct ProcessIdentity {
5    pub start_token: String,
6}
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ProcessProbe {
10    pub exists: bool,
11    pub start_token: Option<String>,
12}
13
14pub fn current_process_identity() -> Option<ProcessIdentity> {
15    let probe = probe_process(process::id());
16    probe
17        .exists
18        .then_some(probe.start_token)
19        .flatten()
20        .map(|start_token| ProcessIdentity { start_token })
21}
22
23pub fn probe_process(pid: u32) -> ProcessProbe {
24    if pid == 0 {
25        return ProcessProbe {
26            exists: false,
27            start_token: None,
28        };
29    }
30
31    #[cfg(target_os = "linux")]
32    {
33        return probe_process_linux(pid);
34    }
35
36    #[cfg(all(unix, not(target_os = "linux")))]
37    {
38        return probe_process_unix(pid);
39    }
40
41    #[cfg(windows)]
42    {
43        return probe_process_windows(pid);
44    }
45
46    #[allow(unreachable_code)]
47    ProcessProbe {
48        exists: true,
49        start_token: None,
50    }
51}
52
53#[cfg(target_os = "linux")]
54fn probe_process_linux(pid: u32) -> ProcessProbe {
55    use std::fs;
56    use std::path::Path;
57
58    let proc_dir = Path::new("/proc").join(pid.to_string());
59    if !proc_dir.exists() {
60        return ProcessProbe {
61            exists: false,
62            start_token: None,
63        };
64    }
65
66    let stat_path = proc_dir.join("stat");
67    let start_token = fs::read_to_string(stat_path)
68        .ok()
69        .and_then(|raw| parse_linux_start_time_ticks(&raw))
70        .map(|ticks| ticks.to_string());
71
72    ProcessProbe {
73        exists: true,
74        start_token,
75    }
76}
77
78#[cfg(target_os = "linux")]
79fn parse_linux_start_time_ticks(raw_stat: &str) -> Option<u64> {
80    // /proc/<pid>/stat format includes "(comm)" which may contain spaces.
81    // Field 22 (1-based) is starttime; after stripping "(comm) ", it is index 19.
82    let close_paren = raw_stat.rfind(") ")?;
83    let rest = &raw_stat[(close_paren + 2)..];
84    let fields: Vec<&str> = rest.split_whitespace().collect();
85    fields.get(19)?.parse::<u64>().ok()
86}
87
88#[cfg(all(unix, not(target_os = "linux")))]
89fn probe_process_unix(pid: u32) -> ProcessProbe {
90    use std::process::Command;
91
92    let exists = kill_zero_exists(pid);
93    if !exists {
94        return ProcessProbe {
95            exists: false,
96            start_token: None,
97        };
98    }
99
100    let start_token = Command::new("ps")
101        .args(["-o", "lstart=", "-p", &pid.to_string()])
102        .output()
103        .ok()
104        .filter(|out| out.status.success())
105        .and_then(|out| String::from_utf8(out.stdout).ok())
106        .map(|stdout| stdout.trim().to_string())
107        .filter(|token| !token.is_empty());
108
109    ProcessProbe {
110        exists: true,
111        start_token,
112    }
113}
114
115#[cfg(all(unix, not(target_os = "linux")))]
116fn kill_zero_exists(pid: u32) -> bool {
117    use std::process::Command;
118
119    match Command::new("kill").arg("-0").arg(pid.to_string()).output() {
120        Ok(output) if output.status.success() => true,
121        Ok(output) => {
122            let stderr = String::from_utf8_lossy(&output.stderr).to_ascii_lowercase();
123            if stderr.contains("no such process") {
124                false
125            } else {
126                // Conservative fallback on permission or unknown errors.
127                true
128            }
129        }
130        Err(_) => true,
131    }
132}
133
134#[cfg(windows)]
135fn probe_process_windows(pid: u32) -> ProcessProbe {
136    use std::mem::MaybeUninit;
137    use winapi::shared::minwindef::{DWORD, FALSE, FILETIME};
138    use winapi::shared::winerror::{ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER};
139    use winapi::um::errhandlingapi::GetLastError;
140    use winapi::um::handleapi::CloseHandle;
141    use winapi::um::minwinbase::STILL_ACTIVE;
142    use winapi::um::processthreadsapi::{GetExitCodeProcess, GetProcessTimes, OpenProcess};
143    use winapi::um::winnt::PROCESS_QUERY_LIMITED_INFORMATION;
144
145    unsafe {
146        let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid as DWORD);
147        if handle.is_null() {
148            let err = GetLastError();
149            return match err {
150                ERROR_INVALID_PARAMETER => ProcessProbe {
151                    exists: false,
152                    start_token: None,
153                },
154                ERROR_ACCESS_DENIED => ProcessProbe {
155                    exists: true,
156                    start_token: None,
157                },
158                _ => ProcessProbe {
159                    exists: true,
160                    start_token: None,
161                },
162            };
163        }
164
165        let mut exit_code: DWORD = 0;
166        if GetExitCodeProcess(handle, &mut exit_code as *mut DWORD) == 0 {
167            CloseHandle(handle);
168            return ProcessProbe {
169                exists: true,
170                start_token: None,
171            };
172        }
173
174        if exit_code != STILL_ACTIVE {
175            CloseHandle(handle);
176            return ProcessProbe {
177                exists: false,
178                start_token: None,
179            };
180        }
181
182        let mut creation = MaybeUninit::<FILETIME>::uninit();
183        let mut exit = MaybeUninit::<FILETIME>::uninit();
184        let mut kernel = MaybeUninit::<FILETIME>::uninit();
185        let mut user = MaybeUninit::<FILETIME>::uninit();
186
187        let start_token = if GetProcessTimes(
188            handle,
189            creation.as_mut_ptr(),
190            exit.as_mut_ptr(),
191            kernel.as_mut_ptr(),
192            user.as_mut_ptr(),
193        ) != 0
194        {
195            let creation = creation.assume_init();
196            let ticks = ((creation.dwHighDateTime as u64) << 32) | (creation.dwLowDateTime as u64);
197            Some(ticks.to_string())
198        } else {
199            None
200        };
201
202        CloseHandle(handle);
203        ProcessProbe {
204            exists: true,
205            start_token,
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::{current_process_identity, probe_process};
213
214    #[test]
215    fn probe_current_process_reports_exists() {
216        let probe = probe_process(std::process::id());
217        assert!(probe.exists);
218    }
219
220    #[test]
221    fn probe_pid_zero_reports_not_exists() {
222        let probe = probe_process(0);
223        assert!(!probe.exists);
224    }
225
226    #[test]
227    fn current_identity_token_is_non_empty_when_available() {
228        if let Some(identity) = current_process_identity() {
229            assert!(!identity.start_token.trim().is_empty());
230        }
231    }
232
233    #[cfg(target_os = "linux")]
234    #[test]
235    fn parse_linux_start_time_from_stat_line() {
236        let raw = "1234 (process name) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 212121 20";
237        let parsed = super::parse_linux_start_time_ticks(raw);
238        assert_eq!(parsed, Some(212121));
239    }
240}