From 20338e08322855b2f83d2c3fca18ee351b387e6f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 10:02:58 -0800 Subject: [PATCH] Updates: the updater reports installing, ready, installed and failed, and installs at exit Moves the craft-libs crates (craft-update, craft-docpath, craft-media, craft-ui) from c5a1cbfb5f to a branch that merges storytold/craft-libs #687 and #696. - The result dialog shows whenever the updater reports a status, so the first start after an update says whether it installed or failed. Closing it dismisses the status; the startup prompt still takes over. - Restart to Update in the dialog asks the app to close through the usual close path. - on_exit starts the helper of a ready update, since the system Quit ends the process before the updater is dropped. --- Cargo.lock | 25 +++------- Cargo.toml | 8 +-- apps/soundcraft/src/main.rs | 4 +- crates/ui-egui/src/updates_ui.rs | 86 +++++++++++++++++++++++++++++--- 4 files changed, 92 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e112f975..c6a0eb02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1064,17 +1064,17 @@ dependencies = [ [[package]] name = "craft-docpath" 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-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", @@ -1097,7 +1097,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", ] @@ -1105,7 +1105,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", @@ -1126,7 +1126,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", @@ -6456,7 +6456,6 @@ dependencies = [ "nix", "typed-path", "windows-sys 0.61.2", - "zopfli", ] [[package]] @@ -6471,18 +6470,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" -[[package]] -name = "zopfli" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - [[package]] name = "zune-core" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index 5eb04d03..75882f0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,15 +50,15 @@ zip = { version = "9", default-features = false } cpal = "0.18" # Which file paths stored in a session may be opened. -craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7" } +craft-docpath = { version = "0.1.0", git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae" } # Video containers and bitstreams, shared with the other craft apps. -craft-media = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7" } +craft-media = { version = "0.1.0", git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae" } # craft-update: app-specific pretend versions apply to startup and manual checks. -craft-update = { version = "=0.1.0", git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["egui"] } +craft-update = { version = "=0.1.0", git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", features = ["egui"] } # ui -craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["serde"] } +craft-ui = { version = "0.1.0", git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", features = ["serde"] } egui = { version = "0.37" } # Resizable table columns (About ▸ Contributors / Models); no image loaders. egui_extras = { version = "0.37", default-features = false } diff --git a/apps/soundcraft/src/main.rs b/apps/soundcraft/src/main.rs index ddf57e05..dc029dac 100644 --- a/apps/soundcraft/src/main.rs +++ b/apps/soundcraft/src/main.rs @@ -108,9 +108,11 @@ impl eframe::App for App { logger.report_audio_thread(); } // The system Quit (⌘Q) ends the process from inside the event loop, so `run_native` never - // returns: release the player's plugin instances and shut plugin hosting down here. + // returns: release the player's plugin instances and shut plugin hosting down here, and + // start the helper of an update that waits for the app to close. self.0.player = None; soundcraft_engine::shutdown_plugin_hosts(); + self.0.start_pending_update(); } } diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index 8fee933a..821ad596 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -43,6 +43,19 @@ impl SoundApp { pub fn set_startup_update_check(&mut self, on: bool) { self.updates_rt.startup_check = on; } + + /// Starts the helper of an update that waits for the app to close. Call it when the exit is certain. + #[cfg(not(target_arch = "wasm32"))] + pub fn start_pending_update(&mut self) { + self.updates_rt.updater.start_pending_update(); + } +} + +/// Whether the result dialog shows: while the updater reports something (a requested check, or what the last update +/// came to, which the check at start reads), and not while the startup prompt is open. +#[cfg(not(target_arch = "wasm32"))] +fn shows_result(status: &Status, prompt_open: bool) -> bool { + !matches!(status, Status::Idle) && !prompt_open } pub(crate) fn check(app: &mut SoundApp) { @@ -62,7 +75,6 @@ pub(crate) fn tick(app: &mut SoundApp, ctx: &egui::Context) { let rt = &mut app.updates_rt; if std::mem::take(&mut rt.check_requested) { rt.updater.check_now(ctx); - rt.manual_window_open = true; } else if rt.startup_check { rt.updater.check_at_start(ctx); } @@ -82,20 +94,19 @@ pub(crate) fn show(app: &mut SoundApp, ctx: &egui::Context) { Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close), Action::None => {} } - // Help ▸ Check for Updates shows its outcome in the shared result dialog until the user closes it. + // Help ▸ Check for Updates, and the first start after an update, show their outcome in the shared result dialog + // until the user closes it. A found update goes to the startup prompt, which takes over from the dialog. let rt = &mut app.updates_rt; + rt.manual_window_open = shows_result(rt.updater.status(), rt.updater.prompt_open()); if rt.manual_window_open { match rt.result_dialog.show(ctx, rt.updater.status()) { CheckResultResponse::Close => rt.manual_window_open = false, CheckResultResponse::CheckAgain => rt.updater.check_now(ctx), + CheckResultResponse::RestartToUpdate => rt.updater.restart_to_update(), CheckResultResponse::None => {} } - // A found update goes to the startup prompt, which takes over from the dialog. - if matches!(rt.updater.status(), Status::Idle) { - rt.manual_window_open = false; - } } - // Closing the dialog dismisses the outcome it showed. + // Closing the dialog, or the prompt taking over, dismisses the outcome it showed. if !rt.manual_window_open { rt.updater.dismiss_status(); } @@ -115,3 +126,64 @@ pub(crate) fn show(app: &mut SoundApp, ctx: &egui::Context) { app.updates_rt.manual_window_open = open; } } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + use crate::Services; + use craft_update::Paths; + use std::sync::Arc; + + /// One pass of the update check and its windows, with `events` as the frame's input, dropping texture deltas the + /// tests do not inspect. + fn frame(app: &mut SoundApp, ctx: &egui::Context, events: Vec) { + let raw = egui::RawInput { events, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| { + tick(app, ui.ctx()); + show(app, ui.ctx()); + }); + out.textures_delta.clear(); + } + + #[test] + fn the_result_dialog_shows_every_reported_status_until_the_prompt_opens() { + for status in [ + Status::Checking, + Status::UpToDate, + Status::Failed("offline".into()), + Status::Installing, + Status::Ready, + Status::Managed, + Status::Updated("0.2.0".into()), + Status::InstallFailed("disk full".into()), + ] { + assert!(shows_result(&status, false), "{status:?}"); + assert!(!shows_result(&status, true), "{status:?} with the prompt open"); + } + assert!(!shows_result(&Status::Idle, false)); + } + + /// The first start after an update says so in the result dialog, which stays until the user closes it. + #[test] + fn the_first_start_after_an_update_reports_it_until_closed() { + let dir = std::env::temp_dir().join(format!("soundcraft-update-result-{}", std::process::id())); + let paths = Paths::under(&dir); + std::fs::create_dir_all(&paths.data).unwrap(); + std::fs::write(paths.data.join("self-update-done.txt"), "v0.2.0").unwrap(); + let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default()); + app.updates_rt.updater = + Updater::new(&crate::UPDATE_APP, "0.2.0").with_source(Arc::new(|| Ok(None))).with_pins_path(dir.join("pins.json")).with_paths(paths); + app.set_startup_update_check(true); + let ctx = egui::Context::default(); + for _ in 0..3 { + frame(&mut app, &ctx, Vec::new()); + } + assert_eq!(app.updates_rt.updater.status(), &Status::Updated("0.2.0".into())); + assert!(app.updates_rt.manual_window_open); + let escape = egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }; + frame(&mut app, &ctx, vec![escape]); + assert_eq!(app.updates_rt.updater.status(), &Status::Idle); + assert!(!app.updates_rt.manual_window_open); + let _ = std::fs::remove_dir_all(dir); + } +}