Merge pull request #1980 from dotCooCoo/chore/craft-update-updater-fixes

Updates: the updater reports installing, ready, installed and failed, and installs at exit
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-08 22:12:17 -05:00
commit 61423ea35b
6 files changed
+110 -28

No files matched your search

Generated
+7 -8
View File
@@ -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]]
+5 -5
View File
@@ -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" }
+3
View File
@@ -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
+8
View File
@@ -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<String>) {
+83 -12
View File
@@ -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);
}
}
+4 -3
View File
@@ -6,7 +6,8 @@ use std::process::Command;
use serde_json::Value;
const SHARED_REV: &str = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7";
const SHARED_REPO: &str = "https://github.com/dotCooCoo/craft-libs";
const SHARED_REV: &str = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae";
const REGISTRY: &str = "registry+https://github.com/rust-lang/crates.io-index";
pub fn run(root: &Path) -> Result<(), String> {
@@ -70,7 +71,7 @@ fn validate(metadata: &Value) -> Result<(), String> {
}
matches.first().copied().ok_or_else(|| format!("missing {name}"))
};
let source = format!("git+https://github.com/storytold/craft-libs?rev={SHARED_REV}#{SHARED_REV}");
let source = format!("git+{SHARED_REPO}?rev={SHARED_REV}#{SHARED_REV}");
for (name, version, expected_source, expected_features) in [
("craft-pdf", "0.1.0", source.as_str(), vec!["write", "write-simple-text"]),
("krilla", "0.8.2", source.as_str(), vec!["raster-images", "rayon", "simple-text"]),
@@ -143,7 +144,7 @@ mod tests {
use serde_json::json;
fn metadata() -> Value {
let source = format!("git+https://github.com/storytold/craft-libs?rev={SHARED_REV}#{SHARED_REV}");
let source = format!("git+{SHARED_REPO}?rev={SHARED_REV}#{SHARED_REV}");
json!({
"packages": [
{"id":"app", "name":"deckcraft-pdf"},