procinsh 0.1.0

A local, read-only Linux x86-64 process inspector
use super::{ProcessId, procfs};
use anyhow::Result;
use serde::Serialize;
use std::{collections::HashMap, fs, time::Instant};

#[derive(Clone, Debug, Serialize)]
pub struct ProcessSummary {
    pub identity: ProcessId,
    #[serde(skip)]
    pub parent_pid: i32,
    pub name: String,
    pub executable: Option<String>,
    pub command_line: Option<Vec<String>>,
    pub uid: Option<u32>,
    pub username: Option<String>,
    pub euid: Option<u32>,
    pub effective_username: Option<String>,
    pub state: String,
    pub cpu_percent: Option<f64>,
    pub rss_bytes: u64,
    pub thread_count: u32,
}

pub fn summary(stat: &procfs::Stat, users: &HashMap<u32, String>) -> ProcessSummary {
    let base = format!("/proc/{}", stat.pid);
    let (uid, euid) = procfs::fields(&format!("{base}/status"))
        .ok()
        .map(|f| parse_uids(f.get("Uid").map(String::as_str).unwrap_or("")))
        .unwrap_or((None, None));
    ProcessSummary {
        identity: ProcessId {
            pid: stat.pid,
            start_time_ticks: stat.start_time,
        },
        parent_pid: stat.parent_pid,
        name: stat.name.clone(),
        executable: fs::read_link(format!("{base}/exe"))
            .ok()
            .map(|p| p.to_string_lossy().into_owned()),
        command_line: fs::read(format!("{base}/cmdline")).ok().map(|b| {
            b.split(|c| *c == 0)
                .filter(|s| !s.is_empty())
                .map(|s| String::from_utf8_lossy(s).into_owned())
                .collect()
        }),
        uid,
        username: uid.and_then(|u| users.get(&u).cloned()),
        euid,
        effective_username: euid.and_then(|u| users.get(&u).cloned()),
        state: stat.state.clone(),
        cpu_percent: None,
        rss_bytes: stat.rss,
        thread_count: stat.thread_count,
    }
}

pub fn users() -> HashMap<u32, String> {
    fs::read_to_string("/etc/passwd")
        .unwrap_or_default()
        .lines()
        .filter_map(|line| {
            let p: Vec<_> = line.split(':').collect();
            Some((p.get(2)?.parse().ok()?, p.first()?.to_string()))
        })
        .collect()
}

#[derive(Default)]
pub struct Discovery {
    previous: HashMap<ProcessId, (u64, Instant)>,
}
impl Discovery {
    pub fn collect(&mut self) -> Result<Vec<ProcessSummary>> {
        let users = users();
        let now = Instant::now();
        let mut next = HashMap::new();
        let mut result = Vec::new();
        for entry in fs::read_dir("/proc")?.flatten() {
            let Some(pid) = entry
                .file_name()
                .to_str()
                .and_then(|s| s.parse::<i32>().ok())
            else {
                continue;
            };
            let Ok(stat) = procfs::read_stat(&format!("/proc/{pid}/stat")) else {
                continue;
            };
            let mut item = summary(&stat, &users);
            if let Some((ticks, time)) = self.previous.get(&item.identity) {
                let elapsed = now.duration_since(*time).as_secs_f64();
                if elapsed > 0.0 {
                    item.cpu_percent = Some(
                        stat.ticks.saturating_sub(*ticks) as f64
                            / procfs::ticks_per_second()
                            / elapsed
                            * 100.0,
                    );
                }
            }
            next.insert(item.identity, (stat.ticks, now));
            result.push(item);
        }
        self.previous = next;
        result.sort_by(|a, b| {
            b.cpu_percent
                .unwrap_or(0.0)
                .total_cmp(&a.cpu_percent.unwrap_or(0.0))
                .then(a.identity.pid.cmp(&b.identity.pid))
        });
        Ok(result)
    }
}

fn parse_uids(value: &str) -> (Option<u32>, Option<u32>) {
    let mut values = value.split_whitespace();
    (
        values.next().and_then(|v| v.parse().ok()),
        values.next().and_then(|v| v.parse().ok()),
    )
}
#[cfg(test)]
mod uid_tests {
    use super::*;
    #[test]
    fn real_and_effective_users() {
        assert_eq!(parse_uids("1000 0 0 0"), (Some(1000), Some(0)));
        assert_eq!(parse_uids("42 42 42 42"), (Some(42), Some(42)));
        assert_eq!(parse_uids(""), (None, None));
    }
}