use sysinfo::System;
#[cfg(target_os = "linux")]
pub(crate) fn ancestors(_sys: &System, max: usize) -> Vec<(u32, String)> {
ancestors_with(std::process::id(), max, |pid| {
std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()
})
}
#[cfg(not(target_os = "linux"))]
pub(crate) fn ancestors(sys: &System, max: usize) -> Vec<(u32, String)> {
let mut chain = Vec::new();
let mut current = sys.process(sysinfo::Pid::from_u32(std::process::id()));
while let Some(proc) = current {
if chain.len() == max {
break;
}
chain.push((
proc.pid().as_u32(),
proc.name().to_string_lossy().into_owned(),
));
current = proc.parent().and_then(|pid| sys.process(pid));
}
chain
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn ancestors_with(
start: u32,
max: usize,
read_stat: impl Fn(u32) -> Option<String>,
) -> Vec<(u32, String)> {
let mut chain: Vec<(u32, String)> = Vec::new();
let mut pid = start;
while chain.len() < max && pid != 0 {
let Some((name, ppid)) = read_stat(pid).as_deref().and_then(parse_stat) else {
break;
};
chain.push((pid, name));
if ppid == pid || chain.iter().any(|(seen, _)| *seen == ppid) {
break;
}
pid = ppid;
}
chain
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn parse_stat(stat: &str) -> Option<(String, u32)> {
let open = stat.find('(')?;
let close = stat.rfind(')')?;
if close < open {
return None;
}
let comm = stat[open + 1..close].to_string();
let mut rest = stat[close + 1..].split_ascii_whitespace();
let _state = rest.next()?;
let ppid = rest.next()?.parse().ok()?;
Some((comm, ppid))
}
#[cfg(target_os = "linux")]
pub(crate) fn task_count() -> usize {
std::fs::read_to_string("/proc/loadavg")
.ok()
.as_deref()
.and_then(parse_loadavg_tasks)
.unwrap_or_else(|| pids().count())
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn parse_loadavg_tasks(loadavg: &str) -> Option<usize> {
let field = loadavg.split_ascii_whitespace().nth(3)?;
field.split_once('/')?.1.parse().ok()
}
#[cfg(target_os = "linux")]
pub(crate) fn audio_server() -> &'static str {
let runtime = std::env::var_os("XDG_RUNTIME_DIR").map(std::path::PathBuf::from);
audio_server_with(runtime.as_deref(), || {
audio_server_from_names(
pids().filter_map(|pid| std::fs::read_to_string(format!("/proc/{pid}/comm")).ok()),
)
})
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn audio_server_with(
runtime_dir: Option<&std::path::Path>,
scan: impl FnOnce() -> &'static str,
) -> &'static str {
let socket = |rel: &str| runtime_dir.is_some_and(|d| is_unix_socket(&d.join(rel)));
server_from_sockets(socket("pipewire-0"), socket("pulse/native")).unwrap_or_else(scan)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn server_from_sockets(pipewire: bool, pulse: bool) -> Option<&'static str> {
if pipewire {
Some("PipeWire")
} else if pulse {
Some("PulseAudio")
} else {
None
}
}
#[cfg(unix)]
fn is_unix_socket(path: &std::path::Path) -> bool {
use std::os::unix::fs::FileTypeExt;
std::fs::metadata(path).is_ok_and(|m| m.file_type().is_socket())
}
#[cfg(not(unix))]
fn is_unix_socket(_path: &std::path::Path) -> bool {
false
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn audio_server_from_names(names: impl IntoIterator<Item = String>) -> &'static str {
let mut server = "ALSA";
for name in names {
let name = name.to_lowercase();
if name.contains("pipewire") {
return "PipeWire";
}
if name.contains("pulseaudio") {
server = "PulseAudio";
}
}
server
}
#[cfg(target_os = "linux")]
fn pids() -> impl Iterator<Item = u32> {
std::fs::read_dir("/proc")
.into_iter()
.flatten()
.filter_map(|e| e.ok()?.file_name().to_str()?.parse().ok())
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn stat(pid: u32, comm: &str, ppid: u32) -> String {
format!("{pid} ({comm}) S {ppid} {pid} {pid} 0 -1 4194560 1234 0 0 0")
}
#[test]
fn parse_stat_reads_comm_and_ppid() {
assert_eq!(
parse_stat(&stat(4242, "zsh", 4100)),
Some(("zsh".to_string(), 4100))
);
}
#[test]
fn parse_stat_takes_comm_to_the_last_paren() {
assert_eq!(
parse_stat(&stat(7, "evil) S 1 (x", 99)),
Some(("evil) S 1 (x".to_string(), 99))
);
assert_eq!(
parse_stat(&stat(8, "Web Content", 5)),
Some(("Web Content".to_string(), 5))
);
}
#[test]
fn parse_stat_rejects_malformed_input() {
assert_eq!(parse_stat(""), None);
assert_eq!(parse_stat("12 zsh S 1"), None, "no parentheses");
assert_eq!(parse_stat("12 (zsh)"), None, "nothing after comm");
assert_eq!(parse_stat("12 (zsh) S notanumber"), None);
}
fn table(entries: &[(u32, &str, u32)]) -> HashMap<u32, String> {
entries
.iter()
.map(|&(pid, comm, ppid)| (pid, stat(pid, comm, ppid)))
.collect()
}
#[test]
fn ancestors_walks_nearest_first_up_to_init() {
let t = table(&[
(500, "retch", 400),
(400, "zsh", 300),
(300, "rio", 1),
(1, "systemd", 0),
]);
let chain = ancestors_with(500, 64, |pid| t.get(&pid).cloned());
let names: Vec<&str> = chain.iter().map(|(_, n)| n.as_str()).collect();
assert_eq!(names, ["retch", "zsh", "rio", "systemd"]);
assert_eq!(chain[1].0, 400, "pids travel with their names");
}
#[test]
fn ancestors_respects_max() {
let t = table(&[(5, "a", 4), (4, "b", 3), (3, "c", 2), (2, "d", 1)]);
let chain = ancestors_with(5, 2, |pid| t.get(&pid).cloned());
assert_eq!(chain.len(), 2);
assert_eq!(chain[1].1, "b");
}
#[test]
fn ancestors_stops_where_a_process_cannot_be_read() {
let t = table(&[(5, "retch", 4)]);
let chain = ancestors_with(5, 64, |pid| t.get(&pid).cloned());
assert_eq!(chain, vec![(5, "retch".to_string())]);
}
#[test]
fn ancestors_cannot_loop() {
let self_parent = table(&[(5, "odd", 5)]);
assert_eq!(
ancestors_with(5, 64, |pid| self_parent.get(&pid).cloned()).len(),
1
);
let cycle = table(&[(5, "a", 6), (6, "b", 5)]);
assert_eq!(
ancestors_with(5, 64, |pid| cycle.get(&pid).cloned()).len(),
2
);
}
#[test]
fn loadavg_total_is_the_fourth_fields_denominator() {
assert_eq!(
parse_loadavg_tasks("0.52 0.58 0.59 3/2109 123456\n"),
Some(2109)
);
assert_eq!(parse_loadavg_tasks("0.52 0.58 0.59"), None);
assert_eq!(parse_loadavg_tasks("0.52 0.58 0.59 2109 1"), None);
}
fn names(list: &[&str]) -> Vec<String> {
list.iter().map(|s| format!("{s}\n")).collect()
}
#[test]
fn audio_server_prefers_pipewire_wherever_it_appears() {
assert_eq!(
audio_server_from_names(names(&["systemd", "pulseaudio", "pipewire"])),
"PipeWire"
);
assert_eq!(
audio_server_from_names(names(&["pipewire-pulse", "pulseaudio"])),
"PipeWire"
);
}
#[test]
fn audio_server_falls_back_to_pulseaudio_then_alsa() {
assert_eq!(
audio_server_from_names(names(&["bash", "pulseaudio"])),
"PulseAudio"
);
assert_eq!(audio_server_from_names(names(&["bash", "sshd"])), "ALSA");
assert_eq!(audio_server_from_names(Vec::new()), "ALSA");
}
#[test]
fn server_from_sockets_prefers_pipewire() {
assert_eq!(server_from_sockets(true, true), Some("PipeWire"));
assert_eq!(server_from_sockets(true, false), Some("PipeWire"));
assert_eq!(server_from_sockets(false, true), Some("PulseAudio"));
assert_eq!(server_from_sockets(false, false), None);
}
#[cfg(unix)]
fn runtime_with(name: &str, sockets: &[&str]) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("retch-rt-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("pulse")).unwrap();
for s in sockets {
std::mem::forget(std::os::unix::net::UnixListener::bind(dir.join(s)).unwrap());
}
dir
}
#[cfg(unix)]
#[test]
fn session_sockets_answer_without_scanning() {
let never = || -> &'static str { panic!("the process scan must not run") };
let both = runtime_with("both", &["pipewire-0", "pulse/native"]);
assert_eq!(audio_server_with(Some(&both), never), "PipeWire");
let pulse = runtime_with("pulse", &["pulse/native"]);
assert_eq!(audio_server_with(Some(&pulse), never), "PulseAudio");
let _ = std::fs::remove_dir_all(&both);
let _ = std::fs::remove_dir_all(&pulse);
}
#[cfg(unix)]
#[test]
fn no_session_sockets_falls_back_to_the_scan() {
assert_eq!(audio_server_with(None, || "PipeWire"), "PipeWire");
let empty = runtime_with("empty", &[]);
assert_eq!(audio_server_with(Some(&empty), || "ALSA"), "ALSA");
std::fs::write(empty.join("pipewire-0"), b"").unwrap();
assert_eq!(audio_server_with(Some(&empty), || "ALSA"), "ALSA");
let _ = std::fs::remove_dir_all(&empty);
}
#[test]
fn audio_server_matches_case_insensitively() {
assert_eq!(audio_server_from_names(names(&["PipeWire"])), "PipeWire");
}
#[cfg(target_os = "linux")]
#[test]
fn live_ancestors_start_at_this_process() {
let sys = System::new();
let chain = ancestors(&sys, 64);
assert_eq!(chain.first().map(|(pid, _)| *pid), Some(std::process::id()));
assert!(chain.len() >= 2, "the test runner has a parent: {chain:?}");
}
#[cfg(target_os = "linux")]
#[test]
fn live_loadavg_parses_and_counts_at_least_every_process() {
let raw = std::fs::read_to_string("/proc/loadavg").expect("/proc/loadavg");
let tasks = parse_loadavg_tasks(&raw).expect("loadavg must parse on Linux");
let processes = pids().count();
assert!(
tasks + 16 >= processes,
"{tasks} tasks < {processes} processes"
);
}
}