1use crate::cmd::run_cmd;
22use serde::{Deserialize, Serialize};
23use std::sync::Mutex;
24
25static PROCESS_CACHE: Mutex<Option<(ProcessSnapshot, std::time::Instant)>> = Mutex::new(None);
26const CACHE_TTL_SECS: u64 = 30;
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ProcessSnapshot {
33 pub timestamp: u64,
35 pub processes: Vec<ProcessInfo>,
37 pub summary: ProcessSummary,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ProcessInfo {
46 pub pid: u32,
48 pub ppid: u32,
50 pub user: String,
52 pub cpu_percent: f32,
54 pub mem_percent: f32,
56 pub vsz: u64,
58 pub rss: u64,
60 pub stat: String,
62 pub start_time: String,
64 pub elapsed: String,
66 pub command: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ProcessSummary {
73 pub total_processes: usize,
75 pub total_cpu_percent: f32,
77 pub total_mem_percent: f32,
79 pub top_cpu_consumer: Option<String>,
81 pub top_mem_consumer: Option<String>,
83 pub zombie_count: usize,
85}
86
87impl ProcessSnapshot {
88 pub fn capture() -> Self {
93 let now = std::time::Instant::now();
94 {
95 let cache = PROCESS_CACHE.lock().unwrap_or_else(|e| e.into_inner());
97 if let Some((cached, instant)) = cache.as_ref() {
98 if now.duration_since(*instant).as_secs() < CACHE_TTL_SECS {
99 return cached.clone();
100 }
101 }
102 }
103
104 let timestamp = std::time::SystemTime::now()
105 .duration_since(std::time::UNIX_EPOCH)
106 .map(|d| d.as_secs())
107 .unwrap_or(0);
108
109 let mut processes = Vec::new();
110 let ps_output =
113 run_cmd("ps -eo pid,ppid,user,%cpu,%mem,vsz,rss,stat,start,time,comm --no-headers");
114
115 for line in ps_output.lines() {
116 if let Some(proc) = parse_ps_line(line) {
117 processes.push(proc);
118 }
119 }
120
121 let summary = ProcessSummary::compute(&processes);
122
123 let snapshot = Self {
124 timestamp,
125 processes,
126 summary,
127 };
128
129 let mut cache = PROCESS_CACHE.lock().unwrap_or_else(|e| e.into_inner());
131 *cache = Some((snapshot.clone(), now));
132 snapshot
133 }
134
135 pub fn clear_cache() {
139 let mut cache = PROCESS_CACHE.lock().unwrap_or_else(|e| e.into_inner());
140 *cache = None;
141 }
142
143 pub fn top_by_cpu(&self, n: usize) -> Vec<&ProcessInfo> {
145 let mut procs: Vec<_> = self.processes.iter().collect();
146 procs.sort_by(|a, b| {
147 b.cpu_percent
148 .partial_cmp(&a.cpu_percent)
149 .unwrap_or(std::cmp::Ordering::Equal)
150 });
151 procs.into_iter().take(n).collect()
152 }
153
154 pub fn top_by_mem(&self, n: usize) -> Vec<&ProcessInfo> {
156 let mut procs: Vec<_> = self.processes.iter().collect();
157 procs.sort_by(|a, b| {
158 b.mem_percent
159 .partial_cmp(&a.mem_percent)
160 .unwrap_or(std::cmp::Ordering::Equal)
161 });
162 procs.into_iter().take(n).collect()
163 }
164
165 pub fn print_report(&self) {
167 println!("\n{}", "=".repeat(80));
168 println!(" PROCESS SNAPSHOT [{}]", self.timestamp);
169 println!("{}", "=".repeat(80));
170
171 println!("\n--- SUMMARY ---");
172 println!(" Total Processes: {}", self.summary.total_processes);
173 println!(" Total CPU: {:.1}%", self.summary.total_cpu_percent);
174 println!(" Total Memory: {:.1}%", self.summary.total_mem_percent);
175 println!(" Zombies: {}", self.summary.zombie_count);
176
177 if let Some(ref top_cpu) = self.summary.top_cpu_consumer {
178 println!(
179 " Top CPU: {} ({:.1}%)",
180 top_cpu,
181 self.processes
182 .iter()
183 .find(|p| p.command == *top_cpu)
184 .map(|p| p.cpu_percent)
185 .unwrap_or(0.0)
186 );
187 }
188
189 if let Some(ref top_mem) = self.summary.top_mem_consumer {
190 println!(
191 " Top Memory: {} ({:.1}%)",
192 top_mem,
193 self.processes
194 .iter()
195 .find(|p| p.command == *top_mem)
196 .map(|p| p.mem_percent)
197 .unwrap_or(0.0)
198 );
199 }
200
201 println!("\n--- TOP 10 BY CPU ---");
202 for (i, proc) in self.top_by_cpu(10).iter().enumerate() {
203 println!(
204 "{:2}. {:6} {:6} {:5.1} {:5.1} {:8} {:8} {:?} {}",
205 i + 1,
206 proc.pid,
207 proc.user,
208 proc.cpu_percent,
209 proc.mem_percent,
210 format_size(proc.vsz),
211 format_size(proc.rss),
212 proc.stat,
213 truncate(&proc.command, 50)
214 );
215 }
216
217 println!("\n--- TOP 10 BY MEMORY ---");
218 for (i, proc) in self.top_by_mem(10).iter().enumerate() {
219 println!(
220 "{:2}. {:6} {:6} {:5.1} {:5.1} {:8} {:8} {:?} {}",
221 i + 1,
222 proc.pid,
223 proc.user,
224 proc.cpu_percent,
225 proc.mem_percent,
226 format_size(proc.vsz),
227 format_size(proc.rss),
228 proc.stat,
229 truncate(&proc.command, 50)
230 );
231 }
232
233 println!("\n{}", "=".repeat(80));
234 }
235}
236
237fn parse_ps_line(line: &str) -> Option<ProcessInfo> {
242 let parts: Vec<&str> = line.split_whitespace().collect();
243 if parts.len() < 10 {
244 return None;
245 }
246
247 let pid = parts[0].parse().ok()?;
248 let ppid = parts[1].parse().ok()?;
249 let user = parts[2].to_string();
250 let cpu_percent = parts[3].parse().unwrap_or(0.0);
251 let mem_percent = parts[4].parse().unwrap_or(0.0);
252 let vsz: u64 = parts[5].parse().unwrap_or(0);
253 let rss: u64 = parts[6].parse().unwrap_or(0);
254 let stat = parts[7].to_string();
255 let start_time = parts[8].to_string();
256 let elapsed = parts[9].to_string();
257 let command = parts.get(10..).map(|s| s.join(" ")).unwrap_or_default();
258
259 Some(ProcessInfo {
260 pid,
261 ppid,
262 user,
263 cpu_percent,
264 mem_percent,
265 vsz: vsz * 1024,
266 rss: rss * 1024,
267 stat,
268 start_time,
269 elapsed,
270 command,
271 })
272}
273
274impl ProcessSummary {
275 fn compute(processes: &[ProcessInfo]) -> Self {
276 let total_processes = processes.len();
277 let total_cpu_percent: f32 = processes.iter().map(|p| p.cpu_percent).sum();
278 let total_mem_percent: f32 = processes.iter().map(|p| p.mem_percent).sum();
279
280 let top_cpu_consumer = processes
281 .iter()
282 .max_by(|a, b| {
283 a.cpu_percent
284 .partial_cmp(&b.cpu_percent)
285 .unwrap_or(std::cmp::Ordering::Equal)
286 })
287 .map(|p| p.command.clone());
288
289 let top_mem_consumer = processes
290 .iter()
291 .max_by(|a, b| {
292 a.mem_percent
293 .partial_cmp(&b.mem_percent)
294 .unwrap_or(std::cmp::Ordering::Equal)
295 })
296 .map(|p| p.command.clone());
297
298 let zombie_count = processes.iter().filter(|p| p.stat.starts_with('Z')).count();
299
300 Self {
301 total_processes,
302 total_cpu_percent,
303 total_mem_percent,
304 top_cpu_consumer,
305 top_mem_consumer,
306 zombie_count,
307 }
308 }
309}
310
311fn format_size(kb: u64) -> String {
313 if kb >= 1024 * 1024 {
314 format!("{:.1}G", kb as f64 / (1024.0 * 1024.0))
315 } else if kb >= 1024 {
316 format!("{:.1}M", kb as f64 / 1024.0)
317 } else {
318 format!("{}K", kb / 1024)
319 }
320}
321
322fn truncate(s: &str, max_len: usize) -> String {
327 if s.chars().count() > max_len {
328 let end = max_len.saturating_sub(3);
329 let byte_end = s.char_indices().nth(end).map(|(i, _)| i).unwrap_or(s.len());
330 format!("{}...", &s[..byte_end])
331 } else {
332 s.to_string()
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 #[test]
340 fn test_process_snapshot() {
341 let snapshot = ProcessSnapshot::capture();
342 assert!(!snapshot.processes.is_empty());
343 assert!(snapshot.summary.total_processes > 0);
344 }
345
346 #[test]
347 fn test_truncate_ascii() {
348 assert_eq!(truncate("hello world", 8), "hello...");
349 assert_eq!(truncate("short", 10), "short");
350 }
351
352 #[test]
353 fn test_truncate_multibyte_utf8() {
354 let cjk = "你好世界这是一个很长的命令行参数"; let result = truncate(cjk, 8);
357 assert!(result.ends_with("..."));
358 assert!(result.is_char_boundary(result.len()));
360 }
361}