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

1197 lines
40 KiB
Rust
Raw Normal View History

2024-09-20 09:52:13 +08:00
use crate::steam;
2025-11-08 18:21:35 +08:00
use crate::tool::*;
2025-11-08 23:57:26 +08:00
use tauri_plugin_updater::UpdaterExt;
use crate::vdf::preset;
2025-03-27 13:32:30 +08:00
use crate::vdf::preset::VideoConfig;
use crate::wrap_err;
use anyhow::Result;
2025-11-08 18:21:35 +08:00
use serde::{Deserialize, Serialize};
use std::fs;
2025-11-08 18:21:35 +08:00
use std::fs::File;
2025-11-08 23:57:26 +08:00
use std::io::{BufRead, BufReader, Write};
2025-11-08 18:21:35 +08:00
use std::path::Path;
2025-11-08 23:57:26 +08:00
use reqwest;
use std::sync::atomic::AtomicBool;
2025-11-08 18:09:35 +08:00
use std::sync::{Arc, OnceLock};
use tauri::path::BaseDirectory;
2025-11-08 23:57:26 +08:00
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>;
2024-09-20 09:52:13 +08:00
2025-11-08 18:09:35 +08:00
// 全局下载取消标志
static DOWNLOAD_CANCELLED: OnceLock<Arc<AtomicBool>> = OnceLock::new();
fn get_download_cancelled() -> Arc<AtomicBool> {
2025-11-08 18:21:35 +08:00
DOWNLOAD_CANCELLED
.get_or_init(|| Arc::new(AtomicBool::new(false)))
.clone()
2025-11-08 18:09:35 +08:00
}
2024-09-20 09:52:13 +08:00
#[tauri::command]
pub fn greet(name: &str) -> Result<String, String> {
Ok(format!("Hello, {}! You've been greeted from Rust!", name))
2024-09-20 09:52:13 +08:00
}
// 工具
#[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);
2024-09-20 09:52:13 +08:00
Ok(format!(
2024-09-20 09:52:13 +08:00
"Launching game on server: {}, with launch Option {}",
server, launch_option
))
2024-09-20 09:52:13 +08:00
}
#[tauri::command]
pub fn kill_game() -> Result<String, String> {
Ok(common::kill("cs2.exe"))
2024-09-20 09:52:13 +08:00
}
#[tauri::command]
pub fn check_process_running(process_name: &str) -> Result<bool, String> {
Ok(common::check_process_running(process_name))
}
2024-09-20 09:52:13 +08:00
#[tauri::command]
pub fn kill_steam() -> Result<String, String> {
Ok(common::kill("steam.exe"))
2024-09-20 09:52:13 +08:00
}
// 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))
2024-09-20 09:52:13 +08:00
}
#[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"))]
2025-03-27 11:30:03 +08:00
let powerplan = powerplan::PowerPlanMode::Other as i32;
2025-03-24 09:55:21 +08:00
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))
}
2024-09-20 09:52:13 +08:00
#[tauri::command]
pub fn set_auto_login_user(user: &str) -> Result<String, String> {
2024-09-20 09:52:13 +08:00
#[cfg(target_os = "windows")]
steam::reg::set_auto_login_user(user)?;
2024-09-20 09:52:13 +08:00
Ok(format!("Set auto login user to {}", user))
2024-09-20 09:52:13 +08:00
}
2025-11-05 11:32:43 +08:00
#[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> {
2025-11-08 18:21:35 +08:00
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(())
}
2025-03-27 13:32:30 +08:00
#[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(())
}
2024-09-20 09:52:13 +08:00
#[tauri::command]
pub fn check_path(path: &str) -> Result<bool, String> {
Ok(std::path::Path::new(&path).exists())
2024-09-20 09:52:13 +08:00
}
#[tauri::command]
pub fn check_steam_dir_valid(steam_dir: &str) -> Result<bool, String> {
use std::path::Path;
2025-11-08 18:21:35 +08:00
let path = Path::new(steam_dir);
if !path.exists() {
return Ok(false);
}
2025-11-08 18:21:35 +08:00
// 检查是否存在 steam.exe 或 config 目录(至少有一个即可)
let steam_exe = path.join("steam.exe");
let config_dir = path.join("config");
2025-11-08 18:21:35 +08:00
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 文件夹");
}
2025-11-08 18:21:35 +08:00
// 提取文件名部分
let file_name = std::path::Path::new(path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("default_filename");
2025-11-08 18:21:35 +08:00
// 拼接输出文件路径
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())
}
2025-11-05 02:24:17 +08:00
// 帧数测试相关
#[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);
2025-11-08 18:21:35 +08:00
2025-11-05 02:24:17 +08:00
if !path.exists() {
return Err("console.log 文件不存在".to_string());
}
let file = File::open(path).map_err(|e| format!("无法打开文件: {}", e))?;
let reader = BufReader::new(file);
2025-11-08 18:21:35 +08:00
2025-11-05 02:24:17 +08:00
let mut vprof_lines = Vec::new();
let mut in_vprof_section = false;
let mut empty_line_count = 0;
2025-11-08 18:21:35 +08:00
2025-11-05 02:24:17 +08:00
for line_result in reader.lines() {
let line = line_result.map_err(|e| format!("读取行错误: {}", e))?;
2025-11-08 18:21:35 +08:00
2025-11-05 02:24:17 +08:00
// 检测 [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());
}
}
}
2025-11-08 18:21:35 +08:00
2025-11-05 02:24:17 +08:00
if vprof_lines.is_empty() {
return Err("未找到 [VProf] 报告".to_string());
}
2025-11-08 18:21:35 +08:00
2025-11-05 02:24:17 +08:00
Ok(vprof_lines.join("\n"))
}
// 更新相关命令
2025-11-08 23:57:26 +08:00
/// 更新信息结构(用于前端)
#[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,
2025-11-08 18:09:35 +08:00
endpoint: Option<String>,
include_prerelease: Option<bool>,
2025-11-09 00:07:44 +08:00
use_cdn: Option<bool>,
) -> Result<Option<UpdateInfo>, String> {
2025-11-08 23:57:26 +08:00
info!("开始检查更新...");
info!("include_prerelease: {:?}", include_prerelease);
2025-11-09 00:07:44 +08:00
info!("use_cdn: {:?}", use_cdn);
2025-11-08 23:57:26 +08:00
// 构建更新端点 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));
},
};
2025-11-08 18:21:35 +08:00
2025-11-08 23:57:26 +08:00
match update {
Ok(Some(update)) => {
info!("发现新版本: {}", update.version);
info!("原始下载 URL: {}", update.download_url);
// Update 类型没有实现 Debug trait所以不能使用 {:?} 格式化
// 如果需要更多信息,可以单独记录各个字段
2025-11-09 00:07:44 +08:00
// 根据 use_cdn 参数决定是否使用 CDN 链接
2025-11-08 23:57:26 +08:00
let mut download_url = update.download_url.to_string();
let original_url = download_url.clone();
2025-11-09 00:07:44 +08:00
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);
}
2025-11-08 23:57:26 +08:00
} else {
2025-11-09 00:07:44 +08:00
// 不使用 CDN保持原始 URL
info!("不使用 CDN保持原始下载 URL: {}", download_url);
2025-11-08 23:57:26 +08:00
}
// 存储更新对象和 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,
2025-11-09 00:07:44 +08:00
use_cdn: Option<bool>,
2025-11-08 23:57:26 +08:00
) -> Result<(), String> {
info!("开始下载更新...");
2025-11-09 00:07:44 +08:00
info!("use_cdn: {:?}", use_cdn);
2025-11-08 23:57:26 +08:00
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();
2025-11-09 00:07:44 +08:00
// 根据 use_cdn 参数决定使用原始 URL 还是 CDN URL
let use_cdn_value = use_cdn.unwrap_or(true); // 默认使用 CDN
// 在锁内获取 update 和 URL然后在锁外使用
2025-11-08 23:57:26 +08:00
let cloned_data = {
let update_guard = pending.lock().unwrap();
if let Some(ref update_with_cdn) = *update_guard {
2025-11-09 00:07:44 +08:00
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: {}",
2025-11-08 23:57:26 +08:00
update_with_cdn.update.version,
update_with_cdn.update.download_url,
2025-11-09 00:07:44 +08:00
update_with_cdn.cdn_url,
download_url);
Some((update_with_cdn.update.clone(), update_with_cdn.cdn_url.clone(), download_url))
2025-11-08 23:57:26 +08:00
} else {
None
}
};
// 现在锁已经释放,可以安全地下载
2025-11-09 00:07:44 +08:00
if let Some((update, cdn_url, download_url)) = cloned_data {
info!("开始下载更新文件: {} (使用 CDN: {})", download_url, use_cdn_value);
2025-11-08 23:57:26 +08:00
// 使用 reqwest 手动下载文件
let client = reqwest::Client::new();
let mut response = client
2025-11-09 00:07:44 +08:00
.get(&download_url)
2025-11-08 23:57:26 +08:00
.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())
}
}
2025-11-08 18:09:35 +08:00
/// 取消下载
#[tauri::command]
pub fn cancel_download_update() -> Result<(), String> {
2025-11-08 23:57:26 +08:00
info!("取消下载更新");
// 官方 updater 插件没有直接的取消方法
// 可以通过删除待安装的更新来实现
let pending = get_pending_update();
*pending.lock().unwrap() = None;
info!("已清除待下载的更新");
2025-11-08 18:09:35 +08:00
Ok(())
}
/// 安装更新
#[tauri::command]
2025-11-08 23:57:26 +08:00
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())
}
}
2025-11-08 13:24:00 +08:00
/// 获取 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;
2025-11-08 18:21:35 +08:00
2025-11-08 13:24:00 +08:00
// 异步执行 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
2025-11-08 13:24:00 +08:00
.output()
.await
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
2025-11-08 18:21:35 +08:00
2025-11-08 13:24:00 +08:00
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("PowerShell 命令执行失败: {}", stderr));
}
2025-11-08 18:21:35 +08:00
2025-11-08 13:24:00 +08:00
// 处理 PowerShell 输出,移除 BOM 和空白字符
let stdout = String::from_utf8_lossy(&output.stdout);
let cleaned = stdout.trim().trim_start_matches('\u{feff}'); // 移除 BOM
2025-11-08 18:21:35 +08:00
2025-11-08 13:24:00 +08:00
// 如果输出为空,返回空对象
if cleaned.is_empty() {
return Ok(serde_json::json!({}));
}
2025-11-08 18:21:35 +08:00
let mut json: serde_json::Value = serde_json::from_str(cleaned)
2025-11-08 13:24:00 +08:00
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
2025-11-08 18:21:35 +08:00
// 对于 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());
}
}
}
2025-11-08 18:21:35 +08:00
// 如果没有从 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;
2025-11-08 18:21:35 +08:00
if let Ok(release_output) = release_id_output {
if release_output.status.success() {
2025-11-08 18:21:35 +08:00
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);
}
}
}
}
2025-11-08 18:21:35 +08:00
2025-11-08 13:24:00 +08:00
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;
2025-11-08 18:21:35 +08:00
match active_gpu() {
Ok(gpu) => {
let info = gpu.info();
let temp = info.temperature() as f64 / 1000.0;
2025-11-08 18:21:35 +08:00
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)
}
}
}
2025-11-08 16:34:37 +08:00
/// 内存信息结构体
#[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;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 执行 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
2025-11-08 16:34:37 +08:00
.output()
.await
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("PowerShell 命令执行失败: {}", stderr));
}
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
let stdout = String::from_utf8_lossy(&output.stdout);
let cleaned = stdout.trim().trim_start_matches('\u{feff}');
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
if cleaned.is_empty() {
return Ok(vec![]);
}
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// PowerShell 可能返回数组或单个对象
let json: serde_json::Value = serde_json::from_str(cleaned)
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
let mut memory_list = Vec::new();
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
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));
}
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
Ok(memory_list)
}
/// 解析内存信息
fn parse_memory_info(json: &serde_json::Value) -> MemoryInfo {
// 容量(字节)
2025-11-08 18:21:35 +08:00
let capacity = json.get("Capacity").and_then(|v| v.as_u64());
let manufacturer = json
.get("Manufacturer")
2025-11-08 16:34:37 +08:00
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 实际频率:优先使用 ConfiguredClockSpeed
2025-11-08 18:21:35 +08:00
let speed = json
.get("ConfiguredClockSpeed")
2025-11-08 16:34:37 +08:00
.and_then(|v| v.as_u64())
.map(|v| v as u32);
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 默认频率Speed如果存在
2025-11-08 18:21:35 +08:00
let default_speed = json.get("Speed").and_then(|v| v.as_u64()).map(|v| v as u32);
2025-11-08 16:34:37 +08:00
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;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 执行 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
2025-11-08 16:34:37 +08:00
.output()
.await
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("PowerShell 命令执行失败: {}", stderr));
}
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
let stdout = String::from_utf8_lossy(&output.stdout);
let cleaned = stdout.trim().trim_start_matches('\u{feff}');
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
if cleaned.is_empty() {
return Ok(vec![]);
}
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
let json: serde_json::Value = serde_json::from_str(cleaned)
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
let mut monitor_list = Vec::new();
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
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));
}
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 尝试获取刷新率和分辨率信息
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;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 获取刷新率信息
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;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 获取当前显示器的分辨率和刷新率
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;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 合并显示器信息
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();
2025-11-08 18:21:35 +08:00
let display_str = display_str
.trim()
.trim_start_matches('\u{feff}')
.to_string();
2025-11-08 16:34:37 +08:00
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);
}
}
}
}
}
}
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
Ok(monitor_list)
}
/// 解析显示器信息
fn parse_monitor_info(json: &serde_json::Value) -> MonitorInfo {
2025-11-08 18:21:35 +08:00
let manufacturer = json
.get("Manufacturer")
2025-11-08 16:34:37 +08:00
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
2025-11-08 18:21:35 +08:00
let model = json
.get("Model")
2025-11-08 16:34:37 +08:00
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 组合制造商和型号作为名称
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,
};
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
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![])
2025-11-08 16:34:37 +08:00
}
/// 获取主板信息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;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 执行 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
2025-11-08 16:34:37 +08:00
.output()
.await
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("PowerShell 命令执行失败: {}", stderr));
}
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
let stdout = String::from_utf8_lossy(&output.stdout);
let cleaned = stdout.trim().trim_start_matches('\u{feff}');
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
if cleaned.is_empty() {
return Ok(MotherboardInfo {
manufacturer: None,
model: None,
version: None,
});
}
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
let json: serde_json::Value = serde_json::from_str(cleaned)
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
// 分别获取制造商和型号
2025-11-08 18:21:35 +08:00
let manufacturer = json
.get("Manufacturer")
2025-11-08 16:34:37 +08:00
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
2025-11-08 18:21:35 +08:00
let model = json
.get("Product")
2025-11-08 16:34:37 +08:00
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
2025-11-08 18:21:35 +08:00
2025-11-08 16:34:37 +08:00
Ok(MotherboardInfo {
manufacturer,
model,
2025-11-08 18:21:35 +08:00
version: json
.get("Version")
2025-11-08 16:34:37 +08:00
.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,
})
2025-11-08 18:21:35 +08:00
}