Implement updating at next exit
This commit is contained in:
parent
7b73aa2908
commit
9b103e063a
@ -348,6 +348,7 @@ async function handleCommand(e) {
|
||||
|
||||
const availableUpdate = ref(null)
|
||||
const updateSkipped = ref(false)
|
||||
const enqueuedUpdate = ref(null)
|
||||
const updateModal = useTemplateRef('updateModal')
|
||||
async function checkUpdates() {
|
||||
if (!(await areUpdatesEnabled())) {
|
||||
@ -399,12 +400,18 @@ async function checkUpdates() {
|
||||
}
|
||||
|
||||
async function skipUpdate(version) {
|
||||
enqueuedUpdate.value = null
|
||||
|
||||
updateSkipped.value = true
|
||||
let settings = await getSettings()
|
||||
settings.skipped_update = version
|
||||
await setSettings(settings)
|
||||
}
|
||||
|
||||
async function updateEnqueuedForLater(version) {
|
||||
enqueuedUpdate.value = version
|
||||
}
|
||||
|
||||
async function forceOpenUpdateModal() {
|
||||
if (updateSkipped.value) {
|
||||
updateSkipped.value = false
|
||||
@ -456,7 +463,11 @@ function handleAuxClick(e) {
|
||||
<div id="teleports"></div>
|
||||
<div v-if="stateInitialized" class="app-grid-layout experimental-styles-within relative">
|
||||
<Suspense @resolve="checkUpdates">
|
||||
<UpdateModal ref="updateModal" @update-skipped="skipUpdate" />
|
||||
<UpdateModal
|
||||
ref="updateModal"
|
||||
@update-skipped="skipUpdate"
|
||||
@update-enqueued-for-later="updateEnqueuedForLater"
|
||||
/>
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<AppSettingsModal ref="settingsModal" />
|
||||
@ -510,11 +521,14 @@ function handleAuxClick(e) {
|
||||
<div class="flex flex-grow"></div>
|
||||
<NavButton
|
||||
v-if="!!availableUpdate"
|
||||
v-tooltip.right="'Update available'"
|
||||
v-tooltip.right="
|
||||
enqueuedUpdate === availableUpdate?.version
|
||||
? 'Update installation queued for next restart'
|
||||
: 'Update available'
|
||||
"
|
||||
:to="forceOpenUpdateModal"
|
||||
>
|
||||
<!-- TODO: Also gray if updating on next restart -->
|
||||
<DownloadIcon v-if="updateSkipped" />
|
||||
<DownloadIcon v-if="updateSkipped || enqueuedUpdate === availableUpdate?.version" />
|
||||
<DownloadIcon v-else class="text-brand-green" />
|
||||
</NavButton>
|
||||
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
ref="modal"
|
||||
:header="formatMessage(messages.header)"
|
||||
:on-hide="onHide"
|
||||
:closable="!updateInProgress"
|
||||
:closable="!updatingImmediately"
|
||||
>
|
||||
<div>{{ formatMessage(messages.bodyVersion, { version: update!.version }) }}</div>
|
||||
<div v-if="updateSize">
|
||||
@ -17,19 +17,22 @@
|
||||
<ProgressBar class="mt-4" :progress="downloadProgress" />
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<ButtonStyled color="green">
|
||||
<button :disabled="updateInProgress" @click="installUpdateNow">
|
||||
<button :disabled="updatingImmediately" @click="installUpdateNow">
|
||||
<RefreshCwIcon />
|
||||
{{ formatMessage(messages.restartNow) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button :disabled="updateInProgress">
|
||||
<button
|
||||
:disabled="updatingImmediately || downloadInProgress || update!.version == enqueuedUpdate"
|
||||
@click="updateAtNextExit"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
{{ formatMessage(messages.later) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button :disabled="updateInProgress" @click="skipUpdate">
|
||||
<button :disabled="updatingImmediately || downloadInProgress" @click="skipUpdate">
|
||||
<XIcon />
|
||||
{{ formatMessage(messages.skip) }}
|
||||
</button>
|
||||
@ -44,14 +47,16 @@ import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { useTemplateRef, ref } from 'vue'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import { RefreshCwIcon, XIcon, RightArrowIcon } from '@modrinth/assets'
|
||||
import { getUpdateSize } from '@/helpers/utils'
|
||||
import { enqueueUpdateForInstallation, getUpdateSize, removeEnqueuedUpdate } from '@/helpers/utils'
|
||||
import { formatBytes } from '@modrinth/utils'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
import { Update } from '@tauri-apps/plugin-updater'
|
||||
import { loading_listener } from '@/helpers/events'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateSkipped', version: string): void
|
||||
(e: 'updateSkipped', version: string): Promise<void>
|
||||
(e: 'updateEnqueuedForLater', version: string | null): Promise<void>
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
@ -98,15 +103,26 @@ type UpdateData = {
|
||||
|
||||
const update = ref<UpdateData>()
|
||||
const updateSize = ref<number>()
|
||||
const updateInProgress = ref(false)
|
||||
|
||||
const updatingImmediately = ref(false)
|
||||
const downloadInProgress = ref(false)
|
||||
const downloadProgress = ref(0)
|
||||
|
||||
const enqueuedUpdate = ref<string | null>(null)
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
const isOpen = ref(false)
|
||||
|
||||
async function show(newUpdate: UpdateData) {
|
||||
const oldVersion = update.value?.version
|
||||
|
||||
update.value = newUpdate
|
||||
updateSize.value = await getUpdateSize(newUpdate.rid).catch(handleError)
|
||||
|
||||
if (oldVersion !== update.value?.version) {
|
||||
downloadProgress.value = 0
|
||||
}
|
||||
|
||||
modal.value!.show()
|
||||
isOpen.value = true
|
||||
}
|
||||
@ -121,25 +137,62 @@ function hide() {
|
||||
|
||||
defineExpose({ show, hide, isOpen })
|
||||
|
||||
function installUpdateNow() {
|
||||
updateInProgress.value = true
|
||||
let totalSize = 0
|
||||
let totalDownloaded = 0
|
||||
new Update(update.value!).downloadAndInstall((event) => {
|
||||
if (event.event === 'Started') {
|
||||
totalSize = event.data.contentLength!
|
||||
} else if (event.event === 'Progress') {
|
||||
totalDownloaded += event.data.chunkLength
|
||||
} else if (event.event === 'Finished') {
|
||||
totalDownloaded = totalSize
|
||||
loading_listener((event) => {
|
||||
if (event.event.type === 'launcher_update') {
|
||||
if (event.event.version === update.value!.version) {
|
||||
downloadProgress.value = (event.fraction ?? 1.0) * 100
|
||||
}
|
||||
}
|
||||
downloadProgress.value = (totalDownloaded / totalSize) * 100
|
||||
})
|
||||
|
||||
function installUpdateNow() {
|
||||
updatingImmediately.value = true
|
||||
|
||||
if (enqueuedUpdate.value !== update.value!.version) {
|
||||
downloadUpdate()
|
||||
} else if (!downloadInProgress.value) {
|
||||
// Update already downloaded. Simply close the app
|
||||
getCurrentWindow().close()
|
||||
}
|
||||
}
|
||||
|
||||
function updateAtNextExit() {
|
||||
enqueuedUpdate.value = update.value!.version
|
||||
emit('updateEnqueuedForLater', update.value!.version)
|
||||
|
||||
downloadUpdate()
|
||||
hide()
|
||||
}
|
||||
|
||||
async function downloadUpdate() {
|
||||
const versionToDownload = update.value!.version
|
||||
|
||||
downloadInProgress.value = true
|
||||
try {
|
||||
await enqueueUpdateForInstallation(update.value!.rid)
|
||||
} catch (e) {
|
||||
downloadInProgress.value = false
|
||||
|
||||
handleError(e)
|
||||
|
||||
enqueuedUpdate.value = null
|
||||
updatingImmediately.value = false
|
||||
await emit('updateEnqueuedForLater', null)
|
||||
return
|
||||
}
|
||||
downloadInProgress.value = false
|
||||
|
||||
if (updatingImmediately.value && update.value!.version === versionToDownload) {
|
||||
await getCurrentWindow().close()
|
||||
}
|
||||
}
|
||||
|
||||
function skipUpdate() {
|
||||
hide()
|
||||
enqueuedUpdate.value = null
|
||||
emit('updateSkipped', update.value!.version)
|
||||
|
||||
removeEnqueuedUpdate()
|
||||
hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -13,6 +13,14 @@ export async function getUpdateSize(updateRid) {
|
||||
return await invoke('get_update_size', { rid: updateRid })
|
||||
}
|
||||
|
||||
export async function enqueueUpdateForInstallation(updateRid) {
|
||||
return await invoke('enqueue_update_for_installation', { rid: updateRid })
|
||||
}
|
||||
|
||||
export async function removeEnqueuedUpdate() {
|
||||
return await invoke('remove_enqueued_update')
|
||||
}
|
||||
|
||||
// One of 'Windows', 'Linux', 'MacOS'
|
||||
export async function getOS() {
|
||||
return await invoke('plugin:utils|get_os')
|
||||
|
||||
@ -45,8 +45,12 @@ pub enum TheseusSerializableError {
|
||||
Tauri(#[from] tauri::Error),
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
#[error("Tauri updater error: {0}")]
|
||||
TauriUpdater(#[from] tauri_plugin_updater::Error),
|
||||
#[error("Updater error: {0}")]
|
||||
Updater(#[from] tauri_plugin_updater::Error),
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] tauri_plugin_http::reqwest::Error),
|
||||
}
|
||||
|
||||
// Generic implementation of From<T> for ErrorTypeA
|
||||
@ -104,5 +108,6 @@ impl_serialize! {
|
||||
impl_serialize! {
|
||||
IO,
|
||||
Tauri,
|
||||
TauriUpdater,
|
||||
Updater,
|
||||
Http,
|
||||
}
|
||||
|
||||
@ -15,7 +15,9 @@ mod error;
|
||||
mod macos;
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
mod update_size_checker;
|
||||
mod updater_impl;
|
||||
#[cfg(not(feature = "updater"))]
|
||||
mod updater_impl_noop;
|
||||
|
||||
// Should be called in launcher initialization
|
||||
#[tracing::instrument(skip_all)]
|
||||
@ -68,13 +70,10 @@ fn are_updates_enabled() -> bool {
|
||||
}
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
pub use update_size_checker::get_update_size;
|
||||
pub use updater_impl::*;
|
||||
|
||||
#[cfg(not(feature = "updater"))]
|
||||
#[tauri::command]
|
||||
fn get_update_size() -> theseus::Result<()> {
|
||||
Err(theseus::ErrorKind::OtherError("Updates are disabled in this build.".to_string()).into())
|
||||
}
|
||||
pub use updater_impl_noop::*;
|
||||
|
||||
// Toggles decorations
|
||||
#[tauri::command]
|
||||
@ -212,11 +211,14 @@ fn main() {
|
||||
.plugin(api::ads::init())
|
||||
.plugin(api::friends::init())
|
||||
.plugin(api::worlds::init())
|
||||
.manage(PendingUpdateData::default())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
initialize_state,
|
||||
is_dev,
|
||||
are_updates_enabled,
|
||||
get_update_size,
|
||||
enqueue_update_for_installation,
|
||||
remove_enqueued_update,
|
||||
toggle_decorations,
|
||||
show_window,
|
||||
restart_app,
|
||||
@ -228,8 +230,9 @@ fn main() {
|
||||
match app {
|
||||
Ok(app) => {
|
||||
app.run(|app, event| {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(not(any(target_os = "macos", feature = "updater")))]
|
||||
drop((app, event));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if let tauri::RunEvent::Opened { urls } = event {
|
||||
tracing::info!("Handling webview open {urls:?}");
|
||||
@ -254,9 +257,29 @@ fn main() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
if matches!(event, tauri::RunEvent::Exit) {
|
||||
let update_data = app.state::<PendingUpdateData>().inner();
|
||||
if let Some((update, data)) = &*update_data.0.lock().unwrap() {
|
||||
if let Err(e) = update.install(data) {
|
||||
tracing::error!("Error while updating: {e}");
|
||||
|
||||
DialogBuilder::message()
|
||||
.set_level(MessageLevel::Error)
|
||||
.set_title("Update error")
|
||||
.set_text(format!("Failed to install update due to an error:\n{e}"))
|
||||
.alert()
|
||||
.show()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error while running tauri application: {:?}", e);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// tauri doesn't expose runtime errors, so matching a string representation seems like the only solution
|
||||
@ -285,7 +308,6 @@ fn main() {
|
||||
.show()
|
||||
.unwrap();
|
||||
|
||||
tracing::error!("Error while running tauri application: {:?}", e);
|
||||
panic!("{1}: {:?}", e, "error while running tauri application")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
use tauri::{Manager, ResourceId, Runtime, Webview};
|
||||
use tauri::http::header::ACCEPT;
|
||||
use tauri::http::HeaderValue;
|
||||
use tauri_plugin_http::reqwest;
|
||||
use tauri_plugin_http::reqwest::ClientBuilder;
|
||||
use tauri_plugin_updater::Error;
|
||||
use tauri_plugin_updater::Result;
|
||||
|
||||
const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
// Reimplementation of Update::download mostly, minus the actual download part
|
||||
#[tauri::command]
|
||||
pub async fn get_update_size<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> Result<Option<u64>> {
|
||||
use tauri_plugin_updater::Update;
|
||||
|
||||
let update = webview.resources_table().get::<Update>(rid)?;
|
||||
|
||||
let mut headers = update.headers.clone();
|
||||
if !headers.contains_key(ACCEPT) {
|
||||
headers.insert(ACCEPT, HeaderValue::from_static("application/octet-stream"));
|
||||
}
|
||||
|
||||
let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
|
||||
if let Some(timeout) = update.timeout {
|
||||
request = request.timeout(timeout);
|
||||
}
|
||||
if let Some(ref proxy) = update.proxy {
|
||||
let proxy = reqwest::Proxy::all(proxy.as_str())?;
|
||||
request = request.proxy(proxy);
|
||||
}
|
||||
let response = request
|
||||
.build()?
|
||||
.get(update.download_url.clone())
|
||||
.headers(headers)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(Error::Network(format!(
|
||||
"Download request failed with status: {}",
|
||||
response.status()
|
||||
)).into());
|
||||
}
|
||||
|
||||
let content_length = response
|
||||
.headers()
|
||||
.get("Content-Length")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse().ok());
|
||||
|
||||
Ok(content_length)
|
||||
}
|
||||
122
apps/app/src/updater_impl.rs
Normal file
122
apps/app/src/updater_impl.rs
Normal file
@ -0,0 +1,122 @@
|
||||
use crate::api::Result;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::http::HeaderValue;
|
||||
use tauri::http::header::ACCEPT;
|
||||
use tauri::{Manager, ResourceId, Runtime, Webview};
|
||||
use tauri_plugin_http::reqwest;
|
||||
use tauri_plugin_http::reqwest::ClientBuilder;
|
||||
use tauri_plugin_updater::Error;
|
||||
use tauri_plugin_updater::Update;
|
||||
use theseus::{LoadingBarType, emit_loading, init_loading};
|
||||
use tokio::time::Instant;
|
||||
|
||||
const UPDATER_USER_AGENT: &str =
|
||||
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PendingUpdateData(pub Mutex<Option<(Arc<Update>, Vec<u8>)>>);
|
||||
|
||||
// Reimplementation of Update::download mostly, minus the actual download part
|
||||
#[tauri::command]
|
||||
pub async fn get_update_size<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
rid: ResourceId,
|
||||
) -> Result<Option<u64>> {
|
||||
let update = webview.resources_table().get::<Update>(rid)?;
|
||||
|
||||
let mut headers = update.headers.clone();
|
||||
if !headers.contains_key(ACCEPT) {
|
||||
headers.insert(
|
||||
ACCEPT,
|
||||
HeaderValue::from_static("application/octet-stream"),
|
||||
);
|
||||
}
|
||||
|
||||
let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
|
||||
if let Some(timeout) = update.timeout {
|
||||
request = request.timeout(timeout);
|
||||
}
|
||||
if let Some(ref proxy) = update.proxy {
|
||||
let proxy = reqwest::Proxy::all(proxy.as_str())?;
|
||||
request = request.proxy(proxy);
|
||||
}
|
||||
let response = request
|
||||
.build()?
|
||||
.get(update.download_url.clone())
|
||||
.headers(headers)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(Error::Network(format!(
|
||||
"Download request failed with status: {}",
|
||||
response.status()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let content_length = response
|
||||
.headers()
|
||||
.get("Content-Length")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse().ok());
|
||||
|
||||
Ok(content_length)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn enqueue_update_for_installation<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
rid: ResourceId,
|
||||
) -> Result<()> {
|
||||
let pending_data = webview.state::<PendingUpdateData>().inner();
|
||||
|
||||
let update = webview.resources_table().get::<Update>(rid)?;
|
||||
|
||||
let progress = init_loading(
|
||||
LoadingBarType::LauncherUpdate {
|
||||
version: update.version.clone(),
|
||||
current_version: update.current_version.clone(),
|
||||
},
|
||||
1.0,
|
||||
"Downloading update...",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let download_start = Instant::now();
|
||||
let update_data = update
|
||||
.download(
|
||||
|chunk_size, total_size| {
|
||||
let Some(total_size) = total_size else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = emit_loading(
|
||||
&progress,
|
||||
chunk_size as f64 / total_size as f64,
|
||||
None,
|
||||
) {
|
||||
tracing::error!(
|
||||
"Failed to update download progress bar: {e}"
|
||||
);
|
||||
}
|
||||
},
|
||||
|| {},
|
||||
)
|
||||
.await?;
|
||||
let download_duration = download_start.elapsed();
|
||||
tracing::info!("Downloaded update in {download_duration:?}");
|
||||
|
||||
pending_data
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace((update, update_data));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_enqueued_update<R: Runtime>(webview: Webview<R>) {
|
||||
let pending_data = webview.state::<PendingUpdateData>().inner();
|
||||
pending_data.0.lock().unwrap().take();
|
||||
}
|
||||
26
apps/app/src/updater_impl_noop.rs
Normal file
26
apps/app/src/updater_impl_noop.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use crate::api::Result;
|
||||
use theseus::ErrorKind;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PendingUpdateData;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_update_size() -> Result<()> {
|
||||
updates_are_disabled()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn enqueue_update_for_installation() -> Result<()> {
|
||||
updates_are_disabled()
|
||||
}
|
||||
|
||||
fn updates_are_disabled() -> Result<()> {
|
||||
let error: theseus::Error = ErrorKind::OtherError(
|
||||
"Updates are disabled in this build.".to_string(),
|
||||
)
|
||||
.into();
|
||||
Err(error.into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_enqueued_update() {}
|
||||
@ -176,7 +176,6 @@ pub enum LoadingBarType {
|
||||
import_location: PathBuf,
|
||||
profile_name: String,
|
||||
},
|
||||
CheckingForUpdates,
|
||||
LauncherUpdate {
|
||||
version: String,
|
||||
current_version: String,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user