git_policy_cli/
process_start.rs1#[cfg(unix)]
25use super::process_identity::signal_probe;
26use std::path::{Path, PathBuf};
28
29pub const LINUX_USER_HZ: i64 = 100;
32
33pub const LINUX_RESOLUTION_MS: i64 = 2 * (1000 / LINUX_USER_HZ);
36
37pub const DARWIN_RESOLUTION_MS: i64 = 1000;
40
41pub const WINDOWS_RESOLUTION_MS: i64 = 1;
43
44pub const START_TICKS_FIELD_INDEX: usize = 19;
46
47#[derive(Clone, Debug, PartialEq)]
58pub enum ProcessStart {
59 Running {
61 started_at_ms: f64,
63 resolution_ms: i64,
65 },
66 Missing,
69 Unknown(
71 String,
73 ),
74}
75
76pub fn parse_linux_stat(stat: &str) -> Option<(String, i64)> {
89 let command_end: usize = stat.rfind(')')?;
90 let after: &str = stat.get(command_end + 2..).unwrap_or("");
91 let fields: Vec<&str> = super::js_text::trim_javascript(after).split(' ').collect();
92 let state: &str = fields.first().copied().unwrap_or("");
93 let ticks_text: &str = fields.get(START_TICKS_FIELD_INDEX).copied()?;
94 if state.is_empty() || ticks_text.is_empty() || !ticks_text.bytes().all(is_digit) {
95 return None;
96 }
97 let ticks: i64 = ticks_text.parse::<i64>().ok()?;
98 if ticks > super::json_record::MAX_SAFE_INTEGER {
99 return None;
100 }
101 return Some((String::from(state), ticks));
102}
103
104fn is_digit(byte: u8) -> bool {
114 return byte.is_ascii_digit();
115}
116
117pub fn parse_uptime(text: &str) -> Option<f64> {
128 let first: &str = text.split(' ').next()?;
129 let trimmed: &str = super::js_text::trim_javascript(first);
130 let seconds: f64 = if trimmed.is_empty() { 0.0 } else { trimmed.parse::<f64>().ok()? };
132 if !seconds.is_finite() {
133 return None;
134 }
135 return Some(seconds);
136}
137
138pub fn now_ms() -> f64 {
148 match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
149 Ok(elapsed) => return elapsed.as_secs_f64() * 1000.0,
150 Err(_) => return 0.0,
151 }
152}
153
154pub fn linux_start(pid: i64, proc_root: &Path, now: f64) -> ProcessStart {
165 let stat_path: PathBuf = proc_root.join(pid.to_string()).join("stat");
166 let stat: String = match std::fs::read(&stat_path) {
167 Ok(bytes) => String::from_utf8_lossy(bytes.as_slice()).into_owned(),
168 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return ProcessStart::Missing,
169 #[cfg(unix)]
170 Err(error) if error.raw_os_error() == Some(libc::ESRCH) => return ProcessStart::Missing,
171 Err(error) => return ProcessStart::Unknown(format!("reading {} failed: {error}", stat_path.display())),
172 };
173 let Some((state, ticks)) = parse_linux_stat(stat.as_str()) else {
174 return ProcessStart::Unknown(String::from("Malformed Linux process stat."));
175 };
176 if state == "Z" || state == "X" {
177 return ProcessStart::Missing;
178 }
179 let uptime_path: PathBuf = proc_root.join("uptime");
180 let uptime: f64 = match std::fs::read_to_string(&uptime_path).ok().as_deref().and_then(parse_uptime) {
181 Some(seconds) => seconds,
182 None => return ProcessStart::Unknown(String::from("Malformed /proc/uptime.")),
183 };
184 let ticks_ms: f64 = (ticks as f64) * 1000.0 / (LINUX_USER_HZ as f64);
186 return ProcessStart::Running {
187 started_at_ms: (now - uptime * 1000.0) + ticks_ms,
188 resolution_ms: LINUX_RESOLUTION_MS,
189 };
190}
191
192const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
197
198pub fn parse_lstart(text: &str) -> Option<(i32, i32, i32, i32, i32, i32)> {
215 let words: Vec<&str> = text.split_whitespace().collect();
216 if words.len() != 5 {
217 return None;
218 }
219 let month: i32 = month_number(words[1])?;
220 let day: i32 = words[2].parse::<i32>().ok()?;
221 let clock: Vec<&str> = words[3].split(':').collect();
222 if clock.len() != 3 {
223 return None;
224 }
225 let hour: i32 = clock[0].parse::<i32>().ok()?;
226 let minute: i32 = clock[1].parse::<i32>().ok()?;
227 let second: i32 = clock[2].parse::<i32>().ok()?;
228 let year: i32 = words[4].parse::<i32>().ok()?;
229 return Some((year, month, day, hour, minute, second));
230}
231
232fn month_number(name: &str) -> Option<i32> {
242 for (index, month) in MONTHS.iter().enumerate() {
243 if *month == name {
244 return i32::try_from(index).ok();
245 }
246 }
247 return None;
248}
249
250pub fn process_exists(pid: i64) -> bool {
261 #[cfg(unix)]
262 {
263 match signal_probe(pid) {
264 Ok(alive) => return alive,
265 Err(error) => return error.kind() == std::io::ErrorKind::PermissionDenied,
266 }
267 }
268 #[cfg(not(unix))]
269 {
270 let _ = pid;
271 return true;
272 }
273}
274
275pub fn resolve_process_start(pid: i64) -> ProcessStart {
285 if !process_exists(pid) {
286 return ProcessStart::Missing;
287 }
288 #[cfg(target_os = "linux")]
289 {
290 return linux_start(pid, Path::new("/proc"), now_ms());
291 }
292 #[cfg(target_os = "macos")]
293 {
294 return darwin_start(pid);
295 }
296 #[cfg(windows)]
297 {
298 return windows_start(pid);
299 }
300 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
301 {
302 return ProcessStart::Unknown(String::from("no start-time source on this platform"));
303 }
304}
305
306#[cfg(target_os = "macos")]
318fn darwin_start(pid: i64) -> ProcessStart {
319 let output: std::process::Output = match std::process::Command::new("ps")
320 .args(["-o", "lstart=", "-p", pid.to_string().as_str()])
321 .env("LC_ALL", "C")
322 .stdin(std::process::Stdio::null())
323 .output()
324 {
325 Ok(finished) => finished,
326 Err(error) => return ProcessStart::Unknown(format!("ps could not start: {error}")),
327 };
328 let text: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string();
329 let Some((year, month, day, hour, minute, second)) = parse_lstart(text.as_str()) else {
330 return ProcessStart::Unknown(format!("unparsable ps start time {text:?}"));
331 };
332 let mut broken: libc::tm = unsafe { std::mem::zeroed() };
334 broken.tm_year = year - 1900;
335 broken.tm_mon = month;
336 broken.tm_mday = day;
337 broken.tm_hour = hour;
338 broken.tm_min = minute;
339 broken.tm_sec = second;
340 broken.tm_isdst = -1;
341 let seconds: libc::time_t = unsafe { libc::mktime(&mut broken) };
343 if seconds == -1 {
344 return ProcessStart::Unknown(format!("unparsable ps start time {text:?}"));
345 }
346 return ProcessStart::Running {
347 started_at_ms: (seconds as f64) * 1000.0,
348 resolution_ms: DARWIN_RESOLUTION_MS,
349 };
350}
351
352#[cfg(windows)]
363fn windows_start(pid: i64) -> ProcessStart {
364 let command: String = format!("(Get-Process -Id {pid}).StartTime.ToUniversalTime().Ticks");
365 let output: std::process::Output = match std::process::Command::new("powershell.exe")
366 .args(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command.as_str()])
367 .stdin(std::process::Stdio::null())
368 .output()
369 {
370 Ok(finished) => finished,
371 Err(error) => return ProcessStart::Unknown(format!("powershell could not start: {error}")),
372 };
373 let text: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string();
374 let Ok(ticks) = text.parse::<i128>() else {
375 return ProcessStart::Unknown(format!("unparsable start ticks {text:?}"));
376 };
377 let milliseconds: i128 = (ticks - 621_355_968_000_000_000) / 10_000;
378 return ProcessStart::Running {
379 started_at_ms: milliseconds as f64,
380 resolution_ms: WINDOWS_RESOLUTION_MS,
381 };
382}
383
384#[cfg(test)]
386#[path = "process_start_tests.rs"]
387mod tests;