use crate::session::{ClaudeSession, SessionStatus};
pub fn fetch_and_enrich(sessions: &mut [ClaudeSession]) {
if sessions.is_empty() {
return;
}
let pids: Vec<String> = sessions.iter().map(|s| s.pid.to_string()).collect();
let pid_arg = pids.join(",");
let output = std::process::Command::new("ps")
.args(["-o", "pid=,tty=,%cpu=,rss=,command=", "-p", &pid_arg])
.env_clear()
.output();
let output = match output {
Ok(o) => o,
Err(e) => {
crate::logger::log("ERROR", &format!("ps command failed: {e}"));
for s in sessions.iter_mut() {
s.cpu_percent = 0.0;
if pid_alive(s.pid) {
if s.status == SessionStatus::Finished {
s.status = SessionStatus::Unknown;
}
} else {
s.status = SessionStatus::Finished;
}
}
return;
}
};
if !output.status.success() && !ps_supports_query() {
crate::logger::log(
"WARN",
"ps cannot answer -o/-p (busybox?); falling back to kill(pid, 0) for liveness",
);
for s in sessions.iter_mut() {
s.cpu_percent = 0.0;
if pid_alive(s.pid) {
if s.status == SessionStatus::Finished {
s.status = SessionStatus::Unknown;
}
} else {
s.status = SessionStatus::Finished;
}
}
return;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut alive_pids = std::collections::HashSet::new();
for line in stdout.lines() {
let trimmed = line.trim();
let fields: Vec<&str> = trimmed.split_whitespace().collect();
if fields.len() < 5 {
continue;
}
let Ok(pid) = fields[0].parse::<u32>() else {
continue;
};
let tty = fields[1].to_string();
let cpu = fields[2].parse::<f32>().unwrap_or(0.0);
let rss_kb = fields[3].parse::<f64>().unwrap_or(0.0);
let mem_mb = rss_kb / 1024.0;
let command = fields[4..].join(" ");
if !command.contains("claude") {
continue;
}
alive_pids.insert(pid);
for session in sessions.iter_mut() {
if session.pid == pid {
session.tty = tty.clone();
session.mem_mb = mem_mb;
session.cpu_history.push(cpu);
if session.cpu_history.len() > 3 {
session.cpu_history.remove(0);
}
session.cpu_percent =
session.cpu_history.iter().sum::<f32>() / session.cpu_history.len() as f32;
if let Some(idx) = command.find("claude") {
let after_claude = &command[idx + 6..];
session.command_args = after_claude.trim().to_string();
}
let cmd_parts: Vec<&str> = command.split_whitespace().collect();
extract_session_meta(&cmd_parts, session);
break;
}
}
}
for session in sessions.iter_mut() {
if alive_pids.contains(&session.pid) {
if session.status == crate::session::SessionStatus::Finished {
session.status = crate::session::SessionStatus::Unknown;
}
} else {
session.status = crate::session::SessionStatus::Finished;
session.cpu_percent = 0.0;
}
}
}
fn ps_supports_query() -> bool {
std::process::Command::new("ps")
.args(["-o", "pid=", "-p", "1"])
.env_clear()
.output()
.map(|o| o.status.success() && !o.stdout.is_empty())
.unwrap_or(false)
}
pub fn pid_alive(pid: u32) -> bool {
if pid == 0 {
return false;
}
let rc = unsafe { libc::kill(pid as libc::pid_t, 0) };
if rc == 0 {
return true;
}
std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
fn extract_session_meta(cmd: &[&str], session: &mut ClaudeSession) {
let mut i = 0;
while i < cmd.len() {
match cmd[i] {
"--name" | "-n" if i + 1 < cmd.len() => {
session.session_name = cmd[i + 1].to_string();
i += 2;
continue;
}
"--resume" | "-r" if i + 1 < cmd.len() => {
let val = cmd[i + 1];
if !looks_like_uuid(val) {
session.session_name = val.to_string();
}
i += 2;
continue;
}
_ => {}
}
i += 1;
}
}
fn looks_like_uuid(s: &str) -> bool {
s.len() == 36
&& s.chars().all(|c| c.is_ascii_hexdigit() || c == '-')
&& s.matches('-').count() == 4
}