use std::collections::HashMap;
use super::census::{foreign_groups, host_name};
use super::slots::{HolderRecord, summarize_command};
use crate::core::build_probe::{
BuildGroup, ProcessSampler, ProcessSnapshot, SysinfoSampler, is_build_driver,
};
const OWNER_NAMES: &[&str] = &[
"claude",
"tm",
"trusty-mpm",
"tmux",
"screen",
"sshd",
"login",
"launchd",
"systemd",
"init",
"cron",
"Terminal",
"iTerm2",
"Code Helper",
"Cursor",
"zed",
"Runner.Worker",
"Runner.Listener",
];
const SUMMARY_MAX_CHARS: usize = 60;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct GroupDetail {
pub pgid: Option<u32>,
pub leader: String,
pub driver: String,
pub compilers: String,
pub chain: Vec<String>,
}
impl GroupDetail {
#[must_use]
pub fn render(&self) -> String {
format!(
"pgid {} | leader `{}` | driver {} | {} | chain {}",
self.pgid.map_or_else(|| "?".to_string(), |p| p.to_string()),
self.leader,
self.driver,
self.compilers,
self.chain.join(" < ")
)
}
}
#[must_use]
pub fn describe(
groups: &[BuildGroup],
rows: &[ProcessSnapshot],
argv_of: &dyn Fn(u32) -> Option<Vec<String>>,
pgid_of: &dyn Fn(u32) -> Option<u32>,
) -> Vec<GroupDetail> {
let by_pid: HashMap<u32, &ProcessSnapshot> = rows.iter().map(|r| (r.pid, r)).collect();
groups
.iter()
.map(|group| {
let pgid = pgid_of(group.root_pid);
let leader_pid = pgid
.filter(|p| by_pid.contains_key(p))
.unwrap_or(group.root_pid);
let shown = |pid: u32| argv_of(pid).map_or_else(|| "?".to_string(), |a| redact(&a));
GroupDetail {
pgid,
leader: shown(leader_pid),
driver: format!(
"{} (pid {}) `{}`",
group.root_name,
group.root_pid,
shown(group.root_pid)
),
compilers: group.render(),
chain: owner_chain(&group.ancestry, &by_pid),
}
})
.collect()
}
fn redact(argv: &[String]) -> String {
let Some(program) = argv.first() else {
return "?".to_string();
};
let base = program.rsplit('/').next().unwrap_or(program);
let shown = if is_build_driver(base) {
summarize_command(argv)
} else {
base.to_string()
};
if shown.chars().count() > SUMMARY_MAX_CHARS {
let cut: String = shown.chars().take(SUMMARY_MAX_CHARS - 1).collect();
format!("{cut}…")
} else {
shown
}
}
fn owner_chain(ancestry: &[u32], by_pid: &HashMap<u32, &ProcessSnapshot>) -> Vec<String> {
let mut chain = Vec::new();
for pid in ancestry {
let name = by_pid.get(pid).map_or("?", |p| p.name.as_str());
chain.push(format!("{name}({pid})"));
if OWNER_NAMES.contains(&name) {
break;
}
}
chain
}
pub fn live_breakdown(holders: &[HolderRecord]) -> Result<Vec<GroupDetail>, String> {
let sampler = SysinfoSampler::new();
std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL);
let rows = sampler.sample().map_err(|err| err.to_string())?;
let groups = foreign_groups(&rows, holders);
let mut wanted: Vec<u32> = groups.iter().map(|g| g.root_pid).collect();
wanted.extend(groups.iter().filter_map(|g| pgid_of(g.root_pid)));
let argv = argv_of_pids(&wanted);
Ok(describe(
&groups,
&rows,
&|pid| argv.get(&pid).cloned(),
&pgid_of,
))
}
#[must_use]
pub fn header(groups: usize, holders: usize, ceiling: u32) -> String {
format!(
"tm build-lease census on {}: {groups} build group(s) without a lease, {holders} \
lease(s) held, ceiling {ceiling}",
host_name()
)
}
fn argv_of_pids(pids: &[u32]) -> HashMap<u32, Vec<String>> {
use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};
let pids: Vec<Pid> = pids.iter().map(|p| Pid::from_u32(*p)).collect();
let mut sys = System::new();
sys.refresh_processes_specifics(
ProcessesToUpdate::Some(&pids),
true,
ProcessRefreshKind::nothing().with_cmd(UpdateKind::Always),
);
sys.processes()
.iter()
.map(|(pid, proc_)| {
let argv = proc_
.cmd()
.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect();
(pid.as_u32(), argv)
})
.collect()
}
fn pgid_of(pid: u32) -> Option<u32> {
let pid = libc::pid_t::try_from(pid).ok()?;
let pgid = unsafe { libc::getpgid(pid) };
u32::try_from(pgid).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::build_probe::build_groups;
fn row(pid: u32, parent: Option<u32>, name: &str) -> ProcessSnapshot {
ProcessSnapshot {
pid,
parent,
name: name.to_string(),
exe: None,
cpu_pct: 10.0,
}
}
fn argv(words: &[&str]) -> Option<Vec<String>> {
Some(words.iter().map(ToString::to_string).collect())
}
#[test]
fn a_group_is_attributed_to_its_owner() {
let rows = vec![
row(1, None, "launchd"),
row(50, Some(1), "tmux"),
row(60, Some(50), "zsh"),
row(200, Some(60), "cargo"),
row(300, Some(200), "rustc"),
row(301, Some(200), "clippy-driver"),
];
let groups = build_groups(&rows);
let details = describe(
&groups,
&rows,
&|pid| match pid {
200 => argv(&["/x/cargo", "clippy", "-p", "tm", "--token", "s3cr3t"]),
_ => None,
},
&|_| Some(200),
);
assert_eq!(details.len(), 1);
let line = details[0].render();
assert!(
line.starts_with("pgid 200 | leader `cargo clippy -p tm`"),
"{line}"
);
assert!(
line.contains("driver cargo (pid 200) `cargo clippy -p tm`"),
"{line}"
);
assert!(
line.contains("chain cargo(200) < zsh(60) < tmux(50)"),
"{line}"
);
assert!(
!line.contains("launchd"),
"stops at the first owner: {line}"
);
assert!(!line.contains("s3cr3t"), "{line}");
}
#[test]
fn a_non_driver_argv_is_shown_by_basename_only() {
let rows = vec![row(10, None, "bash"), row(11, Some(10), "rustc")];
let groups = build_groups(&rows);
let details = describe(
&groups,
&rows,
&|pid| match pid {
10 => argv(&["/bin/bash", "-c", "mysql -phunter2 && cargo build"]),
11 => argv(&["rustc", "--crate-name", "secret", "-Cpasswd=hunter2"]),
_ => None,
},
&|_| Some(10),
);
let line = details[0].render();
assert!(line.starts_with("pgid 10 | leader `bash`"), "{line}");
assert!(line.contains("driver rustc (pid 11) `rustc`"), "{line}");
assert!(!line.contains("hunter2"), "{line}");
assert!(!line.contains("secret"), "{line}");
let long: Vec<String> = std::iter::once("cargo".to_string())
.chain((0..40).map(|i| format!("-p crate-number-{i}")))
.collect();
assert!(redact(&long).chars().count() <= SUMMARY_MAX_CHARS);
}
#[test]
fn live_breakdown_reads_this_host() {
assert!(live_breakdown(&[]).is_ok());
assert_eq!(pgid_of(u32::MAX), None, "no such process");
assert!(pgid_of(std::process::id()).is_some());
assert!(
header(2, 1, 4)
.ends_with("2 build group(s) without a lease, 1 lease(s) held, ceiling 4")
);
}
}