[fix] update setup

This commit is contained in:
2025-11-08 23:57:26 +08:00
parent cd19faba47
commit 5e663dc79e
20 changed files with 1064 additions and 626 deletions

View File

@@ -1,6 +1,6 @@
use crate::steam;
use crate::tool::updater::{check_update, download_update, install_update, UpdateInfo};
use crate::tool::*;
use tauri_plugin_updater::UpdaterExt;
use crate::vdf::preset;
use crate::vdf::preset::VideoConfig;
use crate::wrap_err;
@@ -8,12 +8,15 @@ use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use reqwest;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, OnceLock};
use tauri::path::BaseDirectory;
use tauri::Manager;
use tauri::{Emitter, Manager};
use url::Url;
use log::{debug, error, info, warn};
// use tauri_plugin_shell::ShellExt;
@@ -314,62 +317,345 @@ pub fn read_vprof_report(console_log_path: &str) -> Result<String, String> {
// 更新相关命令
/// 检查更新(支持 GitHub Release 和自定义端点
/// 更新信息结构(用于前端
#[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>,
use_mirror: Option<bool>,
include_prerelease: Option<bool>,
) -> Result<Option<UpdateInfo>, String> {
let current_version = app.package_info().version.to_string();
let use_mirror = use_mirror.unwrap_or(false);
let include_prerelease = include_prerelease.unwrap_or(false);
info!("开始检查更新...");
info!("include_prerelease: {:?}", include_prerelease);
// 构建更新端点 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
};
// 从环境变量获取 GitHub 仓库信息,如果没有则使用默认值
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);
let result = wrap_err!(
check_update(
endpoint.as_deref(),
&current_version,
use_mirror,
Some(github_repo),
include_prerelease
)
.await
)?;
// 构建 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));
},
};
Ok(result)
match update {
Ok(Some(update)) => {
info!("发现新版本: {}", update.version);
info!("原始下载 URL: {}", update.download_url);
// Update 类型没有实现 Debug trait所以不能使用 {:?} 格式化
// 如果需要更多信息,可以单独记录各个字段
// 将下载 URL 替换为 CDN 链接
let mut download_url = update.download_url.to_string();
let original_url = download_url.clone();
// 如果 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);
}
// 存储更新对象和 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,
download_url: String,
) -> Result<String, String> {
// 重置取消标志
let cancelled = get_download_cancelled();
cancelled.store(false, Ordering::Relaxed);
let path = wrap_err!(download_update(&app, &download_url, cancelled).await)?;
Ok(path.to_string_lossy().to_string())
) -> Result<(), String> {
info!("开始下载更新...");
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();
// 在锁内获取 update 和 CDN URL然后在锁外使用
let cloned_data = {
let update_guard = pending.lock().unwrap();
if let Some(ref update_with_cdn) = *update_guard {
info!("准备下载更新 - 版本: {}, 原始 URL: {}, CDN URL: {}",
update_with_cdn.update.version,
update_with_cdn.update.download_url,
update_with_cdn.cdn_url);
Some((update_with_cdn.update.clone(), update_with_cdn.cdn_url.clone()))
} else {
None
}
};
// 现在锁已经释放,可以安全地下载
if let Some((update, cdn_url)) = cloned_data {
info!("开始使用 CDN URL 下载更新文件: {}", cdn_url);
// 使用 reqwest 手动下载文件
let client = reqwest::Client::new();
let mut response = client
.get(&cdn_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> {
let cancelled = get_download_cancelled();
cancelled.store(true, Ordering::Relaxed);
info!("取消下载更新");
// 官方 updater 插件没有直接的取消方法
// 可以通过删除待安装的更新来实现
let pending = get_pending_update();
*pending.lock().unwrap() = None;
info!("已清除待下载的更新");
Ok(())
}
/// 安装更新
#[tauri::command]
pub fn install_app_update(installer_path: String) -> Result<(), String> {
wrap_err!(install_update(&installer_path))
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 信息(异步版本)
@@ -378,8 +664,6 @@ pub fn install_app_update(installer_path: String) -> Result<(), String> {
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
use tokio::process::Command;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;
// 异步执行 PowerShell 命令获取计算机信息并转换为 JSON
@@ -565,8 +849,6 @@ pub struct MotherboardInfo {
pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
use tokio::process::Command;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;
// 执行 PowerShell 命令获取内存信息
@@ -657,8 +939,6 @@ pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
use tokio::process::Command;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;
// 执行 PowerShell 命令获取显示器信息
@@ -823,8 +1103,6 @@ pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
pub async fn get_motherboard_info() -> Result<MotherboardInfo, String> {
use tokio::process::Command;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;
// 执行 PowerShell 命令获取主板信息