#[cfg(unix)]
use crate::process_identity::signal_probe;
use std::path::{Path, PathBuf};
pub const LINUX_USER_HZ: i64 = 100;
pub const LINUX_RESOLUTION_MS: i64 = 2 * (1000 / LINUX_USER_HZ);
pub const DARWIN_RESOLUTION_MS: i64 = 1000;
pub const WINDOWS_RESOLUTION_MS: i64 = 1;
pub const START_TICKS_FIELD_INDEX: usize = 19;
#[derive(Clone, Debug, PartialEq)]
pub enum ProcessStart {
Running {
started_at_ms: f64,
resolution_ms: i64,
},
Missing,
Unknown(
String,
),
}
pub fn parse_linux_stat(stat: &str) -> Option<(String, i64)> {
let command_end: usize = stat.rfind(')')?;
let after: &str = stat.get(command_end + 2..).unwrap_or("");
let fields: Vec<&str> = crate::js_text::trim_javascript(after).split(' ').collect();
let state: &str = fields.first().copied().unwrap_or("");
let ticks_text: &str = fields.get(START_TICKS_FIELD_INDEX).copied()?;
if state.is_empty() || ticks_text.is_empty() || !ticks_text.bytes().all(is_digit) {
return None;
}
let ticks: i64 = ticks_text.parse::<i64>().ok()?;
if ticks > crate::json_record::MAX_SAFE_INTEGER {
return None;
}
return Some((String::from(state), ticks));
}
fn is_digit(byte: u8) -> bool {
return byte.is_ascii_digit();
}
pub fn parse_uptime(text: &str) -> Option<f64> {
let first: &str = text.split(' ').next()?;
let trimmed: &str = crate::js_text::trim_javascript(first);
let seconds: f64 = if trimmed.is_empty() { 0.0 } else { trimmed.parse::<f64>().ok()? };
if !seconds.is_finite() {
return None;
}
return Some(seconds);
}
pub fn now_ms() -> f64 {
match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Ok(elapsed) => return elapsed.as_secs_f64() * 1000.0,
Err(_) => return 0.0,
}
}
pub fn linux_start(pid: i64, proc_root: &Path, now: f64) -> ProcessStart {
let stat_path: PathBuf = proc_root.join(pid.to_string()).join("stat");
let stat: String = match std::fs::read(&stat_path) {
Ok(bytes) => String::from_utf8_lossy(bytes.as_slice()).into_owned(),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return ProcessStart::Missing,
#[cfg(unix)]
Err(error) if error.raw_os_error() == Some(libc::ESRCH) => return ProcessStart::Missing,
Err(error) => return ProcessStart::Unknown(format!("reading {} failed: {error}", stat_path.display())),
};
let Some((state, ticks)) = parse_linux_stat(stat.as_str()) else {
return ProcessStart::Unknown(String::from("Malformed Linux process stat."));
};
if state == "Z" || state == "X" {
return ProcessStart::Missing;
}
let uptime_path: PathBuf = proc_root.join("uptime");
let uptime: f64 = match std::fs::read_to_string(&uptime_path).ok().as_deref().and_then(parse_uptime) {
Some(seconds) => seconds,
None => return ProcessStart::Unknown(String::from("Malformed /proc/uptime.")),
};
let ticks_ms: f64 = (ticks as f64) * 1000.0 / (LINUX_USER_HZ as f64);
return ProcessStart::Running {
started_at_ms: (now - uptime * 1000.0) + ticks_ms,
resolution_ms: LINUX_RESOLUTION_MS,
};
}
const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
pub fn parse_lstart(text: &str) -> Option<(i32, i32, i32, i32, i32, i32)> {
let words: Vec<&str> = text.split_whitespace().collect();
if words.len() != 5 {
return None;
}
let month: i32 = month_number(words[1])?;
let day: i32 = words[2].parse::<i32>().ok()?;
let clock: Vec<&str> = words[3].split(':').collect();
if clock.len() != 3 {
return None;
}
let hour: i32 = clock[0].parse::<i32>().ok()?;
let minute: i32 = clock[1].parse::<i32>().ok()?;
let second: i32 = clock[2].parse::<i32>().ok()?;
let year: i32 = words[4].parse::<i32>().ok()?;
return Some((year, month, day, hour, minute, second));
}
fn month_number(name: &str) -> Option<i32> {
for (index, month) in MONTHS.iter().enumerate() {
if *month == name {
return i32::try_from(index).ok();
}
}
return None;
}
pub fn process_exists(pid: i64) -> bool {
#[cfg(unix)]
{
match signal_probe(pid) {
Ok(alive) => return alive,
Err(error) => return error.kind() == std::io::ErrorKind::PermissionDenied,
}
}
#[cfg(not(unix))]
{
let _ = pid;
return true;
}
}
pub fn resolve_process_start(pid: i64) -> ProcessStart {
if !process_exists(pid) {
return ProcessStart::Missing;
}
#[cfg(target_os = "linux")]
{
return linux_start(pid, Path::new("/proc"), now_ms());
}
#[cfg(target_os = "macos")]
{
return darwin_start(pid);
}
#[cfg(windows)]
{
return windows_start(pid);
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
{
return ProcessStart::Unknown(String::from("no start-time source on this platform"));
}
}
#[cfg(target_os = "macos")]
fn darwin_start(pid: i64) -> ProcessStart {
let output: std::process::Output = match std::process::Command::new("ps")
.args(["-o", "lstart=", "-p", pid.to_string().as_str()])
.env("LC_ALL", "C")
.stdin(std::process::Stdio::null())
.output()
{
Ok(finished) => finished,
Err(error) => return ProcessStart::Unknown(format!("ps could not start: {error}")),
};
let text: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string();
let Some((year, month, day, hour, minute, second)) = parse_lstart(text.as_str()) else {
return ProcessStart::Unknown(format!("unparsable ps start time {text:?}"));
};
let mut broken: libc::tm = unsafe { std::mem::zeroed() };
broken.tm_year = year - 1900;
broken.tm_mon = month;
broken.tm_mday = day;
broken.tm_hour = hour;
broken.tm_min = minute;
broken.tm_sec = second;
broken.tm_isdst = -1;
let seconds: libc::time_t = unsafe { libc::mktime(&mut broken) };
if seconds == -1 {
return ProcessStart::Unknown(format!("unparsable ps start time {text:?}"));
}
return ProcessStart::Running {
started_at_ms: (seconds as f64) * 1000.0,
resolution_ms: DARWIN_RESOLUTION_MS,
};
}
#[cfg(windows)]
fn windows_start(pid: i64) -> ProcessStart {
let command: String = format!("(Get-Process -Id {pid}).StartTime.ToUniversalTime().Ticks");
let output: std::process::Output = match std::process::Command::new("powershell.exe")
.args(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command.as_str()])
.stdin(std::process::Stdio::null())
.output()
{
Ok(finished) => finished,
Err(error) => return ProcessStart::Unknown(format!("powershell could not start: {error}")),
};
let text: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string();
let Ok(ticks) = text.parse::<i128>() else {
return ProcessStart::Unknown(format!("unparsable start ticks {text:?}"));
};
let milliseconds: i128 = (ticks - 621_355_968_000_000_000) / 10_000;
return ProcessStart::Running {
started_at_ms: milliseconds as f64,
resolution_ms: WINDOWS_RESOLUTION_MS,
};
}
#[cfg(test)]
#[path = "process_start_tests.rs"]
mod tests;