From 6f813b74e94006f8dd2baf3e17517ab5fa9655f6 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 13:58:13 -0400 Subject: [PATCH] refactor(updates): drive the flow with craft-update's Updater Startup check is opt-in for the desktop app, the Windows update helper is dispatched before argument parsing, and a successful update closes through the app's own close path. --- Cargo.lock | 21 +-- Cargo.toml | 2 +- apps/effectcraft/Cargo.toml | 3 + apps/effectcraft/src/main.rs | 6 + crates/ui-egui/src/updates_ui.rs | 215 ++++++++----------------------- 5 files changed, 72 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f70ae710..175f3690a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1033,7 +1033,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=fcd3441993ee238144ee56538ad81b4a333f1ed9#fcd3441993ee238144ee56538ad81b4a333f1ed9" +source = "git+https://github.com/storytold/craft-libs?rev=13ceaf2f31620531f39a54f6b994d581a6082a93#13ceaf2f31620531f39a54f6b994d581a6082a93" dependencies = [ "chrono", "dunce", @@ -1255,6 +1255,7 @@ name = "effectcraft" version = "0.17.0" dependencies = [ "cpal", + "craft-update", "effectcraft-engine", "effectcraft-host", "effectcraft-ui-egui", @@ -2048,7 +2049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3422,7 +3423,7 @@ dependencies = [ "once_cell", "png", "thiserror 2.0.21", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3547,7 +3548,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4714,7 +4715,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4771,7 +4772,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5130,7 +5131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5446,7 +5447,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5724,7 +5725,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6516,7 +6517,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1ec15fce1..073e744d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/storytold/effectcraft" [workspace.dependencies] craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef", version = "=0.1.0", default-features = false, features = ["read"] } -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "fcd3441993ee238144ee56538ad81b4a333f1ed9", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "13ceaf2f31620531f39a54f6b994d581a6082a93", features = ["egui"] } craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" } # internal effectcraft-time = { path = "crates/time" } diff --git a/apps/effectcraft/Cargo.toml b/apps/effectcraft/Cargo.toml index 8a1ea3ae3..e09e7d8b2 100644 --- a/apps/effectcraft/Cargo.toml +++ b/apps/effectcraft/Cargo.toml @@ -48,3 +48,6 @@ objc2-app-kit = { version = "0.3", default-features = false, features = ["std", # PhotoCraft and PdfCraft) registers the handler (Objective-C interop needs unsafe, which our # workspace forbids) behind a safe API. Pinned exactly: review the diff before bumping. fmv-macos-events = "=0.1.0" + +[target.'cfg(windows)'.dependencies] +craft-update = { workspace = true } diff --git a/apps/effectcraft/src/main.rs b/apps/effectcraft/src/main.rs index a95e79a49..ab0e219e9 100644 --- a/apps/effectcraft/src/main.rs +++ b/apps/effectcraft/src/main.rs @@ -139,6 +139,11 @@ fn install_file_dialogs(app: &mut EffectcraftApp, ctx: &egui::Context) { } fn main() -> eframe::Result { + // Windows: the update helper is a copy of this program started with `--finish-update`. + #[cfg(windows)] + if let Some(result) = craft_update::self_update::finish(&craft_update::suite::EFFECTCRAFT, &std::env::args_os().skip(1).collect::>()) { + std::process::exit(i32::from(result.is_err())); + } // First, so the panic hook and every start-up warning are recorded. Records go to stderr // and the session log; Help ▸ Enable Logging writes through this logger too, and warnings // feed the compatibility report. @@ -264,6 +269,7 @@ fn main() -> eframe::Result { // reported in a toast and forgotten). let _ = session.execute("window.restoreScriptPanels", json!({})); let mut app = EffectcraftApp::new(session); + app.set_startup_update_check(true); app.set_gpu_failure_bridge(gpu_failures); app.ui.start_screen = show_home; if let Some(n) = notice { diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index 5d300e6e4..a93517c70 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -2,47 +2,48 @@ use crate::EffectcraftApp; +#[cfg(not(target_arch = "wasm32"))] +use craft_update::updater::{Action, Status, Updater}; + pub(crate) struct Runtime { - startup_checked: bool, - manual_checked: bool, + #[cfg(not(target_arch = "wasm32"))] + updater: Updater, + /// The desktop app turns the check at start on; tests and the web build leave it off. + #[cfg(not(target_arch = "wasm32"))] + startup_check: bool, + #[cfg(not(target_arch = "wasm32"))] + check_requested: bool, pub(crate) manual_window_open: bool, pub(crate) status_message: Option, - #[cfg(not(target_arch = "wasm32"))] - pub(crate) prompt: Option, - #[cfg(not(target_arch = "wasm32"))] - rx: Option, String>>>, - #[cfg(not(target_arch = "wasm32"))] - checking: bool, - #[cfg(not(target_arch = "wasm32"))] - update_rx: Option>>, } impl Default for Runtime { fn default() -> Self { Self { - startup_checked: false, - manual_checked: false, + #[cfg(not(target_arch = "wasm32"))] + updater: Updater::new(&craft_update::suite::EFFECTCRAFT, env!("CARGO_PKG_VERSION")), + #[cfg(not(target_arch = "wasm32"))] + startup_check: false, + #[cfg(not(target_arch = "wasm32"))] + check_requested: false, manual_window_open: false, status_message: None, - #[cfg(not(target_arch = "wasm32"))] - prompt: None, - #[cfg(not(target_arch = "wasm32"))] - rx: None, - #[cfg(not(target_arch = "wasm32"))] - checking: false, - #[cfg(not(target_arch = "wasm32"))] - update_rx: None, } } } +impl EffectcraftApp { + /// Whether the app asks for a newer release when it starts (off until the desktop app turns it on). + #[cfg(not(target_arch = "wasm32"))] + pub fn set_startup_update_check(&mut self, on: bool) { + self.updates_rt.startup_check = on; + } +} + pub(crate) fn check(app: &mut EffectcraftApp) { #[cfg(not(target_arch = "wasm32"))] { - app.updates_rt.manual_checked = true; - app.updates_rt.manual_window_open = false; - app.updates_rt.status_message = None; - start_check(app); + app.updates_rt.check_requested = true; } #[cfg(target_arch = "wasm32")] { @@ -51,157 +52,37 @@ pub(crate) fn check(app: &mut EffectcraftApp) { } } -#[cfg(not(target_arch = "wasm32"))] -fn start_check(app: &mut EffectcraftApp) { - if app.updates_rt.checking { - return; - } - let (tx, rx) = std::sync::mpsc::channel(); - app.updates_rt.rx = Some(rx); - app.updates_rt.checking = true; - - std::thread::spawn(move || { - let http = match craft_update::Http::new("EffectCraft/Update") { - Ok(h) => h, - Err(e) => { - let _ = tx.send(Err(e)); - return; - } - }; - let res = craft_update::check_for_update(&craft_update::suite::EFFECTCRAFT, &http, env!("CARGO_PKG_VERSION")); - let _ = tx.send(res); - }); -} - pub(crate) fn tick(app: &mut EffectcraftApp, ctx: &egui::Context) { #[cfg(not(target_arch = "wasm32"))] { - if !app.updates_rt.startup_checked { - app.updates_rt.startup_checked = true; - start_check(app); - } - - if app.updates_rt.checking { - ctx.request_repaint_after(std::time::Duration::from_millis(100)); - } - - let mut completed = None; - if let Some(rx) = &app.updates_rt.rx { - if let Ok(res) = rx.try_recv() { - completed = Some(res); - } - } - - if let Some(res) = completed { - app.updates_rt.rx = None; - app.updates_rt.checking = false; - match res { - Ok(Some(release)) => { - let mut prompt = craft_update::StartupPrompt::new(&craft_update::suite::EFFECTCRAFT, release, env!("CARGO_PKG_VERSION")); - prompt.open = true; - app.updates_rt.prompt = Some(prompt); - ctx.request_repaint(); - } - Ok(None) => { - if app.updates_rt.manual_checked { - app.updates_rt.manual_checked = false; - app.updates_rt.status_message = Some(format!("EffectCraft {} is currently the newest version available.", env!("CARGO_PKG_VERSION"))); - app.updates_rt.manual_window_open = true; - ctx.request_repaint(); - } - } - Err(e) => { - if app.updates_rt.manual_checked { - app.updates_rt.manual_checked = false; - app.updates_rt.status_message = Some(format!("Could not check for updates:\n{e}")); - app.updates_rt.manual_window_open = true; - ctx.request_repaint(); - } - } - } + let rt = &mut app.updates_rt; + if std::mem::take(&mut rt.check_requested) { + rt.updater.check_now(ctx); + } else if rt.startup_check { + rt.updater.check_at_start(ctx); } + rt.updater.tick(ctx); } + #[cfg(target_arch = "wasm32")] + let _ = (app, ctx); } pub(crate) fn show(app: &mut EffectcraftApp, ctx: &egui::Context) { #[cfg(not(target_arch = "wasm32"))] { - let mut download_url = None; - if let Some(prompt) = &mut app.updates_rt.prompt { - if let Some(result) = app.updates_rt.update_rx.as_ref().and_then(|rx| rx.try_recv().ok()) { - app.updates_rt.update_rx = None; - match result { - // The app's close path asks about unsaved work; the restart helper relaunches once the process exits. - Ok(()) => ctx.send_viewport_cmd(egui::ViewportCommand::Close), - Err(e) => { - prompt.status_message = Some(e); - prompt.is_installing = false; - } - } - } - match prompt.show(ctx) { - craft_update::StartupPromptResponse::Pending => {} - craft_update::StartupPromptResponse::UpdateAndRestart => { - prompt.is_installing = true; - if let Some(install) = craft_update::self_update::current(&craft_update::suite::EFFECTCRAFT) { - let mut release = prompt.release.clone(); - let (tx, rx) = std::sync::mpsc::channel(); - app.updates_rt.update_rx = Some(rx); - let ctx = ctx.clone(); - std::thread::spawn(move || { - let mut run = || -> Result<(), String> { - let http = craft_update::Http::new("EffectCraft/Update").map_err(|e| format!("could not initialize HTTP: {e}"))?; - if release.assets.is_empty() - && let Some(full) = craft_update::check_for_update(&craft_update::suite::EFFECTCRAFT, &http, env!("CARGO_PKG_VERSION")) - .map_err(|e| format!("could not fetch release assets: {e}"))? - { - release = full; - } - let paths = - craft_update::Paths::for_app(&craft_update::suite::EFFECTCRAFT).map_err(|e| format!("could not find paths: {e}"))?; - let platform = craft_update::Platform::current().ok_or("unsupported platform for self update")?; - craft_update::self_update::update( - &craft_update::suite::EFFECTCRAFT, - &install, - &release, - platform, - &http, - &paths, - &craft_update::Stop::default(), - &mut |_| {}, - &|| Ok(()), - Some(env!("CARGO_PKG_VERSION")), - ) - }; - let result = run(); - if let Err(e) = &result { - log::warn!("self-update failed: {e}"); - } - let _ = tx.send(result); - ctx.request_repaint(); - }); - } else { - prompt.status_message = Some("Cannot auto-update this installation type. Please download manually.".to_string()); - prompt.is_installing = false; - } - } - craft_update::StartupPromptResponse::Download => { - download_url = Some(prompt.release.html_url.clone()); - prompt.open = false; - } - craft_update::StartupPromptResponse::Later => { - prompt.open = false; - } - } - if !prompt.open { - app.updates_rt.prompt = None; - } - } - - if let Some(url) = download_url { - app.updates_rt.prompt = None; - ctx.open_url(egui::OpenUrl::new_tab(url)); + match app.updates_rt.updater.show(ctx) { + Action::OpenUrl(url) => ctx.open_url(egui::OpenUrl::new_tab(url)), + Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close), + Action::None => {} } + // The window answers Help ▸ Check for Updates, so it follows the updater's status. + let rt = &mut app.updates_rt; + (rt.manual_window_open, rt.status_message) = match rt.updater.status() { + Status::Idle => (false, None), + Status::Checking => (true, None), + Status::UpToDate => (true, Some(format!("EffectCraft {} is currently the newest version available.", env!("CARGO_PKG_VERSION")))), + Status::Failed(e) => (true, Some(format!("Could not check for updates:\n{e}"))), + }; } if app.updates_rt.manual_window_open { @@ -230,4 +111,10 @@ pub(crate) fn show(app: &mut EffectcraftApp, ctx: &egui::Context) { app.updates_rt.status_message = None; } } + + // Closing the window dismisses the outcome it showed. + #[cfg(not(target_arch = "wasm32"))] + if !app.updates_rt.manual_window_open { + app.updates_rt.updater.dismiss_status(); + } }