Updates: the updater reports installing, ready, installed and failed, and installs at exit

craft-update moves from c5a1cbfb to the head of storytold/craft-libs#696.
The check result window now shows for every status but idle, so the first
start after an update reports that it installed or why it failed. Restart
to Update on a ready update asks the app to close.

The install helper now starts when the app exits. on_exit calls
Updater::start_pending_update through VectorcraftApp, because the macOS
exit in end_before_teardown ends the process without dropping the app.
This commit is contained in:
dotCooCoo
2026-11-08 18:12:22 -08:00
parent 9ddcd40098
commit ac076655db
4 changed files with 51 additions and 31 deletions
Generated
+1 -14
View File
@@ -1059,7 +1059,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=c365d75c48b0e47f4b43cdb6592d0cdc39d2d52e#c365d75c48b0e47f4b43cdb6592d0cdc39d2d52e"
dependencies = [
"chrono",
"dunce",
@@ -8040,7 +8040,6 @@ dependencies = [
"nix",
"typed-path",
"windows-sys 0.61.2",
"zopfli",
]
[[package]]
@@ -8055,18 +8054,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.4.12"
+1 -1
View File
@@ -22,7 +22,7 @@ craft-window = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63
# The `.vectorcraft` envelope: its members, version, embedded images and the data after the JSON.
craft-vectorcraft-format = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" }
# 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-update = { git = "https://github.com/dotCooCoo/craft-libs", version = "=0.1.0", rev = "c365d75c48b0e47f4b43cdb6592d0cdc39d2d52e", features = ["egui"] }
# internal
vectorcraft-geom = { path = "crates/geom" }
vectorcraft-color = { path = "crates/color" }
+3
View File
@@ -92,6 +92,9 @@ impl eframe::App for App {
}
fn on_exit(&mut self) {
file_access::unconfined(|| save_prefs(&self.app));
// A ready update installs once the process has exited. The macOS exit below skips
// dropping the app, so the helper is started here.
self.app.start_pending_update();
end_before_teardown(self.frames);
}
}
+46 -16
View File
@@ -5,7 +5,7 @@ use serde_json::Value;
use crate::VectorcraftApp;
#[cfg(not(target_arch = "wasm32"))]
use craft_update::updater::{Action, Updater};
use craft_update::updater::{Action, Status, Updater};
#[cfg(not(target_arch = "wasm32"))]
use craft_update::{CheckResultDialog, CheckResultResponse};
@@ -20,10 +20,10 @@ pub(crate) struct Runtime {
/// The context of the last frame, to wake the UI when a check is queued from a menu callback.
#[cfg(not(target_arch = "wasm32"))]
repaint: Option<egui::Context>,
/// Whether the result of Help ▸ Check for Updates is in view.
pub(crate) manual_window_open: bool,
/// The web build has no updater; its window only says so.
#[cfg(target_arch = "wasm32")]
pub(crate) manual_window_open: bool,
#[cfg(target_arch = "wasm32")]
pub(crate) status_message: Option<String>,
}
@@ -38,6 +38,7 @@ impl Default for Runtime {
check_requested: false,
#[cfg(not(target_arch = "wasm32"))]
repaint: None,
#[cfg(target_arch = "wasm32")]
manual_window_open: false,
#[cfg(target_arch = "wasm32")]
status_message: None,
@@ -51,13 +52,20 @@ impl VectorcraftApp {
pub fn set_startup_update_check(&mut self, on: bool) {
self.updates_rt.startup_check = on;
}
/// Starts the install helper of an update that is ready, so it installs once the process has
/// exited (does nothing when no update is ready). Call it in `on_exit` when the process ends
/// there, since the app is then never dropped.
#[cfg(not(target_arch = "wasm32"))]
pub fn start_pending_update(&mut self) {
self.updates_rt.updater.start_pending_update();
}
}
pub(crate) fn check(app: &mut VectorcraftApp) -> Result<Value, String> {
#[cfg(not(target_arch = "wasm32"))]
{
app.updates_rt.check_requested = true;
app.updates_rt.manual_window_open = true;
if let Some(ctx) = &app.updates_rt.repaint {
ctx.request_repaint();
}
@@ -87,10 +95,18 @@ pub(crate) fn tick(app: &mut VectorcraftApp, ctx: &egui::Context) {
let _ = (app, ctx);
}
/// Whether the check result window shows: for every status but idle (a requested check, and the
/// first start after an update, which reports that it installed or why it failed), and never
/// over the startup prompt.
#[cfg(not(target_arch = "wasm32"))]
fn result_window_shows(status: &Status, prompt_open: bool) -> bool {
!prompt_open && !matches!(status, Status::Idle)
}
pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
#[cfg(not(target_arch = "wasm32"))]
{
// The restart helper is scheduled before the app closes, so no update starts over unsaved work.
// The close for a ready update waits until no document holds unsaved work.
let clean = !crate::unsaved::any_dirty(app);
match app.updates_rt.updater.show_if(ctx, clean) {
Action::OpenUrl(url) => app.open_url(&url),
@@ -99,20 +115,15 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
}
// An available update goes to the prompt above, which replaces the result dialog.
let rt = &mut app.updates_rt;
if rt.manual_window_open && rt.updater.prompt_open() {
rt.manual_window_open = false;
}
if rt.manual_window_open {
if result_window_shows(rt.updater.status(), rt.updater.prompt_open()) {
match CheckResultDialog::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")).show(ctx, rt.updater.status()) {
CheckResultResponse::None => {}
CheckResultResponse::Close => rt.manual_window_open = false,
// Closing the dialog dismisses the outcome it showed.
CheckResultResponse::Close => rt.updater.dismiss_status(),
CheckResultResponse::CheckAgain => rt.updater.check_now(ctx),
CheckResultResponse::RestartToUpdate => rt.updater.restart_to_update(),
}
}
// Closing the dialog dismisses the outcome it showed.
if !rt.manual_window_open {
rt.updater.dismiss_status();
}
}
#[cfg(target_arch = "wasm32")]
@@ -142,13 +153,12 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(target_arch = "wasm32"))]
use craft_update::updater::Status;
use vectorcraft_engine::Session;
#[test]
fn default_runtime_is_idle() {
let rt = Runtime::default();
#[cfg(target_arch = "wasm32")]
assert!(!rt.manual_window_open);
#[cfg(not(target_arch = "wasm32"))]
{
@@ -158,6 +168,26 @@ mod tests {
}
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn result_window_shows_every_status_but_idle() {
let shown = [
Status::Checking,
Status::UpToDate,
Status::Failed("offline".into()),
Status::Installing,
Status::Ready,
Status::Managed,
Status::Updated("99.0.0".into()),
Status::InstallFailed("locked".into()),
];
for status in &shown {
assert!(result_window_shows(status, false), "{status:?} shows");
assert!(!result_window_shows(status, true), "{status:?} waits for the prompt");
}
assert!(!result_window_shows(&Status::Idle, false));
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn startup_prompt_shows_and_closes() {