Эх сурвалжийг харах

feat(updater): add download progress events (#3734)

Lucas Fernandes Nogueira 3 жил өмнө
parent
commit
f0db3f9b83

+ 5 - 0
.changes/http-api-stream.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Added `bytes_stream` method to `tauri::api::http::Response`.

+ 5 - 0
.changes/updater-download-events.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Added download progress events to the updater.

+ 3 - 3
core/tauri/Cargo.toml

@@ -73,7 +73,7 @@ percent-encoding = "2.1"
 base64 = { version = "0.13", optional = true }
 clap = { version = "3", optional = true }
 notify-rust = { version = "4.5", optional = true }
-reqwest = { version = "0.11", features = [ "json", "multipart" ], optional = true }
+reqwest = { version = "0.11", features = [ "json", "multipart", "stream" ], optional = true }
 bytes = { version = "1", features = [ "serde" ], optional = true }
 attohttpc = { version = "0.18", features = [ "json", "form" ], optional = true }
 open = { version = "2.0", optional = true }
@@ -137,10 +137,10 @@ updater = [
   "fs-extract-api"
 ]
 __updater-docs = [ "minisign-verify", "base64", "http-api", "dialog-ask" ]
-http-api = [ "attohttpc" ]
+http-api = [ "attohttpc", "bytes" ]
 shell-open-api = [ "open", "regex", "tauri-macros/shell-scope" ]
 fs-extract-api = [ "zip" ]
-reqwest-client = [ "reqwest", "bytes" ]
+reqwest-client = [ "reqwest" ]
 process-command-api = [ "shared_child", "os_pipe", "memchr" ]
 dialog = [ "rfd" ]
 notification = [ "notify-rust" ]

+ 70 - 1
core/tauri/src/api/http.rs

@@ -5,6 +5,7 @@
 //! Types and functions related to HTTP request.
 
 use http::{header::HeaderName, Method};
+pub use http::{HeaderMap, StatusCode};
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 use serde_repr::{Deserialize_repr, Serialize_repr};
@@ -352,10 +353,46 @@ pub struct Response(ResponseType, reqwest::Response);
 #[derive(Debug)]
 pub struct Response(ResponseType, attohttpc::Response, Url);
 
+#[cfg(not(feature = "reqwest-client"))]
+struct AttohttpcByteReader(attohttpc::ResponseReader);
+
+#[cfg(not(feature = "reqwest-client"))]
+impl futures::Stream for AttohttpcByteReader {
+  type Item = crate::api::Result<bytes::Bytes>;
+
+  fn poll_next(
+    mut self: std::pin::Pin<&mut Self>,
+    _cx: &mut futures::task::Context<'_>,
+  ) -> futures::task::Poll<Option<Self::Item>> {
+    use std::io::Read;
+    let mut buf = [0; 256];
+    match self.0.read(&mut buf) {
+      Ok(b) => {
+        if b == 0 {
+          futures::task::Poll::Ready(None)
+        } else {
+          futures::task::Poll::Ready(Some(Ok(buf[0..b].to_vec().into())))
+        }
+      }
+      Err(_) => futures::task::Poll::Ready(None),
+    }
+  }
+}
+
 impl Response {
+  /// Get the [`StatusCode`] of this Response.
+  pub fn status(&self) -> StatusCode {
+    self.1.status()
+  }
+
+  /// Get the headers of this Response.
+  pub fn headers(&self) -> &HeaderMap {
+    self.1.headers()
+  }
+
   /// Reads the response as raw bytes.
   pub async fn bytes(self) -> crate::api::Result<RawResponse> {
-    let status = self.1.status().as_u16();
+    let status = self.status().as_u16();
     #[cfg(feature = "reqwest-client")]
     let data = self.1.bytes().await?.to_vec();
     #[cfg(not(feature = "reqwest-client"))]
@@ -363,6 +400,38 @@ impl Response {
     Ok(RawResponse { status, data })
   }
 
+  /// Convert the response into a Stream of [`bytes::Bytes`] from the body.
+  ///
+  /// # Examples
+  ///
+  /// ```no_run
+  /// use futures::StreamExt;
+  ///
+  /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
+  /// let client = tauri::api::http::ClientBuilder::new().build()?;
+  /// let mut stream = client.send(tauri::api::http::HttpRequestBuilder::new("GET", "http://httpbin.org/ip")?)
+  ///   .await?
+  ///   .bytes_stream();
+  ///
+  /// while let Some(item) = stream.next().await {
+  ///     println!("Chunk: {:?}", item?);
+  /// }
+  /// # Ok(())
+  /// # }
+  /// ```
+  pub fn bytes_stream(self) -> impl futures::Stream<Item = crate::api::Result<bytes::Bytes>> {
+    #[cfg(not(feature = "reqwest-client"))]
+    {
+      let (_, _, reader) = self.1.split();
+      AttohttpcByteReader(reader)
+    }
+    #[cfg(feature = "reqwest-client")]
+    {
+      use futures::StreamExt;
+      self.1.bytes_stream().map(|res| res.map_err(Into::into))
+    }
+  }
+
   /// Reads the response.
   ///
   /// Note that the body is serialized to a [`Value`].

+ 4 - 12
core/tauri/src/app.rs

@@ -579,13 +579,9 @@ impl<R: Runtime> App<R> {
 impl<R: Runtime> App<R> {
   /// Runs the updater hook with built-in dialog.
   fn run_updater_dialog(&self) {
-    let updater_config = self.manager.config().tauri.updater.clone();
-    let package_info = self.manager.package_info().clone();
     let handle = self.handle();
 
-    crate::async_runtime::spawn(async move {
-      updater::check_update_with_dialog(updater_config, package_info, handle).await
-    });
+    crate::async_runtime::spawn(async move { updater::check_update_with_dialog(handle).await });
   }
 
   fn run_updater(&self) {
@@ -597,22 +593,18 @@ impl<R: Runtime> App<R> {
       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
-          });
+          crate::async_runtime::spawn(
+            async move { updater::check_update_with_dialog(handle).await },
+          );
         });
       } else {
         // we only listen for `tauri://update`

+ 9 - 1
core/tauri/src/lib.rs

@@ -242,8 +242,16 @@ pub enum UpdaterEvent {
     /// The update version.
     version: String,
   },
-  /// The update is pending.
+  /// The update is pending and about to be downloaded.
   Pending,
+  /// The update download received a progress event.
+  DownloadProgress {
+    /// The amount that was downloaded on this iteration.
+    /// Does not accumulate with previous chunks.
+    chunk_length: usize,
+    /// The total
+    content_length: Option<u64>,
+  },
   /// The update has been applied and the app is now up to date.
   Updated,
   /// The app is already up to date.

+ 94 - 44
core/tauri/src/updater/core.rs

@@ -5,11 +5,15 @@
 use super::error::{Error, Result};
 #[cfg(feature = "updater")]
 use crate::api::file::{ArchiveFormat, Extract, Move};
-use crate::api::{
-  http::{ClientBuilder, HttpRequestBuilder},
-  version,
+use crate::{
+  api::{
+    http::{ClientBuilder, HttpRequestBuilder},
+    version,
+  },
+  AppHandle, Manager, Runtime,
 };
 use base64::decode;
+use futures::StreamExt;
 use http::StatusCode;
 use minisign_verify::{PublicKey, Signature};
 use tauri_utils::{platform::current_exe, Env};
@@ -176,9 +180,9 @@ impl RemoteRelease {
 }
 
 #[derive(Debug)]
-pub struct UpdateBuilder<'a> {
-  /// Environment information.
-  pub env: Env,
+pub struct UpdateBuilder<'a, R: Runtime> {
+  /// Application handle.
+  pub app: AppHandle<R>,
   /// Current version we are running to compare with announced version
   pub current_version: &'a str,
   /// The URLs to checks updates. We suggest at least one fallback on a different domain.
@@ -190,10 +194,10 @@ pub struct UpdateBuilder<'a> {
 }
 
 // Create new updater instance and return an Update
-impl<'a> UpdateBuilder<'a> {
-  pub fn new(env: Env) -> Self {
+impl<'a, R: Runtime> UpdateBuilder<'a, R> {
+  pub fn new(app: AppHandle<R>) -> Self {
     UpdateBuilder {
-      env,
+      app,
       urls: Vec::new(),
       target: None,
       executable_path: None,
@@ -247,7 +251,7 @@ impl<'a> UpdateBuilder<'a> {
     self
   }
 
-  pub async fn build(self) -> Result<Update> {
+  pub async fn build(self) -> Result<Update<R>> {
     let mut remote_release: Option<RemoteRelease> = None;
 
     // make sure we have at least one url
@@ -271,7 +275,7 @@ impl<'a> UpdateBuilder<'a> {
       .ok_or(Error::UnsupportedPlatform)?;
 
     // Get the extract_path from the provided executable_path
-    let extract_path = extract_path_from_executable(&self.env, &executable_path);
+    let extract_path = extract_path_from_executable(&self.app.state::<Env>(), &executable_path);
 
     // Set SSL certs for linux if they aren't available.
     // We do not require to recheck in the download_and_install as we use
@@ -364,7 +368,7 @@ impl<'a> UpdateBuilder<'a> {
 
     // create our new updater
     Ok(Update {
-      env: self.env,
+      app: self.app,
       target,
       extract_path,
       should_update,
@@ -380,14 +384,14 @@ impl<'a> UpdateBuilder<'a> {
   }
 }
 
-pub fn builder<'a>(env: Env) -> UpdateBuilder<'a> {
-  UpdateBuilder::new(env)
+pub fn builder<'a, R: Runtime>(app: AppHandle<R>) -> UpdateBuilder<'a, R> {
+  UpdateBuilder::new(app)
 }
 
-#[derive(Debug, Clone)]
-pub struct Update {
-  /// Environment information.
-  pub env: Env,
+#[derive(Debug)]
+pub struct Update<R: Runtime> {
+  /// Application handle.
+  pub app: AppHandle<R>,
   /// Update description
   pub body: Option<String>,
   /// Should we update or not
@@ -413,17 +417,40 @@ pub struct Update {
   with_elevated_task: bool,
 }
 
-impl Update {
+impl<R: Runtime> Clone for Update<R> {
+  fn clone(&self) -> Self {
+    Self {
+      app: self.app.clone(),
+      body: self.body.clone(),
+      should_update: self.should_update,
+      version: self.version.clone(),
+      current_version: self.current_version.clone(),
+      date: self.date.clone(),
+      target: self.target.clone(),
+      extract_path: self.extract_path.clone(),
+      download_url: self.download_url.clone(),
+      signature: self.signature.clone(),
+      #[cfg(target_os = "windows")]
+      with_elevated_task: self.with_elevated_task,
+    }
+  }
+}
+
+impl<R: Runtime> Update<R> {
   // Download and install our update
   // @todo(lemarier): Split into download and install (two step) but need to be thread safe
-  pub async fn download_and_install(&self, pub_key: String) -> Result {
+  pub(crate) async fn download_and_install<F: Fn(usize, Option<u64>)>(
+    &self,
+    pub_key: String,
+    on_chunk: F,
+  ) -> Result {
     // make sure we can install the update on linux
     // We fail here because later we can add more linux support
     // actually if we use APPIMAGE, our extract path should already
     // be set with our APPIMAGE env variable, we don't need to do
     // anythin with it yet
     #[cfg(target_os = "linux")]
-    if self.env.appimage.is_none() {
+    if self.app.state::<Env>().appimage.is_none() {
       return Err(Error::UnsupportedPlatform);
     }
 
@@ -433,7 +460,7 @@ impl Update {
     headers.insert("User-Agent".into(), "tauri/updater".into());
 
     // Create our request
-    let resp = ClientBuilder::new()
+    let response = ClientBuilder::new()
       .build()?
       .send(
         HttpRequestBuilder::new("GET", self.download_url.as_str())?
@@ -441,23 +468,33 @@ impl Update {
           // wait 20sec for the firewall
           .timeout(20),
       )
-      .await?
-      .bytes()
       .await?;
 
     // make sure it's success
-    if !StatusCode::from_u16(resp.status)
-      .map_err(|e| Error::Network(e.to_string()))?
-      .is_success()
-    {
+    if !response.status().is_success() {
       return Err(Error::Network(format!(
         "Download request failed with status: {}",
-        resp.status
+        response.status()
       )));
     }
 
+    let content_length: Option<u64> = response
+      .headers()
+      .get("Content-Length")
+      .and_then(|value| value.to_str().ok())
+      .and_then(|value| value.parse().ok());
+
+    let mut buffer = Vec::new();
+    let mut stream = response.bytes_stream();
+    while let Some(chunk) = stream.next().await {
+      let chunk = chunk?;
+      let bytes = chunk.as_ref().to_vec();
+      on_chunk(bytes.len(), content_length);
+      buffer.extend(bytes);
+    }
+
     // create memory buffer from our archive (Seek + Read)
-    let mut archive_buffer = Cursor::new(resp.data);
+    let mut archive_buffer = Cursor::new(buffer);
 
     // We need an announced signature by the server
     // if there is no signature, bail out.
@@ -875,7 +912,8 @@ mod test {
       .with_body(generate_sample_raw_json())
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .current_version("0.0.0")
       .url(mockito::server_url())
       .build());
@@ -894,7 +932,8 @@ mod test {
       .with_body(generate_sample_raw_json())
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .current_version("0.0.0")
       .url(mockito::server_url())
       .build());
@@ -913,7 +952,8 @@ mod test {
       .with_body(generate_sample_raw_json())
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .current_version("0.0.0")
       .target("win64")
       .url(mockito::server_url())
@@ -939,7 +979,8 @@ mod test {
       .with_body(generate_sample_raw_json())
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .current_version("10.0.0")
       .url(mockito::server_url())
       .build());
@@ -962,7 +1003,8 @@ mod test {
       ))
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .current_version("1.0.0")
       .url(format!(
         "{}/darwin/{{{{current_version}}}}",
@@ -988,7 +1030,8 @@ mod test {
       ))
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .current_version("1.0.0")
       .url(
         url::Url::parse(&format!(
@@ -1005,7 +1048,8 @@ mod test {
 
     assert!(updater.should_update);
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .current_version("1.0.0")
       .urls(&[url::Url::parse(&format!(
         "{}/darwin/{{{{current_version}}}}",
@@ -1034,7 +1078,8 @@ mod test {
       ))
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .current_version("1.0.0")
       .url(format!(
         "{}/win64/{{{{current_version}}}}",
@@ -1060,7 +1105,8 @@ mod test {
       ))
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .current_version("10.0.0")
       .url(format!(
         "{}/darwin/{{{{current_version}}}}",
@@ -1082,7 +1128,8 @@ mod test {
       .with_body(generate_sample_raw_json())
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .url("http://badurl.www.tld/1".into())
       .url(mockito::server_url())
       .current_version("0.0.1")
@@ -1102,7 +1149,8 @@ mod test {
       .with_body(generate_sample_raw_json())
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .urls(&["http://badurl.www.tld/1".into(), mockito::server_url(),])
       .current_version("0.0.1")
       .build());
@@ -1121,7 +1169,8 @@ mod test {
       .with_body(generate_sample_bad_json())
       .create();
 
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .url(mockito::server_url())
       .current_version("0.0.1")
       .build());
@@ -1202,7 +1251,8 @@ mod test {
     let my_executable = &tmp_dir_path.join("my_app.exe");
 
     // configure the updater
-    let check_update = block!(builder(Default::default())
+    let app = crate::test::mock_app();
+    let check_update = block!(builder(app.handle())
       .url(mockito::server_url())
       // It should represent the executable path, that's why we add my_app.exe in our
       // test path -- in production you shouldn't have to provide it
@@ -1228,7 +1278,7 @@ mod test {
     assert_eq!(updater.version, "2.0.1");
 
     // download, install and validate signature
-    let install_process = block!(updater.download_and_install(pubkey));
+    let install_process = block!(updater.download_and_install(pubkey, |_, _| ()));
     assert!(install_process.is_ok());
 
     // make sure the extraction went well (it should have skipped the main app.app folder)

+ 195 - 76
core/tauri/src/updater/mod.rs

@@ -73,7 +73,7 @@
 //! import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
 //!
 //! try {
-//!     const {shouldUpdate, manifest} = await checkUpdate();
+//!     const { shouldUpdate, manifest } = await checkUpdate();
 //!
 //!     if (shouldUpdate) {
 //!         // display dialog
@@ -93,21 +93,28 @@
 //!
 //! ### Initialize updater and check if a new version is available
 //!
-//! #### If a new version is available, the event `tauri://update-available` is emitted.
-//!
 //! Event : `tauri://update`
 //!
-//! ### Rust
-//! ```ignore
-//! dispatcher.emit("tauri://update", None);
+//! #### Rust
+//! ```no_run
+//! tauri::Builder::default()
+//!   .setup(|app| {
+//!     let handle = app.handle();
+//!     tauri::async_runtime::spawn(async move {
+//!       let response = handle.check_for_updates().await;
+//!     });
+//!     Ok(())
+//!   });
 //! ```
 //!
-//! ### Javascript
+//! #### Javascript
 //! ```js
 //! import { emit } from "@tauri-apps/api/event";
 //! emit("tauri://update");
 //! ```
 //!
+//! **If a new version is available, the event `tauri://update-available` is emitted.**
+//!
 //! ### Listen New Update Available
 //!
 //! Event : `tauri://update-available`
@@ -119,14 +126,26 @@
 //! body       Note announced by the server
 //! ```
 //!
-//! ### Rust
-//! ```ignore
-//! dispatcher.listen("tauri://update-available", move |msg| {
-//!     println("New version available: {:?}", msg);
-//! })
+//! #### Rust
+//! ```no_run
+//! let app = tauri::Builder::default()
+//!   // on an actual app, remove the string argument
+//!   .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
+//!   .expect("error while building tauri application");
+//! app.run(|_app_handle, event| match event {
+//!   tauri::RunEvent::Updater(updater_event) => {
+//!     match updater_event {
+//!       tauri::UpdaterEvent::UpdateAvailable { body, date, version } => {
+//!         println!("update available {} {} {}", body, date, version);
+//!       }
+//!       _ => (),
+//!     }
+//!   }
+//!   _ => {}
+//! });
 //! ```
 //!
-//! ### Javascript
+//! #### Javascript
 //! ```js
 //! import { listen } from "@tauri-apps/api/event";
 //! listen("tauri://update-available", function (res) {
@@ -140,42 +159,124 @@
 //!
 //! Event : `tauri://update-install`
 //!
-//! ### Rust
-//! ```ignore
-//! dispatcher.emit("tauri://update-install", None);
+//! #### Rust
+//! ```no_run
+//! tauri::Builder::default()
+//!   .setup(|app| {
+//!     let handle = app.handle();
+//!     tauri::async_runtime::spawn(async move {
+//!       match handle.check_for_updates().await {
+//!         Ok(update) => {
+//!           if update.is_update_available() {
+//!             update.download_and_install().await.unwrap();
+//!           }
+//!         }
+//!         Err(e) => {
+//!           println!("failed to update: {}", e);
+//!         }
+//!       }
+//!     });
+//!     Ok(())
+//!   });
 //! ```
 //!
-//! ### Javascript
+//! #### Javascript
 //! ```js
 //! import { emit } from "@tauri-apps/api/event";
 //! emit("tauri://update-install");
 //! ```
 //!
-//! ### Listen Install Progress
+//! ### Listen Download Progress
+//!
+//! The event payload informs the length of the chunk that was just downloaded, and the total download size if known.
+//!
+//! #### Rust
+//! ```no_run
+//! let app = tauri::Builder::default()
+//!   // on an actual app, remove the string argument
+//!   .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
+//!   .expect("error while building tauri application");
+//! app.run(|_app_handle, event| match event {
+//!   tauri::RunEvent::Updater(updater_event) => {
+//!     match updater_event {
+//!       tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length } => {
+//!         println!("downloaded {} of {:?}", chunk_length, content_length);
+//!       }
+//!       _ => (),
+//!     }
+//!   }
+//!   _ => {}
+//! });
+//! ```
 //!
-//! Event : `tauri://update-status`
+//! #### Javascript
+//!
+//! Event : `tauri://update-download-progress`
 //!
 //! Emitted data:
 //! ```text
-//! status    [ERROR/PENDING/DONE]
-//! error     String/null
+//! chunkLength       number
+//! contentLength     number/null
 //! ```
 //!
-//! PENDING is emitted when the download is started and DONE when the install is complete. You can then ask to restart the application.
+//! ```js
+//! import { listen } from "@tauri-apps/api/event";
+//! listen<{ chunkLength: number, contentLength?: number }>("tauri://update-download-progress", function (event) {
+//!     console.log(`downloaded ${event.payload.chunkLength} of ${event.payload.contentLength}`);
+//! });
+//! ```
 //!
-//! ERROR is emitted when there is an error with the updater. We suggest to listen to this event even if the dialog is enabled.
+//! ### Listen Install Progress
 //!
-//! ### Rust
-//! ```ignore
-//! dispatcher.listen("tauri://update-status", move |msg| {
-//!     println("New status: {:?}", msg);
-//! })
+//! **Pending** is emitted when the download is started and **Done** when the install is complete. You can then ask to restart the application.
+//!
+//! **UpToDate** is emitted when the app already has the latest version installed and an update is not needed.
+//!
+//! **Error** is emitted when there is an error with the updater. We suggest to listen to this event even if the dialog is enabled.
+//!
+//! #### Rust
+//! ```no_run
+//! let app = tauri::Builder::default()
+//!   // on an actual app, remove the string argument
+//!   .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
+//!   .expect("error while building tauri application");
+//! app.run(|_app_handle, event| match event {
+//!   tauri::RunEvent::Updater(updater_event) => {
+//!     match updater_event {
+//!       tauri::UpdaterEvent::UpdateAvailable { body, date, version } => {
+//!         println!("update available {} {} {}", body, date, version);
+//!       }
+//!       tauri::UpdaterEvent::Pending => {
+//!         println!("update is pending!");
+//!       }
+//!       tauri::UpdaterEvent::Updated => {
+//!         println!("app has been updated");
+//!       }
+//!       tauri::UpdaterEvent::AlreadyUpToDate => {
+//!         println!("app is already up to date");
+//!       }
+//!       tauri::UpdaterEvent::Error(error) => {
+//!         println!("failed to update: {}", error);
+//!       }
+//!       _ => (),
+//!     }
+//!   }
+//!   _ => {}
+//! });
+//! ```
+//!
+//! #### Javascript
+//! Event : `tauri://update-status`
+//!
+//! Emitted data:
+//! ```text
+//! status    ERROR | PENDING | UPTODATE | DONE
+//! error     string/null
 //! ```
 //!
-//! ### Javascript
 //! ```js
 //! import { listen } from "@tauri-apps/api/event";
-//! listen("tauri://update-status", function (res) {
+//! listen<{ status: string, error?: string }>("tauri://update-status", function (res) {
 //!     console.log("New status: ", res);
 //! });
 //! ```
@@ -335,8 +436,8 @@ pub use self::error::Error;
 pub type Result<T> = std::result::Result<T, Error>;
 
 use crate::{
-  api::dialog::blocking::ask, runtime::EventLoopProxy, utils::config::UpdaterConfig, AppHandle,
-  Env, EventLoopMessage, Manager, Runtime, UpdaterEvent,
+  api::dialog::blocking::ask, runtime::EventLoopProxy, AppHandle, EventLoopMessage, Manager,
+  Runtime, UpdaterEvent,
 };
 
 /// Check for new updates
@@ -349,6 +450,8 @@ pub const EVENT_INSTALL_UPDATE: &str = "tauri://update-install";
 /// always listen for this event. It'll send you the install progress
 /// and any error triggered during update check and install
 pub const EVENT_STATUS_UPDATE: &str = "tauri://update-status";
+/// The name of the event that is emitted on download progress.
+pub const EVENT_DOWNLOAD_PROGRESS: &str = "tauri://update-download-progress";
 /// this is the status emitted when the download start
 pub const EVENT_STATUS_PENDING: &str = "PENDING";
 /// When you got this status, something went wrong
@@ -365,6 +468,13 @@ struct StatusEvent {
   error: Option<String>,
 }
 
+#[derive(Clone, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+struct DownloadProgressEvent {
+  chunk_length: usize,
+  content_length: Option<u64>,
+}
+
 #[derive(Clone, serde::Serialize)]
 struct UpdateManifest {
   version: String,
@@ -372,17 +482,15 @@ struct UpdateManifest {
   body: String,
 }
 
-/// The response of an updater [`check`].
+/// The response of an updater check.
 pub struct UpdateResponse<R: Runtime> {
-  update: core::Update,
-  handle: AppHandle<R>,
+  update: core::Update<R>,
 }
 
 impl<R: Runtime> Clone for UpdateResponse<R> {
   fn clone(&self) -> Self {
     Self {
       update: self.update.clone(),
-      handle: self.handle.clone(),
     }
   }
 }
@@ -405,24 +513,21 @@ impl<R: Runtime> UpdateResponse<R> {
 
   /// Downloads and installs the update.
   pub async fn download_and_install(self) -> Result<()> {
-    download_and_install(self.handle, self.update).await
+    download_and_install(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,
-  package_info: crate::PackageInfo,
-  handle: AppHandle<R>,
-) {
+pub(crate) async fn check_update_with_dialog<R: Runtime>(handle: AppHandle<R>) {
+  let updater_config = handle.config().tauri.updater.clone();
+  let package_info = handle.package_info().clone();
   if let Some(endpoints) = updater_config.endpoints.clone() {
     let endpoints = endpoints
       .iter()
       .map(|e| e.to_string())
       .collect::<Vec<String>>();
-    let env = handle.state::<Env>().inner().clone();
     // check updates
-    match self::core::builder(env)
+    match self::core::builder(handle.clone())
       .urls(&endpoints[..])
       .current_version(&package_info.version)
       .build()
@@ -434,15 +539,8 @@ pub(crate) async fn check_update_with_dialog<R: Runtime>(
         // if dialog enabled only
         if updater.should_update && updater_config.dialog {
           let body = updater.body.clone().unwrap_or_else(|| String::from(""));
-          let handle_ = handle.clone();
-          let dialog = prompt_for_install(
-            handle_,
-            &updater.clone(),
-            &package_info.name,
-            &body.clone(),
-            pubkey,
-          )
-          .await;
+          let dialog =
+            prompt_for_install(&updater.clone(), &package_info.name, &body.clone(), pubkey).await;
 
           if let Err(e) = dialog {
             send_status_update(&handle, UpdaterEvent::Error(e.to_string()));
@@ -469,31 +567,32 @@ pub(crate) fn listener<R: Runtime>(handle: AppHandle<R>) {
   });
 }
 
-pub(crate) async fn download_and_install<R: Runtime>(
-  handle: AppHandle<R>,
-  update: core::Update,
-) -> Result<()> {
-  let update = update.clone();
-
+pub(crate) async fn download_and_install<R: Runtime>(update: core::Update<R>) -> Result<()> {
   // Start installation
   // emit {"status": "PENDING"}
-  send_status_update(&handle, UpdaterEvent::Pending);
+  send_status_update(&update.app, UpdaterEvent::Pending);
+
+  let handle = update.app.clone();
 
   // 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())
+    .download_and_install(
+      update.app.config().tauri.updater.pubkey.clone(),
+      move |chunk_length, content_length| {
+        send_download_progress_event(&handle, chunk_length, content_length);
+      },
+    )
     .await;
 
   if let Err(err) = &update_result {
     // emit {"status": "ERROR", "error": "The error message"}
-    send_status_update(&handle, UpdaterEvent::Error(err.to_string()));
+    send_status_update(&update.app, UpdaterEvent::Error(err.to_string()));
   } else {
     // emit {"status": "DONE"}
-    send_status_update(&handle, UpdaterEvent::Updated);
+    send_status_update(&update.app, UpdaterEvent::Updated);
   }
   update_result
 }
@@ -512,9 +611,7 @@ pub(crate) async fn check<R: Runtime>(handle: AppHandle<R>) -> Result<UpdateResp
     .collect::<Vec<String>>();
 
   // check updates
-  let env = handle.state::<Env>().inner().clone();
-
-  match self::core::builder(env)
+  match self::core::builder(handle.clone())
     .urls(&endpoints[..])
     .current_version(&package_info.version)
     .build()
@@ -543,17 +640,16 @@ pub(crate) async fn check<R: Runtime>(handle: AppHandle<R>) -> Result<UpdateResp
         ));
 
         // 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;
+            let _ = download_and_install(update_).await;
           });
         });
       } else {
         send_status_update(&handle, UpdaterEvent::AlreadyUpToDate);
       }
-      Ok(UpdateResponse { update, handle })
+      Ok(UpdateResponse { update })
     }
     Err(e) => {
       send_status_update(&handle, UpdaterEvent::Error(e.to_string()));
@@ -562,6 +658,28 @@ pub(crate) async fn check<R: Runtime>(handle: AppHandle<R>) -> Result<UpdateResp
   }
 }
 
+// Send a status update via `tauri://update-download-progress` event.
+fn send_download_progress_event<R: Runtime>(
+  handle: &AppHandle<R>,
+  chunk_length: usize,
+  content_length: Option<u64>,
+) {
+  let _ = handle.emit_all(
+    EVENT_DOWNLOAD_PROGRESS,
+    DownloadProgressEvent {
+      chunk_length,
+      content_length,
+    },
+  );
+  let _ =
+    handle
+      .create_proxy()
+      .send_event(EventLoopMessage::Updater(UpdaterEvent::DownloadProgress {
+        chunk_length,
+        content_length,
+      }));
+}
+
 // Send a status update via `tauri://update-status` event.
 fn send_status_update<R: Runtime>(handle: &AppHandle<R>, message: UpdaterEvent) {
   let _ = handle.emit_all(
@@ -586,15 +704,14 @@ fn send_status_update<R: Runtime>(handle: &AppHandle<R>, message: UpdaterEvent)
 // Prompt a dialog asking if the user want to install the new version
 // Maybe we should add an option to customize it in future versions.
 async fn prompt_for_install<R: Runtime>(
-  handle: AppHandle<R>,
-  updater: &self::core::Update,
+  update: &self::core::Update<R>,
   app_name: &str,
   body: &str,
   pubkey: String,
 ) -> Result<()> {
   // remove single & double quote
   let escaped_body = body.replace(&['\"', '\''][..], "");
-  let windows = handle.windows();
+  let windows = update.app.windows();
   let parent_window = windows.values().next();
 
   // todo(lemarier): We should review this and make sure we have
@@ -609,7 +726,7 @@ Would you like to install it now?
 
 Release Notes:
 {}"#,
-      app_name, updater.version, updater.current_version, escaped_body,
+      app_name, update.version, update.current_version, escaped_body,
     ),
   );
 
@@ -618,7 +735,9 @@ Release Notes:
     // 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)
-    updater.download_and_install(pubkey.clone()).await?;
+    update
+      .download_and_install(pubkey.clone(), |_, _| ())
+      .await?;
 
     // Ask user if we need to restart the application
     let should_exit = ask(
@@ -627,7 +746,7 @@ Release Notes:
       "The installation was successful, do you want to restart the application now?",
     );
     if should_exit {
-      handle.restart();
+      update.app.restart();
     }
   }
 

+ 3 - 2
core/tauri/tests/restart/Cargo.lock

@@ -1596,9 +1596,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.9.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
+checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
 
 [[package]]
 name = "pango"
@@ -2601,6 +2601,7 @@ name = "tauri-runtime-wry"
 version = "0.3.3"
 dependencies = [
  "gtk",
+ "rand 0.8.5",
  "tauri-runtime",
  "tauri-utils",
  "uuid",

+ 1 - 0
tooling/api/src/helpers/event.ts

@@ -21,6 +21,7 @@ export interface Event<T> {
 export type EventName = LiteralUnion<
   | 'tauri://update'
   | 'tauri://update-available'
+  | 'tauri://update-download-progress'
   | 'tauri://update-install'
   | 'tauri://update-status'
   | 'tauri://resize'