use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq)]
pub struct ProcessInfo {
pub pid: u32,
pub ppid: Option<u32>,
pub name: String,
pub cmd: String,
pub cpu: f32,
pub rss: u64,
pub run_time: Option<u64>,
pub cwd: Option<String>,
pub exe: Option<String>,
pub user: Option<String>,
pub status: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Proto {
Tcp,
Udp,
}
impl Proto {
pub fn label(self) -> &'static str {
match self {
Proto::Tcp => "tcp",
Proto::Udp => "udp",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PortEntry {
pub proto: Proto,
pub addr: String,
pub port: u16,
pub pid: Option<u32>,
pub name: Option<String>,
}
impl PortEntry {
pub fn address(&self) -> String {
format!("{}:{}", self.addr, self.port)
}
pub fn sort_key(&self) -> (u16, Proto, &str) {
(self.port, self.proto, self.addr.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortKey {
#[default]
Cpu,
Mem,
Pid,
}
impl SortKey {
pub fn next(self) -> Self {
match self {
SortKey::Cpu => SortKey::Mem,
SortKey::Mem => SortKey::Pid,
SortKey::Pid => SortKey::Cpu,
}
}
pub fn label(self) -> &'static str {
match self {
SortKey::Cpu => "CPU",
SortKey::Mem => "MEM",
SortKey::Pid => "PID",
}
}
pub fn compare(self, a: &ProcessInfo, b: &ProcessInfo) -> Ordering {
match self {
SortKey::Cpu => b
.cpu
.partial_cmp(&a.cpu)
.unwrap_or(Ordering::Equal)
.then(a.pid.cmp(&b.pid)),
SortKey::Mem => b.rss.cmp(&a.rss).then(a.pid.cmp(&b.pid)),
SortKey::Pid => a.pid.cmp(&b.pid),
}
}
}
pub fn sort_processes(procs: &mut [ProcessInfo], key: SortKey) {
procs.sort_by(|a, b| key.compare(a, b));
}
pub fn format_elapsed(secs: u64) -> String {
let (d, h, m, s) = (
secs / 86_400,
(secs / 3600) % 24,
(secs / 60) % 60,
secs % 60,
);
if d > 0 {
format!("{d}d {h}h")
} else if h > 0 {
format!("{h}h {m}m")
} else if m > 0 {
format!("{m}m {s}s")
} else {
format!("{s}s")
}
}
#[cfg(test)]
pub(crate) fn proc(pid: u32, ppid: Option<u32>, cpu: f32, rss: u64) -> ProcessInfo {
ProcessInfo {
pid,
ppid,
name: format!("p{pid}"),
cmd: format!("p{pid} --arg"),
cpu,
rss,
run_time: None,
cwd: None,
exe: None,
user: None,
status: "Run".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sort_key_cycles_cpu_mem_pid() {
assert_eq!(SortKey::Cpu.next(), SortKey::Mem);
assert_eq!(SortKey::Mem.next(), SortKey::Pid);
assert_eq!(SortKey::Pid.next(), SortKey::Cpu);
assert_eq!(SortKey::default(), SortKey::Cpu);
}
#[test]
fn sort_orders_by_key_with_pid_tie_break() {
let mut v = vec![
proc(30, None, 1.0, 500),
proc(10, None, 5.0, 100),
proc(20, None, 1.0, 900),
];
sort_processes(&mut v, SortKey::Cpu);
assert_eq!(v.iter().map(|p| p.pid).collect::<Vec<_>>(), [10, 20, 30]);
sort_processes(&mut v, SortKey::Mem);
assert_eq!(v.iter().map(|p| p.pid).collect::<Vec<_>>(), [20, 30, 10]);
sort_processes(&mut v, SortKey::Pid);
assert_eq!(v.iter().map(|p| p.pid).collect::<Vec<_>>(), [10, 20, 30]);
}
#[test]
fn elapsed_formats() {
assert_eq!(format_elapsed(45), "45s");
assert_eq!(format_elapsed(125), "2m 5s");
assert_eq!(format_elapsed(3600 * 3 + 60 * 12), "3h 12m");
assert_eq!(format_elapsed(86_400 * 2 + 3600 * 4), "2d 4h");
}
#[test]
fn port_entry_address() {
let e = PortEntry {
proto: Proto::Tcp,
addr: "[::1]".into(),
port: 8080,
pid: None,
name: None,
};
assert_eq!(e.address(), "[::1]:8080");
}
}