iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0
Rust

Rust 實戰專案集:30 個漸進式專案從工具到服務系列 第 7

系統監控器 - 即時顯示 CPU、記憶體、磁碟使用率

  • 分享至 

  • xImage
  •  

前言

今天我們要用 Rust 打造一個即時的系統監控器,能夠顯示 CPU 使用率、記憶體使用情況和磁碟空間資訊
仿間中也有很多像這樣的東西,其中最著名的就是 htop,畢竟在學習 rust 製作專案的過程中,我建議這樣的東西
可以自己嘗試做做看。

專案目標

  • 即時監控 CPU 使用率
  • 顯示記憶體使用情況(總量、已用、可用)
  • 監控磁碟使用率
  • 提供清晰的終端介面顯示
  • 支援自動刷新功能

依賴

[package]
name = "system_monitor"
version = "0.1.0"
edition = "2021"

[dependencies]
sysinfo = "0.29"
crossterm = "0.27"
tokio = { version = "1.0", features = ["full"] }
chrono = "0.4"
anyhow = "1.0"

這裡簡單介紹一下個別依賴他們的作用

systeminfo -> 這個可以獲取系統的資訊,像是 CPU, 磁碟,記憶體
corssterm -> 這個可以用於跨平台的終端控制
tokio -> 可以進行異步操作的東西,這東西很常見,應該不用特別解說XD
chrono -> 日期時間處理相關元件(這個也很常用到)
anyhow -> 錯誤處理

完整程式碼

use anyhow::Result;
use chrono::Local;
use crossterm::{
    cursor,
    event::{self, Event, KeyCode},
    execute,
    style::{Color, Print, ResetColor, SetForegroundColor},
    terminal::{self, ClearType},
};
use std::io::{self, Write};
use std::time::Duration;
use sysinfo::{CpuExt, DiskExt, System, SystemExt};
use tokio::time;

pub struct SystemMonitor {
    system: System,
}

impl SystemMonitor {
    pub fn new() -> Self {
        let mut system = System::new_all();
        system.refresh_all();
        Self { system }
    }

    pub fn refresh(&mut self) {
        self.system.refresh_all();
        // 等待一小段時間讓 CPU 使用率計算更準確
        std::thread::sleep(Duration::from_millis(100));
        self.system.refresh_cpu();
    }

    pub fn get_cpu_usage(&self) -> f32 {
        self.system.global_cpu_info().cpu_usage()
    }

    pub fn get_memory_info(&self) -> (u64, u64, u64) {
        let total = self.system.total_memory();
        let used = self.system.used_memory();
        let available = total - used;
        (total, used, available)
    }

    pub fn get_disk_info(&self) -> Vec<(String, u64, u64)> {
        self.system
            .disks()
            .iter()
            .map(|disk| {
                let name = disk.mount_point().to_string_lossy().to_string();
                let total = disk.total_space();
                let available = disk.available_space();
                (name, total, available)
            })
            .collect()
    }

    pub fn get_cpu_count(&self) -> usize {
        self.system.cpus().len()
    }
}

pub struct Display {
    stdout: io::Stdout,
}

impl Display {
    pub fn new() -> Result<Self> {
        let mut stdout = io::stdout();
        terminal::enable_raw_mode()?;
        execute!(stdout, terminal::Clear(ClearType::All))?;
        Ok(Self { stdout })
    }

    pub fn clear_screen(&mut self) -> Result<()> {
        execute!(
            self.stdout,
            terminal::Clear(ClearType::All),
            cursor::MoveTo(0, 0)
        )?;
        Ok(())
    }

    pub fn print_header(&mut self) -> Result<()> {
        let now = Local::now().format("%Y-%m-%d %H:%M:%S");
        execute!(
            self.stdout,
            SetForegroundColor(Color::Cyan),
            Print("=".repeat(60)),
            Print("\n"),
            Print(format!("            系統監控器 - {}\n", now)),
            Print("=".repeat(60)),
            Print("\n\n"),
            ResetColor
        )?;
        Ok(())
    }

