mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-11 04:52:13 +00:00
craft-update: start a waiting update helper when the app exits without Close
The Windows helper of a finished installation started only when show_if returned Action::Close. An app that exited another way (the window's close button, a quit while unsaved work held the close back, an OS shutdown) dropped it, so the update never installed and the download stayed in the cache. Dropping the Updater now starts a helper that is still waiting, once, including one from an installation that finished after the last tick. Updater::start_pending_update does the same for an app that ends the process in on_exit without dropping the Updater; Action::Close's doc comment names it.
This commit is contained in:
1 parent
df66c35b07
commit
f5f45b090c
1 file changed
+89
-1
@@ -44,7 +44,10 @@ pub enum Action {
|
||||
OpenUrl(String),
|
||||
/// Close the app through its normal close path (which asks about unsaved work): the update is
|
||||
/// installed and a helper restarts the app once it has exited. On Windows the helper, which
|
||||
/// installs the update, starts when this is returned.
|
||||
/// installs the update, starts when this is returned. When the app exits another way (the
|
||||
/// window's close button, a quit with unsaved work, an OS shutdown), dropping the [`Updater`]
|
||||
/// starts it. An app that ends the process in `on_exit` without dropping the `Updater` calls
|
||||
/// [`Updater::start_pending_update`] there first.
|
||||
Close,
|
||||
}
|
||||
|
||||
@@ -394,6 +397,26 @@ impl Updater {
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the helper of an installed update that is still waiting for the app to close, so
|
||||
/// that the update installs once the app has exited. Call it when the app exits by a path
|
||||
/// that did not come through [`Action::Close`]; dropping the `Updater` calls it too. Later
|
||||
/// calls do nothing. An installation still running is left alone.
|
||||
pub fn start_pending_update(&mut self) {
|
||||
// An installation that finished since the last `tick`.
|
||||
if let Some((rx, release)) = &self.install
|
||||
&& let Ok(Ok(restart)) = rx.try_recv()
|
||||
{
|
||||
self.restart = Some((restart, release.clone()));
|
||||
self.install = None;
|
||||
self.installed = true;
|
||||
}
|
||||
if let Some((restart, _)) = self.restart.take()
|
||||
&& let Err(e) = restart.start()
|
||||
{
|
||||
log::warn!("{} self-update failed: {e}", self.app.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn request_install(&mut self, ctx: &egui::Context, can_start: bool) {
|
||||
if can_start {
|
||||
self.start_install(ctx);
|
||||
@@ -439,6 +462,14 @@ impl Updater {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Updater {
|
||||
/// The app is exiting: an installed update that is still waiting starts its helper, which
|
||||
/// waits for this process to exit before it installs.
|
||||
fn drop(&mut self) {
|
||||
self.start_pending_update();
|
||||
}
|
||||
}
|
||||
|
||||
fn install(app: &'static App, installed: &self_update::Install, mut release: Release, version: &str) -> Result<Restart, String> {
|
||||
let http = Http::new(&format!("{}/Update", app.name)).map_err(|e| format!("could not initialize HTTP: {e}"))?;
|
||||
if release.assets.is_empty()
|
||||
@@ -827,6 +858,63 @@ mod tests {
|
||||
assert_eq!(starts.load(Ordering::SeqCst), 1, "the helper starts once");
|
||||
}
|
||||
|
||||
/// A counter and a helper that adds one to it each time it starts.
|
||||
fn counted_restart() -> (Arc<std::sync::atomic::AtomicUsize>, Restart) {
|
||||
let starts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let counter = Arc::clone(&starts);
|
||||
let restart = Restart::later(move || {
|
||||
counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok(())
|
||||
});
|
||||
(starts, restart)
|
||||
}
|
||||
|
||||
/// An app that exits without [`Action::Close`] still starts the waiting helper, once, when
|
||||
/// the updater is dropped.
|
||||
#[test]
|
||||
fn dropping_the_updater_starts_a_waiting_helper_once() {
|
||||
use std::sync::atomic::Ordering;
|
||||
let ctx = egui::Context::default();
|
||||
let mut u = updater(Ok(None));
|
||||
let (starts, restart) = counted_restart();
|
||||
let (tx, rx) = channel();
|
||||
u.install = Some((rx, newer()));
|
||||
assert!(tx.send(Ok(restart)).is_ok());
|
||||
u.tick(&ctx);
|
||||
assert_eq!(frame(&mut u, &ctx, false), Action::None, "unsaved work holds the close back");
|
||||
assert_eq!(starts.load(Ordering::SeqCst), 0);
|
||||
drop(u);
|
||||
assert_eq!(starts.load(Ordering::SeqCst), 1);
|
||||
|
||||
// Picked up from the worker without a `tick` first.
|
||||
let mut u = updater(Ok(None));
|
||||
let (starts, restart) = counted_restart();
|
||||
let (tx, rx) = channel();
|
||||
u.install = Some((rx, newer()));
|
||||
assert!(tx.send(Ok(restart)).is_ok());
|
||||
u.start_pending_update();
|
||||
assert_eq!(starts.load(Ordering::SeqCst), 1);
|
||||
u.start_pending_update();
|
||||
drop(u);
|
||||
assert_eq!(starts.load(Ordering::SeqCst), 1, "the helper starts once");
|
||||
}
|
||||
|
||||
/// A helper started at [`Action::Close`] does not start again when the updater is dropped.
|
||||
#[test]
|
||||
fn a_helper_started_at_close_does_not_start_again_on_drop() {
|
||||
use std::sync::atomic::Ordering;
|
||||
let ctx = egui::Context::default();
|
||||
let mut u = updater(Ok(None));
|
||||
let (starts, restart) = counted_restart();
|
||||
let (tx, rx) = channel();
|
||||
u.install = Some((rx, newer()));
|
||||
assert!(tx.send(Ok(restart)).is_ok());
|
||||
u.tick(&ctx);
|
||||
assert_eq!(frame(&mut u, &ctx, true), Action::Close);
|
||||
drop(u);
|
||||
assert_eq!(starts.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
/// A helper that can't start keeps the app open and offers the update again with the reason.
|
||||
#[test]
|
||||
fn a_helper_that_cannot_start_keeps_the_app_open() {
|
||||
|
||||
Reference in new issue
Block a user