desktop dev update channel: every push to main builds dev bundles (<next patch>-dev.<UTC>), the VDS signs and publishes dev/latest.json (newest 5); app channel setting stable|dev, background updater at start / 15 min / resume / channel switch, deferred while in use, no downgrade, autostart rewritten on every start, card shows version and channel

This commit is contained in:
CRTkafa 2026-10-23 23:14:08 +08:00
parent 4dff7d9b2e
commit 39b2f73ad7
25 changed files with 732 additions and 47 deletions

View file

@ -26,7 +26,6 @@ use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu, WINDOW_SUBMENU_ID
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::{AppHandle, Emitter, Manager, RunEvent, Runtime, WindowEvent};
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
use tauri_plugin_updater::UpdaterExt;
use tauri_plugin_deep_link::DeepLinkExt;
/// WebKitGTK's DMABUF renderer leaves the window blank on some Linux GPU / driver / VM combinations. Turn it off unless
@ -86,29 +85,79 @@ fn tray(app: &tauri::App) -> tauri::Result<()> {
/// Silent self-update: at start (after a minute) and every 30 minutes, a signed update from storebound.dev is
/// downloaded, installed and the app restarts with the same arguments (`--hidden` stays hidden). Errors are logged.
/// Background updater (docs/desktop-release.md, "Dev channel"): asks the chosen channel's latest.json at start, every
/// 15 minutes, right after a resume from sleep and when Settings switched the channel; installs a strictly newer, signed
/// build silently (the plugin checks the minisign signature first), deferred while the main window is in use (at most
/// updates::MAX_DEFERS checks in a row). Every step goes to host.log as `auto-update ...`.
fn auto_update(app: AppHandle) {
tauri::async_runtime::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
use std::time::{Duration, Instant, SystemTime};
tokio::time::sleep(Duration::from_secs(10)).await;
let mut first = true;
let mut last_check = Instant::now();
let mut last_wall = SystemTime::now();
let mut defers = 0u32;
loop {
match app.updater() {
Ok(u) => match u.check().await {
Ok(Some(update)) => {
logs::host("info", &format!("auto-update: {} available, installing", update.version));
match update.download_and_install(|_, _| {}, || {}).await {
Ok(()) => app.restart(),
Err(e) => logs::host("warn", &format!("auto-update: install failed: {e}")),
}
}
Ok(None) => {}
Err(e) => logs::host("warn", &format!("auto-update: check failed: {e}")),
},
Err(e) => logs::host("warn", &format!("auto-update: no updater: {e}")),
let now_wall = SystemTime::now();
let gap = now_wall.duration_since(last_wall).map(|d| d.as_secs()).unwrap_or(0);
last_wall = now_wall;
let kicked = updates::take_kick();
if updates::check_due(first, kicked, gap, last_check.elapsed().as_secs()) {
let why = if first { "start" } else if kicked { "channel switched" } else if gap > updates::TICK_SECS + 60 { "resume" } else { "every 15 min" };
first = false;
last_check = Instant::now();
defers = auto_update_once(&app, why, defers).await;
}
tokio::time::sleep(std::time::Duration::from_secs(30 * 60)).await;
tokio::time::sleep(Duration::from_secs(updates::TICK_SECS)).await;
}
});
}
/// One check of the background updater; returns the new count of deferred installs.
async fn auto_update_once(app: &AppHandle, why: &str, defers: u32) -> u32 {
let state = app.state::<commands::AppState>();
let channel = match state.saved.lock() {
Ok(s) => s.settings.channel.clone(),
Err(_) => return defers,
};
let current = env!("CARGO_PKG_VERSION");
let update = match commands::find_update(app, &channel).await {
Ok(Some(u)) => u,
Ok(None) => {
logs::host("info", &format!("auto-update ({why}): {current} is current on {channel}"));
return 0;
}
Err(e) => {
logs::host("warn", &format!("auto-update ({why}): check of {channel} failed: {}", e.message));
return defers;
}
};
if !updates::should_install(current, &update.version) {
logs::host("info", &format!("auto-update ({why}): {channel} offers {}, not newer than {current}: no downgrade", update.version));
return 0;
}
let main = app.get_webview_window(policy::MAIN);
let visible = main.as_ref().and_then(|w| w.is_visible().ok()).unwrap_or(false);
let focused = main.as_ref().and_then(|w| w.is_focused().ok()).unwrap_or(false);
if updates::defer_install(visible, focused, defers) {
logs::host("info", &format!("auto-update ({why}): {} on {channel} waits, the window is in use ({}/{})", update.version, defers + 1, updates::MAX_DEFERS));
return defers + 1;
}
logs::host("info", &format!("auto-update ({why}): installing {} from {channel} (this is {current})", update.version));
// the sidecar binary lives inside the app bundle that is about to be replaced
state.sidecar.shutdown().await;
match update.download_and_install(|_, _| {}, || {}).await {
Ok(()) => {
logs::host("info", &format!("auto-update: {} installed, restarting", update.version));
app.restart()
}
Err(e) => {
logs::host("warn", &format!("auto-update: install of {} failed: {}", update.version, updates::error_text(&e.to_string())));
0
}
}
}
fn show_main<R: Runtime>(app: &AppHandle<R>) {
if let Some(w) = app.get_webview_window(policy::MAIN) {
let _ = w.unminimize();
@ -281,13 +330,11 @@ pub fn run() {
if let Err(e) = commands::sync_machine(&state) {
logs::host("warn", &format!("accounts / app folders not synced at start: {}", e.message));
}
// start at logon, in the tray: set once (first run); the owner can turn it off in the OS afterwards
let marker = state.data_dir.join("autostart-set");
if !marker.exists() {
match app.autolaunch().enable() {
Ok(()) => { let _ = std::fs::write(&marker, "1"); logs::host("info", "autostart at logon enabled (--hidden, tray)"); }
Err(e) => logs::host("warn", &format!("autostart not enabled: {e}")),
}
// start at logon, in the tray: rewritten on every start (Windows: the HKCU Run entry then points at the
// running exe, so a moved or updated install fixes itself)
match app.autolaunch().enable() {
Ok(()) => logs::host("info", "autostart at logon set (--hidden, tray)"),
Err(e) => logs::host("warn", &format!("autostart not set: {e}")),
}
app.manage(state);
tray(app)?;