Files
cstb-next/src-tauri/src/cmds.rs

1197 lines
40 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<T, String = ()> = Result<T, String>;
// 全局下载取消标志
static DOWNLOAD_CANCELLED: OnceLock<Arc<AtomicBool>> = OnceLock::new();
fn get_download_cancelled() -> Arc<AtomicBool> {
DOWNLOAD_CANCELLED
.get_or_init(|| Arc::new(AtomicBool::new(false)))
.clone()
}
#[tauri::command]
pub fn greet(name: &str) -> Result<String, String> {
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<String, String> {
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<String, String> {
Ok(common::kill("cs2.exe"))
}
#[tauri::command]
pub async fn check_process_running(process_name: &str) -> Result<bool, String> {
Ok(common::check_process_running_async(process_name).await)
}
#[tauri::command]
pub fn kill_steam() -> Result<String, String> {
Ok(common::kill("steam.exe"))
}
// Steam
#[tauri::command]
pub fn get_steam_path() -> Result<String, String> {
wrap_err!(steam::path::get_steam_path())
}
#[tauri::command]
pub fn get_cs_path(name: &str, steam_dir: &str) -> Result<String, String> {
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<i32, String> {
#[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<Vec<preset::User>, String> {
wrap_err!(preset::get_users(steam_dir))
}
#[tauri::command]
pub fn set_auto_login_user(user: &str) -> Result<String, String> {
#[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<VideoConfig, String> {
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<bool, String> {
Ok(std::path::Path::new(&path).exists())
}
#[tauri::command]
pub fn check_steam_dir_valid(steam_dir: &str) -> Result<bool, String> {
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<String, String> {
// 检测文件是否存在
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<String, String> {
// 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<String, String> {
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<String>,
pub download_url: String,
}
// 包装结构,用于存储 update 对象和修改后的 CDN URL
struct UpdateWithCdnUrl {
update: tauri_plugin_updater::Update,
cdn_url: String,
file_path: Option<std::path::PathBuf>, // 保存下载的文件路径
}
// 全局存储待安装的更新
use std::sync::Mutex;
static PENDING_UPDATE: OnceLock<Mutex<Option<UpdateWithCdnUrl>>> = OnceLock::new();
fn get_pending_update() -> &'static Mutex<Option<UpdateWithCdnUrl>> {
PENDING_UPDATE.get_or_init(|| Mutex::new(None))
}
/// 检查更新(使用官方 updater 插件)
#[tauri::command]
pub async fn check_app_update(
app: tauri::AppHandle,
endpoint: Option<String>,
include_prerelease: Option<bool>,
use_cdn: Option<bool>,
) -> Result<Option<UpdateInfo>, 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<bool>,
) -> 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<serde_json::Value, String> {
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<serde_json::Value, String> {
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<Option<GpuInfo>, 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<u64>, // 容量(字节)
#[serde(skip_serializing_if = "Option::is_none")]
manufacturer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
speed: Option<u32>, // MHz实际频率 ConfiguredClockSpeed
#[serde(skip_serializing_if = "Option::is_none")]
default_speed: Option<u32>, // MHz默认频率 Speed如果存在
}
/// 显示器信息结构体
#[derive(Debug, Serialize, Deserialize)]
pub struct MonitorInfo {
#[serde(skip_serializing_if = "Option::is_none")]
manufacturer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
refresh_rate: Option<u32>, // Hz
#[serde(skip_serializing_if = "Option::is_none")]
resolution_width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
resolution_height: Option<u32>,
}
/// 主板信息结构体
#[derive(Debug, Serialize, Deserialize)]
pub struct MotherboardInfo {
#[serde(skip_serializing_if = "Option::is_none")]
manufacturer: Option<String>, // 制造商
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>, // 型号
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<String>,
}
/// 获取内存信息Windows
#[tauri::command]
#[cfg(target_os = "windows")]
pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, 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<Vec<MemoryInfo>, String> {
Ok(vec![])
}
/// 获取显示器信息Windows
#[tauri::command]
#[cfg(target_os = "windows")]
pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, 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::<serde_json::Value>(&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<Vec<MonitorInfo>, String> {
Ok(vec![])
}
/// 获取主板信息Windows
#[tauri::command]
#[cfg(target_os = "windows")]
pub async fn get_motherboard_info() -> Result<MotherboardInfo, 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 -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<MotherboardInfo, String> {
Ok(MotherboardInfo {
manufacturer: None,
model: None,
version: None,
})
}