use crate::host::Process;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
pub(super) fn processes(program: &str) -> Option<Vec<Process>> {
let mut ps = Command::new("/bin/ps");
ps.args(["-x", "-o", "pid=,comm="]);
let out = super::helper::output_within(ps, b"", Duration::from_secs(5)).ok()?;
if !out.status.success() {
return None;
}
Some(named(&String::from_utf8_lossy(&out.stdout), program))
}
fn named(listing: &str, program: &str) -> Vec<Process> {
listing
.lines()
.filter_map(|line| {
let (pid, path) = line.trim_start().split_once(char::is_whitespace)?;
let path = PathBuf::from(path.trim());
(path.file_name()? == program).then_some(Process {
pid: pid.parse().ok()?,
path,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_program_is_found_by_its_own_name_and_nothing_else() {
let listing = " 412 /Users/a/.codex/packages/standalone/bin/codex\n\
7031 codex\n\
7032 /bin/zsh\n\
88 /usr/bin/codex-helper\n\
9001 /Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex\n\
9002 /Users/a/Codex Things/bin/codex\n\
node\n";
let found = named(listing, "codex");
assert_eq!(
found.iter().map(|p| p.pid).collect::<Vec<_>>(),
[412, 7031, 9001, 9002]
);
assert_eq!(
found[3].path,
PathBuf::from("/Users/a/Codex Things/bin/codex")
);
assert!(named(listing, "gemini").is_empty());
}
#[test]
fn this_users_processes_can_be_read() {
assert_eq!(processes("definitely-not-a-program-name"), Some(Vec::new()));
let me = std::env::current_exe().expect("this test's own binary");
let name = me.file_name().unwrap().to_string_lossy().into_owned();
let found = processes(&name).expect("readable");
let mine = found
.iter()
.find(|p| p.pid == std::process::id())
.unwrap_or_else(|| panic!("{name} is running: {found:?}"));
assert_eq!(mine.path.file_name(), me.file_name());
}
#[test]
fn another_users_processes_are_not_listed() {
let owner = |pid: u32| {
let mut ps = Command::new("ps");
ps.args(["-o", "uid=", "-p", &pid.to_string()]);
super::super::helper::output_within(ps, b"", Duration::from_secs(5))
.map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string())
.unwrap_or_default()
};
if owner(1) == owner(std::process::id()) {
return;
}
let found = processes("launchd").expect("readable");
assert!(found.iter().all(|p| p.pid != 1), "launchd: {found:?}");
}
}