ソースを参照

feat(core): add API to manually trigger updater check (#3712)

Lucas Fernandes Nogueira 3 年 前
コミット
4094494a1b
3 ファイル変更179 行追加122 行削除
  1. 5 0
      .changes/updater-check-api.md
  2. 36 32
      core/tauri/src/app.rs
  3. 138 90
      core/tauri/src/updater/mod.rs

+ 5 - 0
.changes/updater-check-api.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Added `check_for_updates` method to `App` and `AppHandle`.

+ 36 - 32
core/tauri/src/app.rs

@@ -388,6 +388,14 @@ impl<R: Runtime> ManagerBase<R> for App<R> {
 macro_rules! shared_app_impl {
   ($app: ty) => {
     impl<R: Runtime> $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::UpdateResponse<R>> {
+        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<R: Runtime> App<R> {
     });
   }
 
-  /// Listen updater events when dialog are disabled.
-  fn listen_updater_events(&self, handle: AppHandle<R>) {
-    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);
+      }
     }
   }
 }

+ 138 - 90
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<T> = std::result::Result<T, Error>;
 
 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<R: Runtime> {
+  update: core::Update,
+  handle: AppHandle<R>,
+}
+
+impl<R: Runtime> Clone for UpdateResponse<R> {
+  fn clone(&self) -> Self {
+    Self {
+      update: self.update.clone(),
+      handle: self.handle.clone(),
+    }
+  }
+}
+
+impl<R: Runtime> UpdateResponse<R> {
+  /// 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<R: Runtime>(
   updater_config: UpdaterConfig,
@@ -417,103 +456,112 @@ pub(crate) async fn check_update_with_dialog<R: Runtime>(
   }
 }
 
-/// Experimental listener
+/// Updater listener
 /// This function should be run on the main thread once.
-pub(crate) fn listener<R: Runtime>(
-  updater_config: UpdaterConfig,
-  package_info: crate::PackageInfo,
-  handle: &AppHandle<R>,
-) {
-  let handle_ = handle.clone();
-
+pub(crate) fn listener<R: Runtime>(handle: AppHandle<R>) {
   // 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::<Vec<String>>();
-
-    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::<Env>().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<R: Runtime>(
+  handle: AppHandle<R>,
+  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<R: Runtime>(handle: AppHandle<R>) -> Result<UpdateResponse<R>> {
+  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::<Vec<String>>();
+
+  // check updates
+  let env = handle.state::<Env>().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<R: Runtime>(handle: &AppHandle<R>, message: UpdaterEvent) {
   let _ = handle.emit_all(
@@ -543,7 +591,7 @@ async fn prompt_for_install<R: Runtime>(
   app_name: &str,
   body: &str,
   pubkey: String,
-) -> crate::Result<()> {
+) -> Result<()> {
   // remove single & double quote
   let escaped_body = body.replace(&['\"', '\''][..], "");
   let windows = handle.windows();