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.
This commit is contained in:
Dylan M. Taylor
2026-10-25 13:58:39 -04:00
parent 9932ca0cf2
commit 6f813b74e9
5 changed files with 72 additions and 175 deletions
Generated
+11 -10
View File
@@ -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]]
+1 -1
View File
@@ -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" }
+3
View File
@@ -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 }
+6
View File
@@ -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::<Vec<_>>()) {
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 {
+51 -164
View File
@@ -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<String>,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) prompt: Option<craft_update::StartupPrompt>,
#[cfg(not(target_arch = "wasm32"))]
rx: Option<std::sync::mpsc::Receiver<Result<Option<craft_update::Release>, String>>>,
#[cfg(not(target_arch = "wasm32"))]
checking: bool,
#[cfg(not(target_arch = "wasm32"))]
update_rx: Option<std::sync::mpsc::Receiver<Result<(), String>>>,
}
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();
}
}