Skip to main content

netwatch_rs/
safe_system.rs

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        // Try to collect system info, but don't fail if it crashes
87        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        // CPU usage with panic protection
107        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        // Memory stats with panic protection
120        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        // Load average with panic protection
134        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        // Disk usage with panic protection
147        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        // Top processes with panic protection
160        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; // Convert KB to bytes
343                            }
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                // Parse format like "{ sec = 1234567890, usec = 123456 }"
362                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) // 1 hour ago fallback
372        }
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) // 1 hour ago fallback
387        }
388
389        #[cfg(not(any(target_os = "macos", target_os = "linux")))]
390        {
391            SystemTime::now() - Duration::from_secs(3600) // 1 hour ago fallback
392        }
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        // Fallback for macOS and other systems
457        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                // Only parse if all characters are digits
558                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            // Try /proc/loadavg first (Linux)
573            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            // Fallback to uptime command (macOS)
584            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                // Skip header
611                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        // Find where numbers end and suffix begins
646        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            // Skip header
683            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                // Safely build command string
694                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        // Sort by CPU percentage and take top 5
725        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}