[feat] better installer + changelog test version switch + better video config view

This commit is contained in:
2025-11-05 11:19:43 +08:00
parent ea0a42dc43
commit 41008cf13c
19 changed files with 1499 additions and 183 deletions

1
src-tauri/Cargo.lock generated
View File

@@ -8,6 +8,7 @@ version = "0.0.6"
dependencies = [
"anyhow",
"base64 0.22.1",
"futures-util",
"log",
"regex",
"reqwest",

View File

@@ -31,7 +31,8 @@ walkdir = "2.5.0"
serde_json = "1.0.145"
regex = "1.12.2"
serde = { version = "1.0.228", features = ["derive"] }
reqwest = { version = "0.12.24", features = ["blocking"] }
reqwest = { version = "0.12.24", features = ["json", "stream", "blocking"] }
futures-util = "0.3.30"
tauri = { version = "2.9.2", features = [ "macos-private-api",
"tray-icon"
] }

View File

@@ -1,5 +1,6 @@
use crate::steam;
use crate::tool::*;
use crate::tool::updater::{check_update, download_update, install_update, UpdateInfo};
use crate::vdf::preset;
use crate::vdf::preset::VideoConfig;
use crate::wrap_err;
@@ -47,6 +48,11 @@ pub fn kill_game() -> Result<String, String> {
Ok(common::kill("cs2.exe"))
}
#[tauri::command]
pub fn check_process_running(process_name: &str) -> Result<bool, String> {
Ok(common::check_process_running(process_name))
}
#[tauri::command]
pub fn kill_steam() -> Result<String, String> {
Ok(common::kill("steam.exe"))
@@ -255,3 +261,42 @@ pub fn read_vprof_report(console_log_path: &str) -> Result<String, String> {
Ok(vprof_lines.join("\n"))
}
// 更新相关命令
/// 检查更新
#[tauri::command]
pub async fn check_app_update(
app: tauri::AppHandle,
custom_endpoint: Option<String>,
github_repo: Option<String>,
) -> Result<Option<UpdateInfo>, String> {
let current_version = app.package_info().version.to_string();
wrap_err!(check_update(
custom_endpoint.as_deref(),
github_repo.as_deref(),
&current_version
).await)
}
/// 下载更新
#[tauri::command]
pub async fn download_app_update(
app: tauri::AppHandle,
download_url: String,
) -> Result<String, String> {
let path = wrap_err!(download_update(
&app,
&download_url,
None // 可以添加进度回调
).await)?;
Ok(path.to_string_lossy().to_string())
}
/// 安装更新
#[tauri::command]
pub fn install_app_update(installer_path: String) -> Result<(), String> {
wrap_err!(install_update(&installer_path))
}

View File

@@ -81,6 +81,40 @@ pub fn open_path(path: &str) -> Result<(), std::io::Error> {
Ok(())
}
pub fn check_process_running(name: &str) -> bool {
// 使用tasklist命令检查进程是否存在
#[cfg(windows)]
{
let output = Command::new("tasklist")
.args(&["/FI", &format!("IMAGENAME eq {}", name)])
.creation_flags(CREATE_NO_WINDOW)
.output();
if let Ok(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
// 检查输出中是否包含进程名(排除表头)
stdout.contains(name) && stdout.contains("exe")
} else {
false
}
}
#[cfg(not(windows))]
{
// 对于非Windows系统可以使用ps命令
let output = Command::new("pgrep")
.arg("-f")
.arg(name)
.output();
if let Ok(output) = output {
!output.stdout.is_empty()
} else {
false
}
}
}
mod tests {
#[test]
fn test_open_path() {

View File

@@ -1,3 +1,4 @@
pub mod common;
pub mod macros;
pub mod powerplan;
pub mod updater;

View File

@@ -0,0 +1,429 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tauri::path::BaseDirectory;
use tauri::Manager;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;
/// 更新信息结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateInfo {
pub version: String,
pub notes: Option<String>,
pub pub_date: Option<String>,
pub download_url: String,
pub signature: Option<String>,
}
/// 自定义更新服务器响应格式
#[derive(Debug, Deserialize)]
struct CustomUpdateResponse {
version: String,
notes: Option<String>,
pub_date: Option<String>,
download_url: String,
signature: Option<String>,
platforms: Option<PlatformDownloads>,
}
/// 平台特定的下载链接
#[derive(Debug, Deserialize)]
struct PlatformDownloads {
#[serde(rename = "windows-x86_64")]
windows_x86_64: Option<PlatformInfo>,
#[serde(rename = "darwin-x86_64")]
darwin_x86_64: Option<PlatformInfo>,
#[serde(rename = "darwin-aarch64")]
darwin_aarch64: Option<PlatformInfo>,
#[serde(rename = "linux-x86_64")]
linux_x86_64: Option<PlatformInfo>,
}
#[derive(Debug, Deserialize)]
struct PlatformInfo {
url: String,
signature: Option<String>,
}
/// GitHub Release API 响应
#[derive(Debug, Deserialize)]
struct GitHubRelease {
tag_name: String,
name: Option<String>,
body: Option<String>,
published_at: Option<String>,
assets: Vec<GitHubAsset>,
}
#[derive(Debug, Deserialize)]
struct GitHubAsset {
name: String,
browser_download_url: String,
content_type: String,
}
/// 检查更新
///
/// # 参数
/// - `custom_endpoint`: 自定义更新服务器端点 URL可选
/// - `github_repo`: GitHub 仓库格式owner/repo可选
/// - `current_version`: 当前应用版本
///
/// # 返回
/// 如果有更新,返回 UpdateInfo否则返回 None
pub async fn check_update(
custom_endpoint: Option<&str>,
github_repo: Option<&str>,
current_version: &str,
) -> Result<Option<UpdateInfo>> {
// 首先尝试自定义服务器
if let Some(endpoint) = custom_endpoint {
if !endpoint.is_empty() {
match check_custom_update(endpoint).await {
Ok(Some(info)) => {
if compare_versions(&info.version, current_version) > 0 {
return Ok(Some(info));
}
}
Err(e) => {
log::warn!("自定义更新服务器检查失败: {}", e);
}
Ok(None) => {}
}
}
}
// Fallback 到 GitHub Release
if let Some(repo) = github_repo {
match check_github_update(repo).await {
Ok(Some(info)) => {
if compare_versions(&info.version, current_version) > 0 {
return Ok(Some(info));
}
}
Err(e) => {
log::warn!("GitHub Release 检查失败: {}", e);
}
Ok(None) => {}
}
}
Ok(None)
}
/// 检查自定义更新服务器
async fn check_custom_update(endpoint: &str) -> Result<Option<UpdateInfo>> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let response = client.get(endpoint).send().await?;
if !response.status().is_success() {
return Err(anyhow::anyhow!("HTTP 状态码: {}", response.status()));
}
let text = response.text().await?;
let update_resp: CustomUpdateResponse = serde_json::from_str(&text)
.context("解析自定义更新服务器响应失败")?;
// 获取平台特定的下载链接
let download_url = if let Some(platforms) = update_resp.platforms {
#[cfg(target_os = "windows")]
{
#[cfg(target_arch = "x86_64")]
{
platforms
.windows_x86_64
.map(|p| p.url)
.unwrap_or(update_resp.download_url)
}
}
#[cfg(target_os = "macos")]
{
#[cfg(target_arch = "x86_64")]
{
platforms
.darwin_x86_64
.map(|p| p.url)
.unwrap_or(update_resp.download_url)
}
#[cfg(target_arch = "aarch64")]
{
platforms
.darwin_aarch64
.map(|p| p.url)
.unwrap_or(update_resp.download_url)
}
}
#[cfg(target_os = "linux")]
{
#[cfg(target_arch = "x86_64")]
{
platforms
.linux_x86_64
.map(|p| p.url)
.unwrap_or(update_resp.download_url)
}
}
} else {
update_resp.download_url
};
Ok(Some(UpdateInfo {
version: update_resp.version,
notes: update_resp.notes,
pub_date: update_resp.pub_date,
download_url,
signature: update_resp.signature,
}))
}
/// 检查 GitHub Release
async fn check_github_update(repo: &str) -> Result<Option<UpdateInfo>> {
let url = format!("https://api.github.com/repos/{}/releases/latest", repo);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.user_agent("CS工具箱/1.0")
.build()?;
let response = client.get(&url).send().await?;
if !response.status().is_success() {
return Err(anyhow::anyhow!("HTTP 状态码: {}", response.status()));
}
let release: GitHubRelease = response.json().await?;
// 从 tag_name 中提取版本号(移除可能的 'v' 前缀)
let version = release.tag_name.trim_start_matches('v').to_string();
// 查找适合当前平台的安装包
let download_url = find_platform_asset(&release.assets)?;
Ok(Some(UpdateInfo {
version,
notes: release.body,
pub_date: release.published_at,
download_url,
signature: None, // GitHub Release 通常不包含签名
}))
}
/// 查找适合当前平台的资源文件
fn find_platform_asset(assets: &[GitHubAsset]) -> Result<String> {
#[cfg(target_os = "windows")]
{
// 查找 .exe 或 .msi 或 .nsis 安装包
for asset in assets {
let name = asset.name.to_lowercase();
if name.ends_with(".exe")
|| name.ends_with(".msi")
|| (name.contains("windows") && name.contains("x86_64"))
{
return Ok(asset.browser_download_url.clone());
}
}
}
#[cfg(target_os = "macos")]
{
#[cfg(target_arch = "x86_64")]
{
for asset in assets {
let name = asset.name.to_lowercase();
if name.ends_with(".dmg") {
if name.contains("x86_64") || name.contains("darwin-x86_64") || (!name.contains("aarch64") && !name.contains("arm64")) {
return Ok(asset.browser_download_url.clone());
}
}
}
}
#[cfg(target_arch = "aarch64")]
{
for asset in assets {
let name = asset.name.to_lowercase();
if name.ends_with(".dmg") {
if name.contains("aarch64") || name.contains("darwin-aarch64") || name.contains("arm64") || (!name.contains("x86_64") && !name.contains("intel")) {
return Ok(asset.browser_download_url.clone());
}
}
}
}
}
#[cfg(target_os = "linux")]
{
for asset in assets {
let name = asset.name.to_lowercase();
if name.ends_with(".deb")
|| name.ends_with(".rpm")
|| name.ends_with(".appimage")
|| (name.contains("linux") && name.contains("x86_64"))
{
return Ok(asset.browser_download_url.clone());
}
}
}
// 如果找不到特定平台的,返回第一个资源
if let Some(asset) = assets.first() {
return Ok(asset.browser_download_url.clone());
}
Err(anyhow::anyhow!("未找到适合当前平台的安装包"))
}
/// 比较版本号
/// 返回: 1 表示 version1 > version2, -1 表示 version1 < version2, 0 表示相等
fn compare_versions(version1: &str, version2: &str) -> i32 {
let v1_parts: Vec<&str> = version1
.split(|c: char| c == '.' || c == '-' || c == '_')
.collect();
let v2_parts: Vec<&str> = version2
.split(|c: char| c == '.' || c == '-' || c == '_')
.collect();
let max_len = v1_parts.len().max(v2_parts.len());
for i in 0..max_len {
let v1_part = v1_parts.get(i).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
let v2_part = v2_parts.get(i).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
if v1_part > v2_part {
return 1;
} else if v1_part < v2_part {
return -1;
}
}
0
}
/// 下载更新文件
pub async fn download_update(
app: &tauri::AppHandle,
download_url: &str,
progress_callback: Option<Box<dyn Fn(u64, u64) + Send + Sync>>,
) -> Result<PathBuf> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(300))
.build()?;
let response = client.get(download_url).send().await?;
if !response.status().is_success() {
return Err(anyhow::anyhow!("下载失败HTTP 状态码: {}", response.status()));
}
let total_size = response.content_length().unwrap_or(0);
// 获取缓存目录
let cache_dir = app
.path()
.resolve("updates", BaseDirectory::AppCache)
.context("无法获取缓存目录")?;
fs::create_dir_all(&cache_dir)?;
// 从 URL 中提取文件名
let filename = download_url
.split('/')
.last()
.unwrap_or("update")
.split('?')
.next()
.unwrap_or("update");
let file_path = cache_dir.join(filename);
// 下载文件
let mut file = fs::File::create(&file_path)?;
let mut stream = response.bytes_stream();
let mut downloaded: u64 = 0;
use futures_util::StreamExt;
use std::io::Write;
while let Some(item) = stream.next().await {
let chunk = item?;
file.write_all(&chunk)?;
downloaded += chunk.len() as u64;
if let Some(ref callback) = progress_callback {
callback(downloaded, total_size);
}
}
file.sync_all()?;
Ok(file_path)
}
/// 安装更新Windows NSIS
#[cfg(target_os = "windows")]
pub fn install_update(installer_path: &str) -> Result<()> {
// 使用静默安装参数
let mut cmd = Command::new(installer_path);
cmd.args(&["/S", "/D=C:\\Program Files\\CS工具箱"]);
cmd.creation_flags(CREATE_NO_WINDOW);
cmd.spawn()?;
Ok(())
}
/// 安装更新macOS
#[cfg(target_os = "macos")]
pub fn install_update(installer_path: &str) -> Result<()> {
// macOS 通常需要用户手动安装 DMG
// 这里打开安装包
Command::new("open")
.arg(installer_path)
.spawn()?;
Ok(())
}
/// 安装更新Linux
#[cfg(target_os = "linux")]
pub fn install_update(installer_path: &str) -> Result<()> {
// Linux 根据文件类型选择安装方式
if installer_path.ends_with(".deb") {
Command::new("sudo")
.args(&["dpkg", "-i", installer_path])
.spawn()?;
} else if installer_path.ends_with(".rpm") {
Command::new("sudo")
.args(&["rpm", "-i", installer_path])
.spawn()?;
} else if installer_path.ends_with(".AppImage") {
// AppImage 通常只需要设置执行权限
Command::new("chmod")
.args(&["+x", installer_path])
.spawn()?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compare_versions() {
assert_eq!(compare_versions("1.0.1", "1.0.0"), 1);
assert_eq!(compare_versions("1.0.0", "1.0.1"), -1);
assert_eq!(compare_versions("1.0.0", "1.0.0"), 0);
assert_eq!(compare_versions("0.0.6-beta.4", "0.0.6"), 0); // 简单比较,不考虑 beta
}
}

View File

@@ -7,7 +7,9 @@ use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use tauri_plugin_http::reqwest::blocking::get;
use tauri_plugin_http::reqwest::
blocking::get;
use walkdir::WalkDir;
use crate::steam;