use crate::steam; use crate::tool::*; use tauri_plugin_updater::UpdaterExt; use crate::vdf::preset; use crate::vdf::preset::VideoConfig; use crate::wrap_err; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::fs; use std::fs::File; use std::io::{BufRead, BufReader, Write}; use std::path::Path; use reqwest; use std::sync::atomic::AtomicBool; use std::sync::{Arc, OnceLock}; use tauri::path::BaseDirectory; use tauri::{Emitter, Manager}; use url::Url; use log::{debug, error, info, warn}; // use tauri_plugin_shell::ShellExt; // pub type Result = Result; // 全局下载取消标志 static DOWNLOAD_CANCELLED: OnceLock> = OnceLock::new(); fn get_download_cancelled() -> Arc { DOWNLOAD_CANCELLED .get_or_init(|| Arc::new(AtomicBool::new(false))) .clone() } #[tauri::command] pub fn greet(name: &str) -> Result { Ok(format!("Hello, {}! You've been greeted from Rust!", name)) } // 工具 #[tauri::command] pub fn launch_game(steam_path: &str, launch_option: &str, server: &str) -> Result { println!( "{}: launching game on server: {}, with launch Option {}", steam_path, server, launch_option ); // wrap_err!(steam::launch_game(steam_path, launch_option, server)); // 如果有错误,打印出来 if let Err(e) = steam::launch_game(steam_path, launch_option, server) { println!("Error: {}", e); return Err(e.to_string()); } // steam::launch_game(steam_path, launch_option, server); Ok(format!( "Launching game on server: {}, with launch Option {}", server, launch_option )) } #[tauri::command] pub fn kill_game() -> Result { Ok(common::kill("cs2.exe")) } #[tauri::command] pub async fn check_process_running(process_name: &str) -> Result { Ok(common::check_process_running_async(process_name).await) } #[tauri::command] pub fn kill_steam() -> Result { Ok(common::kill("steam.exe")) } // Steam #[tauri::command] pub fn get_steam_path() -> Result { wrap_err!(steam::path::get_steam_path()) } #[tauri::command] pub fn get_cs_path(name: &str, steam_dir: &str) -> Result { wrap_err!(steam::path::get_cs_path(name, steam_dir)) } #[tauri::command] pub fn open_path(path: &str) -> Result<(), String> { wrap_err!(common::open_path(path)) } #[tauri::command] pub fn get_powerplan() -> Result { #[cfg(target_os = "windows")] let powerplan = powerplan::get_powerplan()?; #[cfg(not(target_os = "windows"))] let powerplan = powerplan::PowerPlanMode::Other as i32; Ok(powerplan) } #[tauri::command] pub fn set_powerplan(plan: i32) -> Result<(), String> { #[cfg(target_os = "windows")] powerplan::set_powerplan(plan)?; Ok(()) } #[tauri::command] pub fn get_steam_users(steam_dir: &str) -> Result, String> { wrap_err!(preset::get_users(steam_dir)) } #[tauri::command] pub fn set_auto_login_user(user: &str) -> Result { #[cfg(target_os = "windows")] steam::reg::set_auto_login_user(user)?; Ok(format!("Set auto login user to {}", user)) } #[tauri::command] pub fn start_watch_loginusers(app: tauri::AppHandle, steam_dir: String) -> Result<(), String> { wrap_err!(steam::watch::start_watch_loginusers(app, steam_dir)) } #[tauri::command] pub fn start_watch_cs2_video( app: tauri::AppHandle, steam_dir: String, steam_id32: u32, ) -> Result<(), String> { wrap_err!(steam::watch::start_watch_cs2_video( app, steam_dir, steam_id32 )) } #[tauri::command] pub fn stop_watch_cs2_video() -> Result<(), String> { steam::watch::stop_watch_cs2_video(); Ok(()) } #[tauri::command] pub fn get_cs2_video_config(steam_dir: &str, steam_id32: u32) -> Result { let p = format!( "{}/userdata/{}/730/local/cfg/cs2_video.txt", steam_dir, steam_id32 ); let video = preset::get_cs2_video(p.as_str()).map_err(|e| e.to_string())?; Ok(video) } #[tauri::command] pub fn set_cs2_video_config( steam_dir: &str, steam_id32: u32, video_config: VideoConfig, ) -> Result<(), String> { let p = format!( "{}/userdata/{}/730/local/cfg/cs2_video.txt", steam_dir, steam_id32 ); preset::set_cs2_video(p.as_str(), video_config).map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn check_path(path: &str) -> Result { Ok(std::path::Path::new(&path).exists()) } #[tauri::command] pub fn check_steam_dir_valid(steam_dir: &str) -> Result { use std::path::Path; let path = Path::new(steam_dir); if !path.exists() { return Ok(false); } // 检查是否存在 steam.exe 或 config 目录(至少有一个即可) let steam_exe = path.join("steam.exe"); let config_dir = path.join("config"); Ok(steam_exe.exists() || config_dir.exists()) } ///// 录像 #[tauri::command] pub async fn analyze_replay(app: tauri::AppHandle, path: &str) -> Result { // 检测文件是否存在 if !std::path::Path::new(&path).exists() { return Err("文件不存在".to_string()); } // 获取应用配置目录 let config_dir = app .path() .resolve("metadata", BaseDirectory::AppConfig) .expect("无法获取配置目录"); // 确保 metadata 文件夹存在 if !config_dir.exists() { fs::create_dir_all(&config_dir).expect("无法创建 metadata 文件夹"); } // 提取文件名部分 let file_name = std::path::Path::new(path) .file_name() .and_then(|name| name.to_str()) .unwrap_or("default_filename"); // 拼接输出文件路径 let output_path = config_dir.join(format!("{}.json", file_name)); // 确保输出文件存在,如果不存在则创建空文件 if !output_path.exists() { File::create(&output_path).expect("无法创建输出文件"); } // 调用项目绑定cli程序 let cli_path = app .path() .resolve("resources/csda", BaseDirectory::Resource) .expect("analyzer not found"); println!("cli path: {}", cli_path.display()); let output = std::process::Command::new(cli_path) .arg("-demo-path") .arg(path) .arg("-format") .arg("json") .arg("-minify") .arg("-output") .arg(output_path.to_str().expect("路径转换失败")) .output() .expect("Failed to execute command"); // 获取输出 let output_str = String::from_utf8_lossy(&output.stdout); // 打印输出 println!("{}", output_str); // 返回结果 Ok(output_str.to_string()) } // 帧数测试相关 #[tauri::command] pub fn get_console_log_path(cs_path: &str) -> Result { // cs_path 是类似 "game\bin\win64" 的路径,需要向上找到 game\csgo\console.log let path = Path::new(cs_path); // 向上找到 game 目录 if let Some(game_dir) = path.ancestors().find(|p| { p.file_name() .and_then(|n| n.to_str()) .map(|n| n == "game") .unwrap_or(false) }) { let console_log_path = game_dir.join("csgo").join("console.log"); Ok(console_log_path.to_string_lossy().to_string()) } else { Err("无法找到 game 目录".to_string()) } } #[tauri::command] pub fn read_vprof_report(console_log_path: &str) -> Result { let path = Path::new(console_log_path); if !path.exists() { return Err("console.log 文件不存在".to_string()); } let file = File::open(path).map_err(|e| format!("无法打开文件: {}", e))?; let reader = BufReader::new(file); let mut vprof_lines = Vec::new(); let mut in_vprof_section = false; let mut empty_line_count = 0; for line_result in reader.lines() { let line = line_result.map_err(|e| format!("读取行错误: {}", e))?; // 检测 [VProf] 标记 if line.contains("[VProf]") { in_vprof_section = true; empty_line_count = 0; vprof_lines.push(line.clone()); } else if in_vprof_section { // 如果在 VProf 部分中 if line.trim().is_empty() { empty_line_count += 1; // 如果遇到两个连续的空行,结束 VProf 部分 if empty_line_count >= 2 { break; } vprof_lines.push(line.clone()); } else { empty_line_count = 0; vprof_lines.push(line.clone()); } } } if vprof_lines.is_empty() { return Err("未找到 [VProf] 报告".to_string()); } Ok(vprof_lines.join("\n")) } // 更新相关命令 /// 更新信息结构(用于前端) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateInfo { pub version: String, pub notes: Option, pub download_url: String, } // 包装结构,用于存储 update 对象和修改后的 CDN URL struct UpdateWithCdnUrl { update: tauri_plugin_updater::Update, cdn_url: String, file_path: Option, // 保存下载的文件路径 } // 全局存储待安装的更新 use std::sync::Mutex; static PENDING_UPDATE: OnceLock>> = OnceLock::new(); fn get_pending_update() -> &'static Mutex> { PENDING_UPDATE.get_or_init(|| Mutex::new(None)) } /// 检查更新(使用官方 updater 插件) #[tauri::command] pub async fn check_app_update( app: tauri::AppHandle, endpoint: Option, include_prerelease: Option, use_cdn: Option, ) -> Result, String> { info!("开始检查更新..."); info!("include_prerelease: {:?}", include_prerelease); info!("use_cdn: {:?}", use_cdn); // 构建更新端点 URL // Tauri updater 需要支持 {{target}} 和 {{arch}} 变量的端点 let update_url = if let Some(custom_endpoint) = endpoint { info!("使用自定义端点: {}", custom_endpoint); custom_endpoint } else { // 使用默认的 Tauri updater 格式端点 // 端点应该支持 {{target}} 和 {{arch}} 变量 const DEFAULT_GITHUB_REPO: &str = "plsgo/cstb"; let github_repo_str = std::env::var("GITHUB_REPO").ok(); let github_repo = github_repo_str.as_deref().unwrap_or(DEFAULT_GITHUB_REPO); // 构建标准的 Tauri updater 端点格式 // 端点需要返回包含 platforms 字段的 JSON let url = if include_prerelease.unwrap_or(false) { format!("https://gh-info.okk.cool/repos/{}/releases/latest/pre/tauri", github_repo) } else { format!("https://gh-info.okk.cool/repos/{}/releases/latest/tauri", github_repo) }; info!("使用默认端点: {}", url); url }; // 构建 updater let mut builder = app.updater_builder(); // 设置端点(如果提供了自定义端点,使用自定义端点;否则使用默认端点) let endpoint_url = Url::parse(&update_url) .map_err(|e| { error!("解析更新端点 URL 失败: {} (URL: {})", e, update_url); format!("解析更新端点 URL 失败: {}", e) })?; info!("解析端点 URL 成功: {}", endpoint_url); builder = builder.endpoints(vec![endpoint_url]) .map_err(|e| { error!("设置更新端点失败: {}", e); format!("设置更新端点失败: {}", e) })?; // 设置超时 builder = builder.timeout(std::time::Duration::from_secs(30)); info!("设置超时为 30 秒"); // 检查更新 let update = match builder.build() { Ok(updater) => { info!("构建 updater 成功,开始检查更新..."); updater.check().await }, Err(e) => { error!("构建 updater 失败: {}", e); return Err(format!("构建 updater 失败: {}", e)); }, }; match update { Ok(Some(update)) => { info!("发现新版本: {}", update.version); info!("原始下载 URL: {}", update.download_url); // Update 类型没有实现 Debug trait,所以不能使用 {:?} 格式化 // 如果需要更多信息,可以单独记录各个字段 // 根据 use_cdn 参数决定是否使用 CDN 链接 let mut download_url = update.download_url.to_string(); let original_url = download_url.clone(); let use_cdn_value = use_cdn.unwrap_or(true); // 默认使用 CDN if use_cdn_value { // 如果 URL 不是 CDN 链接,则在 CDN 域名后拼接原 URL if !download_url.contains("cdn.upup.cool") { download_url = format!("https://cdn.upup.cool/{}", original_url); info!("将下载 URL 从 {} 替换为 CDN 链接: {}", original_url, download_url); } else { info!("下载 URL 已经是 CDN 链接: {}", download_url); } } else { // 不使用 CDN,保持原始 URL info!("不使用 CDN,保持原始下载 URL: {}", download_url); } // 存储更新对象和 CDN URL 供后续使用 let pending = get_pending_update(); *pending.lock().unwrap() = Some(UpdateWithCdnUrl { update: update.clone(), cdn_url: download_url.clone(), file_path: None, }); // 转换为前端需要的格式 let update_info = UpdateInfo { version: update.version.to_string(), notes: update.body.clone(), download_url: download_url.clone(), }; info!("更新信息准备完成 - 版本: {}, 下载 URL: {}", update_info.version, download_url); Ok(Some(update_info)) } Ok(None) => { info!("当前已是最新版本"); Ok(None) }, Err(e) => { error!("检查更新失败: {}", e); Err(format!("检查更新失败: {}", e)) }, } } /// 下载更新 #[tauri::command] pub async fn download_app_update( app: tauri::AppHandle, use_cdn: Option, ) -> Result<(), String> { info!("开始下载更新..."); info!("use_cdn: {:?}", use_cdn); let pending = get_pending_update(); // 检查是否有待下载的更新 let has_update = { let update_guard = pending.lock().unwrap(); update_guard.is_some() }; if !has_update { warn!("没有待下载的更新"); return Err("没有待下载的更新".to_string()); } // 监听下载进度 // 克隆 app_handle 用于两个闭包 let app_handle_progress = app.clone(); let app_handle_complete = app.clone(); // 根据 use_cdn 参数决定使用原始 URL 还是 CDN URL let use_cdn_value = use_cdn.unwrap_or(true); // 默认使用 CDN // 在锁内获取 update 和 URL,然后在锁外使用 let cloned_data = { let update_guard = pending.lock().unwrap(); if let Some(ref update_with_cdn) = *update_guard { let download_url = if use_cdn_value { update_with_cdn.cdn_url.clone() } else { update_with_cdn.update.download_url.to_string() }; info!("准备下载更新 - 版本: {}, 原始 URL: {}, CDN URL: {}, 使用 URL: {}", update_with_cdn.update.version, update_with_cdn.update.download_url, update_with_cdn.cdn_url, download_url); Some((update_with_cdn.update.clone(), update_with_cdn.cdn_url.clone(), download_url)) } else { None } }; // 现在锁已经释放,可以安全地下载 if let Some((update, cdn_url, download_url)) = cloned_data { info!("开始下载更新文件: {} (使用 CDN: {})", download_url, use_cdn_value); // 使用 reqwest 手动下载文件 let client = reqwest::Client::new(); let mut response = client .get(&download_url) .send() .await .map_err(|e| { error!("下载更新失败: {}", e); format!("下载更新失败: {}", e) })?; // 获取文件大小(用于进度计算) let content_length = response.content_length(); let mut downloaded: u64 = 0; // 创建临时文件 let temp_dir = std::env::temp_dir(); let file_name = Path::new(update.download_url.as_str()) .file_name() .and_then(|n| n.to_str()) .unwrap_or("update.exe"); let temp_file_path = temp_dir.join(file_name); info!("临时文件路径: {:?}", temp_file_path); // 创建文件并写入 let mut file = std::fs::File::create(&temp_file_path) .map_err(|e| { error!("创建临时文件失败: {}", e); format!("创建临时文件失败: {}", e) })?; // 下载并写入文件,同时报告进度 while let Some(chunk) = response.chunk().await .map_err(|e| { error!("下载更新失败: {}", e); format!("下载更新失败: {}", e) })? { file.write_all(&chunk) .map_err(|e| { error!("写入文件失败: {}", e); format!("写入文件失败: {}", e) })?; downloaded += chunk.len() as u64; // 计算并报告进度 if let Some(total) = content_length { let progress = (downloaded * 100) / total; debug!("下载进度: {} / {} ({}%)", downloaded, total, progress); let _ = app_handle_progress.emit("update-download-progress", progress); } else { debug!("下载进度: {} 字节", downloaded); let _ = app_handle_progress.emit("update-download-progress", downloaded); } } info!("文件下载完成,大小: {} 字节", downloaded); // 下载完成 let _ = app_handle_complete.emit("update-download-progress", 100u64); // 注意:由于我们手动下载了文件,我们需要确保 update 对象知道文件的位置 // 但是,update.download() 方法可能还会验证签名等,所以我们需要确保手动下载的文件也能通过验证 // 目前,我们仍然使用原始的 update 对象,但文件已经下载到临时目录 // 如果 update.install() 需要文件路径,我们可能需要修改它 // 更新存储的 update 对象,保存文件路径 let mut update_guard = pending.lock().unwrap(); *update_guard = Some(UpdateWithCdnUrl { update, cdn_url, file_path: Some(temp_file_path.clone()), }); info!("更新文件下载完成并已存储,文件路径: {:?}", temp_file_path); Ok(()) } else { warn!("没有待下载的更新(克隆失败)"); Err("没有待下载的更新".to_string()) } } /// 取消下载 #[tauri::command] pub fn cancel_download_update() -> Result<(), String> { info!("取消下载更新"); // 官方 updater 插件没有直接的取消方法 // 可以通过删除待安装的更新来实现 let pending = get_pending_update(); *pending.lock().unwrap() = None; info!("已清除待下载的更新"); Ok(()) } /// 安装更新 #[tauri::command] pub fn install_app_update(_app: tauri::AppHandle) -> Result<(), String> { info!("开始安装更新..."); let pending = get_pending_update(); let mut update_guard = pending.lock().unwrap(); if let Some(update_with_cdn) = update_guard.take() { let update = update_with_cdn.update; info!("准备安装更新 - 版本: {}", update.version); info!("下载 URL: {}", update.download_url); // 使用 tauri updater 的 install API,传递已下载文件的字节内容 // 这样可以确保 tauri updater 正确处理应用的关闭和重启逻辑 if let Some(ref file_path) = update_with_cdn.file_path { if file_path.exists() { info!("找到下载的安装程序: {:?}", file_path); // 读取文件内容 let file_bytes = std::fs::read(file_path) .map_err(|e| { error!("读取安装文件失败: {}", e); format!("读取安装文件失败: {}", e) })?; info!("读取安装文件成功,大小: {} 字节", file_bytes.len()); // 使用 tauri updater 的 install 方法,传递文件字节内容 // 这样 tauri updater 可以正确处理应用的关闭和重启 match update.install(&file_bytes) { Ok(_) => { info!("安装更新成功,应用将退出以完成安装"); Ok(()) }, Err(e) => { error!("安装更新失败: {}", e); error!("错误详情: {:?}", e); let error_msg = format!("安装更新失败: {}", e); Err(error_msg) } } } else { error!("下载的安装程序不存在: {:?}", file_path); Err(format!("下载的安装程序不存在: {:?}", file_path)) } } else { warn!("没有找到下载的文件路径,尝试使用 update.install() 空参数"); // 如果没有文件路径,尝试使用 update.install() 空参数 // 这可能会让 updater 自己下载文件 match update.install(&[]) { Ok(_) => { info!("安装更新成功,应用将退出以完成安装"); Ok(()) }, Err(e) => { error!("安装更新失败: {}", e); error!("错误详情: {:?}", e); let error_msg = format!("安装更新失败: {}", e); Err(error_msg) } } } } else { warn!("没有待安装的更新"); Err("没有待安装的更新".to_string()) } } /// 获取 PowerShell Get-ComputerInfo 信息(异步版本) #[tauri::command] #[cfg(target_os = "windows")] pub async fn get_computer_info() -> Result { use tokio::process::Command; #[cfg(windows)] const CREATE_NO_WINDOW: u32 = 0x08000000; // 异步执行 PowerShell 命令获取计算机信息并转换为 JSON let mut cmd = Command::new("powershell"); cmd.args(&[ "-NoProfile", "-WindowStyle", "Hidden", "-Command", "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-ComputerInfo | Select-Object OsName, OSDisplayVersion, BiosSMBIOSBIOSVersion, CsManufacturer, CsName | ConvertTo-Json -Compress" ]); #[cfg(windows)] cmd.creation_flags(CREATE_NO_WINDOW); let output = cmd .output() .await .map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("PowerShell 命令执行失败: {}", stderr)); } // 处理 PowerShell 输出,移除 BOM 和空白字符 let stdout = String::from_utf8_lossy(&output.stdout); let cleaned = stdout.trim().trim_start_matches('\u{feff}'); // 移除 BOM // 如果输出为空,返回空对象 if cleaned.is_empty() { return Ok(serde_json::json!({})); } let mut json: serde_json::Value = serde_json::from_str(cleaned) .map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?; // 对于 Windows 11,优先使用 OSDisplayVersion 获取版本代码(如 25H2) // 如果 OSDisplayVersion 存在且包含版本代码,提取它 if let Some(os_display_version) = json.get("OSDisplayVersion").and_then(|v| v.as_str()) { // OSDisplayVersion 格式可能是 "25H2" 或 "Windows 11 版本 25H2" 等 // 尝试提取版本代码(如 25H2) if let Some(capture) = regex::Regex::new(r"(\d+H\d+)") .ok() .and_then(|re| re.captures(os_display_version)) { if let Some(version_code) = capture.get(1) { json["ReleaseId"] = serde_json::Value::String(version_code.as_str().to_string()); } } } // 如果没有从 OSDisplayVersion 获取到版本代码,尝试从注册表获取 ReleaseId if !json.get("ReleaseId").and_then(|v| v.as_str()).is_some() { let mut release_cmd = Command::new("powershell"); release_cmd.args(&[ "-NoProfile", "-WindowStyle", "Hidden", "-Command", "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; try { (Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion').ReleaseId } catch { $null }" ]); #[cfg(windows)] release_cmd.creation_flags(CREATE_NO_WINDOW); let release_id_output = release_cmd.output().await; if let Ok(release_output) = release_id_output { if release_output.status.success() { let release_str = String::from_utf8_lossy(&release_output.stdout) .trim() .to_string(); if !release_str.is_empty() { json["ReleaseId"] = serde_json::Value::String(release_str); } } } } Ok(json) } /// 获取 PowerShell Get-ComputerInfo 信息(非 Windows 平台返回空对象) #[tauri::command] #[cfg(not(target_os = "windows"))] pub async fn get_computer_info() -> Result { Ok(serde_json::json!({})) } /// GPU 信息结构体 #[derive(Debug, Serialize, Deserialize)] pub struct GpuInfo { vendor: String, model: String, family: String, device_id: String, total_vram: u64, used_vram: u64, load_pct: u32, temperature: f64, } /// 辅助函数:格式化字节数 fn format_bytes(bytes: u64) -> String { let gb = bytes as f64 / 1024.0 / 1024.0 / 1024.0; if gb >= 1.0 { format!("{:.2}GB", gb) } else { let mb = bytes as f64 / 1024.0 / 1024.0; format!("{:.2}MB", mb) } } /// 获取 GPU 信息 #[tauri::command] pub fn get_gpu_info() -> Result, String> { use gfxinfo::active_gpu; match active_gpu() { Ok(gpu) => { let info = gpu.info(); let temp = info.temperature() as f64 / 1000.0; Ok(Some(GpuInfo { vendor: gpu.vendor().to_string(), model: gpu.model().to_string(), family: gpu.family().to_string(), device_id: gpu.device_id().to_string(), total_vram: info.total_vram(), used_vram: info.used_vram(), load_pct: info.load_pct(), temperature: temp, })) } Err(e) => { println!("✗ GPU 信息获取失败: {}", e); Ok(None) } } } /// 内存信息结构体 #[derive(Debug, Serialize, Deserialize)] pub struct MemoryInfo { #[serde(skip_serializing_if = "Option::is_none")] capacity: Option, // 容量(字节) #[serde(skip_serializing_if = "Option::is_none")] manufacturer: Option, #[serde(skip_serializing_if = "Option::is_none")] speed: Option, // MHz,实际频率 ConfiguredClockSpeed #[serde(skip_serializing_if = "Option::is_none")] default_speed: Option, // MHz,默认频率 Speed(如果存在) } /// 显示器信息结构体 #[derive(Debug, Serialize, Deserialize)] pub struct MonitorInfo { #[serde(skip_serializing_if = "Option::is_none")] manufacturer: Option, #[serde(skip_serializing_if = "Option::is_none")] model: Option, #[serde(skip_serializing_if = "Option::is_none")] name: Option, #[serde(skip_serializing_if = "Option::is_none")] refresh_rate: Option, // Hz #[serde(skip_serializing_if = "Option::is_none")] resolution_width: Option, #[serde(skip_serializing_if = "Option::is_none")] resolution_height: Option, } /// 主板信息结构体 #[derive(Debug, Serialize, Deserialize)] pub struct MotherboardInfo { #[serde(skip_serializing_if = "Option::is_none")] manufacturer: Option, // 制造商 #[serde(skip_serializing_if = "Option::is_none")] model: Option, // 型号 #[serde(skip_serializing_if = "Option::is_none")] version: Option, } /// 获取内存信息(Windows) #[tauri::command] #[cfg(target_os = "windows")] pub async fn get_memory_info() -> Result, String> { use tokio::process::Command; #[cfg(windows)] const CREATE_NO_WINDOW: u32 = 0x08000000; // 执行 PowerShell 命令获取内存信息 let mut cmd = Command::new("powershell"); cmd.args(&[ "-NoProfile", "-WindowStyle", "Hidden", "-Command", "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-WmiObject Win32_PhysicalMemory | Select-Object Capacity, Manufacturer, ConfiguredClockSpeed, Speed | ConvertTo-Json -Compress" ]); #[cfg(windows)] cmd.creation_flags(CREATE_NO_WINDOW); let output = cmd .output() .await .map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("PowerShell 命令执行失败: {}", stderr)); } let stdout = String::from_utf8_lossy(&output.stdout); let cleaned = stdout.trim().trim_start_matches('\u{feff}'); if cleaned.is_empty() { return Ok(vec![]); } // PowerShell 可能返回数组或单个对象 let json: serde_json::Value = serde_json::from_str(cleaned) .map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?; let mut memory_list = Vec::new(); if let Some(array) = json.as_array() { // 如果是数组 for item in array { memory_list.push(parse_memory_info(item)); } } else { // 如果是单个对象 memory_list.push(parse_memory_info(&json)); } Ok(memory_list) } /// 解析内存信息 fn parse_memory_info(json: &serde_json::Value) -> MemoryInfo { // 容量(字节) let capacity = json.get("Capacity").and_then(|v| v.as_u64()); let manufacturer = json .get("Manufacturer") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()); // 实际频率:优先使用 ConfiguredClockSpeed let speed = json .get("ConfiguredClockSpeed") .and_then(|v| v.as_u64()) .map(|v| v as u32); // 默认频率:Speed(如果存在) let default_speed = json.get("Speed").and_then(|v| v.as_u64()).map(|v| v as u32); MemoryInfo { capacity, manufacturer, speed, default_speed, } } /// 获取内存信息(非 Windows 平台返回空) #[tauri::command] #[cfg(not(target_os = "windows"))] pub async fn get_memory_info() -> Result, String> { Ok(vec![]) } /// 获取显示器信息(Windows) #[tauri::command] #[cfg(target_os = "windows")] pub async fn get_monitor_info() -> Result, String> { use tokio::process::Command; #[cfg(windows)] const CREATE_NO_WINDOW: u32 = 0x08000000; // 执行 PowerShell 命令获取显示器信息 let mut cmd = Command::new("powershell"); cmd.args(&[ "-NoProfile", "-WindowStyle", "Hidden", "-Command", "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -Namespace root/wmi -ClassName WmiMonitorID | ForEach-Object { [PSCustomObject]@{ Manufacturer = [System.Text.Encoding]::ASCII.GetString($_.ManufacturerName) -replace \"`0\"; Model = [System.Text.Encoding]::ASCII.GetString($_.UserFriendlyName) -replace \"`0\" } } | ConvertTo-Json -Compress" ]); #[cfg(windows)] cmd.creation_flags(CREATE_NO_WINDOW); let output = cmd .output() .await .map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("PowerShell 命令执行失败: {}", stderr)); } let stdout = String::from_utf8_lossy(&output.stdout); let cleaned = stdout.trim().trim_start_matches('\u{feff}'); if cleaned.is_empty() { return Ok(vec![]); } let json: serde_json::Value = serde_json::from_str(cleaned) .map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?; let mut monitor_list = Vec::new(); if let Some(array) = json.as_array() { for item in array { monitor_list.push(parse_monitor_info(item)); } } else { monitor_list.push(parse_monitor_info(&json)); } // 尝试获取刷新率和分辨率信息 let mut _display_cmd = Command::new("powershell"); _display_cmd.args(&[ "-NoProfile", "-WindowStyle", "Hidden", "-Command", "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -Namespace root/wmi -ClassName WmiMonitorBasicDisplayParams | Select-Object MaxHorizontalImageSize, MaxVerticalImageSize | ConvertTo-Json -Compress" ]); #[cfg(windows)] _display_cmd.creation_flags(CREATE_NO_WINDOW); let _display_output = _display_cmd.output().await; // 获取刷新率信息 let mut _refresh_cmd = Command::new("powershell"); _refresh_cmd.args(&[ "-NoProfile", "-WindowStyle", "Hidden", "-Command", "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -Namespace root/wmi -ClassName WmiMonitorListedSupportedSourceModes | Select-Object -First 1 -ExpandProperty ModeTimings | Select-Object -First 1 -ExpandProperty RefreshRate | ConvertTo-Json -Compress" ]); #[cfg(windows)] _refresh_cmd.creation_flags(CREATE_NO_WINDOW); let _refresh_output = _refresh_cmd.output().await; // 获取当前显示器的分辨率和刷新率 let mut current_display_cmd = Command::new("powershell"); current_display_cmd.args(&[ "-NoProfile", "-WindowStyle", "Hidden", "-Command", "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms; $screens = [System.Windows.Forms.Screen]::AllScreens; $screens | ForEach-Object { [PSCustomObject]@{ Width = $_.Bounds.Width; Height = $_.Bounds.Height; Primary = $_.Primary } } | ConvertTo-Json -Compress" ]); #[cfg(windows)] current_display_cmd.creation_flags(CREATE_NO_WINDOW); let current_display_output = current_display_cmd.output().await; // 合并显示器信息 if let Ok(display_result) = current_display_output { if display_result.status.success() { let display_str = String::from_utf8_lossy(&display_result.stdout).to_string(); let display_str = display_str .trim() .trim_start_matches('\u{feff}') .to_string(); if let Ok(display_json) = serde_json::from_str::(&display_str) { if let Some(displays) = display_json.as_array() { for (i, display) in displays.iter().enumerate() { if i < monitor_list.len() { if let Some(width) = display.get("Width").and_then(|v| v.as_u64()) { monitor_list[i].resolution_width = Some(width as u32); } if let Some(height) = display.get("Height").and_then(|v| v.as_u64()) { monitor_list[i].resolution_height = Some(height as u32); } } } } else if let Some(display) = display_json.as_object() { if monitor_list.len() > 0 { if let Some(width) = display.get("Width").and_then(|v| v.as_u64()) { monitor_list[0].resolution_width = Some(width as u32); } if let Some(height) = display.get("Height").and_then(|v| v.as_u64()) { monitor_list[0].resolution_height = Some(height as u32); } } } } } } Ok(monitor_list) } /// 解析显示器信息 fn parse_monitor_info(json: &serde_json::Value) -> MonitorInfo { let manufacturer = json .get("Manufacturer") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()); let model = json .get("Model") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()); // 组合制造商和型号作为名称 let name = match (&manufacturer, &model) { (Some(mfg), Some(model_name)) => Some(format!("{} {}", mfg, model_name)), (Some(mfg), None) => Some(mfg.clone()), (None, Some(model_name)) => Some(model_name.clone()), (None, None) => None, }; MonitorInfo { manufacturer, model, name, refresh_rate: None, // 需要从其他来源获取 resolution_width: None, resolution_height: None, } } /// 获取显示器信息(非 Windows 平台返回空) #[tauri::command] #[cfg(not(target_os = "windows"))] pub async fn get_monitor_info() -> Result, String> { Ok(vec![]) } /// 获取主板信息(Windows) #[tauri::command] #[cfg(target_os = "windows")] pub async fn get_motherboard_info() -> Result { use tokio::process::Command; #[cfg(windows)] const CREATE_NO_WINDOW: u32 = 0x08000000; // 执行 PowerShell 命令获取主板信息 let mut cmd = Command::new("powershell"); cmd.args(&[ "-NoProfile", "-WindowStyle", "Hidden", "-Command", "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -ClassName Win32_BaseBoard | Select-Object Manufacturer, Product, Version | ConvertTo-Json -Compress" ]); #[cfg(windows)] cmd.creation_flags(CREATE_NO_WINDOW); let output = cmd .output() .await .map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("PowerShell 命令执行失败: {}", stderr)); } let stdout = String::from_utf8_lossy(&output.stdout); let cleaned = stdout.trim().trim_start_matches('\u{feff}'); if cleaned.is_empty() { return Ok(MotherboardInfo { manufacturer: None, model: None, version: None, }); } let json: serde_json::Value = serde_json::from_str(cleaned) .map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?; // 分别获取制造商和型号 let manufacturer = json .get("Manufacturer") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()); let model = json .get("Product") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()); Ok(MotherboardInfo { manufacturer, model, version: json .get("Version") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()), }) } /// 获取主板信息(非 Windows 平台返回空) #[tauri::command] #[cfg(not(target_os = "windows"))] pub async fn get_motherboard_info() -> Result { Ok(MotherboardInfo { manufacturer: None, model: None, version: None, }) }