[fix] update restart process

This commit is contained in:
2025-11-08 20:56:02 +08:00
parent 11afc6dc9e
commit 4c151c3dd5
4 changed files with 87 additions and 52 deletions

View File

@@ -49,12 +49,26 @@ fn main() {
let store_dir = config_dir.join(app_name).join("cstb"); let store_dir = config_dir.join(app_name).join("cstb");
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _, _| { .plugin(tauri_plugin_single_instance::init(
let window = app.get_webview_window("main").expect("no main window"); |app: &tauri::AppHandle, args: Vec<String>, _cwd: String| {
// 检查是否是"更新启动"的特殊请求
let is_update_launch = args.contains(&"--update-launch".to_string());
window.show().expect("no main window, can't show"); if is_update_launch {
window.set_focus().expect("no main window, can't set focus") // 若存在旧实例,强制关闭(避免残留进程阻止新实例)
})) if let Some(old_window) = app.get_webview_window("main") {
// 先尝试优雅关闭,再强制退出
old_window.close().ok();
app.exit(0);
}
} else {
// 常规单实例逻辑:激活现有窗口
let window = app.get_webview_window("main").expect("no main window");
window.show().expect("can't show main window");
window.set_focus().expect("can't focus main window");
}
},
))
.plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_notification::init())

View File

