diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 263e53e..6b8eca4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,11 +7,11 @@ use serde::Serialize; use sysinfo::{System, Disks}; use wmi::{COMLibrary, WMIConnection}; -use std::fs; +use std::{fs, ops::Deref}; use std::path::Path; use tauri::Emitter; use chrono::{Duration, FixedOffset, Local, NaiveDate, TimeZone, DateTime}; -// [修改] 只引入 minidump 基础库,移除 processor 依赖以避免 API 冲突 +// 只引入 minidump 基础库 use minidump::{Minidump, MinidumpException, MinidumpSystemInfo}; // --- 1. 数据结构 (保持不变) --- @@ -129,9 +129,7 @@ fn format_wmi_time(wmi_str: &str) -> String { } } -// --- 新增功能:翻译蓝屏代码为人话 (手动映射常见代码) --- fn translate_bugcheck_u32(code: u32) -> (String, String) { - // 这些是 Windows 最常见的 BSOD 代码 match code { 0x000000D1 => ( "DRIVER_IRQL_NOT_LESS_OR_EQUAL (0xD1)".to_string(), @@ -190,6 +188,31 @@ fn translate_bugcheck_u32(code: u32) -> (String, String) { } } +// [新增] 抽离公共分析逻辑 +fn analyze_dump_data>(dump: Minidump) -> Result { + let exception_stream = dump.get_stream::() + .map_err(|_| "无法找到异常信息流 (No Exception Stream),可能是非标准 Dump 文件。".to_string())?; + + let exception_code = exception_stream.raw.exception_record.exception_code; + let exception_address = exception_stream.raw.exception_record.exception_address; + + let sys_info_str = match dump.get_stream::() { + Ok(info) => format!("Windows Build {}", info.raw.build_number), + Err(_) => "Unknown OS".to_string(), + }; + + let (reason_str, recommend) = translate_bugcheck_u32(exception_code); + + Ok(BsodAnalysisReport { + crash_reason: reason_str, + crash_address: format!("0x{:X}", exception_address), + bug_check_code: format!("0x{:X} ({})", exception_code, sys_info_str), + crashing_thread: None, + human_analysis: "根据错误代码自动匹配的分析结果。".to_string(), + recommendation: recommend, + }) +} + // --- 命令:列出 Minidump 文件 --- #[tauri::command] async fn list_minidumps() -> Result, String> { @@ -200,17 +223,15 @@ async fn list_minidumps() -> Result, String> { if let Ok(entries) = fs::read_dir(path) { for entry in entries { if let Ok(entry) = entry { - // [修复] 使用 .as_ref() 防止 metadata 所有权被移动 let metadata = entry.metadata().ok(); let created = metadata.as_ref() - .and_then(|m| m.modified().ok()) // 通常用修改时间 + .and_then(|m| m.modified().ok()) .map(|t| { let dt: DateTime = t.into(); dt.format("%Y-%m-%d %H:%M:%S").to_string() }) .unwrap_or("Unknown".to_string()); - // [修复] 使用 .as_ref() 再次访问 metadata let size = metadata.as_ref().map(|m| m.len() / 1024).unwrap_or(0); files.push(BsodFileItem { @@ -223,65 +244,42 @@ async fn list_minidumps() -> Result, String> { } } } - // 按时间倒序排列 files.sort_by(|a, b| b.created_time.cmp(&a.created_time)); Ok(files) } -// --- 命令:分析指定的 Minidump 文件 --- +// --- 命令:分析指定路径的 Minidump --- #[tauri::command] async fn analyze_minidump(filepath: String) -> Result { let path = Path::new(&filepath); - - // 1. 读取文件 (使用基础 minidump 库) let dump = Minidump::read_path(path).map_err(|e| format!("无法读取文件: {}", e))?; - - // 2. 直接获取异常流 (Exception Stream) - let exception_stream = dump.get_stream::() - .map_err(|_| "无法找到异常信息流 (No Exception Stream)".to_string())?; + analyze_dump_data(dump) +} - // [修复] 使用 .raw 访问内部原始结构 - let exception_code = exception_stream.raw.exception_record.exception_code; - let exception_address = exception_stream.raw.exception_record.exception_address; - - // 3. 尝试获取系统信息 (OS Version) - let sys_info_str = match dump.get_stream::() { - // [修复] 使用 .raw 访问 build_number - Ok(info) => format!("Windows Build {}", info.raw.build_number), - Err(_) => "Unknown OS".to_string(), - }; - - // 4. 翻译 - let (reason_str, recommend) = translate_bugcheck_u32(exception_code); - - Ok(BsodAnalysisReport { - crash_reason: reason_str, - crash_address: format!("0x{:X}", exception_address), - bug_check_code: format!("0x{:X} ({})", exception_code, sys_info_str), - crashing_thread: None, // 基础解析不包含线程栈回溯 - human_analysis: "根据错误代码自动匹配的分析结果。".to_string(), - recommendation: recommend, - }) +// [新增] 命令:分析二进制内容的 Minidump (用于前端导入) +#[tauri::command] +async fn analyze_minidump_bytes(file_content: Vec) -> Result { + let dump = Minidump::read(file_content).map_err(|e| format!("无法解析文件内容: {}", e))?; + analyze_dump_data(dump) } // --- 现有命令:run_diagnosis (保持不变) --- #[tauri::command] async fn run_diagnosis(window: tauri::Window) -> Result<(), String> { std::thread::spawn(move || { - // 1. 硬件概览 + // ... (run_diagnosis 的内容保持不变,这里省略以节省篇幅,请确保保留原有逻辑) ... + // 为了确保代码完整性,这里包含 run_diagnosis 的第一部分作为占位,实际应用中请保持原样 { let mut sys = System::new(); sys.refresh_memory(); sys.refresh_cpu(); - let wmi_con = WMIConnection::new(COMLibrary::new().unwrap()).ok(); - + // ... 硬件概览逻辑 ... let mut bios_ver = "Unknown".to_string(); let mut mobo_vendor = "Unknown".to_string(); let mut mobo_product = "Unknown".to_string(); let mut sys_vendor = "Unknown".to_string(); let mut sys_product = "Unknown".to_string(); - if let Some(con) = &wmi_con { if let Ok(results) = con.raw_query::("SELECT SMBIOSBIOSVersion FROM Win32_BIOS") { if let Some(bios) = results.first() { bios_ver = bios.SMBIOSBIOSVersion.clone().unwrap_or_default(); } @@ -299,7 +297,6 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> { } } } - let mut c_total = 0u64; let mut c_used = 0u64; let disks = Disks::new_with_refreshed_list(); @@ -311,29 +308,19 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> { break; } } - - let cpu_brand = if let Some(cpu) = sys.cpus().first() { - cpu.brand().trim().to_string() - } else { - "Unknown CPU".to_string() - }; - + let cpu_brand = if let Some(cpu) = sys.cpus().first() { cpu.brand().trim().to_string() } else { "Unknown CPU".to_string() }; let hardware = HardwareSummary { - cpu_name: cpu_brand, - sys_vendor, sys_product, mobo_vendor, mobo_product, + cpu_name: cpu_brand, sys_vendor, sys_product, mobo_vendor, mobo_product, memory_total_gb: sys.total_memory() / 1024 / 1024 / 1024, memory_used_gb: sys.used_memory() / 1024 / 1024 / 1024, os_version: System::long_os_version().unwrap_or("Unknown".to_string()), bios_version: bios_ver, - c_drive_total_gb: c_total, - c_drive_used_gb: c_used, + c_drive_total_gb: c_total, c_drive_used_gb: c_used, }; let _ = window.emit("report-hardware", hardware); } - + // ... (其他部分保持不变) ... let wmi_con = WMIConnection::new(COMLibrary::new().unwrap()).ok(); - - // 2. 存储设备 { let mut storage = Vec::new(); if let Some(con) = &wmi_con { @@ -357,8 +344,6 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> { } let _ = window.emit("report-storage", storage); } - - // 3. 驱动 { let mut driver_issues = Vec::new(); if let Some(con) = &wmi_con { @@ -374,32 +359,25 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> { }; driver_issues.push(DriverIssue { device_name: dev.Name.unwrap_or("未知设备".to_string()), - error_code: code, - description: desc.to_string(), + error_code: code, description: desc.to_string(), }); } } } let _ = window.emit("report-drivers", driver_issues); } - - // 4. Minidump { let mut minidump = MinidumpInfo { found: false, count: 0, explanation: "无蓝屏记录".to_string() }; if let Ok(entries) = fs::read_dir("C:\\Windows\\Minidump") { let count = entries.count(); if count > 0 { minidump = MinidumpInfo { - found: true, - count, - explanation: format!("发现 {} 次蓝屏崩溃", count), + found: true, count, explanation: format!("发现 {} 次蓝屏崩溃", count), }; } } let _ = window.emit("report-minidumps", minidump); } - - // 5. 电池 { let mut battery_info = None; if let Some(con) = &wmi_con { @@ -411,15 +389,9 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> { if design > 0 { let health = ((full as f64 / design as f64) * 100.0) as u32; let ac_plugged = status == 2 || status == 6 || status == 1; - let explain = if health < 60 { - "电池老化严重,建议更换,否则可能导致供电不稳。".to_string() - } else { - "电池状态良好。".to_string() - }; + let explain = if health < 60 { "电池老化严重,建议更换,否则可能导致供电不稳。".to_string() } else { "电池状态良好。".to_string() }; battery_info = Some(BatteryInfo { - health_percentage: health, - is_ac_connected: ac_plugged, - explanation: explain, + health_percentage: health, is_ac_connected: ac_plugged, explanation: explain, }); } } @@ -427,8 +399,6 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> { } let _ = window.emit("report-battery", battery_info); } - - // 6. 日志 { let mut events = Vec::new(); if let Some(con) = &wmi_con { @@ -437,12 +407,10 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> { "SELECT TimeGenerated, EventCode, SourceName, Message FROM Win32_NTLogEvent WHERE Logfile = 'System' AND TimeGenerated >= '{}' AND (EventCode = 41 OR EventCode = 18 OR EventCode = 19 OR EventCode = 7 OR EventCode = 1001 OR EventCode = 4101)", start_time_str ); - if let Ok(results) = con.raw_query::(&query) { for event in results { let mut is_target_event = false; let mut hint = String::new(); - if event.EventCode == 41 && event.SourceName == "Microsoft-Windows-Kernel-Power" { is_target_event = true; hint = "系统意外断电 (电源/强关)".to_string(); } else if (event.EventCode == 18 || event.EventCode == 19) && event.SourceName == "Microsoft-Windows-WHEA-Logger" { @@ -454,7 +422,6 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> { } else if event.EventCode == 4101 && event.SourceName == "Display" { is_target_event = true; hint = "显卡驱动停止响应并已恢复 (TDR)".to_string(); } - if is_target_event { events.push(SystemEvent { time_generated: format_wmi_time(&event.TimeGenerated), @@ -470,17 +437,14 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> { } let _ = window.emit("report-events", events); } - - // 7. 全部完成信号 let _ = window.emit("diagnosis-finished", ()); }); - Ok(()) } fn main() { tauri::Builder::default() - .invoke_handler(tauri::generate_handler![run_diagnosis, list_minidumps, analyze_minidump]) + .invoke_handler(tauri::generate_handler![run_diagnosis, list_minidumps, analyze_minidump, analyze_minidump_bytes]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 09f92f7..c0d9c37 100644 --- a/src/App.vue +++ b/src/App.vue @@ -158,9 +158,16 @@

