From 4094494a1b3125bf01676dabaa69e56cc8741d59 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Thu, 17 Mar 2022 10:04:13 -0300 Subject: [PATCH] feat(core): add API to manually trigger updater check (#3712) --- .changes/updater-check-api.md | 5 + core/tauri/src/app.rs | 68 +++++----- core/tauri/src/updater/mod.rs | 228 ++++++++++++++++++++-------------- 3 files changed, 179 insertions(+), 122 deletions(-) create mode 100644 .changes/updater-check-api.md diff --git a/.changes/updater-check-api.md b/.changes/updater-check-api.md new file mode 100644 index 000000000..cb77ea7c0 --- /dev/null +++ b/.changes/updater-check-api.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Added `check_for_updates` method to `App` and `AppHandle`. diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index edde7a00f..64da62f88 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -388,6 +388,14 @@ impl ManagerBase for App { macro_rules! shared_app_impl { ($app: ty) => { impl $app { + #[cfg(feature = "updater")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))] + /// Runs the updater to check if there is a new app version. + /// It is the same as triggering the `tauri://update` event. + pub async fn check_for_updates(&self) -> updater::Result> { + updater::check(self.app_handle()).await + } + /// Creates a new webview window. /// /// Data URLs are only supported with the `window-data-url` feature flag. @@ -580,44 +588,40 @@ impl App { }); } - /// Listen updater events when dialog are disabled. - fn listen_updater_events(&self, handle: AppHandle) { - let updater_config = self.manager.config().tauri.updater.clone(); - updater::listener(updater_config, self.manager.package_info().clone(), &handle); - } - fn run_updater(&self) { let handle = self.handle(); let handle_ = handle.clone(); let updater_config = self.manager.config().tauri.updater.clone(); // check if updater is active or not - if updater_config.dialog && updater_config.active { - // if updater dialog is enabled spawn a new task - self.run_updater_dialog(); - let config = self.manager.config().tauri.updater.clone(); - let package_info = self.manager.package_info().clone(); - // When dialog is enabled, if user want to recheck - // if an update is available after first start - // invoke the Event `tauri://update` from JS or rust side. - handle.listen_global(updater::EVENT_CHECK_UPDATE, move |_msg| { - let handle = handle_.clone(); - let package_info = package_info.clone(); - let config = config.clone(); - // re-spawn task inside tokyo to launch the download - // we don't need to emit anything as everything is handled - // by the process (user is asked to restart at the end) - // and it's handled by the updater - crate::async_runtime::spawn(async move { - updater::check_update_with_dialog(config, package_info, handle).await + if updater_config.active { + if updater_config.dialog { + // if updater dialog is enabled spawn a new task + self.run_updater_dialog(); + let config = self.manager.config().tauri.updater.clone(); + let package_info = self.manager.package_info().clone(); + // When dialog is enabled, if user want to recheck + // if an update is available after first start + // invoke the Event `tauri://update` from JS or rust side. + handle.listen_global(updater::EVENT_CHECK_UPDATE, move |_msg| { + let handle = handle_.clone(); + let package_info = package_info.clone(); + let config = config.clone(); + // re-spawn task inside tokyo to launch the download + // we don't need to emit anything as everything is handled + // by the process (user is asked to restart at the end) + // and it's handled by the updater + crate::async_runtime::spawn(async move { + updater::check_update_with_dialog(config, package_info, handle).await + }); }); - }); - } else if updater_config.active { - // we only listen for `tauri://update` - // once we receive the call, we check if an update is available or not - // if there is a new update we emit `tauri://update-available` with details - // this is the user responsabilities to display dialog and ask if user want to install - // to install the update you need to invoke the Event `tauri://update-install` - self.listen_updater_events(handle); + } else { + // we only listen for `tauri://update` + // once we receive the call, we check if an update is available or not + // if there is a new update we emit `tauri://update-available` with details + // this is the user responsabilities to display dialog and ask if user want to install + // to install the update you need to invoke the Event `tauri://update-install` + updater::listener(handle); + } } } } diff --git a/core/tauri/src/updater/mod.rs b/core/tauri/src/updater/mod.rs index 7fb873be7..64244f17c 100644 --- a/core/tauri/src/updater/mod.rs +++ b/core/tauri/src/updater/mod.rs @@ -331,6 +331,8 @@ mod core; mod error; pub use self::error::Error; +/// Alias for [`std::result::Result`] using our own [`Error`]. +pub type Result = std::result::Result; use crate::{ api::dialog::blocking::ask, runtime::EventLoopProxy, utils::config::UpdaterConfig, AppHandle, @@ -370,6 +372,43 @@ struct UpdateManifest { body: String, } +/// The response of an updater [`check`]. +pub struct UpdateResponse { + update: core::Update, + handle: AppHandle, +} + +impl Clone for UpdateResponse { + fn clone(&self) -> Self { + Self { + update: self.update.clone(), + handle: self.handle.clone(), + } + } +} + +impl UpdateResponse { + /// Whether the updater found a newer release or not. + pub fn is_update_available(&self) -> bool { + self.update.should_update + } + + /// The current version of the application as read by the updater. + pub fn current_version(&self) -> &str { + &self.update.current_version + } + + /// The latest version of the application found by the updater. + pub fn latest_version(&self) -> &str { + &self.update.version + } + + /// Downloads and installs the update. + pub async fn download_and_install(self) -> Result<()> { + download_and_install(self.handle, self.update).await + } +} + /// Check if there is any new update with builtin dialog. pub(crate) async fn check_update_with_dialog( updater_config: UpdaterConfig, @@ -417,103 +456,112 @@ pub(crate) async fn check_update_with_dialog( } } -/// Experimental listener +/// Updater listener /// This function should be run on the main thread once. -pub(crate) fn listener( - updater_config: UpdaterConfig, - package_info: crate::PackageInfo, - handle: &AppHandle, -) { - let handle_ = handle.clone(); - +pub(crate) fn listener(handle: AppHandle) { // Wait to receive the event `"tauri://update"` + let handle_ = handle.clone(); handle.listen_global(EVENT_CHECK_UPDATE, move |_msg| { - let handle = handle_.clone(); - let package_info = package_info.clone(); - - // prepare our endpoints - let endpoints = updater_config - .endpoints - .as_ref() - .expect("Something wrong with endpoints") - .iter() - .map(|e| e.to_string()) - .collect::>(); - - let pubkey = updater_config.pubkey.clone(); - - // check updates + let handle_ = handle_.clone(); crate::async_runtime::spawn(async move { - let handle = handle.clone(); - let handle_ = handle.clone(); - let pubkey = pubkey.clone(); - let env = handle.state::().inner().clone(); - - match self::core::builder(env) - .urls(&endpoints[..]) - .current_version(&package_info.version) - .build() - .await - { - Ok(updater) => { - // send notification if we need to update - if updater.should_update { - let body = updater.body.clone().unwrap_or_else(|| String::from("")); - - // Emit `tauri://update-available` - let _ = handle.emit_all( - EVENT_UPDATE_AVAILABLE, - UpdateManifest { - body: body.clone(), - date: updater.date.clone(), - version: updater.version.clone(), - }, - ); - let _ = handle.create_proxy().send_event(EventLoopMessage::Updater( - UpdaterEvent::UpdateAvailable { - body, - date: updater.date.clone(), - version: updater.version.clone(), - }, - )); - - // Listen for `tauri://update-install` - handle.once_global(EVENT_INSTALL_UPDATE, move |_msg| { - let handle = handle_.clone(); - let updater = updater.clone(); - - // Start installation - crate::async_runtime::spawn(async move { - // emit {"status": "PENDING"} - send_status_update(&handle, UpdaterEvent::Pending); - - // Launch updater download process - // macOS we display the `Ready to restart dialog` asking to restart - // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here) - // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here) - let update_result = updater.clone().download_and_install(pubkey.clone()).await; - - if let Err(err) = update_result { - // emit {"status": "ERROR", "error": "The error message"} - send_status_update(&handle, UpdaterEvent::Error(err.to_string())); - } else { - // emit {"status": "DONE"} - send_status_update(&handle, UpdaterEvent::Updated); - } - }); - }); - } else { - send_status_update(&handle, UpdaterEvent::AlreadyUpToDate); - } - } - Err(e) => { - send_status_update(&handle, UpdaterEvent::Error(e.to_string())); - } - } + let _ = check(handle_.clone()).await; }); }); } +pub(crate) async fn download_and_install( + handle: AppHandle, + update: core::Update, +) -> Result<()> { + let update = update.clone(); + + // Start installation + // emit {"status": "PENDING"} + send_status_update(&handle, UpdaterEvent::Pending); + + // Launch updater download process + // macOS we display the `Ready to restart dialog` asking to restart + // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here) + // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here) + let update_result = update + .clone() + .download_and_install(handle.config().tauri.updater.pubkey.clone()) + .await; + + if let Err(err) = &update_result { + // emit {"status": "ERROR", "error": "The error message"} + send_status_update(&handle, UpdaterEvent::Error(err.to_string())); + } else { + // emit {"status": "DONE"} + send_status_update(&handle, UpdaterEvent::Updated); + } + update_result +} + +pub(crate) async fn check(handle: AppHandle) -> Result> { + let updater_config = &handle.config().tauri.updater; + let package_info = handle.package_info().clone(); + + // prepare our endpoints + let endpoints = updater_config + .endpoints + .as_ref() + .expect("Something wrong with endpoints") + .iter() + .map(|e| e.to_string()) + .collect::>(); + + // check updates + let env = handle.state::().inner().clone(); + + match self::core::builder(env) + .urls(&endpoints[..]) + .current_version(&package_info.version) + .build() + .await + { + Ok(update) => { + // send notification if we need to update + if update.should_update { + let body = update.body.clone().unwrap_or_else(|| String::from("")); + + // Emit `tauri://update-available` + let _ = handle.emit_all( + EVENT_UPDATE_AVAILABLE, + UpdateManifest { + body: body.clone(), + date: update.date.clone(), + version: update.version.clone(), + }, + ); + let _ = handle.create_proxy().send_event(EventLoopMessage::Updater( + UpdaterEvent::UpdateAvailable { + body, + date: update.date.clone(), + version: update.version.clone(), + }, + )); + + // Listen for `tauri://update-install` + let handle_ = handle.clone(); + let update_ = update.clone(); + handle.once_global(EVENT_INSTALL_UPDATE, move |_msg| { + crate::async_runtime::spawn(async move { + let _ = download_and_install(handle_, update_).await; + }); + }); + } else { + send_status_update(&handle, UpdaterEvent::AlreadyUpToDate); + } + Ok(UpdateResponse { update, handle }) + } + Err(e) => { + send_status_update(&handle, UpdaterEvent::Error(e.to_string())); + Err(e) + } + } +} + // Send a status update via `tauri://update-status` event. fn send_status_update(handle: &AppHandle, message: UpdaterEvent) { let _ = handle.emit_all( @@ -543,7 +591,7 @@ async fn prompt_for_install( app_name: &str, body: &str, pubkey: String, -) -> crate::Result<()> { +) -> Result<()> { // remove single & double quote let escaped_body = body.replace(&['\"', '\''][..], ""); let windows = handle.windows();