use std::sync::Arc;
#[cfg(any(target_os = "macos", test))]
use std::collections::HashMap;
pub struct Overlay {
#[cfg(target_os = "macos")]
ps: Ps,
#[cfg(target_os = "macos")]
rx: std::sync::mpsc::Receiver<Ps>,
}
#[cfg(target_os = "macos")]
impl Overlay {
const EVERY: std::time::Duration = std::time::Duration::from_secs(5);
pub fn new() -> Self {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
loop {
std::thread::sleep(Self::EVERY);
if tx.send(ps()).is_err() {
break;
}
}
});
Overlay { ps: ps(), rx }
}
pub fn refresh(&mut self) {
if let Some(ps) = self.rx.try_iter().last() {
self.ps = ps;
}
}
pub fn fill(
&self,
pid: u32,
name: &str,
start: u64,
user: &mut Option<Arc<str>>,
mem: &mut u64,
command: &mut String,
) {
let Some(e) = self.ps.get(&pid).filter(|e| e.is(name, start)) else {
return;
};
if user.is_none() {
*user = Some(e.user.clone());
}
if *mem == 0 {
*mem = e.rss;
}
if command.is_empty() {
command.clone_from(&e.command);
}
}
}
#[cfg(not(target_os = "macos"))]
impl Overlay {
pub fn new() -> Self {
Overlay {}
}
pub fn refresh(&mut self) {}
pub fn fill(
&self,
_: u32,
_: &str,
_: u64,
_: &mut Option<Arc<str>>,
_: &mut u64,
_: &mut String,
) {
}
}
#[cfg(any(target_os = "macos", test))]
#[derive(Debug, PartialEq)]
struct PsEntry {
user: Arc<str>,
rss: u64,
command: String,
start: u64,
}
#[cfg(any(target_os = "macos", test))]
impl PsEntry {
fn is(&self, name: &str, start: u64) -> bool {
if start != 0 {
return self.start.abs_diff(start) <= 2;
}
let program = self.command.split_whitespace().next().unwrap_or("");
let program = program.rsplit('/').next().unwrap_or(program);
!name.is_empty() && (program.starts_with(name) || name.starts_with(program))
}
}
#[cfg(any(target_os = "macos", test))]
type Ps = HashMap<u32, PsEntry>;
#[cfg(target_os = "macos")]
fn ps() -> Ps {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
std::process::Command::new("ps")
.args(["-axo", "pid=,etime=,rss=,user=,args="])
.output()
.map(|o| parse(&String::from_utf8_lossy(&o.stdout), now))
.unwrap_or_default()
}
#[cfg(any(target_os = "macos", test))]
fn parse(out: &str, now: u64) -> Ps {
out.lines()
.filter_map(|l| {
let mut f = l.split_whitespace();
let pid = f.next()?.parse().ok()?;
let start = now.checked_sub(elapsed(f.next()?)?)?;
let rss: u64 = f.next()?.parse().ok()?;
let user = f.next()?.into();
let command = f.collect::<Vec<_>>().join(" ");
let rss = rss * 1024;
Some((
pid,
PsEntry {
user,
rss,
command,
start,
},
))
})
.collect()
}
#[cfg(any(target_os = "macos", test))]
fn elapsed(etime: &str) -> Option<u64> {
let (days, rest) = match etime.split_once('-') {
Some((d, r)) => (d.parse::<u64>().ok()?, r),
None => (0, etime),
};
let mut secs = 0;
for part in rest.split(':') {
secs = secs * 60 + part.parse::<u64>().ok()?;
}
Some(days * 86400 + secs)
}
#[cfg(test)]
mod tests {
use super::{PsEntry, elapsed, parse};
fn entry(command: &str, start: u64) -> PsEntry {
PsEntry {
user: "root".into(),
rss: 0,
command: command.into(),
start,
}
}
#[test]
fn a_known_start_must_match() {
let e = entry("/usr/sbin/distnoted agent", 1000);
assert!(e.is("distnoted", 1001));
assert!(!e.is("distnoted", 1100));
}
#[test]
fn an_unknown_start_matches_by_name() {
let e = entry("/usr/sbin/distnoted agent", 1000);
assert!(e.is("distnoted", 0));
assert!(!e.is("launchd", 0));
assert!(!e.is("", 0));
let helper = entry(
"/Applications/Claude.app/Contents/Frameworks/Claude Helper (Renderer).app/x",
1,
);
assert!(helper.is("Claude Helper (R", 0));
}
#[test]
fn reads_ps() {
let ps = parse(
" 488 2-03:04:05 1696 root /usr/sbin/distnoted agent\n 1164 00:07 1584 _rmd /usr/libexec/rmd\nbad line\n",
1_000_000,
);
let e = &ps[&488];
assert_eq!(
(&*e.user, e.rss, e.command.as_str()),
("root", 1696 * 1024, "/usr/sbin/distnoted agent")
);
assert_eq!(e.start, 1_000_000 - (2 * 86400 + 3 * 3600 + 4 * 60 + 5));
assert_eq!(&*ps[&1164].user, "_rmd");
assert_eq!(ps[&1164].start, 1_000_000 - 7);
assert_eq!(ps.len(), 2);
}
#[test]
fn reads_elapsed_times() {
assert_eq!(elapsed("00:07"), Some(7));
assert_eq!(elapsed("01:02:03"), Some(3723));
assert_eq!(elapsed("3-00:00:01"), Some(3 * 86400 + 1));
assert_eq!(elapsed("x"), None);
}
}