mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-24 12:42:22 +08:00
chore: sync updater changes into synchronization cleanup
This commit is contained in:
Generated
+1
@@ -1113,6 +1113,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"nanoid",
|
||||
"network-interface",
|
||||
"objc2",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
- 修复端口设置混淆保存值与运行值、输入无效时缺少提示的问题
|
||||
- 修复从开始菜单再次打开应用后,窗口反复弹出并提示启动失败的问题
|
||||
- 修复多个代理集合包含同名节点时,代理组中的节点变灰并显示 ambiguous、无法选择的问题
|
||||
- 修复手动更新时重复下载后台已下载的安装包、启动时选「稍后」后每次启动都重新下载的问题
|
||||
- 修复下载更新时镜像缓慢或卡住不会切换下载地址、点击「取消」无法中止下载的问题
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr as _, 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<()> {
|
||||
@@ -44,6 +49,25 @@ pub async fn restart_app() -> CmdResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_update(
|
||||
app_handle: AppHandle,
|
||||
version: String,
|
||||
on_event: Channel<DownloadEvent>,
|
||||
) -> CmdResult<bool> {
|
||||
SilentUpdater::global()
|
||||
.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<String> {
|
||||
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
|
||||
|
||||
+261
-100
@@ -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::UpdaterExt as _;
|
||||
use tauri_plugin_updater::{Update, UpdaterExt as _};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub struct SilentUpdater {
|
||||
update_ready: AtomicBool,
|
||||
manual_cancel: Mutex<Option<CancellationToken>>,
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +97,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<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 {
|
||||
@@ -140,7 +211,121 @@ 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<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 {
|
||||
let current_version = env!("CARGO_PKG_VERSION");
|
||||
@@ -178,71 +363,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...");
|
||||
@@ -250,49 +394,77 @@ 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"
|
||||
);
|
||||
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"
|
||||
"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);
|
||||
};
|
||||
|
||||
if !success {
|
||||
Self::close_update_splash(app_handle);
|
||||
}
|
||||
logging!(info, Type::System, "Installing update v{version}...");
|
||||
Self::install(update, bytes).await?;
|
||||
Self::delete_cache();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
success
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,24 +621,13 @@ 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 = download(&update, |_| {}).await?;
|
||||
logging!(info, Type::System, "Silent updater: download complete");
|
||||
Self::write_cache(&bytes, &version)?;
|
||||
}
|
||||
|
||||
self.update_ready.store(true, Ordering::Release);
|
||||
|
||||
@@ -142,6 +142,8 @@ mod app_init {
|
||||
cmd::get_network_interfaces,
|
||||
cmd::get_system_hostname,
|
||||
cmd::restart_app,
|
||||
cmd::install_update,
|
||||
cmd::cancel_update_download,
|
||||
cmd::restart_core,
|
||||
cmd::upgrade_clash_core,
|
||||
cmd::get_runtime_state,
|
||||
|
||||
@@ -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 { restartApp } from '@/services/cmds'
|
||||
import {
|
||||
cancelUpdateDownload,
|
||||
installUpdate,
|
||||
restartApp,
|
||||
} from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useSetUpdateState, useUpdateState } from '@/services/states'
|
||||
@@ -271,14 +275,19 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
}
|
||||
|
||||
try {
|
||||
await mutate(() => updateInfo.downloadAndInstall(onDownloadEvent), {
|
||||
id: 'install-app-update',
|
||||
errorNotice: false,
|
||||
})
|
||||
await mutate(() => restartApp(), {
|
||||
id: 'restart-app',
|
||||
errorNotice: false,
|
||||
})
|
||||
const result = await mutate(
|
||||
() => installUpdate(updateInfo.version, onDownloadEvent),
|
||||
{
|
||||
id: 'install-app-update',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
if (result.ok && result.value) {
|
||||
await mutate(() => restartApp(), {
|
||||
id: 'restart-app',
|
||||
errorNotice: false,
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
showNotice.error(err)
|
||||
} finally {
|
||||
@@ -341,7 +350,15 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
okBtn={t('settings.modals.update.actions.update')}
|
||||
cancelBtn={t('shared.actions.cancel')}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onCancel={() => {
|
||||
if (updateState) {
|
||||
void mutate(() => cancelUpdateDownload(), {
|
||||
id: 'cancel-update-download',
|
||||
errorNotice: false,
|
||||
}).catch(showNotice.error)
|
||||
}
|
||||
setOpen(false)
|
||||
}}
|
||||
onOk={onUpdate}
|
||||
>
|
||||
<Box
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Channel, invoke } from '@tauri-apps/api/core'
|
||||
import type { DownloadEvent } from '@tauri-apps/plugin-updater'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import type { CommandFailure } from '@/services/notice-service'
|
||||
@@ -285,6 +286,19 @@ export async function restartApp() {
|
||||
return invoke<void>('restart_app')
|
||||
}
|
||||
|
||||
export async function installUpdate(
|
||||
version: string,
|
||||
onEvent: (event: DownloadEvent) => void,
|
||||
) {
|
||||
const channel = new Channel<DownloadEvent>()
|
||||
channel.onmessage = onEvent
|
||||
return invoke<boolean>('install_update', { version, onEvent: channel })
|
||||
}
|
||||
|
||||
export async function cancelUpdateDownload() {
|
||||
return invoke<void>('cancel_update_download')
|
||||
}
|
||||
|
||||
export async function getAppDir() {
|
||||
return invoke<string>('get_app_dir')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user