mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-15 05:31:42 +03:00
feat(updater): add download progress events (#3734)
This commit is contained in:
parent
348a1ab59d
commit
f0db3f9b83
5
.changes/http-api-stream.md
Normal file
5
.changes/http-api-stream.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Added `bytes_stream` method to `tauri::api::http::Response`.
|
5
.changes/updater-download-events.md
Normal file
5
.changes/updater-download-events.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Added download progress events to the updater.
|
@ -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" ]
|
||||
|
@ -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`].
|
||||
|
@ -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`
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
5
core/tauri/tests/restart/Cargo.lock
generated
5
core/tauri/tests/restart/Cargo.lock
generated
@ -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",
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user