    pub fn print_cpu_info(&mut self, usage: f32, cpu_count: usize) -> Result<()> {
        let color = if usage > 80.0 {
            Color::Red
        } else if usage > 60.0 {
            Color::Yellow
        } else {
            Color::Green
        };

        execute!(
            self.stdout,
            SetForegroundColor(Color::White),
            Print("CPU 資訊:\n"),
            ResetColor
        )?;

        execute!(
            self.stdout,
            Print(format!("  核心數量: {}\n", cpu_count)),
            Print("  使用率: "),
            SetForegroundColor(color),
            Print(format!("{:.1}%", usage)),
            ResetColor,
            Print("\n"),
            Print("  圖表: "),
        )?;

        // CPU 使用率圖表
        let bar_length = 30;
        let filled = (usage / 100.0 * bar_length as f32) as usize;
        execute!(self.stdout, Print("["))?;
        
        for i in 0..bar_length {
            if i < filled {
                execute!(self.stdout, SetForegroundColor(color), Print("█"), ResetColor)?;
            } else {
                execute!(self.stdout, Print("░"))?;
            }
        }
        execute!(self.stdout, Print("]\n\n"))?;

        Ok(())
    }

    pub fn print_memory_info(&mut self, total: u64, used: u64, available: u64) -> Result<()> {
        let usage_percent = (used as f64 / total as f64) * 100.0;
        let color = if usage_percent > 80.0 {
            Color::Red
        } else if usage_percent > 60.0 {
            Color::Yellow
        } else {
            Color::Green
        };

        execute!(
            self.stdout,
            SetForegroundColor(Color::White),
            Print("記憶體資訊:\n"),
            ResetColor
        )?;

        execute!(
            self.stdout,
            Print(format!("  總容量: {:.2} GB\n", self.bytes_to_gb(total))),
            Print(format!("  已使用: {:.2} GB\n", self.bytes_to_gb(used))),
            Print(format!("  可用量: {:.2} GB\n", self.bytes_to_gb(available))),
            Print("  使用率: "),
            SetForegroundColor(color),
            Print(format!("{:.1}%", usage_percent)),
            ResetColor,
            Print("\n"),
            Print("  圖表: "),
        )?;

        // 記憶體使用率圖表
        let bar_length = 30;
        let filled = (usage_percent / 100.0 * bar_length as f64) as usize;
        execute!(self.stdout, Print("["))?;
        
        for i in 0..bar_length {
            if i < filled {
                execute!(self.stdout, SetForegroundColor(color), Print("█"), ResetColor)?;
            } else {
                execute!(self.stdout, Print("░"))?;
            }
        }
        execute!(self.stdout, Print("]\n\n"))?;

        Ok(())
    }

    pub fn print_disk_info(&mut self, disks: &[(String, u64, u64)]) -> Result<()> {
        execute!(
            self.stdout,
            SetForegroundColor(Color::White),
            Print("磁碟資訊:\n"),
            ResetColor
        )?;

        for (name, total, available) in disks {
            if *total == 0 {
                continue;
            }

            let used = total - available;
            let usage_percent = (used as f64 / *total as f64) * 100.0;
            let color = if usage_percent > 90.0 {
                Color::Red
            } else if usage_percent > 75.0 {
                Color::Yellow
            } else {
                Color::Green
            };

            execute!(
                self.stdout,
                Print(format!("  掛載點: {}\n", name)),
                Print(format!("  總容量: {:.2} GB\n", self.bytes_to_gb(*total))),
                Print(format!("  可用量: {:.2} GB\n", self.bytes_to_gb(*available))),
                Print("  使用率: "),
                SetForegroundColor(color),
                Print(format!("{:.1}%", usage_percent)),
                ResetColor,
                Print("\n"),
                Print("  圖表: "),
            )?;

            // 磁碟使用率圖表
            let bar_length = 30;
            let filled = (usage_percent / 100.0 * bar_length as f64) as usize;
            execute!(self.stdout, Print("["))?;
            
            for i in 0..bar_length {
                if i < filled {
                    execute!(self.stdout, SetForegroundColor(color), Print("█"), ResetColor)?;
                } else {
                    execute!(self.stdout, Print("░"))?;
                }
            }
            execute!(self.stdout, Print("]\n\n"))?;
        }

        Ok(())
    }

    pub fn print_footer(&mut self) -> Result<()> {
        execute!(
            self.stdout,
            SetForegroundColor(Color::DarkGrey),
            Print("按 'q' 或 'Ctrl+C' 退出程式 | 每 2 秒自動刷新\n"),
            ResetColor
        )?;
        self.stdout.flush()?;
        Ok(())
    }