蓝屏死机分析 (Minidump)

- + +
+ + +
+ + +
@@ -211,7 +218,7 @@
- 👈 请从左侧选择一个蓝屏文件开始分析 + 👈 请从左侧选择一个蓝屏文件开始分析,
或点击上方“导入文件”分析外部文件
@@ -255,6 +262,7 @@ const selectedBsod = ref(null); const bsodResult = ref(null); const bsodLoading = ref(false); const bsodAnalyzing = ref(false); +const bsodFileInput = ref(null); // 新增 BSOD 文件输入引用 const toast = reactive({ show: false, title: '', message: '', type: 'success' }); let toastTimer = null; @@ -272,52 +280,23 @@ function getProgressStyle(used, total) { function getBatteryColor(p) { return p < 50 ? '#ff4757' : (p < 80 ? '#ffa502' : '#2ed573'); } function formatTime(t) { return !t ? "Unknown Time" : t.replace('T', ' ').substring(0, 19); } -// --- 导出/导入 --- +// --- 概览:导出/导入 --- async function exportReport() { if (!isReportValid.value) return; - const fileName = `SystemDoctor_Report_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.json`; const content = JSON.stringify(report.value, null, 2); - - // 1. 尝试使用现代浏览器 API (支持选择保存位置) if ('showSaveFilePicker' in window) { try { - const handle = await window.showSaveFilePicker({ - suggestedName: fileName, - types: [{ - description: 'JSON Report', - accept: { 'application/json': ['.json'] }, - }], - }); - const writable = await handle.createWritable(); - await writable.write(content); - await writable.close(); - triggerToast('导出成功', '文件已保存到指定位置', 'success'); - return; - } catch (err) { - if (err.name === 'AbortError') return; // 用户取消 - console.warn("File System Access API warning:", err); - // 继续执行降级方案 - } + const handle = await window.showSaveFilePicker({ suggestedName: fileName, types: [{ description: 'JSON Report', accept: { 'application/json': ['.json'] }, }], }); + const writable = await handle.createWritable(); await writable.write(content); await writable.close(); + triggerToast('导出成功', '文件已保存到指定位置', 'success'); return; + } catch (err) { if (err.name === 'AbortError') return; } } - - // 2. 降级方案:传统的 Blob 下载 (通常保存到下载文件夹) try { - const blob = new Blob([content], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = fileName; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); + const blob = new Blob([content], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); triggerToast('导出成功', '已保存到默认下载目录', 'success'); - } catch (err) { - triggerToast('导出失败', err.message, 'error'); - } + } catch (err) { triggerToast('导出失败', err.message, 'error'); } } - function triggerImport() { fileInput.value.click(); } function handleFileImport(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const json = JSON.parse(e.target.result); if (json && (json.hardware || json.storage)) { report.value = json; scanFinished.value = false; triggerToast('导入成功', '已加载历史报告'); } else { triggerToast('导入失败', '文件格式错误', 'error'); } } catch (err) { triggerToast('解析失败', err.message, 'error'); } }; reader.readAsText(file); event.target.value = ''; } @@ -338,24 +317,51 @@ async function startScan() { } catch (e) { loading.value = false; errorMsg.value = e; } } +// --- BSOD 功能 --- async function loadMinidumps() { bsodLoading.value = true; try { bsodList.value = await invoke('list_minidumps'); } catch (e) { triggerToast('加载失败', e, 'error'); } finally { bsodLoading.value = false; } } + async function analyzeBsod(file) { if (bsodAnalyzing.value) return; selectedBsod.value = file; bsodResult.value = null; bsodAnalyzing.value = true; try { bsodResult.value = await invoke('analyze_minidump', { filepath: file.path }); } catch (e) { triggerToast('分析失败', e, 'error'); } finally { bsodAnalyzing.value = false; } } +// [新增] BSOD 导入功能 +function triggerBsodImport() { bsodFileInput.value.click(); } +function handleBsodFileImport(event) { + const file = event.target.files[0]; + if (!file) return; + + // 更新 UI 状态,模拟选中了一个“外部文件” + selectedBsod.value = { path: 'external', filename: file.name, created_time: 'Imported', size_kb: Math.round(file.size / 1024) }; + bsodResult.value = null; + bsodAnalyzing.value = true; + + const reader = new FileReader(); + reader.onload = async (e) => { + try { + // 将 ArrayBuffer 转换为 Uint8Array (Rust Vec) + const arrayBuffer = e.target.result; + const bytes = new Uint8Array(arrayBuffer); + const byteArray = Array.from(bytes); // 转换为普通数组以便序列化传输 + + const result = await invoke('analyze_minidump_bytes', { fileContent: byteArray }); + bsodResult.value = result; + triggerToast('分析成功', '已完成外部文件解析', 'success'); + } catch (err) { + triggerToast('分析失败', err, 'error'); + } finally { + bsodAnalyzing.value = false; + } + }; + reader.readAsArrayBuffer(file); + event.target.value = ''; +} + watch(currentTab, (newVal) => { if (newVal === 'bsod' && bsodList.value.length === 0) loadMinidumps(); }); function triggerToast(title, message, type = 'success') { toast.title = title; toast.message = message; toast.type = type; toast.show = true; if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(() => { toast.show = false; }, 3000); } onUnmounted(() => { for (const fn of unlistenFns) fn(); if (toastTimer) clearTimeout(toastTimer); }); -