@@ -5,8 +5,8 @@ use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use tauri::{Emitter, Manager};
use tauri::path::BaseDirectory; use tauri::path::BaseDirectory;
use tauri::{Emitter, Manager};
#[cfg(windows)] #[cfg(windows)]
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
@@ -61,7 +61,6 @@ pub async fn check_update(
github_repo: Option<&str>, github_repo: Option<&str>,
include_prerelease: bool, include_prerelease: bool,
) -> Result<Option<UpdateInfo>> { ) -> Result<Option<UpdateInfo>> {
// 确定使用的 API 端点 // 确定使用的 API 端点
let api_url = if let Some(custom_endpoint) = endpoint { let api_url = if let Some(custom_endpoint) = endpoint {
// 如果提供了自定义端点,直接使用 // 如果提供了自定义端点,直接使用
@@ -70,12 +69,14 @@ pub async fn check_update(
// 否则使用默认的 gh-info API // 否则使用默认的 gh-info API
let repo = github_repo.unwrap_or("plsgo/cstb"); let repo = github_repo.unwrap_or("plsgo/cstb");
if include_prerelease { if include_prerelease {
format!("https://gh-info.okk.cool/repos/{}/releases/latest/pre", repo) format!(
"https://gh-info.okk.cool/repos/{}/releases/latest/pre",
repo
)
} else { } else {
format!("https://gh-info.okk.cool/repos/{}/releases/latest", repo) format!("https://gh-info.okk.cool/repos/{}/releases/latest", repo)
} }
}; };
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10)) .timeout(std::time::Duration::from_secs(10))
@@ -85,7 +86,10 @@ pub async fn check_update(
let response = client.get(&api_url).send().await?; let response = client.get(&api_url).send().await?;
if !response.status().is_success() { if !response.status().is_success() {
return Err(anyhow::anyhow!("API 请求失败HTTP 状态码: {}", response.status())); return Err(anyhow::anyhow!(
"API 请求失败HTTP 状态码: {}",
response.status()
));
} }
// 获取响应文本以便尝试不同的解析方式 // 获取响应文本以便尝试不同的解析方式
@@ -94,8 +98,9 @@ pub async fn check_update(
// println!("[更新检查] API 响应: {}", response_text); // println!("[更新检查] API 响应: {}", response_text);
// 尝试解析为自定义更新服务器格式 // 尝试解析为自定义更新服务器格式
let update_info = if let Ok(custom_resp) = serde_json::from_str::<CustomUpdateApiResponse>(&response_text) { let update_info = if let Ok(custom_resp) =
serde_json::from_str::<CustomUpdateApiResponse>(&response_text)
{
// 提取版本号(去掉 'v' 前缀) // 提取版本号(去掉 'v' 前缀)
let version = custom_resp.version.trim_start_matches('v').to_string(); let version = custom_resp.version.trim_start_matches('v').to_string();
@@ -118,7 +123,8 @@ pub async fn check_update(
let platform_key = ""; let platform_key = "";
if !platform_key.is_empty() { if !platform_key.is_empty() {
platforms.get(platform_key) platforms
.get(platform_key)
.map(|p| p.url.clone()) .map(|p| p.url.clone())
.unwrap_or_else(|| custom_resp.download_url.clone()) .unwrap_or_else(|| custom_resp.download_url.clone())
} else { } else {
@@ -148,7 +154,6 @@ pub async fn check_update(
let comparison = compare_version(&version, current_version); let comparison = compare_version(&version, current_version);
if comparison > 0 { if comparison > 0 {
// 从 attachments 中获取下载链接 // 从 attachments 中获取下载链接
// 支持两种格式: // 支持两种格式:
// 1. 字符串数组: ["URL1", "URL2", ...] // 1. 字符串数组: ["URL1", "URL2", ...]
@@ -177,15 +182,16 @@ fn extract_download_url(attachments: &serde_json::Value) -> Option<String> {
// 尝试解析为字符串数组格式: ["URL1", "URL2", ...] // 尝试解析为字符串数组格式: ["URL1", "URL2", ...]
if let Ok(urls) = serde_json::from_value::<Vec<String>>(attachments.clone()) { if let Ok(urls) = serde_json::from_value::<Vec<String>>(attachments.clone()) {
// 优先选择 .exe 或 .msi 文件 // 优先选择 .exe 或 .msi 文件
if let Some(url) = urls.iter().find(|url| { if let Some(url) = urls
url.ends_with(".exe") || url.ends_with(".msi") .iter()
}) { .find(|url| url.ends_with(".exe") || url.ends_with(".msi"))
{
return Some(url.clone()); return Some(url.clone());
} }
// 如果没有找到 .exe 或 .msi使用第一个 URL // 如果没有找到 .exe 或 .msi使用第一个 URL
return urls.first().cloned(); return urls.first().cloned();
} }
// 尝试解析为嵌套数组格式: [["文件名", "URL"], ...] // 尝试解析为嵌套数组格式: [["文件名", "URL"], ...]
if let Ok(nested) = serde_json::from_value::<Vec<Vec<String>>>(attachments.clone()) { if let Ok(nested) = serde_json::from_value::<Vec<Vec<String>>>(attachments.clone()) {
// 优先选择 .exe 或 .msi 文件 // 优先选择 .exe 或 .msi 文件
@@ -211,34 +217,32 @@ fn extract_download_url(attachments: &serde_json::Value) -> Option<String> {
} }
} }
} }
None None
} }
/// 改进的版本比较函数支持预发布版本beta.5, beta.6等) /// 改进的版本比较函数支持预发布版本beta.5, beta.6等)
fn compare_version(new: &str, current: &str) -> i32 { fn compare_version(new: &str, current: &str) -> i32 {
// 解析版本号:支持格式如 "0.0.6-beta.5", "beta.6", "0.0.6" 等 // 解析版本号:支持格式如 "0.0.6-beta.5", "beta.6", "0.0.6" 等
let (new_base, new_pre) = parse_version(new); let (new_base, new_pre) = parse_version(new);
let (current_base, current_pre) = parse_version(current); let (current_base, current_pre) = parse_version(current);
// 先比较基础版本号(数字部分) // 先比较基础版本号(数字部分)
let base_comparison = compare_version_parts(&new_base, &current_base); let base_comparison = compare_version_parts(&new_base, &current_base);
if base_comparison != 0 { if base_comparison != 0 {
return base_comparison; return base_comparison;
} }
// 如果基础版本相同(或都为空),比较预发布标识符 // 如果基础版本相同(或都为空),比较预发布标识符
// 如果基础版本都为空,说明是纯预发布版本(如 beta.5 vs beta.6 // 如果基础版本都为空,说明是纯预发布版本(如 beta.5 vs beta.6
let pre_comparison = compare_prerelease(&new_pre, &current_pre); let pre_comparison = compare_prerelease(&new_pre, &current_pre);
// 如果基础版本都为空且预发布比较结果为0说明版本完全相同 // 如果基础版本都为空且预发布比较结果为0说明版本完全相同
if new_base.is_empty() && current_base.is_empty() && pre_comparison == 0 { if new_base.is_empty() && current_base.is_empty() && pre_comparison == 0 {
return 0; return 0;
} }
pre_comparison pre_comparison
} }
@@ -246,7 +250,7 @@ fn compare_version(new: &str, current: &str) -> i32 {
fn parse_version(version: &str) -> (Vec<u32>, Option<String>) { fn parse_version(version: &str) -> (Vec<u32>, Option<String>) {
// 去掉 'v' 前缀 // 去掉 'v' 前缀
let version = version.trim_start_matches('v').trim(); let version = version.trim_start_matches('v').trim();
// 检查是否有预发布标识符(如 -beta.5, -alpha.1 等) // 检查是否有预发布标识符(如 -beta.5, -alpha.1 等)
let (base_str, pre_str) = if let Some(dash_pos) = version.find('-') { let (base_str, pre_str) = if let Some(dash_pos) = version.find('-') {
let (base, pre) = version.split_at(dash_pos); let (base, pre) = version.split_at(dash_pos);
@@ -254,13 +258,10 @@ fn parse_version(version: &str) -> (Vec<u32>, Option<String>) {
} else { } else {
(version, None) (version, None)
}; };
// 解析基础版本号(数字部分) // 解析基础版本号(数字部分)
let base_parts: Vec<u32> = base_str let base_parts: Vec<u32> = base_str.split('.').filter_map(|s| s.parse().ok()).collect();
.split('.')
.filter_map(|s| s.parse().ok())
.collect();
// 如果基础版本号为空且没有预发布标识符,可能是纯预发布版本(如 "beta.5" // 如果基础版本号为空且没有预发布标识符,可能是纯预发布版本(如 "beta.5"
// 这种情况下,整个字符串作为预发布标识符 // 这种情况下,整个字符串作为预发布标识符
if base_parts.is_empty() && pre_str.is_none() { if base_parts.is_empty() && pre_str.is_none() {
@@ -269,25 +270,25 @@ fn parse_version(version: &str) -> (Vec<u32>, Option<String>) {
return (vec![], Some(version.to_string())); return (vec![], Some(version.to_string()));
} }
} }
(base_parts, pre_str) (base_parts, pre_str)
} }
/// 比较版本号数组(数字部分) /// 比较版本号数组(数字部分)
fn compare_version_parts(new: &[u32], current: &[u32]) -> i32 { fn compare_version_parts(new: &[u32], current: &[u32]) -> i32 {
let max_len = new.len().max(current.len()); let max_len = new.len().max(current.len());
for i in 0..max_len { for i in 0..max_len {
let new_val = new.get(i).copied().unwrap_or(0); let new_val = new.get(i).copied().unwrap_or(0);
let current_val = current.get(i).copied().unwrap_or(0); let current_val = current.get(i).copied().unwrap_or(0);
if new_val > current_val { if new_val > current_val {
return 1; return 1;
} else if new_val < current_val { } else if new_val < current_val {
return -1; return -1;
} }
} }
0 0
} }
@@ -308,7 +309,7 @@ fn compare_prerelease(new: &Option<String>, current: &Option<String>) -> i32 {
// 尝试提取数字部分进行比较(如 beta.5 -> 5, beta.6 -> 6 // 尝试提取数字部分进行比较(如 beta.5 -> 5, beta.6 -> 6
let new_num = extract_number_from_prerelease(new_pre); let new_num = extract_number_from_prerelease(new_pre);
let current_num = extract_number_from_prerelease(current_pre); let current_num = extract_number_from_prerelease(current_pre);
if let (Some(new_n), Some(current_n)) = (new_num, current_num) { if let (Some(new_n), Some(current_n)) = (new_num, current_num) {
// 如果都能提取数字,比较数字 // 如果都能提取数字,比较数字
if new_n > current_n { if new_n > current_n {
@@ -350,7 +351,10 @@ pub async fn download_update(
let response = client.get(download_url).send().await?; let response = client.get(download_url).send().await?;
if !response.status().is_success() { if !response.status().is_success() {
return Err(anyhow::anyhow!("下载失败HTTP 状态码: {}", response.status())); return Err(anyhow::anyhow!(
"下载失败HTTP 状态码: {}",
response.status()
));
} }
// 获取文件总大小 // 获取文件总大小
@@ -374,7 +378,7 @@ pub async fn download_update(
.unwrap_or("update"); .unwrap_or("update");
let file_path = cache_dir.join(filename); let file_path = cache_dir.join(filename);
// 下载文件 // 下载文件
let mut file = fs::File::create(&file_path)?; let mut file = fs::File::create(&file_path)?;
let mut stream = response.bytes_stream(); let mut stream = response.bytes_stream();
@@ -417,7 +421,7 @@ pub async fn download_update(
pub fn install_update(installer_path: &str) -> Result<()> { pub fn install_update(installer_path: &str) -> Result<()> {
// 使用 /S 静默安装 // 使用 /S 静默安装
let mut cmd = Command::new(installer_path); let mut cmd = Command::new(installer_path);
cmd.args(&["/S"]); // 静默安装 cmd.args(&["/S", "/appParam=\"--update-launch\""]); // 静默安装
cmd.creation_flags(CREATE_NO_WINDOW); cmd.creation_flags(CREATE_NO_WINDOW);
let mut child = cmd.spawn()?; let mut child = cmd.spawn()?;
// 等待安装程序完成 // 等待安装程序完成

View File

@@ -1,7 +1,16 @@
"use client" "use client"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { Button, CircularProgress, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react" import {
Button,
CircularProgress,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
useDisclosure,
} from "@heroui/react"
import { Download, Refresh, FileText, Close, Check } from "@icon-park/react" import { Download, Refresh, FileText, Close, Check } from "@icon-park/react"
import { invoke } from "@tauri-apps/api/core" import { invoke } from "@tauri-apps/api/core"
import { listen } from "@tauri-apps/api/event" import { listen } from "@tauri-apps/api/event"
@@ -22,7 +31,11 @@ interface UpdateCheckerProps {
includePrerelease?: boolean includePrerelease?: boolean
} }
export function UpdateChecker({ useMirror = true, customEndpoint, includePrerelease = false }: UpdateCheckerProps) { export function UpdateChecker({
useMirror = true,
customEndpoint,
includePrerelease = false,
}: UpdateCheckerProps) {
const app = useAppStore() const app = useAppStore()
const [checking, setChecking] = useState(false) const [checking, setChecking] = useState(false)
const [downloading, setDownloading] = useState(false) const [downloading, setDownloading] = useState(false)
@@ -30,14 +43,18 @@ export function UpdateChecker({ useMirror = true, customEndpoint, includePrerele
const [downloadProgress, setDownloadProgress] = useState(0) const [downloadProgress, setDownloadProgress] = useState(0)
const [installerPath, setInstallerPath] = useState<string | null>(null) const [installerPath, setInstallerPath] = useState<string | null>(null)
const [downloadCompleted, setDownloadCompleted] = useState(false) const [downloadCompleted, setDownloadCompleted] = useState(false)
const { isOpen: isChangelogOpen, onOpen: onChangelogOpen, onOpenChange: onChangelogOpenChange } = useDisclosure() const {
isOpen: isChangelogOpen,
onOpen: onChangelogOpen,
onOpenChange: onChangelogOpenChange,
} = useDisclosure()
// 监听下载进度事件 // 监听下载进度事件
useEffect(() => { useEffect(() => {
const unlisten = listen<number>("update-download-progress", (event) => { const unlisten = listen<number>("update-download-progress", (event) => {
const progress = event.payload const progress = event.payload
setDownloadProgress(progress) setDownloadProgress(progress)
// 如果进度达到 100%,标记下载完成 // 如果进度达到 100%,标记下载完成
if (progress === 100) { if (progress === 100) {
setDownloading(false) setDownloading(false)
@@ -46,7 +63,7 @@ export function UpdateChecker({ useMirror = true, customEndpoint, includePrerele
}) })
return () => { return () => {
unlisten.then(fn => fn()) unlisten.then((fn) => fn())
} }
}, []) }, [])
@@ -122,7 +139,7 @@ export function UpdateChecker({ useMirror = true, customEndpoint, includePrerele
setDownloadProgress(100) setDownloadProgress(100)
setDownloading(false) setDownloading(false)
setDownloadCompleted(true) setDownloadCompleted(true)
addToast({ addToast({
title: "下载完成", title: "下载完成",
description: "可以点击安装按钮进行安装", description: "可以点击安装按钮进行安装",
@@ -182,8 +199,8 @@ export function UpdateChecker({ useMirror = true, customEndpoint, includePrerele
}) })
// 安装完成后,等待一小段时间确保安装程序完全退出 // 安装完成后,等待一小段时间确保安装程序完全退出
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise((resolve) => setTimeout(resolve, 1500))
// 启动新版本 // 启动新版本
await relaunch() await relaunch()
} catch (error) { } catch (error) {
@@ -267,9 +284,7 @@ export function UpdateChecker({ useMirror = true, customEndpoint, includePrerele
{downloadCompleted ? ( {downloadCompleted ? (
<> <>
<Check className="text-green-500 dark:text-green-400" size={14} /> <Check className="text-green-500 dark:text-green-400" size={14} />
<span className="text-xs text-green-500 dark:text-green-400"> <span className="text-xs text-green-500 dark:text-green-400"></span>
</span>
</> </>
) : ( ) : (
<> <>

View File

@@ -32,6 +32,8 @@ export interface MemoryInfo {
} }
export interface MonitorInfo { export interface MonitorInfo {
manufacturer?: string
model?: string
name?: string name?: string
refresh_rate?: number // Hz refresh_rate?: number // Hz
resolution_width?: number resolution_width?: number