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.
This commit is contained in:
dotCooCoo committed 2026-11-08 10:02:58 -08:00
1 parent ddeb7e791f
commit 20338e0832
4 files changed
+92 -31

No files matched your search

Generated
+6 -19
View File
@@ -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"
+4 -4
View File
@@ -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 }
+3 -1
View File
@@ -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();
}
}
+79 -7
View File
@@ -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<egui::Event>) {
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);
}
}