From c341b9b238a7526e487519f180a24200171f4200 Mon Sep 17 00:00:00 2001 From: wonfen Date: Tue, 6 Oct 2026 14:04:04 +0800 Subject: [PATCH 1/2] fix(updater): install the already downloaded update instead of fetching it again --- Cargo.lock | 1 + Changelog.md | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/cmd/app.rs | 10 +- src-tauri/src/core/updater.rs | 249 ++++++++++-------- src-tauri/src/lib.rs | 1 + src/components/setting/mods/update-viewer.tsx | 6 +- src/services/cmds.ts | 4 + 8 files changed, 162 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6e551278..d4bb35416 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1113,6 +1113,7 @@ dependencies = [ "getrandom 0.4.3", "libc", "log", + "minisign-verify", "nanoid", "network-interface", "objc2", diff --git a/Changelog.md b/Changelog.md index a7add527d..a6e67774b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ - 修复端口设置混淆保存值与运行值、输入无效时缺少提示的问题 - 修复从开始菜单再次打开应用后,窗口反复弹出并提示启动失败的问题 - 修复多个代理集合包含同名节点时,代理组中的节点变灰并显示 ambiguous、无法选择的问题 +- 修复手动更新时重复下载后台已下载的安装包、启动时选「稍后」后每次启动都重新下载的问题 **🖥️ Windows** diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 75d4007a7..277aa0ad2 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -86,6 +86,7 @@ flate2 = "1.1.10" reqwest_dav = "0.3.3" aes-gcm = { version = "0.11.1" } base64 = "0.23.1" +minisign-verify = "0.2.5" sha2 = "0.11" getrandom = "0.4.3" futures = "0.3.34" diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 635c8f959..d6b4923ce 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use crate::{cmd::StringifyErr as _, feat, utils::dirs}; +use crate::{cmd::StringifyErr as _, core::SilentUpdater, feat, utils::dirs}; use smartstring::alias::String; use tauri::{AppHandle, Manager as _}; @@ -44,6 +44,14 @@ pub async fn restart_app() -> CmdResult<()> { Ok(()) } +#[tauri::command] +pub async fn install_cached_update(app_handle: AppHandle, version: String) -> CmdResult { + SilentUpdater::global() + .install_cached(&app_handle, &version) + .await + .stringify_err() +} + #[tauri::command] pub fn get_app_dir() -> CmdResult { let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into(); diff --git a/src-tauri/src/core/updater.rs b/src-tauri/src/core/updater.rs index 2cb56f9ea..0af10dee4 100644 --- a/src-tauri/src/core/updater.rs +++ b/src-tauri/src/core/updater.rs @@ -2,7 +2,6 @@ use crate::{config::Config, singleton, utils::dirs}; use anyhow::{Context as _, Result}; use chrono::Utc; use clash_verge_logging::{Type, logging}; -use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::{ path::PathBuf, @@ -12,9 +11,6 @@ use tauri_plugin_updater::{Update, UpdaterExt as _}; pub struct SilentUpdater { update_ready: AtomicBool, - pending_bytes: RwLock>>, - pending_update: RwLock>, - pending_version: RwLock>, } singleton!(SilentUpdater, SILENT_UPDATER); @@ -23,9 +19,6 @@ impl SilentUpdater { const fn new() -> Self { Self { update_ready: AtomicBool::new(false), - pending_bytes: RwLock::new(None), - pending_update: RwLock::new(None), - pending_version: RwLock::new(None), } } @@ -99,6 +92,72 @@ impl SilentUpdater { } } } + + /// Returns the cached installer for `update`, deleting a cache that does not match it. + fn verified_cache(app_handle: &tauri::AppHandle, update: &Update) -> Option> { + let meta = Self::read_cache_meta().ok()?; + let bytes = if meta.version == update.version { + Self::read_cache_bytes().and_then(|bytes| { + verify_signature(app_handle, &bytes, &update.signature, &update.version)?; + Ok(bytes) + }) + } else { + Err(anyhow::anyhow!("server offers v{}", update.version)) + }; + match bytes { + Ok(bytes) => Some(bytes), + Err(e) => { + logging!( + info, + Type::System, + "Update cache v{} is unusable: {e:#}, cleaning up", + meta.version + ); + Self::delete_cache(); + None + } + } + } +} + +/// The cache directory is user-writable and feeds an elevated installer, so it is re-verified +/// with the same rules `Update::download` applies. +fn verify_signature(app_handle: &tauri::AppHandle, bytes: &[u8], signature: &str, version: &str) -> Result<()> { + use base64::Engine as _; + + let decode = |value: &str| -> Result { + Ok(String::from_utf8( + base64::engine::general_purpose::STANDARD.decode(value)?, + )?) + }; + let config: tauri_plugin_updater::Config = serde_json::from_value( + app_handle + .config() + .plugins + .0 + .get("updater") + .cloned() + .context("updater is not configured")?, + )?; + + let public_key = minisign_verify::PublicKey::decode(&decode(&config.pubkey)?)?; + let signature = minisign_verify::Signature::decode(&decode(signature)?)?; + public_key + .verify(bytes, &signature, true) + .context("signature mismatch")?; + + match signature + .trusted_comment() + .split('\t') + .find_map(|field| field.strip_prefix("version:")) + { + Some(signed) => anyhow::ensure!( + signed.trim_start_matches('v') == version, + "signed for v{signed}, not v{version}" + ), + None => anyhow::ensure!(!config.require_signed_version, "signature carries no version"), + } + Ok(()) } pub fn is_build_to_stable(current: &str, remote: &str) -> bool { @@ -148,6 +207,23 @@ fn nsis_language_id(app_language: &str) -> &'static str { } impl SilentUpdater { + /// Checks the server; Windows receives `/LANG` to suppress the NSIS language dialog, + /// see `packages/windows/installer.nsi`. + async fn check(app_handle: &tauri::AppHandle) -> Result> { + let updater_builder = app_handle.updater_builder(); + #[cfg(target_os = "windows")] + let updater_builder = { + let verge_lang = Config::verge().await.latest_arc().language.clone(); + let lang_id = nsis_language_id(&clash_verge_i18n::current_language(verge_lang.as_deref())); + updater_builder.installer_arg(format!("/LANG={lang_id}")) + }; + Ok(updater_builder.build()?.check().await?) + } + + async fn install(update: Update, bytes: Vec) -> Result<()> { + Ok(tokio::task::spawn_blocking(move || update.install(&bytes)).await??) + } + /// Installs a newer cached update before normal startup, if the user confirms. pub async fn try_install_on_startup(&self, app_handle: &tauri::AppHandle) -> bool { let current_version = env!("CARGO_PKG_VERSION"); @@ -185,71 +261,30 @@ impl SilentUpdater { return false; } - let bytes = match Self::read_cache_bytes() { - Ok(b) => b, - Err(e) => { + let update = match Self::check(app_handle).await { + Ok(Some(u)) => u, + Ok(None) => { logging!( - warn, + info, Type::System, - "Failed to read cached update bytes: {e:#}, cleaning up" + "No update available from server, cache may be stale, cleaning up" ); Self::delete_cache(); return false; } - }; - - // Refresh metadata without re-downloading. Windows receives `/LANG` to suppress the - // NSIS language dialog; see `packages/windows/installer.nsi`. - let updater_builder = app_handle.updater_builder(); - #[cfg(target_os = "windows")] - let updater_builder = { - let verge_lang = Config::verge().await.latest_arc().language.clone(); - let lang_id = nsis_language_id(&clash_verge_i18n::current_language(verge_lang.as_deref())); - updater_builder.installer_arg(format!("/LANG={lang_id}")) - }; - let update = match updater_builder.build() { - Ok(updater) => match updater.check().await { - Ok(Some(u)) => u, - Ok(None) => { - logging!( - info, - Type::System, - "No update available from server, cache may be stale, cleaning up" - ); - Self::delete_cache(); - return false; - } - Err(e) => { - logging!( - warn, - Type::System, - "Failed to check for update at startup: {e}, will retry next launch" - ); - return false; // Keep cache for next attempt - } - }, Err(e) => { logging!( warn, Type::System, - "Failed to create updater: {e}, will retry next launch" + "Failed to check for update at startup: {e:#}, will retry next launch" ); - return false; + return false; // Keep cache for next attempt } }; - // A changed server version invalidates the cached bytes. - if update.version != *cached_version { - logging!( - info, - Type::System, - "Server version ({}) != cached version ({}), cache is stale, cleaning up", - update.version, - cached_version - ); - Self::delete_cache(); + let Some(bytes) = Self::verified_cache(app_handle, &update) else { return false; - } + }; let version = update.version.clone(); logging!(info, Type::System, "Installing cached update v{version} at startup..."); @@ -257,49 +292,49 @@ impl SilentUpdater { Self::show_update_splash(app_handle, &version); // `install()` may hang (#2558); on Windows NSIS can take over without returning. - let install_result = tokio::task::spawn_blocking({ - let bytes = bytes.clone(); - let update = update.clone(); - move || update.install(&bytes) - }); - - let success = match tokio::time::timeout(std::time::Duration::from_secs(30), install_result).await { - Ok(Ok(Ok(()))) => { + let install = tokio::time::timeout(std::time::Duration::from_secs(30), Self::install(update, bytes)); + match install + .await + .unwrap_or_else(|_| Err(anyhow::anyhow!("install timed out (30s)"))) + { + Ok(()) => { logging!(info, Type::System, "Update v{version} install triggered at startup"); Self::delete_cache(); true } - Ok(Ok(Err(e))) => { + Err(e) => { logging!( warn, Type::System, - "Startup install failed: {e}, will retry next launch" + "Startup install failed: {e:#}, will retry next launch" ); + Self::close_update_splash(app_handle); false } - Ok(Err(e)) => { - logging!( - warn, - Type::System, - "Startup install task panicked: {e}, will retry next launch" - ); - false - } - Err(_) => { - logging!( - warn, - Type::System, - "Startup install timed out (30s), will retry next launch" - ); - false + } + } + + /// Installs the background-downloaded `version`; `false` sends the caller to a fresh download. + pub async fn install_cached(&self, app_handle: &tauri::AppHandle, version: &str) -> Result { + if Self::read_cache_meta().map_or(true, |meta| meta.version != version) { + return Ok(false); + } + let update = match Self::check(app_handle).await { + Ok(Some(update)) if update.version == version => update, + Ok(_) => return Ok(false), + Err(e) => { + logging!(warn, Type::System, "Cached update check failed: {e:#}"); + return Ok(false); } }; + let Some(bytes) = Self::verified_cache(app_handle, &update) else { + return Ok(false); + }; - if !success { - Self::close_update_splash(app_handle); - } - - success + logging!(info, Type::System, "Installing cached update v{version}..."); + Self::install(update, bytes).await?; + Self::delete_cache(); + Ok(true) } } @@ -454,29 +489,27 @@ impl SilentUpdater { return Ok(()); } - logging!(info, Type::System, "Silent updater: downloading v{version}..."); - let bytes = update - .download( - |chunk_len, content_len| { - logging!( - debug, - Type::System, - "Silent updater download progress: chunk={chunk_len}, total={content_len:?}" - ); - }, - || { - logging!(info, Type::System, "Silent updater: download complete"); - }, - ) - .await?; - - if let Err(e) = Self::write_cache(&bytes, &version) { - logging!(warn, Type::System, "Silent updater: failed to write cache: {e:#}"); + if Self::verified_cache(app_handle, &update).is_some() { + logging!(info, Type::System, "Silent updater: v{version} already cached"); + } else { + logging!(info, Type::System, "Silent updater: downloading v{version}..."); + let bytes = update + .download( + |chunk_len, content_len| { + logging!( + debug, + Type::System, + "Silent updater download progress: chunk={chunk_len}, total={content_len:?}" + ); + }, + || { + logging!(info, Type::System, "Silent updater: download complete"); + }, + ) + .await?; + Self::write_cache(&bytes, &version)?; } - *self.pending_bytes.write() = Some(bytes); - *self.pending_update.write() = Some(update); - *self.pending_version.write() = Some(version.clone()); self.update_ready.store(true, Ordering::Release); logging!( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bbd71c092..89ba4f36d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -139,6 +139,7 @@ mod app_init { cmd::get_network_interfaces, cmd::get_system_hostname, cmd::restart_app, + cmd::install_cached_update, cmd::restart_core, cmd::upgrade_clash_core, cmd::get_runtime_state, diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index e80ac9d93..8b6b65971 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -15,7 +15,7 @@ import type { Options as ReactMarkdownOptions } from 'react-markdown' import { BaseDialog, DialogRef } from '@/components/base' import { useUpdate } from '@/hooks/use-update' -import { restartApp } from '@/services/cmds' +import { installCachedUpdate, restartApp } from '@/services/cmds' import { showNotice } from '@/services/notice-service' import { useSetUpdateState, useUpdateState } from '@/services/states' import { openExternalUrl } from '@/utils/open-external-url' @@ -270,7 +270,9 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { } try { - await updateInfo.downloadAndInstall(onDownloadEvent) + if (!(await installCachedUpdate(updateInfo.version))) { + await updateInfo.downloadAndInstall(onDownloadEvent) + } await restartApp() } catch (err: any) { showNotice.error(err) diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 9713abf4f..640dc3fc1 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -269,6 +269,10 @@ export async function restartApp() { return invoke('restart_app') } +export async function installCachedUpdate(version: string) { + return invoke('install_cached_update', { version }) +} + export async function getAppDir() { return invoke('get_app_dir') } From dba0f471bf242875e079e8978dc6eac760e06054 Mon Sep 17 00:00:00 2001 From: wonfen Date: Tue, 6 Oct 2026 14:13:09 +0800 Subject: [PATCH 2/2] fix(updater): retry slow update downloads from other sources and honour cancel --- Changelog.md | 1 + src-tauri/src/cmd/app.rs | 24 ++- src-tauri/src/core/updater.rs | 176 +++++++++++++++--- src-tauri/src/lib.rs | 3 +- src/components/setting/mods/update-viewer.tsx | 18 +- src/services/cmds.ts | 16 +- 6 files changed, 196 insertions(+), 42 deletions(-) diff --git a/Changelog.md b/Changelog.md index a6e67774b..6078a6885 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ - 修复从开始菜单再次打开应用后,窗口反复弹出并提示启动失败的问题 - 修复多个代理集合包含同名节点时,代理组中的节点变灰并显示 ambiguous、无法选择的问题 - 修复手动更新时重复下载后台已下载的安装包、启动时选「稍后」后每次启动都重新下载的问题 +- 修复下载更新时镜像缓慢或卡住不会切换下载地址、点击「取消」无法中止下载的问题 **🖥️ Windows** diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index d6b4923ce..425d31f7b 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -1,7 +1,12 @@ use super::CmdResult; -use crate::{cmd::StringifyErr as _, core::SilentUpdater, feat, utils::dirs}; +use crate::{ + cmd::StringifyErr as _, + core::{SilentUpdater, updater::DownloadEvent}, + feat, + utils::dirs, +}; use smartstring::alias::String; -use tauri::{AppHandle, Manager as _}; +use tauri::{AppHandle, Manager as _, ipc::Channel}; #[tauri::command] pub async fn open_app_dir() -> CmdResult<()> { @@ -45,13 +50,24 @@ pub async fn restart_app() -> CmdResult<()> { } #[tauri::command] -pub async fn install_cached_update(app_handle: AppHandle, version: String) -> CmdResult { +pub async fn install_update( + app_handle: AppHandle, + version: String, + on_event: Channel, +) -> CmdResult { SilentUpdater::global() - .install_cached(&app_handle, &version) + .install_update(&app_handle, &version, |event| { + let _ = on_event.send(event); + }) .await .stringify_err() } +#[tauri::command] +pub fn cancel_update_download() { + SilentUpdater::global().cancel_download(); +} + #[tauri::command] pub fn get_app_dir() -> CmdResult { let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into(); diff --git a/src-tauri/src/core/updater.rs b/src-tauri/src/core/updater.rs index 0af10dee4..77f06fe87 100644 --- a/src-tauri/src/core/updater.rs +++ b/src-tauri/src/core/updater.rs @@ -2,15 +2,19 @@ use crate::{config::Config, singleton, utils::dirs}; use anyhow::{Context as _, Result}; use chrono::Utc; use clash_verge_logging::{Type, logging}; +use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::{ path::PathBuf, - sync::atomic::{AtomicBool, Ordering}, + sync::atomic::{AtomicBool, AtomicU64, Ordering}, + time::Duration, }; use tauri_plugin_updater::{Update, UpdaterExt as _}; +use tokio_util::sync::CancellationToken; pub struct SilentUpdater { update_ready: AtomicBool, + manual_cancel: Mutex>, } singleton!(SilentUpdater, SILENT_UPDATER); @@ -19,6 +23,7 @@ impl SilentUpdater { const fn new() -> Self { Self { update_ready: AtomicBool::new(false), + manual_cancel: Mutex::new(None), } } @@ -206,6 +211,103 @@ fn nsis_language_id(app_language: &str) -> &'static str { } } +/// Same wire shape as the updater plugin's JS `DownloadEvent`. +#[derive(Clone, Serialize)] +#[serde(tag = "event", content = "data")] +pub enum DownloadEvent { + #[serde(rename_all = "camelCase")] + Started { + content_length: Option, + }, + #[serde(rename_all = "camelCase")] + Progress { + chunk_length: usize, + }, + Finished, +} + +const MIRRORS: [&str; 2] = ["https://update.hwdns.net/", "https://gh-proxy.org/"]; +/// A source delivering less than this per window is dropped for the next one. +const MIN_PROGRESS: u64 = 1024 * 1024; +const PROGRESS_WINDOW: Duration = Duration::from_secs(30); + +/// The manifest's URL, then the other mirror, then GitHub itself. +fn download_sources(url: &tauri::Url) -> Vec { + let origin = MIRRORS + .iter() + .find_map(|mirror| url.as_str().strip_prefix(mirror)) + .unwrap_or(url.as_str()); + let mut sources = vec![url.clone()]; + if origin.starts_with("https://github.com/") { + let candidates = MIRRORS.iter().map(|mirror| format!("{mirror}{origin}")); + for candidate in candidates.chain([origin.to_owned()]) { + if let Ok(candidate) = tauri::Url::parse(&candidate) + && !sources.contains(&candidate) + { + sources.push(candidate); + } + } + } + sources +} + +/// Tries every source with a throughput floor, then again stopping only on a stall, so a link +/// that is slow everywhere still finishes. +async fn download(update: &Update, on_event: impl Fn(DownloadEvent) + Sync) -> Result> { + let sources = download_sources(&update.download_url); + let mut last_error = None; + for floor in [MIN_PROGRESS, 1] { + for url in &sources { + logging!(info, Type::System, "Downloading update v{} from {url}", update.version); + match download_from(update, url, floor, &on_event).await { + Ok(bytes) => { + on_event(DownloadEvent::Finished); + return Ok(bytes); + } + Err(e) => { + logging!(warn, Type::System, "Update download from {url} failed: {e:#}"); + last_error = Some(e); + } + } + } + } + Err(last_error.unwrap_or_else(|| anyhow::anyhow!("no update download source"))) +} + +async fn download_from( + update: &Update, + url: &tauri::Url, + floor: u64, + on_event: &(impl Fn(DownloadEvent) + Sync), +) -> Result> { + let mut source = update.clone(); + source.download_url = url.clone(); + let received = AtomicU64::new(0); + let mut started = false; + let fetch = source.download( + |chunk_length, content_length| { + if !std::mem::replace(&mut started, true) { + on_event(DownloadEvent::Started { content_length }); + } + received.fetch_add(chunk_length as u64, Ordering::Relaxed); + on_event(DownloadEvent::Progress { chunk_length }); + }, + || {}, + ); + tokio::pin!(fetch); + let mut seen = 0; + loop { + tokio::select! { + result = &mut fetch => return Ok(result?), + () = tokio::time::sleep(PROGRESS_WINDOW) => { + let total = received.load(Ordering::Relaxed); + anyhow::ensure!(total - seen >= floor, "only {} bytes in {PROGRESS_WINDOW:?}", total - seen); + seen = total; + } + } + } +} + impl SilentUpdater { /// Checks the server; Windows receives `/LANG` to suppress the NSIS language dialog, /// see `packages/windows/installer.nsi`. @@ -314,28 +416,56 @@ impl SilentUpdater { } } - /// Installs the background-downloaded `version`; `false` sends the caller to a fresh download. - pub async fn install_cached(&self, app_handle: &tauri::AppHandle, version: &str) -> Result { - if Self::read_cache_meta().map_or(true, |meta| meta.version != version) { - return Ok(false); - } - let update = match Self::check(app_handle).await { - Ok(Some(update)) if update.version == version => update, - Ok(_) => return Ok(false), - Err(e) => { - logging!(warn, Type::System, "Cached update check failed: {e:#}"); - return Ok(false); - } + /// Installs `version` for the update dialog, reusing the cache when it holds that version; + /// `false` when cancelled before the installer starts. + pub async fn install_update( + &self, + app_handle: &tauri::AppHandle, + version: &str, + on_event: impl Fn(DownloadEvent) + Sync, + ) -> Result { + let cancel = CancellationToken::new(); + *self.manual_cancel.lock() = Some(cancel.clone()); + let prepared = tokio::select! { + prepared = Self::prepare_install(app_handle, version, on_event) => Some(prepared), + () = cancel.cancelled() => None, }; - let Some(bytes) = Self::verified_cache(app_handle, &update) else { + // `cancel_download` cancels under the lock, so once the token is taken back its state is + // final; a cancel during the last synchronous poll of `prepare_install` still wins. + self.manual_cancel.lock().take(); + let Some((update, bytes)) = prepared.filter(|_| !cancel.is_cancelled()).transpose()? else { + logging!(info, Type::System, "Update v{version} cancelled"); return Ok(false); }; - logging!(info, Type::System, "Installing cached update v{version}..."); + logging!(info, Type::System, "Installing update v{version}..."); Self::install(update, bytes).await?; Self::delete_cache(); Ok(true) } + + async fn prepare_install( + app_handle: &tauri::AppHandle, + version: &str, + on_event: impl Fn(DownloadEvent) + Sync, + ) -> Result<(Update, Vec)> { + let update = Self::check(app_handle) + .await? + .filter(|update| update.version == version) + .with_context(|| format!("v{version} is no longer offered"))?; + let bytes = match Self::verified_cache(app_handle, &update) { + Some(bytes) => bytes, + None => download(&update, on_event).await?, + }; + Ok((update, bytes)) + } + + pub fn cancel_download(&self) { + let mut slot = self.manual_cancel.lock(); + if let Some(cancel) = slot.take() { + cancel.cancel(); + } + } } impl SilentUpdater { @@ -493,20 +623,8 @@ impl SilentUpdater { logging!(info, Type::System, "Silent updater: v{version} already cached"); } else { logging!(info, Type::System, "Silent updater: downloading v{version}..."); - let bytes = update - .download( - |chunk_len, content_len| { - logging!( - debug, - Type::System, - "Silent updater download progress: chunk={chunk_len}, total={content_len:?}" - ); - }, - || { - logging!(info, Type::System, "Silent updater: download complete"); - }, - ) - .await?; + let bytes = download(&update, |_| {}).await?; + logging!(info, Type::System, "Silent updater: download complete"); Self::write_cache(&bytes, &version)?; } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 89ba4f36d..eebd401dd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -139,7 +139,8 @@ mod app_init { cmd::get_network_interfaces, cmd::get_system_hostname, cmd::restart_app, - cmd::install_cached_update, + cmd::install_update, + cmd::cancel_update_download, cmd::restart_core, cmd::upgrade_clash_core, cmd::get_runtime_state, diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index 8b6b65971..537e00dd6 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -15,7 +15,11 @@ import type { Options as ReactMarkdownOptions } from 'react-markdown' import { BaseDialog, DialogRef } from '@/components/base' import { useUpdate } from '@/hooks/use-update' -import { installCachedUpdate, restartApp } from '@/services/cmds' +import { + cancelUpdateDownload, + installUpdate, + restartApp, +} from '@/services/cmds' import { showNotice } from '@/services/notice-service' import { useSetUpdateState, useUpdateState } from '@/services/states' import { openExternalUrl } from '@/utils/open-external-url' @@ -270,10 +274,9 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { } try { - if (!(await installCachedUpdate(updateInfo.version))) { - await updateInfo.downloadAndInstall(onDownloadEvent) + if (await installUpdate(updateInfo.version, onDownloadEvent)) { + await restartApp() } - await restartApp() } catch (err: any) { showNotice.error(err) } finally { @@ -336,7 +339,12 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { okBtn={t('settings.modals.update.actions.update')} cancelBtn={t('shared.actions.cancel')} onClose={() => setOpen(false)} - onCancel={() => setOpen(false)} + onCancel={() => { + if (updateState) { + void cancelUpdateDownload().catch(showNotice.error) + } + setOpen(false) + }} onOk={onUpdate} > ('restart_app') } -export async function installCachedUpdate(version: string) { - return invoke('install_cached_update', { version }) +export async function installUpdate( + version: string, + onEvent: (event: DownloadEvent) => void, +) { + const channel = new Channel() + channel.onmessage = onEvent + return invoke('install_update', { version, onEvent: channel }) +} + +export async function cancelUpdateDownload() { + return invoke('cancel_update_download') } export async function getAppDir() {