1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::panic::{catch_unwind, AssertUnwindSafe};
5use std::process::Command;
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SafeSystemInfo {
10 pub hostname: String,
11 pub os_name: String,
12 pub os_version: String,
13 pub kernel_version: String,
14 pub architecture: String,
15 pub cpu_model: String,
16 pub cpu_cores: u32,
17 pub cpu_threads: u32,
18 pub total_memory: u64,
19 pub boot_time: SystemTime,
20 pub uptime: Duration,
21}
22
23#[derive(Debug, Clone)]
24pub struct SafeSystemStats {
25 pub cpu_usage_percent: f64,
26 pub memory_usage_percent: f64,
27 pub memory_used: u64,
28 pub memory_available: u64,
29 pub load_average: (f64, f64, f64),
30 pub disk_usage: HashMap<String, SafeDiskUsage>,
31 pub top_processes: Vec<SafeProcessInfo>,
32 pub timestamp: SystemTime,
33 pub errors: Vec<String>,
34}
35
36#[derive(Debug, Clone)]
37pub struct SafeDiskUsage {
38 pub total: u64,
39 pub used: u64,
40 pub available: u64,
41 pub usage_percent: f64,
42 pub filesystem: String,
43}
44
45#[derive(Debug, Clone)]
46pub struct SafeProcessInfo {
47 pub pid: u32,
48 pub name: String,
49 pub cpu_percent: f64,
50 pub memory_percent: f64,
51 pub memory_rss: u64,
52 pub memory_vms: u64,
53 pub command: String,
54 pub user: String,
55 pub state: String,
56}
57
58pub struct SafeSystemMonitor {
59 last_cpu_stats: Option<SafeCpuStats>,
60 last_update: SystemTime,
61 system_info: Option<SafeSystemInfo>,
62 errors: Vec<String>,
63}
64
65#[derive(Debug, Clone)]
66struct SafeCpuStats {
67 user: u64,
68 nice: u64,
69 system: u64,
70 idle: u64,
71 iowait: u64,
72 irq: u64,
73 softirq: u64,
74 steal: u64,
75}
76
77impl SafeSystemMonitor {
78 pub fn new() -> Self {
79 let mut monitor = Self {
80 last_cpu_stats: None,
81 last_update: SystemTime::now(),
82 system_info: None,
83 errors: Vec::new(),
84 };
85
86 match catch_unwind(AssertUnwindSafe(Self::collect_system_info_safe)) {
88 Ok(Ok(info)) => monitor.system_info = Some(info),
89 Ok(Err(e)) => monitor.errors.push(format!("System info error: {e}")),
90 Err(_) => monitor
91 .errors
92 .push("System info collection panicked".to_string()),
93 }
94
95 monitor
96 }
97
98 pub fn get_system_info(&self) -> Option<&SafeSystemInfo> {
99 self.system_info.as_ref()
100 }
101
102 pub fn get_current_stats(&mut self) -> SafeSystemStats {
103 let now = SystemTime::now();
104 let mut errors = Vec::new();
105
106 let cpu_usage = match catch_unwind(AssertUnwindSafe(|| self.calculate_cpu_usage_safe())) {
108 Ok(Ok(usage)) => usage,
109 Ok(Err(e)) => {
110 errors.push(format!("CPU usage error: {e}"));
111 0.0
112 }
113 Err(_) => {
114 errors.push("CPU usage calculation panicked".to_string());
115 0.0
116 }
117 };
118
119 let (memory_usage_percent, memory_used, memory_available) =
121 match catch_unwind(AssertUnwindSafe(|| self.get_memory_stats_safe())) {
122 Ok(Ok(stats)) => stats,
123 Ok(Err(e)) => {
124 errors.push(format!("Memory stats error: {e}"));
125 (0.0, 0, 0)
126 }
127 Err(_) => {
128 errors.push("Memory stats calculation panicked".to_string());
129 (0.0, 0, 0)
130 }
131 };
132
133 let load_average = match catch_unwind(AssertUnwindSafe(|| self.get_load_average_safe())) {
135 Ok(Ok(load)) => load,
136 Ok(Err(e)) => {
137 errors.push(format!("Load average error: {e}"));
138 (0.0, 0.0, 0.0)
139 }
140 Err(_) => {
141 errors.push("Load average calculation panicked".to_string());
142 (0.0, 0.0, 0.0)
143 }
144 };
145
146 let disk_usage = match catch_unwind(AssertUnwindSafe(|| self.get_disk_usage_safe())) {
148 Ok(Ok(usage)) => usage,
149 Ok(Err(e)) => {
150 errors.push(format!("Disk usage error: {e}"));
151 HashMap::new()
152 }
153 Err(_) => {
154 errors.push("Disk usage calculation panicked".to_string());
155 HashMap::new()
156 }
157 };
158
159 let top_processes = match catch_unwind(AssertUnwindSafe(|| self.get_top_processes_safe())) {
161 Ok(Ok(processes)) => processes,
162 Ok(Err(e)) => {
163 errors.push(format!("Top processes error: {e}"));
164 Vec::new()
165 }
166 Err(_) => {
167 errors.push("Top processes calculation panicked".to_string());
168 Vec::new()
169 }
170 };
171
172 self.last_update = now;
173
174 SafeSystemStats {
175 cpu_usage_percent: cpu_usage,
176 memory_usage_percent,
177 memory_used,
178 memory_available,
179 load_average,
180 disk_usage,
181 top_processes,
182 timestamp: now,
183 errors,
184 }
185 }
186
187 fn collect_system_info_safe() -> Result<SafeSystemInfo> {
188 let hostname = Self::safe_command("hostname", &[]).unwrap_or_else(|| "unknown".to_string());
189
190 let (os_name, os_version) = Self::get_os_info_safe();
191
192 let kernel_version =
193 Self::safe_command("uname", &["-r"]).unwrap_or_else(|| "unknown".to_string());
194
195 let architecture =
196 Self::safe_command("uname", &["-m"]).unwrap_or_else(|| "unknown".to_string());
197
198 let (cpu_model, cpu_cores, cpu_threads) = Self::get_cpu_info_safe();
199 let total_memory = Self::get_total_memory_safe();
200 let boot_time = Self::get_boot_time_safe();
201 let uptime = SystemTime::now()
202 .duration_since(boot_time)
203 .unwrap_or_default();
204
205 Ok(SafeSystemInfo {
206 hostname,
207 os_name,
208 os_version,
209 kernel_version,
210 architecture,
211 cpu_model,
212 cpu_cores,
213 cpu_threads,
214 total_memory,
215 boot_time,
216 uptime,
217 })
218 }
219
220 fn safe_command(cmd: &str, args: &[&str]) -> Option<String> {
221 match Command::new(cmd).args(args).output() {
222 Ok(output) => {
223 let result = String::from_utf8_lossy(&output.stdout);
224 Some(result.trim().to_string())
225 }
226 Err(_) => None,
227 }
228 }
229
230 fn get_os_info_safe() -> (String, String) {
231 #[cfg(target_os = "macos")]
232 {
233 let name = Self::safe_command("sw_vers", &["-productName"])
234 .unwrap_or_else(|| "macOS".to_string());
235 let version = Self::safe_command("sw_vers", &["-productVersion"])
236 .unwrap_or_else(|| "Unknown".to_string());
237 (name, version)
238 }
239
240 #[cfg(target_os = "linux")]
241 {
242 use std::fs;
243 if let Ok(content) = fs::read_to_string("/etc/os-release") {
244 let mut name = "Linux".to_string();
245 let mut version = "Unknown".to_string();
246
247 for line in content.lines() {
248 if let Some(value) = line.strip_prefix("PRETTY_NAME=") {
249 name = value.trim_matches('"').to_string();
250 } else if let Some(value) = line.strip_prefix("VERSION=") {
251 version = value.trim_matches('"').to_string();
252 }
253 }
254 (name, version)
255 } else {
256 ("Linux".to_string(), "Unknown".to_string())
257 }
258 }
259
260 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
261 {
262 ("Unknown OS".to_string(), "Unknown".to_string())
263 }
264 }
265
266 fn get_cpu_info_safe() -> (String, u32, u32) {
267 #[cfg(target_os = "macos")]
268 {
269 let model = Self::safe_command("sysctl", &["-n", "machdep.cpu.brand_string"])
270 .unwrap_or_else(|| "Unknown CPU".to_string());
271 let cores = Self::safe_command("sysctl", &["-n", "hw.physicalcpu"])
272 .and_then(|s| s.parse().ok())
273 .unwrap_or(1);
274 let threads = Self::safe_command("sysctl", &["-n", "hw.logicalcpu"])
275 .and_then(|s| s.parse().ok())
276 .unwrap_or(1);
277 (model, cores, threads)
278 }
279
280 #[cfg(target_os = "linux")]
281 {
282 use std::fs;
283 let mut model = "Unknown CPU".to_string();
284 let mut cores = 1u32;
285 let mut threads = 1u32;
286
287 if let Ok(content) = fs::read_to_string("/proc/cpuinfo") {
288 let mut cpu_count = 0;
289 let mut core_ids = std::collections::HashSet::new();
290
291 for line in content.lines() {
292 if let Some(name) = line
293 .strip_prefix("model name")
294 .and_then(|l| l.strip_prefix(":"))
295 {
296 model = name.trim().to_string();
297 } else if line.starts_with("processor") {
298 cpu_count += 1;
299 } else if let Some(id_str) = line
300 .strip_prefix("core id")
301 .and_then(|l| l.strip_prefix(":"))
302 {
303 if let Ok(core_id) = id_str.trim().parse::<u32>() {
304 core_ids.insert(core_id);
305 }
306 }
307 }
308
309 threads = cpu_count;
310 cores = core_ids.len() as u32;
311 if cores == 0 {
312 cores = threads;
313 }
314 }
315
316 (model, cores, threads)
317 }
318
319 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
320 {
321 ("Unknown CPU".to_string(), 1, 1)
322 }
323 }
324
325 fn get_total_memory_safe() -> u64 {
326 #[cfg(target_os = "macos")]
327 {
328 Self::safe_command("sysctl", &["-n", "hw.memsize"])
329 .and_then(|s| s.parse().ok())
330 .unwrap_or(0)
331 }
332
333 #[cfg(target_os = "linux")]
334 {
335 use std::fs;
336 if let Ok(content) = fs::read_to_string("/proc/meminfo") {
337 for line in content.lines() {
338 if let Some(value_str) = line.strip_prefix("MemTotal:") {
339 let parts: Vec<&str> = value_str.split_whitespace().collect();
340 if let Some(kb_str) = parts.first() {
341 if let Ok(kb) = kb_str.parse::<u64>() {
342 return kb * 1024; }
344 }
345 }
346 }
347 }
348 0
349 }
350
351 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
352 {
353 0
354 }
355 }
356
357 fn get_boot_time_safe() -> SystemTime {
358 #[cfg(target_os = "macos")]
359 {
360 if let Some(boot_str) = Self::safe_command("sysctl", &["-n", "kern.boottime"]) {
361 if let Some(start) = boot_str.find("sec = ") {
363 let after_sec = &boot_str[start + 6..];
364 if let Some(end) = after_sec.find(',') {
365 if let Ok(secs) = after_sec[..end].parse::<u64>() {
366 return UNIX_EPOCH + Duration::from_secs(secs);
367 }
368 }
369 }
370 }
371 SystemTime::now() - Duration::from_secs(3600) }
373
374 #[cfg(target_os = "linux")]
375 {
376 use std::fs;
377 if let Ok(content) = fs::read_to_string("/proc/stat") {
378 for line in content.lines() {
379 if let Some(time_str) = line.strip_prefix("btime ") {
380 if let Ok(secs) = time_str.trim().parse::<u64>() {
381 return UNIX_EPOCH + Duration::from_secs(secs);
382 }
383 }
384 }
385 }
386 SystemTime::now() - Duration::from_secs(3600) }
388
389 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
390 {
391 SystemTime::now() - Duration::from_secs(3600) }
393 }
394
395 fn calculate_cpu_usage_safe(&mut self) -> Result<f64> {
396 let current_stats = self.read_cpu_stats_safe()?;
397
398 if let Some(ref last_stats) = self.last_cpu_stats {
399 let total_last = last_stats.user
400 + last_stats.nice
401 + last_stats.system
402 + last_stats.idle
403 + last_stats.iowait
404 + last_stats.irq
405 + last_stats.softirq
406 + last_stats.steal;
407
408 let total_current = current_stats.user
409 + current_stats.nice
410 + current_stats.system
411 + current_stats.idle
412 + current_stats.iowait
413 + current_stats.irq
414 + current_stats.softirq
415 + current_stats.steal;
416
417 let total_diff = total_current.saturating_sub(total_last);
418 let idle_diff = current_stats.idle.saturating_sub(last_stats.idle);
419
420 if total_diff > 0 {
421 let usage =
422 ((total_diff.saturating_sub(idle_diff)) as f64 / total_diff as f64) * 100.0;
423 self.last_cpu_stats = Some(current_stats);
424 return Ok(usage.clamp(0.0, 100.0));
425 }
426 }
427
428 self.last_cpu_stats = Some(current_stats);
429 Ok(0.0)
430 }
431
432 fn read_cpu_stats_safe(&self) -> Result<SafeCpuStats> {
433 #[cfg(target_os = "linux")]
434 {
435 use std::fs;
436 let content = fs::read_to_string("/proc/stat")?;
437 if let Some(line) = content.lines().next() {
438 if line.starts_with("cpu ") {
439 let parts: Vec<&str> = line.split_whitespace().collect();
440 if parts.len() >= 8 {
441 return Ok(SafeCpuStats {
442 user: parts[1].parse().unwrap_or(0),
443 nice: parts[2].parse().unwrap_or(0),
444 system: parts[3].parse().unwrap_or(0),
445 idle: parts[4].parse().unwrap_or(0),
446 iowait: parts[5].parse().unwrap_or(0),
447 irq: parts[6].parse().unwrap_or(0),
448 softirq: parts[7].parse().unwrap_or(0),
449 steal: parts.get(8).unwrap_or(&"0").parse().unwrap_or(0),
450 });
451 }
452 }
453 }
454 }
455
456 Ok(SafeCpuStats {
458 user: 0,
459 nice: 0,
460 system: 0,
461 idle: 1000,
462 iowait: 0,
463 irq: 0,
464 softirq: 0,
465 steal: 0,
466 })
467 }
468
469 fn get_memory_stats_safe(&self) -> Result<(f64, u64, u64)> {
470 #[cfg(target_os = "linux")]
471 {
472 use std::fs;
473 let content = fs::read_to_string("/proc/meminfo")?;
474 let mut total = 0u64;
475 let mut available = 0u64;
476
477 for line in content.lines() {
478 if let Some(value_str) = line.strip_prefix("MemTotal:") {
479 let parts: Vec<&str> = value_str.split_whitespace().collect();
480 if let Some(kb_str) = parts.first() {
481 total = kb_str.parse::<u64>().unwrap_or(0) * 1024;
482 }
483 } else if let Some(value_str) = line.strip_prefix("MemAvailable:") {
484 let parts: Vec<&str> = value_str.split_whitespace().collect();
485 if let Some(kb_str) = parts.first() {
486 available = kb_str.parse::<u64>().unwrap_or(0) * 1024;
487 }
488 }
489 }
490
491 let used = total.saturating_sub(available);
492 let usage_percent = if total > 0 {
493 (used as f64 / total as f64) * 100.0
494 } else {
495 0.0
496 };
497
498 Ok((usage_percent, used, available))
499 }
500
501 #[cfg(target_os = "macos")]
502 {
503 if let Ok(output) = Command::new("vm_stat").output() {
504 let content = String::from_utf8_lossy(&output.stdout);
505
506 let mut pages_free = 0u64;
507 let mut pages_active = 0u64;
508 let mut pages_inactive = 0u64;
509 let mut pages_wired = 0u64;
510 let mut pages_compressed = 0u64;
511
512 for line in content.lines() {
513 if line.contains("Pages free:") {
514 pages_free = Self::extract_pages_safe(line);
515 } else if line.contains("Pages active:") {
516 pages_active = Self::extract_pages_safe(line);
517 } else if line.contains("Pages inactive:") {
518 pages_inactive = Self::extract_pages_safe(line);
519 } else if line.contains("Pages wired down:") {
520 pages_wired = Self::extract_pages_safe(line);
521 } else if line.contains("Pages stored in compressor:") {
522 pages_compressed = Self::extract_pages_safe(line);
523 }
524 }
525
526 let page_size = 4096u64;
527 let total = if let Some(info) = &self.system_info {
528 info.total_memory
529 } else {
530 0
531 };
532 let used =
533 (pages_active + pages_inactive + pages_wired + pages_compressed) * page_size;
534 let available = pages_free * page_size;
535 let usage_percent = if total > 0 {
536 (used as f64 / total as f64) * 100.0
537 } else {
538 0.0
539 };
540
541 Ok((usage_percent, used, available))
542 } else {
543 Ok((0.0, 0, 0))
544 }
545 }
546
547 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
548 {
549 Ok((0.0, 0, 0))
550 }
551 }
552
553 #[cfg(target_os = "macos")]
554 fn extract_pages_safe(line: &str) -> u64 {
555 line.split_whitespace()
556 .filter_map(|s| {
557 if s.chars().all(|c| c.is_ascii_digit()) {
559 s.parse().ok()
560 } else {
561 None
562 }
563 })
564 .next()
565 .unwrap_or(0)
566 }
567
568 fn get_load_average_safe(&self) -> Result<(f64, f64, f64)> {
569 #[cfg(any(target_os = "linux", target_os = "macos"))]
570 {
571 use std::fs;
572 if let Ok(content) = fs::read_to_string("/proc/loadavg") {
574 let parts: Vec<&str> = content.split_whitespace().collect();
575 if parts.len() >= 3 {
576 let load_1min = parts[0].parse::<f64>().unwrap_or(0.0);
577 let load_5min = parts[1].parse::<f64>().unwrap_or(0.0);
578 let load_quarter_hour = parts[2].parse::<f64>().unwrap_or(0.0);
579 return Ok((load_1min, load_5min, load_quarter_hour));
580 }
581 }
582
583 if let Some(uptime_output) = Self::safe_command("uptime", &[]) {
585 if let Some(load_start) = uptime_output.find("load average") {
586 let load_section = &uptime_output[load_start..];
587 if let Some(colon_pos) = load_section.find(':') {
588 let numbers_section = &load_section[colon_pos + 1..];
589 let nums: Vec<&str> = numbers_section.split(',').collect();
590 if nums.len() >= 3 {
591 let load_1min = nums[0].trim().parse::<f64>().unwrap_or(0.0);
592 let load_5min = nums[1].trim().parse::<f64>().unwrap_or(0.0);
593 let load_quarter_hour = nums[2].trim().parse::<f64>().unwrap_or(0.0);
594 return Ok((load_1min, load_5min, load_quarter_hour));
595 }
596 }
597 }
598 }
599 }
600
601 Ok((0.0, 0.0, 0.0))
602 }
603
604 fn get_disk_usage_safe(&self) -> Result<HashMap<String, SafeDiskUsage>> {
605 let mut disk_usage = HashMap::new();
606
607 if let Ok(output) = Command::new("df").arg("-h").output() {
608 let content = String::from_utf8_lossy(&output.stdout);
609 for line in content.lines().skip(1) {
610 let parts: Vec<&str> = line.split_whitespace().collect();
612 if parts.len() >= 6 {
613 let filesystem = parts[0].to_string();
614 let mount_point = parts[5].to_string();
615
616 let total = Self::parse_size_safe(parts[1]);
617 let used = Self::parse_size_safe(parts[2]);
618 let available = Self::parse_size_safe(parts[3]);
619 let usage_percent =
620 parts[4].trim_end_matches('%').parse::<f64>().unwrap_or(0.0);
621
622 disk_usage.insert(
623 mount_point,
624 SafeDiskUsage {
625 total,
626 used,
627 available,
628 usage_percent,
629 filesystem,
630 },
631 );
632 }
633 }
634 }
635
636 Ok(disk_usage)
637 }
638
639 fn parse_size_safe(size_str: &str) -> u64 {
640 let size_str = size_str.trim();
641 if size_str.is_empty() || size_str == "-" {
642 return 0;
643 }
644
645 let mut number_end = size_str.len();
647 for (i, c) in size_str.char_indices().rev() {
648 if c.is_ascii_digit() || c == '.' {
649 number_end = i + 1;
650 break;
651 }
652 }
653
654 let number_part = &size_str[..number_end];
655 let suffix = if number_end < size_str.len() {
656 &size_str[number_end..]
657 } else {
658 ""
659 };
660
661 let number: f64 = number_part.parse().unwrap_or(0.0);
662
663 match suffix.to_uppercase().as_str() {
664 "K" => (number * 1024.0) as u64,
665 "M" => (number * 1024.0 * 1024.0) as u64,
666 "G" => (number * 1024.0 * 1024.0 * 1024.0) as u64,
667 "T" => (number * 1024.0 * 1024.0 * 1024.0 * 1024.0) as u64,
668 _ => number as u64,
669 }
670 }
671
672 fn get_top_processes_safe(&self) -> Result<Vec<SafeProcessInfo>> {
673 let output = Command::new("ps")
674 .args(["aux", "--sort=-pcpu"])
675 .output()
676 .or_else(|_| Command::new("ps").args(["aux"]).output())?;
677
678 let content = String::from_utf8_lossy(&output.stdout);
679 let mut processes = Vec::new();
680
681 for line in content.lines().skip(1) {
682 let parts: Vec<&str> = line.split_whitespace().collect();
684 if parts.len() >= 11 {
685 let user = parts[0].to_string();
686 let pid = parts[1].parse::<u32>().unwrap_or(0);
687 let cpu_percent = parts[2].parse::<f64>().unwrap_or(0.0);
688 let memory_percent = parts[3].parse::<f64>().unwrap_or(0.0);
689 let memory_vms = parts[4].parse::<u64>().unwrap_or(0) * 1024;
690 let memory_rss = parts[5].parse::<u64>().unwrap_or(0) * 1024;
691 let state = parts.get(7).unwrap_or(&"?").to_string();
692
693 let command = if parts.len() > 10 {
695 parts[10..].join(" ")
696 } else {
697 "unknown".to_string()
698 };
699
700 let name = if parts.len() > 10 {
701 parts[10]
702 .split('/')
703 .next_back()
704 .unwrap_or("unknown")
705 .to_string()
706 } else {
707 "unknown".to_string()
708 };
709
710 processes.push(SafeProcessInfo {
711 pid,
712 name,
713 cpu_percent,
714 memory_percent,
715 memory_rss,
716 memory_vms,
717 command,
718 user,
719 state,
720 });
721 }
722 }
723
724 processes.sort_by(|a, b| {
726 b.cpu_percent
727 .partial_cmp(&a.cpu_percent)
728 .unwrap_or(std::cmp::Ordering::Equal)
729 });
730 processes.truncate(5);
731
732 Ok(processes)
733 }
734
735 pub fn format_bytes(bytes: u64) -> String {
736 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
737 let mut size = bytes as f64;
738 let mut unit_index = 0;
739
740 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
741 size /= 1024.0;
742 unit_index += 1;
743 }
744
745 let unit = UNITS.get(unit_index).unwrap_or(&"B");
746
747 if size >= 100.0 {
748 format!("{size:.0} {unit}")
749 } else if size >= 10.0 {
750 format!("{size:.1} {unit}")
751 } else {
752 format!("{size:.2} {unit}")
753 }
754 }
755
756 pub fn format_uptime(duration: Duration) -> String {
757 let total_secs = duration.as_secs();
758 let days = total_secs / 86400;
759 let hours = (total_secs % 86400) / 3600;
760 let minutes = (total_secs % 3600) / 60;
761
762 if days > 0 {
763 format!("{days}d {hours}h {minutes}m")
764 } else if hours > 0 {
765 format!("{hours}h {minutes}m")
766 } else {
767 format!("{minutes}m")
768 }
769 }
770}
771
772impl Default for SafeSystemMonitor {
773 fn default() -> Self {
774 Self::new()
775 }
776}