use crate::containment::ORIGINATOR_ENV_VAR;
use sysinfo::{Pid, ProcessRefreshKind, System, UpdateKind};
#[derive(Debug, Clone)]
pub struct OriginatorProcessInfo {
pub pid: u32,
pub name: String,
pub command: String,
pub originator: String,
pub parent_pid: u32,
pub parent_alive: bool,
}
pub fn parse_originator_value(value: &str) -> Option<(&str, u32)> {
let colon_pos = value.rfind(':')?;
if colon_pos == 0 || colon_pos == value.len() - 1 {
return None;
}
let tool = &value[..colon_pos];
let pid_str = &value[colon_pos + 1..];
let pid = pid_str.parse::<u32>().ok()?;
Some((tool, pid))
}
pub fn find_processes_by_originator(tool: &str) -> Vec<OriginatorProcessInfo> {
let prefix = format!("{}:", tool);
let mut system = System::new();
system.refresh_processes_specifics(
ProcessRefreshKind::new()
.with_environ(UpdateKind::Always)
.with_cmd(UpdateKind::Always),
);
let mut results = Vec::new();
for (pid, process) in system.processes() {
let environ = process.environ();
let originator_value = environ.iter().find_map(|env_entry| {
if let Some(val) = env_entry.strip_prefix(ORIGINATOR_ENV_VAR) {
if let Some(val) = val.strip_prefix('=') {
return Some(val.to_string());
}
}
None
});
let Some(originator_value) = originator_value else {
continue;
};
if !originator_value.starts_with(&prefix) {
continue;
}
let Some((_tool, parent_pid)) = parse_originator_value(&originator_value) else {
continue;
};
let child_start_time = process.start_time();
let parent_alive = is_parent_alive(&system, parent_pid, child_start_time);
let cmd_parts = process.cmd();
let command = if cmd_parts.is_empty() {
process.name().to_string()
} else {
cmd_parts.join(" ")
};
results.push(OriginatorProcessInfo {
pid: pid.as_u32(),
name: process.name().to_string(),
command,
originator: originator_value,
parent_pid,
parent_alive,
});
}
results
}
fn is_parent_alive(system: &System, parent_pid: u32, child_start_time: u64) -> bool {
let parent_sysinfo_pid = Pid::from_u32(parent_pid);
let Some(parent_process) = system.process(parent_sysinfo_pid) else {
return false;
};
let parent_start_time = parent_process.start_time();
if parent_start_time > child_start_time {
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_originator_value_valid() {
let (tool, pid) = parse_originator_value("CLUD:12345").unwrap();
assert_eq!(tool, "CLUD");
assert_eq!(pid, 12345);
}
#[test]
fn parse_originator_value_with_colons_in_tool() {
let (tool, pid) = parse_originator_value("MY:TOOL:99999").unwrap();
assert_eq!(tool, "MY:TOOL");
assert_eq!(pid, 99999);
}
#[test]
fn parse_originator_value_invalid_no_colon() {
assert!(parse_originator_value("CLUD").is_none());
}
#[test]
fn parse_originator_value_invalid_no_pid() {
assert!(parse_originator_value("CLUD:").is_none());
}
#[test]
fn parse_originator_value_invalid_no_tool() {
assert!(parse_originator_value(":12345").is_none());
}
#[test]
fn parse_originator_value_invalid_non_numeric_pid() {
assert!(parse_originator_value("CLUD:abc").is_none());
}
#[test]
fn find_processes_returns_empty_for_nonexistent_tool() {
let results = find_processes_by_originator("__NONEXISTENT_TOOL_TEST__");
assert!(results.is_empty());
}
#[test]
fn parse_originator_value_max_pid() {
let (tool, pid) = parse_originator_value("TOOL:4294967295").unwrap();
assert_eq!(tool, "TOOL");
assert_eq!(pid, u32::MAX);
}
#[test]
fn parse_originator_value_pid_overflow() {
assert!(parse_originator_value("TOOL:4294967296").is_none());
}
#[test]
fn parse_originator_value_negative_pid() {
assert!(parse_originator_value("TOOL:-1").is_none());
}
#[test]
fn parse_originator_value_zero_pid() {
let (tool, pid) = parse_originator_value("TOOL:0").unwrap();
assert_eq!(tool, "TOOL");
assert_eq!(pid, 0);
}
#[test]
fn parse_originator_value_empty_string() {
assert!(parse_originator_value("").is_none());
}
#[test]
fn parse_originator_value_only_colon() {
assert!(parse_originator_value(":").is_none());
}
#[test]
fn originator_process_info_debug_and_clone() {
let info = OriginatorProcessInfo {
pid: 123,
name: "test".to_string(),
command: "test --arg".to_string(),
originator: "TOOL:456".to_string(),
parent_pid: 456,
parent_alive: true,
};
let cloned = info.clone();
assert_eq!(cloned.pid, 123);
assert_eq!(cloned.name, "test");
assert_eq!(cloned.command, "test --arg");
assert_eq!(cloned.originator, "TOOL:456");
assert_eq!(cloned.parent_pid, 456);
assert!(cloned.parent_alive);
let debug = format!("{:?}", info);
assert!(debug.contains("OriginatorProcessInfo"));
assert!(debug.contains("123"));
}
#[test]
fn is_parent_alive_returns_true_for_current_process() {
let mut system = System::new();
system.refresh_processes_specifics(ProcessRefreshKind::new().with_cmd(UpdateKind::Always));
let my_pid = std::process::id();
let result = is_parent_alive(&system, my_pid, u64::MAX);
assert!(result, "current process should be reported as alive");
}
#[test]
fn is_parent_alive_returns_false_for_nonexistent_pid() {
let mut system = System::new();
system.refresh_processes_specifics(ProcessRefreshKind::new().with_cmd(UpdateKind::Always));
let result = is_parent_alive(&system, u32::MAX, 0);
assert!(!result, "nonexistent PID should be reported as dead");
}
#[test]
fn is_parent_alive_detects_pid_reuse() {
let mut system = System::new();
system.refresh_processes_specifics(ProcessRefreshKind::new().with_cmd(UpdateKind::Always));
let my_pid = std::process::id();
let result = is_parent_alive(&system, my_pid, 0);
let _ = result;
}
#[test]
fn find_processes_with_empty_tool_prefix() {
let results = find_processes_by_originator("");
let _ = results;
}
}