use std::process::Command;
const ORPHAN_BINARIES: &[&str] = &["wg-quick", "openvpn", "wireguard-go"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrphanProcess {
pub pid: u32,
pub command: String,
}
#[must_use]
pub fn scan_orphans() -> Vec<OrphanProcess> {
if cfg!(not(unix)) {
return Vec::new();
}
let Ok(output) = Command::new("ps").args(["-eo", "pid=,comm="]).output() else {
return Vec::new();
};
if !output.status.success() {
return Vec::new();
}
let stdout = String::from_utf8_lossy(&output.stdout);
parse_ps_output(&stdout)
}
fn parse_ps_output(stdout: &str) -> Vec<OrphanProcess> {
let mut out = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let mut parts = line.splitn(2, char::is_whitespace);
let Some(pid_str) = parts.next() else {
continue;
};
let Some(comm_str) = parts.next() else {
continue;
};
let Ok(pid) = pid_str.parse::<u32>() else {
continue;
};
let base = comm_str
.trim()
.rsplit('/')
.next()
.unwrap_or("")
.trim_start_matches('-');
if ORPHAN_BINARIES.contains(&base) {
out.push(OrphanProcess {
pid,
command: base.to_string(),
});
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_ps_output_returns_empty() {
assert_eq!(parse_ps_output(""), Vec::new());
}
#[test]
fn parses_simple_ps_format() {
let input = " 123 wg-quick\n 456 openvpn\n 789 firefox\n";
let got = parse_ps_output(input);
assert_eq!(
got,
vec![
OrphanProcess {
pid: 123,
command: "wg-quick".into()
},
OrphanProcess {
pid: 456,
command: "openvpn".into()
},
]
);
}
#[test]
fn handles_path_prefixed_commands() {
let input = " 1001 /usr/sbin/openvpn\n 1002 /opt/wireguard/wireguard-go\n";
let got = parse_ps_output(input);
assert_eq!(
got,
vec![
OrphanProcess {
pid: 1001,
command: "openvpn".into()
},
OrphanProcess {
pid: 1002,
command: "wireguard-go".into()
},
]
);
}
#[test]
fn skips_unrelated_processes() {
let input = "1 init\n2 kthreadd\n3 ssh-agent\n4 zsh\n";
assert_eq!(parse_ps_output(input), Vec::new());
}
#[test]
fn skips_malformed_lines() {
let input = " not-a-pid wg-quick\n \n 555 openvpn\n";
let got = parse_ps_output(input);
assert_eq!(
got,
vec![OrphanProcess {
pid: 555,
command: "openvpn".into()
}]
);
}
#[test]
fn scan_orphans_does_not_panic_on_any_platform() {
let _ = scan_orphans();
}
}