@@ -0,0 +1,174 @@ |
| 1 | +//! Process information collection from /proc/[pid]/ |
| 2 | + |
| 3 | +use crate::error::{Error, Result}; |
| 4 | +use gartop_ipc::{ProcessInfo, SortField}; |
| 5 | +use procfs::process::{all_processes, Process}; |
| 6 | +use procfs::{Current, CurrentSI}; |
| 7 | +use std::collections::HashMap; |
| 8 | + |
| 9 | +/// Process collector with previous CPU times for percentage calculation. |
| 10 | +pub struct ProcessCollector { |
| 11 | + /// Previous CPU times per PID for delta calculation. |
| 12 | + prev_times: HashMap<i32, (u64, u64)>, // (utime + stime, total_system_time) |
| 13 | + /// Total memory for percentage calculation. |
| 14 | + total_memory: u64, |
| 15 | +} |
| 16 | + |
| 17 | +impl ProcessCollector { |
| 18 | + /// Create a new process collector. |
| 19 | + pub fn new() -> Result<Self> { |
| 20 | + let meminfo = procfs::Meminfo::current()?; |
| 21 | + |
| 22 | + Ok(Self { |
| 23 | + prev_times: HashMap::new(), |
| 24 | + total_memory: meminfo.mem_total, |
| 25 | + }) |
| 26 | + } |
| 27 | + |
| 28 | + /// Collect all running processes. |
| 29 | + pub fn collect( |
| 30 | + &mut self, |
| 31 | + sort_by: SortField, |
| 32 | + limit: Option<usize>, |
| 33 | + ) -> Result<Vec<ProcessInfo>> { |
| 34 | + let kernel_stats = procfs::KernelStats::current()?; |
| 35 | + let total = &kernel_stats.total; |
| 36 | + let system_total = total.user |
| 37 | + + total.nice |
| 38 | + + total.system |
| 39 | + + total.idle |
| 40 | + + total.iowait.unwrap_or(0) |
| 41 | + + total.irq.unwrap_or(0) |
| 42 | + + total.softirq.unwrap_or(0) |
| 43 | + + total.steal.unwrap_or(0); |
| 44 | + |
| 45 | + let mut processes = Vec::new(); |
| 46 | + let mut current_pids = Vec::new(); |
| 47 | + |
| 48 | + for proc_result in all_processes()? { |
| 49 | + let proc = match proc_result { |
| 50 | + Ok(p) => p, |
| 51 | + Err(_) => continue, |
| 52 | + }; |
| 53 | + |
| 54 | + let pid = proc.pid(); |
| 55 | + current_pids.push(pid); |
| 56 | + |
| 57 | + match self.process_info(&proc, system_total) { |
| 58 | + Ok(info) => processes.push(info), |
| 59 | + Err(_) => continue, // Skip processes we can't read |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + // Clean up old entries |
| 64 | + self.prev_times.retain(|pid, _| current_pids.contains(pid)); |
| 65 | + |
| 66 | + // Sort |
| 67 | + match sort_by { |
| 68 | + SortField::Cpu => processes.sort_by(|a, b| { |
| 69 | + b.cpu_percent |
| 70 | + .partial_cmp(&a.cpu_percent) |
| 71 | + .unwrap_or(std::cmp::Ordering::Equal) |
| 72 | + }), |
| 73 | + SortField::Memory => processes.sort_by(|a, b| { |
| 74 | + b.memory_percent |
| 75 | + .partial_cmp(&a.memory_percent) |
| 76 | + .unwrap_or(std::cmp::Ordering::Equal) |
| 77 | + }), |
| 78 | + SortField::Pid => processes.sort_by_key(|p| p.pid), |
| 79 | + SortField::Name => processes.sort_by(|a, b| a.name.cmp(&b.name)), |
| 80 | + } |
| 81 | + |
| 82 | + // Limit |
| 83 | + if let Some(n) = limit { |
| 84 | + processes.truncate(n); |
| 85 | + } |
| 86 | + |
| 87 | + Ok(processes) |
| 88 | + } |
| 89 | + |
| 90 | + /// Get info for a single process. |
| 91 | + fn process_info(&mut self, proc: &Process, system_total: u64) -> Result<ProcessInfo> { |
| 92 | + let stat = proc.stat()?; |
| 93 | + let status = proc.status()?; |
| 94 | + |
| 95 | + let pid = proc.pid(); |
| 96 | + let name = stat.comm.clone(); |
| 97 | + let cmdline = proc |
| 98 | + .cmdline() |
| 99 | + .map(|v| v.join(" ")) |
| 100 | + .unwrap_or_else(|_| name.clone()); |
| 101 | + |
| 102 | + // Calculate CPU percentage |
| 103 | + let proc_time = stat.utime + stat.stime; |
| 104 | + let cpu_percent = |
| 105 | + if let Some((prev_proc_time, prev_sys_time)) = self.prev_times.get(&pid) { |
| 106 | + let proc_delta = proc_time.saturating_sub(*prev_proc_time); |
| 107 | + let sys_delta = system_total.saturating_sub(*prev_sys_time); |
| 108 | + if sys_delta > 0 { |
| 109 | + (proc_delta as f64 / sys_delta as f64) * 100.0 |
| 110 | + } else { |
| 111 | + 0.0 |
| 112 | + } |
| 113 | + } else { |
| 114 | + 0.0 |
| 115 | + }; |
| 116 | + self.prev_times.insert(pid, (proc_time, system_total)); |
| 117 | + |
| 118 | + // Memory |
| 119 | + let rss = stat.rss as u64 * 4096; // Pages to bytes |
| 120 | + let vsize = stat.vsize; |
| 121 | + let memory_percent = if self.total_memory > 0 { |
| 122 | + (rss as f64 / self.total_memory as f64) * 100.0 |
| 123 | + } else { |
| 124 | + 0.0 |
| 125 | + }; |
| 126 | + |
| 127 | + // State |
| 128 | + let state = match stat.state { |
| 129 | + 'R' => "running", |
| 130 | + 'S' => "sleeping", |
| 131 | + 'D' => "disk sleep", |
| 132 | + 'Z' => "zombie", |
| 133 | + 'T' => "stopped", |
| 134 | + 't' => "tracing stop", |
| 135 | + 'X' | 'x' => "dead", |
| 136 | + _ => "unknown", |
| 137 | + } |
| 138 | + .to_string(); |
| 139 | + |
| 140 | + // User |
| 141 | + let user = users::get_user_by_uid(status.ruid) |
| 142 | + .map(|u| u.name().to_string_lossy().to_string()) |
| 143 | + .unwrap_or_else(|| status.ruid.to_string()); |
| 144 | + |
| 145 | + Ok(ProcessInfo { |
| 146 | + pid, |
| 147 | + name, |
| 148 | + cmdline, |
| 149 | + cpu_percent, |
| 150 | + memory_percent, |
| 151 | + rss, |
| 152 | + vsize, |
| 153 | + state, |
| 154 | + user, |
| 155 | + }) |
| 156 | + } |
| 157 | + |
| 158 | + /// Kill a process by PID. |
| 159 | + pub fn kill(&self, pid: i32, signal: i32) -> Result<()> { |
| 160 | + use nix::sys::signal::{kill, Signal}; |
| 161 | + use nix::unistd::Pid; |
| 162 | + |
| 163 | + let sig = Signal::try_from(signal) |
| 164 | + .map_err(|_| Error::Ipc(format!("Invalid signal: {}", signal)))?; |
| 165 | + |
| 166 | + kill(Pid::from_raw(pid), sig).map_err(|e| match e { |
| 167 | + nix::errno::Errno::ESRCH => Error::ProcessNotFound(pid), |
| 168 | + nix::errno::Errno::EPERM => { |
| 169 | + Error::PermissionDenied(format!("Cannot kill PID {}", pid)) |
| 170 | + } |
| 171 | + _ => Error::Io(std::io::Error::from_raw_os_error(e as i32)), |
| 172 | + }) |
| 173 | + } |
| 174 | +} |