use std::collections::HashSet;
use std::fs;
use super::git;
use super::{Ancestor, HealthWarning, ProcessAncestry};
pub fn build_ancestry(pid: u32) -> Option<ProcessAncestry> {
let chain = walk_ppid_chain(pid);
if chain.is_empty() {
return None;
}
let cgroup = read_cgroup(pid);
let source = super::detect_source(&chain, cgroup.as_deref());
let warnings = detect_warnings(pid);
let systemd_unit = detect_systemd_unit(pid);
let git_context = git::read_process_cwd(pid).and_then(|cwd| git::detect_git_context(&cwd));
Some(ProcessAncestry {
chain,
source,
warnings,
git_context,
systemd_unit,
launchd_label: None,
})
}
fn walk_ppid_chain(pid: u32) -> Vec<Ancestor> {
let mut chain = Vec::new();
let mut current = pid;
let mut visited = HashSet::new();
loop {
if visited.contains(¤t) {
break;
}
visited.insert(current);
let (name, ppid, _state) = match read_proc_stat(current) {
Some(info) => info,
None => break,
};
chain.push(Ancestor {
pid: current,
name,
ppid,
});
if ppid == 0 || current == 1 {
break;
}
current = ppid;
}
chain
}
fn read_proc_stat(pid: u32) -> Option<(String, u32, char)> {
let stat = fs::read_to_string(format!("/proc/{}/stat", pid)).ok()?;
let comm_start = stat.find('(')?;
let comm_end = stat.rfind(')')?;
let name = stat[comm_start + 1..comm_end].to_string();
let rest = stat.get(comm_end + 2..)?;
let mut fields = rest.split_whitespace();
let state = fields.next()?.chars().next()?;
let ppid: u32 = fields.next()?.parse().ok()?;
Some((name, ppid, state))
}
fn read_cgroup(pid: u32) -> Option<String> {
fs::read_to_string(format!("/proc/{}/cgroup", pid)).ok()
}
fn detect_systemd_unit(pid: u32) -> Option<String> {
let cgroup = read_cgroup(pid)?;
for line in cgroup.lines() {
if let Some(path) = line.rsplit(':').next() {
if let Some(unit) = path.rsplit('/').next() {
if unit.ends_with(".service") {
return Some(unit.to_string());
}
}
}
}
None
}
fn detect_warnings(pid: u32) -> Vec<HealthWarning> {
let mut warnings = Vec::new();
let exe_path = format!("/proc/{}/exe", pid);
if let Ok(target) = fs::read_link(&exe_path) {
if target.to_string_lossy().contains("(deleted)") {
warnings.push(HealthWarning::DeletedBinary);
}
}
if let Some((_name, _ppid, state)) = read_proc_stat(pid) {
if state == 'Z' {
warnings.push(HealthWarning::ZombieProcess);
}
}
warnings
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_walk_ppid_chain_self() {
let pid = std::process::id();
let chain = walk_ppid_chain(pid);
assert!(!chain.is_empty(), "Should find at least our own process");
assert_eq!(chain[0].pid, pid);
}
#[test]
fn test_walk_ppid_chain_terminates_at_pid1() {
let chain = walk_ppid_chain(1);
assert!(chain.len() <= 1);
}
#[test]
fn test_walk_ppid_chain_no_infinite_loop() {
let chain = walk_ppid_chain(std::process::id());
assert!(chain.len() < 100, "Chain should be reasonable length");
}
#[test]
fn test_read_proc_stat_self() {
let pid = std::process::id();
let result = read_proc_stat(pid);
assert!(result.is_some());
let (name, ppid, state) = result.unwrap();
assert!(!name.is_empty());
assert!(ppid > 0);
assert!(
matches!(state, 'R' | 'S' | 'D'),
"Expected R/S/D state, got {}",
state
);
}
#[test]
fn test_read_proc_stat_nonexistent() {
let result = read_proc_stat(0);
assert!(result.is_none());
}
#[test]
fn test_detect_warnings_self() {
let warnings = detect_warnings(std::process::id());
assert!(warnings.is_empty());
}
#[test]
fn test_build_ancestry_self() {
let pid = std::process::id();
let ancestry = build_ancestry(pid);
assert!(ancestry.is_some());
let a = ancestry.unwrap();
assert!(!a.chain.is_empty());
assert_eq!(a.chain[0].pid, pid);
}
}