Skip to main content

aster/diagnostics/
system.rs

1//! 系统诊断检查
2//!
3//! 提供 CPU、内存、磁盘等系统资源检查
4
5use super::checker::DiagnosticCheck;
6
7/// 系统检查器
8pub struct SystemChecker;
9
10impl SystemChecker {
11    /// 检查 CPU 负载
12    #[cfg(unix)]
13    pub fn check_cpu_load() -> DiagnosticCheck {
14        // 获取 CPU 核心数
15        let cores = std::thread::available_parallelism()
16            .map(|n| n.get())
17            .unwrap_or(1);
18
19        // 获取负载平均值
20        #[cfg(target_os = "macos")]
21        let load_result = std::process::Command::new("sysctl")
22            .args(["-n", "vm.loadavg"])
23            .output();
24
25        #[cfg(target_os = "linux")]
26        let load_result = std::fs::read_to_string("/proc/loadavg")
27            .map(|s| std::process::Output {
28                status: std::process::ExitStatus::default(),
29                stdout: s.into_bytes(),
30                stderr: Vec::new(),
31            })
32            .map_err(std::io::Error::other);
33
34        match load_result {
35            Ok(output) => {
36                let load_str = String::from_utf8_lossy(&output.stdout);
37                let load_1min: f64 = load_str
38                    .split_whitespace()
39                    .next()
40                    .and_then(|s| s.trim_matches(|c| c == '{' || c == '}').parse().ok())
41                    .unwrap_or(0.0);
42
43                let load_per_core = load_1min / cores as f64;
44
45                if load_per_core >= 2.0 {
46                    DiagnosticCheck::warn(
47                        "CPU 负载",
48                        format!("负载较高: {:.2} ({} 核心)", load_1min, cores),
49                    )
50                    .with_details(format!("每核负载: {:.2}", load_per_core))
51                    .with_fix("系统负载较高,性能可能受影响")
52                } else {
53                    DiagnosticCheck::pass(
54                        "CPU 负载",
55                        format!("负载: {:.2} ({} 核心)", load_1min, cores),
56                    )
57                }
58            }
59            Err(_) => DiagnosticCheck::warn("CPU 负载", "无法获取 CPU 负载"),
60        }
61    }
62
63    #[cfg(not(unix))]
64    pub fn check_cpu_load() -> DiagnosticCheck {
65        let cores = std::thread::available_parallelism()
66            .map(|n| n.get())
67            .unwrap_or(1);
68        DiagnosticCheck::pass("CPU 负载", format!("{} 核心可用", cores))
69    }
70
71    /// 检查会话目录
72    pub fn check_session_directory() -> DiagnosticCheck {
73        let session_dir = dirs::data_dir()
74            .map(|p| p.join("aster").join("sessions"))
75            .unwrap_or_else(|| std::path::PathBuf::from("~/.aster/sessions"));
76
77        if !session_dir.exists() {
78            if std::fs::create_dir_all(&session_dir).is_ok() {
79                return DiagnosticCheck::pass("会话目录", "目录已创建")
80                    .with_details(format!("路径: {}", session_dir.display()));
81            }
82            return DiagnosticCheck::fail("会话目录", "无法创建会话目录")
83                .with_fix(format!("请手动创建: {}", session_dir.display()));
84        }
85
86        // 统计会话文件
87        let session_count = std::fs::read_dir(&session_dir)
88            .map(|entries| {
89                entries
90                    .filter_map(|e| e.ok())
91                    .filter(|e| {
92                        e.path()
93                            .extension()
94                            .map(|ext| ext == "json" || ext == "jsonl")
95                            .unwrap_or(false)
96                    })
97                    .count()
98            })
99            .unwrap_or(0);
100
101        // 计算目录大小
102        let total_size = Self::calculate_dir_size(&session_dir);
103        let size_mb = total_size as f64 / (1024.0 * 1024.0);
104
105        DiagnosticCheck::pass(
106            "会话目录",
107            format!("{} 个会话, {:.2} MB", session_count, size_mb),
108        )
109        .with_details(format!("路径: {}", session_dir.display()))
110    }
111
112    /// 检查缓存目录
113    pub fn check_cache_directory() -> DiagnosticCheck {
114        let cache_dir = dirs::cache_dir()
115            .map(|p| p.join("aster"))
116            .unwrap_or_else(|| std::path::PathBuf::from("~/.cache/aster"));
117
118        if !cache_dir.exists() {
119            return DiagnosticCheck::pass("缓存目录", "无缓存目录(将按需创建)");
120        }
121
122        let total_size = Self::calculate_dir_size(&cache_dir);
123        let size_mb = total_size as f64 / (1024.0 * 1024.0);
124
125        if size_mb > 500.0 {
126            DiagnosticCheck::warn("缓存目录", format!("缓存较大: {:.2} MB", size_mb))
127                .with_details(format!("路径: {}", cache_dir.display()))
128                .with_fix(format!("考虑清理缓存: rm -rf {}", cache_dir.display()))
129        } else {
130            DiagnosticCheck::pass("缓存目录", format!("缓存: {:.2} MB", size_mb))
131                .with_details(format!("路径: {}", cache_dir.display()))
132        }
133    }
134
135    /// 计算目录大小
136    fn calculate_dir_size(path: &std::path::Path) -> u64 {
137        let mut size = 0u64;
138
139        if let Ok(entries) = std::fs::read_dir(path) {
140            for entry in entries.filter_map(|e| e.ok()) {
141                let path = entry.path();
142                if path.is_dir() {
143                    size += Self::calculate_dir_size(&path);
144                } else if let Ok(metadata) = path.metadata() {
145                    size += metadata.len();
146                }
147            }
148        }
149
150        size
151    }
152
153    /// 检查 MCP 服务器配置
154    pub fn check_mcp_servers() -> DiagnosticCheck {
155        let mcp_config_paths = [
156            dirs::config_dir()
157                .map(|p| p.join("aster").join("mcp.json"))
158                .unwrap_or_default(),
159            std::env::current_dir()
160                .map(|p| p.join(".aster").join("mcp.json"))
161                .unwrap_or_default(),
162        ];
163
164        for config_path in &mcp_config_paths {
165            if config_path.exists() {
166                match std::fs::read_to_string(config_path) {
167                    Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
168                        Ok(config) => {
169                            let servers = config
170                                .get("mcpServers")
171                                .and_then(|s| s.as_object())
172                                .map(|o| o.keys().cloned().collect::<Vec<_>>())
173                                .unwrap_or_default();
174
175                            if servers.is_empty() {
176                                return DiagnosticCheck::pass("MCP 服务器", "未配置 MCP 服务器");
177                            }
178
179                            return DiagnosticCheck::pass(
180                                "MCP 服务器",
181                                format!("{} 个服务器: {}", servers.len(), servers.join(", ")),
182                            );
183                        }
184                        Err(e) => {
185                            return DiagnosticCheck::warn("MCP 服务器", "MCP 配置格式错误")
186                                .with_details(e.to_string());
187                        }
188                    },
189                    Err(e) => {
190                        return DiagnosticCheck::warn("MCP 服务器", "无法读取 MCP 配置")
191                            .with_details(e.to_string());
192                    }
193                }
194            }
195        }
196
197        DiagnosticCheck::pass("MCP 服务器", "未配置 MCP 服务器")
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::diagnostics::checker::CheckStatus;
205
206    #[test]
207    fn test_check_cpu_load() {
208        let result = SystemChecker::check_cpu_load();
209        // 应该返回有效结果
210        assert!(result.status == CheckStatus::Pass || result.status == CheckStatus::Warn);
211        assert!(result.name == "CPU 负载");
212    }
213
214    #[test]
215    fn test_check_session_directory() {
216        let result = SystemChecker::check_session_directory();
217        // 应该能创建或已存在
218        assert!(result.status == CheckStatus::Pass || result.status == CheckStatus::Fail);
219    }
220
221    #[test]
222    fn test_check_cache_directory() {
223        let result = SystemChecker::check_cache_directory();
224        // 缓存目录检查应该通过或警告
225        assert!(result.status == CheckStatus::Pass || result.status == CheckStatus::Warn);
226    }
227
228    #[test]
229    fn test_check_mcp_servers() {
230        let result = SystemChecker::check_mcp_servers();
231        // MCP 配置检查应该返回有效结果
232        assert!(result.status == CheckStatus::Pass || result.status == CheckStatus::Warn);
233    }
234
235    #[test]
236    fn test_calculate_dir_size() {
237        let temp_dir = std::env::temp_dir();
238        let size = SystemChecker::calculate_dir_size(&temp_dir);
239        // 临时目录应该存在且可访问(size 是 u64,总是 >= 0)
240        // 这里只验证函数能正常执行
241        let _ = size;
242    }
243
244    #[test]
245    fn test_calculate_dir_size_nonexistent() {
246        let path = std::path::Path::new("/nonexistent/path/12345");
247        let size = SystemChecker::calculate_dir_size(path);
248        assert_eq!(size, 0);
249    }
250}