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