use std::collections::{HashMap, HashSet};
use crate::agent::AgentKind;
type Parent = HashMap<u32, u32>;
type Agents = Vec<(u32, AgentKind)>;
pub fn classify_panes(pane_pids: &HashSet<u32>) -> HashMap<u32, AgentKind> {
let mut parent: Parent = HashMap::with_capacity(256);
let mut agents: Agents = Vec::new();
collect_processes(&mut parent, &mut agents);
agents.sort_unstable_by_key(|(pid, _)| *pid);
let mut owners = HashMap::new();
for (pid, kind) in agents {
if let Some(pane_pid) = climb_to_pane(pid, pane_pids, &parent) {
owners.entry(pane_pid).or_insert(kind);
}
}
owners
}
#[cfg(target_os = "linux")]
fn collect_processes(parent: &mut Parent, agents: &mut Agents) {
use std::fmt::Write as _;
use std::fs::File;
use std::io::Read as _;
let Ok(entries) = std::fs::read_dir("/proc") else {
return;
};
let mut path = String::new();
let mut buf = Vec::with_capacity(512);
for entry in entries.flatten() {
let Some(pid) = entry
.file_name()
.to_str()
.and_then(|name| name.parse::<u32>().ok())
else {
continue;
};
path.clear();
let _ = write!(path, "/proc/{pid}/stat");
buf.clear();
let Ok(mut file) = File::open(&path) else {
continue;
};
if file.read_to_end(&mut buf).is_err() {
continue;
}
if let Some(stat) = parse_stat(&String::from_utf8_lossy(&buf)) {
parent.insert(pid, stat.ppid);
if let Some(kind) = AgentKind::from_name(stat.comm)
&& i64::from(stat.pgrp) == stat.tpgid
{
agents.push((pid, kind));
}
}
}
}
#[cfg(target_os = "macos")]
fn collect_processes(parent: &mut Parent, agents: &mut Agents) {
for pid in list_all_pids() {
let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::uninit();
let size = size_of::<libc::proc_bsdinfo>() as libc::c_int;
let written = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDTBSDINFO,
0,
info.as_mut_ptr().cast(),
size,
)
};
if written != size {
continue;
}
let info = unsafe { info.assume_init() };
let pid = pid as u32;
parent.insert(pid, info.pbi_ppid);
if info.pbi_pgid != info.e_tpgid {
continue;
}
if let Some(kind) = classify_foreground(pid, &info.pbi_comm) {
agents.push((pid, kind));
}
}
}
#[cfg(target_os = "macos")]
fn list_all_pids() -> Vec<libc::pid_t> {
let count = unsafe { libc::proc_listallpids(std::ptr::null_mut(), 0) };
if count <= 0 {
return Vec::new();
}
let capacity = count as usize + 16;
let mut pids: Vec<libc::pid_t> = Vec::with_capacity(capacity);
let bytes = capacity * size_of::<libc::pid_t>();
let filled = unsafe { libc::proc_listallpids(pids.as_mut_ptr().cast(), bytes as libc::c_int) };
if filled <= 0 {
return Vec::new();
}
unsafe { pids.set_len((filled as usize).min(capacity)) };
pids.retain(|&pid| pid > 0);
pids
}
#[cfg(target_os = "macos")]
fn classify_foreground(pid: u32, comm: &[libc::c_char]) -> Option<AgentKind> {
let mut name = [0u8; 32];
for (dst, &src) in name.iter_mut().zip(comm) {
*dst = src as u8;
}
let end = name.iter().position(|&b| b == 0).unwrap_or(name.len());
if let Some(kind) = std::str::from_utf8(&name[..end])
.ok()
.and_then(AgentKind::from_name)
{
return Some(kind);
}
let mut buf = [0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
let len = unsafe {
libc::proc_pidpath(
pid as libc::c_int,
buf.as_mut_ptr().cast(),
buf.len() as u32,
)
};
if len <= 0 {
return None;
}
AgentKind::from_path(std::str::from_utf8(&buf[..len as usize]).ok()?)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn collect_processes(_parent: &mut Parent, _agents: &mut Agents) {}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
struct Stat<'a> {
comm: &'a str,
ppid: u32,
pgrp: u32,
tpgid: i64,
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn parse_stat(stat: &str) -> Option<Stat<'_>> {
let open = stat.find('(')?;
let close = stat.rfind(')')?;
let comm = stat.get(open + 1..close)?;
let after = stat.get(close + 1..)?;
let mut fields = after.split_whitespace();
let _state = fields.next()?;
let ppid = fields.next()?.parse().ok()?;
let pgrp = fields.next()?.parse().ok()?;
let _session = fields.next()?;
let _tty_nr = fields.next()?;
let tpgid = fields.next()?.parse().ok()?;
Some(Stat {
comm,
ppid,
pgrp,
tpgid,
})
}
fn climb_to_pane(start: u32, pane_pids: &HashSet<u32>, parent: &Parent) -> Option<u32> {
let mut pid = start;
for _ in 0..64 {
if pane_pids.contains(&pid) {
return Some(pid);
}
let next = *parent.get(&pid)?;
if next == pid || next == 0 {
return None;
}
pid = next;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_stat_extracts_comm_ppid_pgrp_tpgid() {
let stat = parse_stat("2776949 (codex) S 2776942 2776929 2776929 34850 2776929 0").unwrap();
assert_eq!(
(stat.comm, stat.ppid, stat.pgrp, stat.tpgid),
("codex", 2776942, 2776929, 2776929)
);
}
#[test]
fn parse_stat_handles_spaces_and_parens_in_comm() {
let stat = parse_stat("42 (npm exec @opena) S 7 9 9 0 9 0").unwrap();
assert_eq!((stat.comm, stat.ppid), ("npm exec @opena", 7));
let stat = parse_stat("42 (weird (x)) R 7 9 9 0 9 0").unwrap();
assert_eq!(stat.comm, "weird (x)");
}
#[test]
fn parse_stat_keeps_processes_with_no_controlling_terminal() {
let stat = parse_stat("3 (kthreadd) S 2 0 0 0 -1 0").unwrap();
assert_eq!((stat.ppid, stat.pgrp, stat.tpgid), (2, 0, -1));
}
#[test]
fn climbs_from_agent_leaf_to_owning_pane() {
let parent: Parent = [
(2776949, 2776942),
(2776942, 2776941),
(2776941, 2776929),
(2776929, 2776867),
(2776867, 1),
]
.into();
let panes: HashSet<u32> = [2776867].into();
assert_eq!(climb_to_pane(2776949, &panes, &parent), Some(2776867));
}
#[test]
fn climb_returns_none_without_a_pane_ancestor() {
let parent: Parent = [(50, 40), (40, 1)].into();
let panes: HashSet<u32> = [999].into();
assert_eq!(climb_to_pane(50, &panes, &parent), None);
}
#[test]
fn climb_terminates_on_a_cycle() {
let parent: Parent = [(10, 20), (20, 10)].into();
let panes: HashSet<u32> = [999].into();
assert_eq!(climb_to_pane(10, &panes, &parent), None);
}
}