From ccab980657b3fd9e960512969751f4067369e7c2 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 11:30:36 -0800 Subject: [PATCH] Updates: the updater reports installing, ready, installed and failed, and installs at exit craft-media, craft-pdf, craft-ui, craft-update and krilla move from craft-libs c5a1cbfb to c7c84230, which carries storytold/craft-libs#687 and #696. The check result window now shows whenever the updater's status is not idle: after Help > Check for Updates, and at the first start after an update, to say that it installed or why it failed. Restart to Update in that window asks the app to close once nothing is unsaved. on_exit starts the helper of a ready update, since on macOS it ends the process before the app is dropped. --- Cargo.lock | 15 ++-- Cargo.toml | 10 +-- apps/deckcraft/src/main.rs | 3 + crates/ui-egui/src/app.rs | 8 ++ crates/ui-egui/src/dialogs/updates_ui.rs | 95 +++++++++++++++++++++--- 5 files changed, 106 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ff27fdd2..62d15529a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -847,12 +847,12 @@ dependencies = [ [[package]] name = "craft-fonts-build" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7" +source = "git+https://github.com/dotCooCoo/craft-libs?rev=c7c84230631cbc01ee928f0a0e0d838b1ed612ae#c7c84230631cbc01ee928f0a0e0d838b1ed612ae" [[package]] name = "craft-i18n" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7" +source = "git+https://github.com/dotCooCoo/craft-libs?rev=c7c84230631cbc01ee928f0a0e0d838b1ed612ae#c7c84230631cbc01ee928f0a0e0d838b1ed612ae" dependencies = [ "fixed_decimal", "icu_decimal", @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "craft-media" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7" +source = "git+https://github.com/dotCooCoo/craft-libs?rev=c7c84230631cbc01ee928f0a0e0d838b1ed612ae#c7c84230631cbc01ee928f0a0e0d838b1ed612ae" dependencies = [ "thiserror 2.0.21", ] @@ -883,7 +883,7 @@ dependencies = [ [[package]] name = "craft-pdf" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7" +source = "git+https://github.com/dotCooCoo/craft-libs?rev=c7c84230631cbc01ee928f0a0e0d838b1ed612ae#c7c84230631cbc01ee928f0a0e0d838b1ed612ae" dependencies = [ "krilla", "thiserror 2.0.21", @@ -892,7 +892,7 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7" +source = "git+https://github.com/dotCooCoo/craft-libs?rev=c7c84230631cbc01ee928f0a0e0d838b1ed612ae#c7c84230631cbc01ee928f0a0e0d838b1ed612ae" dependencies = [ "craft-fonts-build", "craft-i18n", @@ -912,7 +912,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7" +source = "git+https://github.com/dotCooCoo/craft-libs?rev=c7c84230631cbc01ee928f0a0e0d838b1ed612ae#c7c84230631cbc01ee928f0a0e0d838b1ed612ae" dependencies = [ "chrono", "dunce", @@ -2791,7 +2791,7 @@ dependencies = [ [[package]] name = "krilla" version = "0.8.2" -source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7" +source = "git+https://github.com/dotCooCoo/craft-libs?rev=c7c84230631cbc01ee928f0a0e0d838b1ed612ae#c7c84230631cbc01ee928f0a0e0d838b1ed612ae" dependencies = [ "base64 0.22.1", "bumpalo", @@ -6571,7 +6571,6 @@ dependencies = [ "nix", "typed-path", "windows-sys 0.61.2", - "zopfli", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ab2b74291..a919e0504 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,11 +59,11 @@ harfrust = "0.13" vello_cpu = "0.2" vello_common = { version = "0.2", default-features = false } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] } -craft-media = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0", default-features = false } -craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] } +craft-media = { git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", version = "=0.1.0", default-features = false } +craft-pdf = { git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] } # craft-update: app-specific pretend versions apply to startup and manual checks. -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["egui"] } -craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0" } +craft-update = { git = "https://github.com/dotCooCoo/craft-libs", version = "=0.1.0", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", features = ["egui"] } +craft-ui = { git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", version = "=0.1.0" } i_overlay = "9" quick-xml = "0.41" zip = { version = "3", default-features = false, features = ["deflate"] } @@ -109,7 +109,7 @@ codegen-units = 16 debug = 0 [patch.crates-io] -krilla = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.8.2" } +krilla = { git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", version = "=0.8.2" } ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } diff --git a/apps/deckcraft/src/main.rs b/apps/deckcraft/src/main.rs index 3f872c214..d71817ceb 100644 --- a/apps/deckcraft/src/main.rs +++ b/apps/deckcraft/src/main.rs @@ -51,6 +51,9 @@ impl eframe::App for App { } fn on_exit(&mut self) { save_prefs(&self.0); + // The window is closing for good: a ready update's helper starts now and installs it once + // the process has exited. + self.0.start_pending_update(); // macOS (#279): end the process here, before eframe tears the window down. AccessKit's // macOS adapter subclasses the window's content view and restores its class on teardown; // AppKit's Touch Bar support observes that view (KVO), so in the next display cycle diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 178ab24ba..8cc53258e 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -137,6 +137,14 @@ impl SlideApp { updates_ui::enable(self); } + /// Start the helper of an update that is ready, so that it installs once the process has + /// exited. The desktop shell calls it in `on_exit`, which ends the process on macOS before the + /// app is dropped. Later calls do nothing. + #[cfg(not(target_arch = "wasm32"))] + pub fn start_pending_update(&mut self) { + updates_ui::start_pending_update(self); + } + /// A message that must be seen wherever the user is: a toast, which also shows over the start /// screen and the File tab, where there is no status bar. pub fn notify(&mut self, msg: impl Into) { diff --git a/crates/ui-egui/src/dialogs/updates_ui.rs b/crates/ui-egui/src/dialogs/updates_ui.rs index cdf2bf207..3c62a254d 100644 --- a/crates/ui-egui/src/dialogs/updates_ui.rs +++ b/crates/ui-egui/src/dialogs/updates_ui.rs @@ -13,13 +13,11 @@ pub(crate) struct Runtime { /// The desktop host turns the check at start on; tests and the web build leave it off. enabled: bool, check_requested: bool, - /// A manual check's result dialog is showing (checking, up to date or failed). - result_open: bool, } impl Default for Runtime { fn default() -> Self { - Self { updater: Updater::new(&crate::UPDATE_APP, VERSION), enabled: false, check_requested: false, result_open: false } + Self { updater: Updater::new(&crate::UPDATE_APP, VERSION), enabled: false, check_requested: false } } } @@ -31,10 +29,15 @@ pub(crate) fn enable(app: &mut SlideApp) { pub(crate) fn request_check(app: &mut SlideApp) { if !matches!(app.updates_rt.updater.status(), Status::Checking) { app.updates_rt.check_requested = true; - app.updates_rt.result_open = true; } } +/// Starts the helper of an update that is ready, so that it installs once the process has exited. +/// Later calls do nothing. +pub(crate) fn start_pending_update(app: &mut SlideApp) { + app.updates_rt.updater.start_pending_update(); +} + pub(crate) fn tick(app: &mut SlideApp, ctx: &egui::Context) { let rt = &mut app.updates_rt; if std::mem::take(&mut rt.check_requested) { @@ -46,21 +49,31 @@ pub(crate) fn tick(app: &mut SlideApp, ctx: &egui::Context) { } pub(crate) fn show(app: &mut SlideApp, ctx: &egui::Context) { - // The restart helper is scheduled before the app closes, so no update starts over unsaved work. + // An installation starts, and a ready update asks the app to close, only while nothing is unsaved. let clean = !app.session.documents().iter().any(|d| d.is_dirty()); match app.updates_rt.updater.show_if(ctx, clean) { Action::OpenUrl(url) => ctx.open_url(egui::OpenUrl::new_tab(url)), - // The app's close path asks about unsaved work; the restart helper relaunches once the process exits. + // The app's close path asks about unsaved work; `on_exit` starts the restart helper. Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close), Action::None => {} } + // The result window reports a requested check, and at the first start after an update what + // that update came to (installed, or why it failed). let rt = &mut app.updates_rt; - if rt.result_open { - match CheckResultDialog::new(&crate::UPDATE_APP, VERSION).show(ctx, rt.updater.status()) { - CheckResultResponse::Close => rt.result_open = false, - CheckResultResponse::CheckAgain => rt.check_requested = true, - CheckResultResponse::None => {} - } + if !matches!(rt.updater.status(), Status::Idle) { + let response = CheckResultDialog::new(&crate::UPDATE_APP, VERSION).show(ctx, rt.updater.status()); + answer(rt, response); + } +} + +/// Acts on the result window's answer. +fn answer(rt: &mut Runtime, response: CheckResultResponse) { + match response { + CheckResultResponse::Close => rt.updater.dismiss_status(), + CheckResultResponse::CheckAgain => rt.check_requested = true, + // The next frame's `show_if` asks the window to close once nothing is unsaved. + CheckResultResponse::RestartToUpdate => rt.updater.restart_to_update(), + CheckResultResponse::None => {} } } @@ -77,4 +90,62 @@ mod tests { ctx.run_ui(egui::RawInput::default(), |ui| show(&mut app, ui.ctx())).textures_delta.clear(); assert!(app.updates_rt.updater.prompt_open()); } + + /// An app whose updater keeps its outcome records under `root`, reads no launcher choice and + /// asks no server, with the check at start turned on. + fn app_with_records(root: &std::path::Path) -> SlideApp { + let mut app = SlideApp::new(deckcraft_engine::Session::new(), crate::Services::default()); + app.updates_rt.updater = Updater::new(&crate::UPDATE_APP, VERSION) + .with_paths(craft_update::Paths::under(root)) + .with_pins_path(root.join("pins.json")) + .with_source(std::sync::Arc::new(|| Ok(None))); + app.updates_rt.enabled = true; + app + } + + fn records_dir(name: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("deckcraft-updates-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + dir + } + + /// The first start after an update reports it; OK dismisses the report. + #[test] + fn the_start_after_an_update_reports_it_until_dismissed() { + let root = records_dir("installed"); + std::fs::create_dir_all(root.join("data")).unwrap(); + std::fs::write(root.join("data").join("self-update-done.txt"), VERSION).unwrap(); + let mut app = app_with_records(&root); + let ctx = egui::Context::default(); + ctx.run_ui(egui::RawInput::default(), |ui| { + tick(&mut app, ui.ctx()); + show(&mut app, ui.ctx()); + }) + .textures_delta + .clear(); + assert!(matches!(app.updates_rt.updater.status(), Status::Updated(_)), "{:?}", app.updates_rt.updater.status()); + answer(&mut app.updates_rt, CheckResultResponse::Close); + assert_eq!(app.updates_rt.updater.status(), &Status::Idle); + let _ = std::fs::remove_dir_all(&root); + } + + /// The first start after a failed update says why; Check Again asks for a new check. + #[test] + fn the_start_after_a_failed_update_reports_why_and_offers_a_new_check() { + let root = records_dir("failed"); + std::fs::create_dir_all(root.join("data").join("logs")).unwrap(); + std::fs::write(root.join("data").join("logs").join("self-update-error.txt"), "the disk is full").unwrap(); + let mut app = app_with_records(&root); + let ctx = egui::Context::default(); + ctx.run_ui(egui::RawInput::default(), |ui| { + tick(&mut app, ui.ctx()); + show(&mut app, ui.ctx()); + }) + .textures_delta + .clear(); + assert_eq!(app.updates_rt.updater.status(), &Status::InstallFailed("the disk is full".into())); + answer(&mut app.updates_rt, CheckResultResponse::CheckAgain); + assert!(app.updates_rt.check_requested); + let _ = std::fs::remove_dir_all(&root); + } }