    fn bytes_to_gb(&self, bytes: u64) -> f64 {
        bytes as f64 / (1024.0 * 1024.0 * 1024.0)
    }
}

impl Drop for Display {
    fn drop(&mut self) {
        let _ = terminal::disable_raw_mode();
        let _ = execute!(self.stdout, terminal::Clear(ClearType::All));
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let mut monitor = SystemMonitor::new();
    let mut display = Display::new()?;

    println!("正在初始化系統監控器...");
    time::sleep(Duration::from_secs(1)).await;

    loop {
        // 檢查是否有按鍵輸入
        if event::poll(Duration::from_millis(100))? {
            if let Event::Key(key_event) = event::read()? {
                match key_event.code {
                    KeyCode::Char('q') | KeyCode::Esc => break,
                    KeyCode::Char('c') if key_event.modifiers.contains(event::KeyModifiers::CONTROL) => break,
                    _ => {}
                }
            }
        }

        // 刷新系統資訊
        monitor.refresh();

        // 清除螢幕並顯示資訊
        display.clear_screen()?;
        display.print_header()?;

        // 顯示 CPU 資訊
        let cpu_usage = monitor.get_cpu_usage();
        let cpu_count = monitor.get_cpu_count();
        display.print_cpu_info(cpu_usage, cpu_count)?;

        // 顯示記憶體資訊
        let (total_mem, used_mem, available_mem) = monitor.get_memory_info();
        display.print_memory_info(total_mem, used_mem, available_mem)?;

        // 顯示磁碟資訊
        let disk_info = monitor.get_disk_info();
        display.print_disk_info(&disk_info)?;

        display.print_footer()?;

        // 等待 2 秒後刷新
        time::sleep(Duration::from_secs(2)).await;
    }

    println!("\n感謝使用系統監控器!");
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_system_monitor_creation() {
        let monitor = SystemMonitor::new();
        assert!(monitor.get_cpu_count() > 0);
    }

    #[test]
    fn test_memory_info() {
        let monitor = SystemMonitor::new();
        let (total, used, available) = monitor.get_memory_info();
        assert!(total > 0);
        assert!(used <= total);
        assert!(available <= total);
        assert_eq!(used + available, total);
    }

    #[test]
    fn test_disk_info() {
        let monitor = SystemMonitor::new();
        let disks = monitor.get_disk_info();
        assert!(!disks.is_empty());
        
        for (name, total, available) in disks {
            assert!(!name.is_empty());
            if total > 0 {
                assert!(available <= total);
            }
        }
    }
}

解析一下

pub struct Display {
    stdout: io::Stdout,
}

彩色輸出支援
進度條視覺化
清晰的資訊排版
即時刷新功能

進度條呈現使用率

let bar_length = 30;
let filled = (usage / 100.0 * bar_length as f32) as usize;
for i in 0..bar_length {
    if i < filled {
        execute!(self.stdout, SetForegroundColor(color), Print("█"), ResetColor)?;
    } else {
        execute!(self.stdout, Print("░"))?;
    }
}

根據使用率設定不同顏色:

綠色:正常使用率(< 60%)
黃色:中等使用率(60-80%)
紅色:高使用率(> 80%)

輸出結果

============================================================
            系統監控器 - 2025-09-21 14:30:25
============================================================

CPU 資訊:
  核心數量: 8
  使用率: 23.5%
  圖表: [███████░░░░░░░░░░░░░░░░░░░░░░░░]

記憶體資訊:
  總容量: 16.00 GB
  已使用: 8.45 GB
  可用量: 7.55 GB
  使用率: 52.8%
  圖表: [████████████████░░░░░░░░░░░░░░]

磁碟資訊:
  掛載點: /
  總容量: 500.00 GB
  可用量: 123.45 GB
  使用率: 75.3%
  圖表: [██████████████████████░░░░░░░░]

按 'q' 或 'Ctrl+C' 退出程式 | 每 2 秒自動刷新

nice~~


上一篇
檔案加密工具 - 使用 AES 加密保護重要檔案
系列文
Rust 實戰專案集:30 個漸進式專案從工具到服務7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言