mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
695 lines
25 KiB
Rust
695 lines
25 KiB
Rust
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, 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<Option<CancellationToken>>,
|
|
background_cancel: Mutex<Option<CancellationToken>>,
|
|
}
|
|
|
|
singleton!(SilentUpdater, SILENT_UPDATER);
|
|
|
|
impl SilentUpdater {
|
|
const fn new() -> Self {
|
|
Self {
|
|
update_ready: AtomicBool::new(false),
|
|
manual_cancel: Mutex::new(None),
|
|
background_cancel: Mutex::new(None),
|
|
}
|
|
}
|
|
|
|
pub fn is_update_ready(&self) -> bool {
|
|
self.update_ready.load(Ordering::Acquire)
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct UpdateCacheMeta {
|
|
version: String,
|
|
downloaded_at: String,
|
|
}
|
|
|
|
impl SilentUpdater {
|
|
fn cache_dir() -> Result<PathBuf> {
|
|
Ok(dirs::app_home_dir()?.join("update_cache"))
|
|
}
|
|
|
|
fn write_cache(bytes: &[u8], version: &str) -> Result<()> {
|
|
let cache_dir = Self::cache_dir()?;
|
|
std::fs::create_dir_all(&cache_dir)
|
|
.with_context(|| format!("failed to create update cache directory {}", cache_dir.display()))?;
|
|
|
|
let bin_path = cache_dir.join("pending_update.bin");
|
|
std::fs::write(&bin_path, bytes)
|
|
.with_context(|| format!("failed to write update cache {}", bin_path.display()))?;
|
|
|
|
let meta = UpdateCacheMeta {
|
|
version: version.to_string(),
|
|
downloaded_at: Utc::now().to_rfc3339(),
|
|
};
|
|
let meta_path = cache_dir.join("pending_update.json");
|
|
std::fs::write(&meta_path, serde_json::to_string_pretty(&meta)?)
|
|
.with_context(|| format!("failed to write update metadata {}", meta_path.display()))?;
|
|
|
|
logging!(
|
|
info,
|
|
Type::System,
|
|
"Update cache written: version={}, size={} bytes",
|
|
version,
|
|
bytes.len()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn read_cache_bytes() -> Result<Vec<u8>> {
|
|
let bin_path = Self::cache_dir()?.join("pending_update.bin");
|
|
std::fs::read(&bin_path).with_context(|| format!("failed to read update cache {}", bin_path.display()))
|
|
}
|
|
|
|
fn read_cache_meta() -> Result<UpdateCacheMeta> {
|
|
let meta_path = Self::cache_dir()?.join("pending_update.json");
|
|
let content = std::fs::read_to_string(meta_path)?;
|
|
Ok(serde_json::from_str(&content)?)
|
|
}
|
|
|
|
fn delete_cache() {
|
|
if let Ok(cache_dir) = Self::cache_dir()
|
|
&& cache_dir.exists()
|
|
{
|
|
if let Err(e) = std::fs::remove_dir_all(&cache_dir) {
|
|
logging!(
|
|
warn,
|
|
Type::System,
|
|
"Failed to delete update cache {}: {e}",
|
|
cache_dir.display()
|
|
);
|
|
} else {
|
|
logging!(info, Type::System, "Update cache deleted");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the cached installer for `update`, deleting a cache that does not match it.
|
|
fn verified_cache(app_handle: &tauri::AppHandle, update: &Update) -> Option<Vec<u8>> {
|
|
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<String> {
|
|
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 {
|
|
let Some((base, build)) = current.trim_start_matches('v').split_once('+') else {
|
|
return false;
|
|
};
|
|
!build.is_empty() && !base.contains('-') && base == remote.trim_start_matches('v')
|
|
}
|
|
|
|
/// Compares numeric version components after stripping version suffixes.
|
|
fn version_lte(a: &str, b: &str) -> bool {
|
|
let parse = |v: &str| -> Vec<u64> {
|
|
v.trim_start_matches('v')
|
|
.split(['-', '+'])
|
|
.next()
|
|
.unwrap_or("0")
|
|
.split('.')
|
|
.filter_map(|part| part.parse::<u64>().ok())
|
|
.collect()
|
|
};
|
|
|
|
let a_parts = parse(a);
|
|
let b_parts = parse(b);
|
|
let len = a_parts.len().max(b_parts.len());
|
|
|
|
for i in 0..len {
|
|
let av = a_parts.get(i).copied().unwrap_or(0);
|
|
let bv = b_parts.get(i).copied().unwrap_or(0);
|
|
if av < bv {
|
|
return true;
|
|
}
|
|
if av > bv {
|
|
return false;
|
|
}
|
|
}
|
|
true // equal
|
|
}
|
|
|
|
/// Maps UI language to one of the three NSIS translations, defaulting to English.
|
|
#[cfg(target_os = "windows")]
|
|
fn nsis_language_id(app_language: &str) -> &'static str {
|
|
match app_language {
|
|
"zh" | "zhtw" => "2052", // SimpChinese
|
|
"ru" => "1049", // Russian
|
|
_ => "1033", // English
|
|
}
|
|
}
|
|
|
|
/// 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<u64>,
|
|
},
|
|
#[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<tauri::Url> {
|
|
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<Vec<u8>> {
|
|
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<Vec<u8>> {
|
|
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`.
|
|
async fn check(app_handle: &tauri::AppHandle) -> Result<Option<Update>> {
|
|
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<u8>) -> 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 {
|
|
if !Config::verge().await.latest_arc().auto_check_update.unwrap_or(true) {
|
|
Self::delete_cache();
|
|
return false;
|
|
}
|
|
|
|
let current_version = env!("CARGO_PKG_VERSION");
|
|
|
|
let meta = match Self::read_cache_meta() {
|
|
Ok(meta) => meta,
|
|
Err(_) => return false, // No cache, nothing to do
|
|
};
|
|
|
|
let cached_version = &meta.version;
|
|
|
|
if !is_build_to_stable(current_version, cached_version) && version_lte(cached_version, current_version) {
|
|
logging!(
|
|
info,
|
|
Type::System,
|
|
"Update cache version ({}) <= current ({}), cleaning up",
|
|
cached_version,
|
|
current_version
|
|
);
|
|
Self::delete_cache();
|
|
return false;
|
|
}
|
|
|
|
logging!(
|
|
info,
|
|
Type::System,
|
|
"Update cache version ({}) > current ({}), asking user to install",
|
|
cached_version,
|
|
current_version
|
|
);
|
|
|
|
// Preserve the cache when skipped so the next launch asks again.
|
|
if !Self::ask_user_to_install(app_handle, cached_version).await {
|
|
logging!(info, Type::System, "User skipped update install, starting normally");
|
|
return false;
|
|
}
|
|
|
|
let update = match Self::check(app_handle).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
|
|
}
|
|
};
|
|
|
|
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...");
|
|
|
|
Self::show_update_splash(app_handle, &version);
|
|
|
|
// `install()` may hang (#2558); on Windows NSIS can take over without returning.
|
|
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
|
|
}
|
|
Err(e) => {
|
|
logging!(
|
|
warn,
|
|
Type::System,
|
|
"Startup install failed: {e:#}, will retry next launch"
|
|
);
|
|
Self::close_update_splash(app_handle);
|
|
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<bool> {
|
|
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,
|
|
};
|
|
// `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 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<u8>)> {
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// Drops the downloaded update and aborts a background download in flight.
|
|
pub fn discard_pending(&self) {
|
|
let cancel = self.background_cancel.lock().take();
|
|
if let Some(cancel) = cancel {
|
|
cancel.cancel();
|
|
}
|
|
self.update_ready.store(false, Ordering::Release);
|
|
Self::delete_cache();
|
|
}
|
|
}
|
|
|
|
impl SilentUpdater {
|
|
async fn ask_user_to_install(app_handle: &tauri::AppHandle, version: &str) -> bool {
|
|
use tauri_plugin_dialog::{DialogExt as _, MessageDialogButtons, MessageDialogKind};
|
|
|
|
let title = clash_verge_i18n::t!("notifications.updateReady.title");
|
|
let body = clash_verge_i18n::t!("notifications.updateReady.body").replace("{version}", version);
|
|
let install_now = clash_verge_i18n::t!("notifications.updateReady.installNow").into_owned();
|
|
let later = clash_verge_i18n::t!("notifications.updateReady.later").into_owned();
|
|
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
|
|
app_handle
|
|
.dialog()
|
|
.message(body)
|
|
.title(title)
|
|
.buttons(MessageDialogButtons::OkCancelCustom(install_now, later))
|
|
.kind(MessageDialogKind::Info)
|
|
.show(move |confirmed| {
|
|
let _ = tx.send(confirmed);
|
|
});
|
|
|
|
rx.await.unwrap_or(false)
|
|
}
|
|
}
|
|
|
|
impl SilentUpdater {
|
|
/// Uses injected HTML so the splash has no bundled-file dependency.
|
|
fn show_update_splash(app_handle: &tauri::AppHandle, version: &str) {
|
|
use tauri::{WebviewUrl, WebviewWindowBuilder};
|
|
|
|
let window = match WebviewWindowBuilder::new(app_handle, "update-splash", WebviewUrl::App("index.html".into()))
|
|
.title("Clash Verge - Updating")
|
|
.inner_size(300.0, 180.0)
|
|
.resizable(false)
|
|
.maximizable(false)
|
|
.minimizable(false)
|
|
.closable(false)
|
|
.decorations(false)
|
|
.center()
|
|
.always_on_top(true)
|
|
.visible(true)
|
|
.build()
|
|
{
|
|
Ok(w) => w,
|
|
Err(e) => {
|
|
logging!(warn, Type::System, "Failed to create update splash: {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let js = format!(
|
|
r#"
|
|
document.documentElement.innerHTML = `
|
|
<head><meta charset="utf-8"/><style>
|
|
*{{margin:0;padding:0;box-sizing:border-box}}
|
|
html,body{{height:100%;overflow:hidden;user-select:none;-webkit-user-select:none;
|
|
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}}
|
|
body{{display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
background:#1e1e2e;color:#cdd6f4}}
|
|
@media(prefers-color-scheme:light){{
|
|
body{{background:#eff1f5;color:#4c4f69}}
|
|
.bar{{background:#dce0e8}}.fill{{background:#1e66f5}}.sub{{color:#6c6f85}}
|
|
}}
|
|
.icon{{width:48px;height:48px;margin-bottom:16px;animation:pulse 2s ease-in-out infinite}}
|
|
.title{{font-size:16px;font-weight:600;margin-bottom:6px}}
|
|
.sub{{font-size:13px;color:#a6adc8;margin-bottom:20px}}
|
|
.bar{{width:200px;height:4px;background:#313244;border-radius:2px;overflow:hidden}}
|
|
.fill{{height:100%;width:30%;background:#89b4fa;border-radius:2px;animation:ind 1.5s ease-in-out infinite}}
|
|
@keyframes ind{{0%{{width:0;margin-left:0}}50%{{width:40%;margin-left:30%}}100%{{width:0;margin-left:100%}}}}
|
|
@keyframes pulse{{0%,100%{{opacity:1}}50%{{opacity:.6}}}}
|
|
</style></head>
|
|
<body>
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
|
</svg>
|
|
<div class="title">Installing Update...</div>
|
|
<div class="sub">v{version}</div>
|
|
<div class="bar"><div class="fill"></div></div>
|
|
</body>`;
|
|
"#
|
|
);
|
|
|
|
// The new webview may not accept evaluation immediately.
|
|
std::thread::spawn(move || {
|
|
for i in 0..10 {
|
|
std::thread::sleep(std::time::Duration::from_millis(100 * (i + 1)));
|
|
if window.eval(&js).is_ok() {
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
logging!(info, Type::System, "Update splash window shown");
|
|
}
|
|
|
|
fn close_update_splash(app_handle: &tauri::AppHandle) {
|
|
use tauri::Manager as _;
|
|
if let Some(window) = app_handle.get_webview_window("update-splash") {
|
|
let _ = window.close();
|
|
logging!(info, Type::System, "Update splash window closed");
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SilentUpdater {
|
|
async fn check_and_download(&self, app_handle: &tauri::AppHandle) -> Result<()> {
|
|
// Registered before reading the switch, so `discard_pending` either cancels it or ran first.
|
|
let cancel = CancellationToken::new();
|
|
*self.background_cancel.lock() = Some(cancel.clone());
|
|
let auto_check = Config::verge().await.latest_arc().auto_check_update.unwrap_or(true);
|
|
if !auto_check {
|
|
logging!(debug, Type::System, "Silent update skipped: auto_check_update is false");
|
|
return Ok(());
|
|
}
|
|
|
|
if self.is_update_ready() {
|
|
logging!(debug, Type::System, "Silent update skipped: update already pending");
|
|
return Ok(());
|
|
}
|
|
|
|
logging!(info, Type::System, "Silent updater: checking for updates...");
|
|
|
|
let updater = app_handle.updater()?;
|
|
let update = match updater.check().await {
|
|
Ok(Some(update)) => update,
|
|
Ok(None) => {
|
|
logging!(info, Type::System, "Silent updater: no update available");
|
|
return Ok(());
|
|
}
|
|
Err(e) => {
|
|
logging!(warn, Type::System, "Silent updater: check failed: {e}");
|
|
return Err(e.into());
|
|
}
|
|
};
|
|
|
|
let version = update.version.clone();
|
|
logging!(info, Type::System, "Silent updater: update available: v{version}");
|
|
|
|
if let Some(body) = &update.body
|
|
&& body.to_lowercase().contains("break change")
|
|
{
|
|
logging!(
|
|
info,
|
|
Type::System,
|
|
"Silent updater: breaking change detected in v{version}, notifying frontend"
|
|
);
|
|
super::handle::Handle::notice_message(
|
|
"info",
|
|
format!("New version v{version} contains breaking changes. Please update manually."),
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
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 downloaded = tokio::select! {
|
|
bytes = download(&update, |_| {}) => Some(bytes),
|
|
() = cancel.cancelled() => None,
|
|
};
|
|
let Some(bytes) = downloaded else {
|
|
logging!(info, Type::System, "Silent updater: download cancelled");
|
|
return Ok(());
|
|
};
|
|
let bytes = bytes?;
|
|
logging!(info, Type::System, "Silent updater: download complete");
|
|
Self::write_cache(&bytes, &version)?;
|
|
}
|
|
|
|
// Serialized with `discard_pending` in the verge patch, which holds the same lock.
|
|
let _config_write = Config::lock_config_write().await;
|
|
if cancel.is_cancelled() || !Config::verge().await.latest_arc().auto_check_update.unwrap_or(true) {
|
|
logging!(
|
|
info,
|
|
Type::System,
|
|
"Silent updater: auto check was disabled, discarding v{version}"
|
|
);
|
|
Self::delete_cache();
|
|
return Ok(());
|
|
}
|
|
|
|
self.update_ready.store(true, Ordering::Release);
|
|
|
|
logging!(
|
|
info,
|
|
Type::System,
|
|
"Silent updater: v{version} ready for startup install on next launch"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn start_background_check(&self, app_handle: tauri::AppHandle) {
|
|
logging!(info, Type::System, "Silent updater: background task started");
|
|
|
|
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
|
|
|
loop {
|
|
if let Err(e) = self.check_and_download(&app_handle).await {
|
|
logging!(warn, Type::System, "Silent updater: cycle error: {e}");
|
|
}
|
|
|
|
tokio::time::sleep(std::time::Duration::from_secs(24 * 60 * 60)).await;
|
|
}
|
|
}
|
|
}
|