bug修复调整

This commit is contained in:
zhongluofeng
2026-08-31 18:05:27 +08:00
parent 28e0c4664a
commit 6b9f71da08
35 changed files with 1004 additions and 296 deletions
+128 -101
View File
@@ -2,18 +2,65 @@
//! 更新源为自建 Gitea`https://gitea.atie.fun/LFeng/Thing` 的 release 资产。
//! - 便携版(无 unins000.exe 且不在 Program Files):下载新 thing.exe → update.bat 覆盖重启
//! - 安装版(NSIS):下载新 setup.exe → 提权静默安装 /S
//! - ThingHK 内核:下载 thing-hk_{v}.zip → 停止监控内核 → 覆盖 {app_data}/monitor/cores/ThingHK.exe
//! mihomo 内核更新继续复用代理模块已有的 GitHub 下载机制,不在此模块处理。
use futures_util::StreamExt;
//! - ThingHK 内核:下载由前端下载模块完成 → apply 命令 need_stop 等待确认 → 解压覆盖
//! {app_data}/monitor/cores/ThingHK.exe(与代理模块 mihomo 内核更新同模式)
use serde::Serialize;
use specta::Type;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::{atomic::{AtomicBool, Ordering}, Mutex};
use tauri::{AppHandle, Emitter, Manager};
use tokio::sync::{oneshot, watch};
use crate::constants::events::UPDATE_PROGRESS;
/// 用户主动取消 ThingHK 更新的标记错误信息(前端据此静默处理,不弹错误 toast)
const THINGHK_UPDATE_CANCELLED: &str = "更新已取消";
/// ThingHK 内核更新的跨命令状态:apply 过程中 need_stop 阶段等待前端确认。
/// 与 MihomoManager 的 install_confirm/kernel_cancel 同构。
pub struct ThinghkUpdateState {
/// need_stop 等待阶段的确认通道(前端调 confirm 命令时唤醒 apply 继续)
confirm_tx: Mutex<Option<oneshot::Sender<()>>>,
/// 取消标志 + 唤醒通道(前端调 cancel 命令时置位,apply 等待循环立即返回)
cancel_flag: AtomicBool,
cancel_tx: watch::Sender<bool>,
cancel_rx: watch::Receiver<bool>,
}
impl ThinghkUpdateState {
pub fn new() -> Self {
let (tx, rx) = watch::channel(false);
Self {
confirm_tx: Mutex::new(None),
cancel_flag: AtomicBool::new(false),
cancel_tx: tx,
cancel_rx: rx,
}
}
/// 前端已停止监控内核,唤醒 apply 继续解压替换
fn confirm(&self) {
if let Some(tx) = self.confirm_tx.lock().unwrap_or_else(|e| e.into_inner()).take() {
let _ = tx.send(());
}
}
/// 取消更新:置位取消标志并唤醒 apply 等待循环
fn cancel(&self) {
self.cancel_flag.store(true, Ordering::SeqCst);
let _ = self.cancel_tx.send(true);
}
/// 进入新的 apply 流程前复位取消标志
fn reset(&self) {
self.cancel_flag.store(false, Ordering::SeqCst);
let _ = self.cancel_tx.send(false);
*self.confirm_tx.lock().unwrap_or_else(|e| e.into_inner()) = None;
}
}
/// 发布仓库(Gitea
const GITEA_REPO: &str = "LFeng/Thing";
const GITEA_BASE: &str = "https://gitea.atie.fun";
@@ -125,72 +172,8 @@ async fn fetch_latest_release() -> Result<LatestRelease, String> {
})
}
// ---------- 下载 / 解压 ----------
/// 下载文件到 dest,期间通过 UPDATE_PROGRESS 事件上报进度
async fn download_with_progress(app: &AppHandle, url: &str, dest: &Path) -> Result<(), String> {
// 注意:reqwest 的 timeout 是"从连接到响应体读完"的总超时。大文件(如
// thing.exe 10+MB)在慢速网络下 30s 内读不完会被掐断流导致下载到一半失败
// (此前 bug:更新包总在 ~50% 处中断)。这里只限制连接建立 15s,
// 总超时放宽到 10 分钟兜底防止永久悬挂。
let client = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(15))
.timeout(std::time::Duration::from_secs(600))
.build()
.map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?;
let resp = client
.get(url)
.header("User-Agent", "thing-app")
.send()
.await
.map_err(|e| format!("下载请求失败: {}", e))?;
if !resp.status().is_success() {
return Err(format!("下载失败: HTTP {}", resp.status()));
}
let total = resp.content_length();
let mut stream = resp.bytes_stream();
let mut file = fs::File::create(dest).map_err(|e| format!("创建文件失败: {}", e))?;
let mut downloaded: u64 = 0;
let mut last_percent: u8 = 0;
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| format!("读取下载流失败: {}", e))?;
file.write_all(&chunk).map_err(|e| format!("写入文件失败: {}", e))?;
downloaded += chunk.len() as u64;
let percent = match total {
Some(t) if t > 0 => ((downloaded as f64 / t as f64) * 100.0) as u8,
_ => 0,
};
if percent >= last_percent + 1 {
last_percent = percent;
let _ = app.emit(
UPDATE_PROGRESS,
UpdateProgress {
stage: "downloading".into(),
percent,
downloaded_bytes: downloaded,
total_bytes: total,
message: format!(
"已下载 {:.2} MB / {:.2} MB",
downloaded as f64 / 1024.0 / 1024.0,
total.unwrap_or(0) as f64 / 1024.0 / 1024.0
),
},
);
}
}
file.flush().map_err(|e| format!("flush 失败: {}", e))?;
let _ = app.emit(
UPDATE_PROGRESS,
UpdateProgress {
stage: "downloaded".into(),
percent: 100,
downloaded_bytes: downloaded,
total_bytes: total,
message: "下载完成".into(),
},
);
Ok(())
}
// ---------- 解压 ----------
// ThingHK 内核更新包由前端下载模块负责下载(同 mihomo),此处仅解压替换。
/// 用 zip crate 解压(纯 Rust,避免 PowerShell 执行策略问题)
fn extract_zip(zip_path: &Path, dest: &Path) -> Result<(), String> {
@@ -377,47 +360,72 @@ pub async fn update_install(app: AppHandle, downloaded_path: String) -> Result<(
Ok(())
}
/// 更新 ThingHK 内核:下载 thing-hk_{v}.zip → 停止监控内核 → 覆盖内核文件
/// 应用 ThingHK 内核更新:下载阶段已由下载模块完成,本命令仅做
/// need_stop(等待前端停止监控内核并确认)→ 解压 → 替换。
/// 进度通过 UPDATE_PROGRESS 事件上报,前端据 need_stop 弹出确认对话框。
#[tauri::command]
#[specta::specta]
pub async fn update_thinghk(app: AppHandle) -> Result<(), String> {
let result = update_thinghk_inner(&app).await;
pub async fn update_thinghk_apply(
app: AppHandle,
state: tauri::State<'_, ThinghkUpdateState>,
zip_path: String,
) -> Result<(), String> {
state.reset();
let result = update_thinghk_apply_inner(&app, &state, PathBuf::from(&zip_path)).await;
if let Err(ref e) = result {
// 失败 emit error 阶段避免前端进度卡在最后状态无提示
let _ = app.emit(
UPDATE_PROGRESS,
UpdateProgress {
stage: "error".into(),
percent: 0,
downloaded_bytes: 0,
total_bytes: None,
message: e.clone(),
},
);
// 取消是用户主动行为,静默返回即可;其余失败 emit error 阶段避免前端进度卡
if e != THINGHK_UPDATE_CANCELLED {
let _ = app.emit(
UPDATE_PROGRESS,
UpdateProgress {
stage: "error".into(),
percent: 0,
downloaded_bytes: 0,
total_bytes: None,
message: e.clone(),
},
);
}
}
result
}
async fn update_thinghk_inner(app: &AppHandle) -> Result<(), String> {
let latest = fetch_latest_release().await?;
let asset = latest
.assets
.iter()
.find(|a| a.name.starts_with("thing-hk_") && a.name.ends_with(".zip"))
.ok_or("未在 release 中找到 ThingHK 内核包".to_string())?;
// 停止监控内核(含提权模式的 /shutdown 兜底由前端先停模块),避免 exe 被占用
if let Some(monitor) = app.try_state::<crate::monitor_kernel::MonitorKernel>() {
monitor.stop_subscription(app).await;
async fn update_thinghk_apply_inner(
app: &AppHandle,
state: &ThinghkUpdateState,
zip_path: PathBuf,
) -> Result<(), String> {
if !zip_path.exists() {
return Err(format!("下载文件不存在: {}", zip_path.display()));
}
if let Some(pm) = app.try_state::<crate::process_manager::ProcessManager>() {
let _ = pm.stop("monitor");
// need_stop:等待前端停止监控内核并确认(exe 被占用会导致覆盖失败)。
// 确认/取消由 confirm/cancel 命令跨命令唤醒(与 mihomo need_stop 同构)。
let _ = app.emit(
UPDATE_PROGRESS,
UpdateProgress {
stage: "need_stop".into(),
percent: 90,
downloaded_bytes: 0,
total_bytes: None,
message: "需要停止监控内核才能继续安装".into(),
},
);
let (tx, mut rx) = oneshot::channel::<()>();
*state.confirm_tx.lock().unwrap_or_else(|e| e.into_inner()) = Some(tx);
let mut cancel_rx = state.cancel_rx.clone();
loop {
if state.cancel_flag.load(Ordering::SeqCst) {
*state.confirm_tx.lock().unwrap_or_else(|e| e.into_inner()) = None;
return Err(THINGHK_UPDATE_CANCELLED.to_string());
}
tokio::select! {
_ = &mut rx => break,
_ = cancel_rx.changed() => {}
}
}
// 等待进程退出释放文件句柄
tokio::time::sleep(std::time::Duration::from_millis(600)).await;
let temp_dir = std::env::temp_dir().join("thing-update");
fs::create_dir_all(&temp_dir).map_err(|e| format!("创建临时目录失败: {}", e))?;
let zip_path = temp_dir.join(&asset.name);
download_with_progress(app, &asset.browser_download_url, &zip_path).await?;
// 解压阶段
let _ = app.emit(
UPDATE_PROGRESS,
UpdateProgress {
@@ -428,9 +436,11 @@ async fn update_thinghk_inner(app: &AppHandle) -> Result<(), String> {
message: "正在解压内核...".into(),
},
);
let temp_dir = std::env::temp_dir().join("thing-update");
let extract_dir = temp_dir.join("thinghk_extract");
let _ = fs::remove_dir_all(&extract_dir);
extract_zip(&zip_path, &extract_dir)?;
// 在解压目录中查找 ThingHK.exe
let exe_path = find_thinghk_exe(&extract_dir).ok_or("内核包中未找到 ThingHK.exe".to_string())?;
let app_data = app
@@ -441,6 +451,7 @@ async fn update_thinghk_inner(app: &AppHandle) -> Result<(), String> {
fs::create_dir_all(&cores_dir).map_err(|e| format!("创建内核目录失败: {}", e))?;
fs::copy(&exe_path, cores_dir.join("ThingHK.exe"))
.map_err(|e| format!("覆盖内核文件失败(请确认监控模块已停止): {}", e))?;
// 清理临时文件
let _ = fs::remove_file(&zip_path);
let _ = fs::remove_dir_all(&extract_dir);
@@ -457,6 +468,22 @@ async fn update_thinghk_inner(app: &AppHandle) -> Result<(), String> {
Ok(())
}
/// 前端已停止监控内核,确认继续解压替换(唤醒 need_stop 等待)
#[tauri::command]
#[specta::specta]
pub fn update_thinghk_confirm(state: tauri::State<'_, ThinghkUpdateState>) -> Result<(), String> {
state.confirm();
Ok(())
}
/// 取消 ThingHK 内核更新(need_stop 等待阶段有效:唤醒 apply 以「已取消」返回,zip 保留便于重试)
#[tauri::command]
#[specta::specta]
pub fn update_thinghk_cancel(state: tauri::State<'_, ThinghkUpdateState>) -> Result<(), String> {
state.cancel();
Ok(())
}
fn find_thinghk_exe(dir: &Path) -> Option<PathBuf> {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {