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

This commit is contained in:
Lucas Fernandes Nogueira 2022-03-18 22:58:44 -03:00 committed by GitHub
parent 348a1ab59d
commit f0db3f9b83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 391 additions and 141 deletions

View File

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

View File

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

View File

@ -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" ]

View File

@ -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`].

View File

@ -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`

View File

@ -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.

View File

@ -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)

View File

@ -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 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);
//! }
//! _ => (),
//! }
//! }
//! _ => {}
//! });
//! ```
//!
//! #### Javascript
//!
//! Event : `tauri://update-download-progress`
//!
//! Emitted data:
//! ```text
//! chunkLength number
//! contentLength number/null
//! ```
//!
//! ```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}`);
//! });
//! ```
//!
//! ### Listen Install Progress
//!
//! **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/DONE]
//! error String/null
//! status ERROR | PENDING | UPTODATE | DONE
//! error string/null
//! ```
//!
//! PENDING is emitted when the download is started and DONE when the install is complete. You can then ask to restart the application.
//!
//! 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
//! ```ignore
//! dispatcher.listen("tauri://update-status", move |msg| {
//! println("New status: {:?}", msg);
//! })
//! ```
//!
//! ### 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();
}
}

View File

@ -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",

View File

@ -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'