Host: native update commands (update_status, update_check, apply_update, app_ready) and the update loop are served for every app that sets AppConfig::with_updates; commands::UPDATE, testkit::assert_update_guards
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1904031e6f
commit
e88339bda6
6 changed files with 103 additions and 5 deletions
|
|
@ -89,6 +89,10 @@ pub struct AppConfig {
|
|||
/// Google Fonts (install, remove, preview; the cache in app data): serves [`commands::GFONTS`]
|
||||
/// and calls [`SiteHooks::fonts_changed`]. Needs the `net` feature.
|
||||
pub fonts: bool,
|
||||
/// Native updates and the hotfix channel: serves [`commands::UPDATE`] and runs the update loop.
|
||||
/// Needs `updater` (the plugin) and the `net` feature.
|
||||
#[cfg(feature = "net")]
|
||||
pub updates: Option<&'static crate::updater::UpdaterConfig>,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
|
|
@ -113,9 +117,18 @@ impl AppConfig {
|
|||
store: None,
|
||||
media: false,
|
||||
fonts: false,
|
||||
#[cfg(feature = "net")]
|
||||
updates: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Serves the update commands ([`commands::UPDATE`]) and runs the update loop.
|
||||
#[cfg(feature = "net")]
|
||||
pub const fn with_updates(mut self, updates: &'static crate::updater::UpdaterConfig) -> Self {
|
||||
self.updates = Some(updates);
|
||||
self
|
||||
}
|
||||
|
||||
/// Serves the Google Fonts commands ([`commands::GFONTS`]).
|
||||
pub const fn with_fonts(mut self) -> Self {
|
||||
self.fonts = true;
|
||||
|
|
@ -1134,6 +1147,17 @@ fn store_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + '
|
|||
|_| false
|
||||
}
|
||||
|
||||
/// The update commands ([`commands::UPDATE`]): with the `net` feature only.
|
||||
#[cfg(feature = "net")]
|
||||
fn update_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static {
|
||||
crate::update_commands::handler()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "net"))]
|
||||
fn update_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static {
|
||||
|_| false
|
||||
}
|
||||
|
||||
/// The Google Fonts commands ([`commands::GFONTS`]): with the `net` feature (downloads) only.
|
||||
#[cfg(feature = "net")]
|
||||
fn fonts_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static {
|
||||
|
|
@ -1183,6 +1207,7 @@ impl App {
|
|||
let store_handler = store_handler();
|
||||
let media_handler = media_handler();
|
||||
let fonts_handler = fonts_handler();
|
||||
let update_handler = update_handler();
|
||||
let builder = tauri::Builder::<Wry>::default()
|
||||
// Must be first: a second launch focuses the running window instead of fighting over
|
||||
// the app's data and WebView profile (and hands over its link).
|
||||
|
|
@ -1265,6 +1290,8 @@ impl App {
|
|||
crate::media_commands::init(&handle);
|
||||
#[cfg(feature = "net")]
|
||||
crate::fonts_commands::init(&handle);
|
||||
#[cfg(feature = "net")]
|
||||
crate::update_commands::init(&handle);
|
||||
// Safe mode for this run: the launch argument, or Shift held while starting (Windows).
|
||||
crate::safe::set(crate::safe::at_launch(std::env::args(), host::shift_held()));
|
||||
// Focus and layering diagnostics for this run: `--diag` or WAYBETTER_DIAG=1 (app log folder, diag.log).
|
||||
|
|
@ -1285,6 +1312,8 @@ impl App {
|
|||
media_handler(invoke)
|
||||
} else if commands::GFONTS.contains(&invoke.message.command()) {
|
||||
fonts_handler(invoke)
|
||||
} else if commands::UPDATE.contains(&invoke.message.command()) {
|
||||
update_handler(invoke)
|
||||
} else if commands::is_host(invoke.message.command()) {
|
||||
host_handler(invoke)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -86,9 +86,13 @@ pub const MEDIA: &[&str] = &[
|
|||
/// a preview), served when the app sets `AppConfig::fonts`; needs the `net` feature.
|
||||
pub const GFONTS: &[&str] = &["gfonts_installed", "gfonts_install", "gfonts_remove", "gfonts_faces", "gfonts_preview"];
|
||||
|
||||
/// Shell-only, optional: native updates (status, check now, apply, the shell reached a working
|
||||
/// state), served when the app sets `AppConfig::updates`; needs the `net` feature.
|
||||
pub const UPDATE: &[&str] = &["update_status", "update_check", "apply_update", "app_ready"];
|
||||
|
||||
/// Whether `command` is one of the host's (dispatched to the host, never to the app's handler).
|
||||
pub fn is_host(command: &str) -> bool {
|
||||
SHELL.contains(&command) || TAB.contains(&command) || SITE.contains(&command) || STORE.contains(&command) || MEDIA.contains(&command) || GFONTS.contains(&command)
|
||||
SHELL.contains(&command) || TAB.contains(&command) || SITE.contains(&command) || STORE.contains(&command) || MEDIA.contains(&command) || GFONTS.contains(&command) || UPDATE.contains(&command)
|
||||
}
|
||||
|
||||
/// The capability permission tauri-build generates for `command` (`tabs_sync` → `allow-tabs-sync`).
|
||||
|
|
@ -103,8 +107,8 @@ pub fn permission(command: &str) -> String {
|
|||
/// tauri_build::AppManifest::new().commands(waybetter_host::commands::manifest(&["draft_list"]))
|
||||
/// ```
|
||||
pub fn manifest(app: &[&'static str]) -> &'static [&'static str] {
|
||||
let mut all: Vec<&'static str> = Vec::with_capacity(app.len() + SHELL.len() + TAB.len() + SITE.len() + STORE.len() + MEDIA.len() + GFONTS.len());
|
||||
for command in app.iter().chain(SHELL).chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS) {
|
||||
let mut all: Vec<&'static str> = Vec::with_capacity(app.len() + SHELL.len() + TAB.len() + SITE.len() + STORE.len() + MEDIA.len() + GFONTS.len() + UPDATE.len());
|
||||
for command in app.iter().chain(SHELL).chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE) {
|
||||
if !all.contains(command) {
|
||||
all.push(command);
|
||||
}
|
||||
|
|
@ -121,7 +125,7 @@ mod tests {
|
|||
let all = manifest(&["draft_list", "tabs_sync"]);
|
||||
assert_eq!(all[0], "draft_list");
|
||||
assert_eq!(all.iter().filter(|c| **c == "tabs_sync").count(), 1);
|
||||
for command in SHELL.iter().chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS) {
|
||||
for command in SHELL.iter().chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE) {
|
||||
assert!(all.contains(command), "{command}");
|
||||
assert!(is_host(command));
|
||||
}
|
||||
|
|
@ -131,7 +135,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn shell_and_tab_commands_do_not_overlap() {
|
||||
assert!(!SHELL.iter().any(|command| TAB.contains(command) || SITE.contains(command) || STORE.contains(command) || MEDIA.contains(command) || GFONTS.contains(command)));
|
||||
assert!(!SHELL.iter().any(|command| TAB.contains(command) || SITE.contains(command) || STORE.contains(command) || MEDIA.contains(command) || GFONTS.contains(command) || UPDATE.contains(command)));
|
||||
assert!(!TAB.iter().any(|command| SITE.contains(command)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ mod store_commands;
|
|||
pub mod media_commands;
|
||||
#[cfg(all(feature = "app", feature = "net"))]
|
||||
mod fonts_commands;
|
||||
#[cfg(all(feature = "app", feature = "net"))]
|
||||
mod update_commands;
|
||||
#[cfg(all(feature = "app", debug_assertions))]
|
||||
pub mod devloop;
|
||||
#[cfg(feature = "tauri")]
|
||||
|
|
|
|||
|
|
@ -235,6 +235,14 @@ pub fn assert_fonts_guards() {
|
|||
}
|
||||
}
|
||||
|
||||
/// The update commands' wrappers check their caller first. Runs against the host's source.
|
||||
pub fn assert_update_guards() {
|
||||
let source = include_str!("update_commands.rs");
|
||||
for command in commands::UPDATE {
|
||||
assert_guarded(source, command, "shell_only(&webview)?");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -244,6 +252,7 @@ mod tests {
|
|||
assert_store_guards();
|
||||
assert_media_guards();
|
||||
assert_fonts_guards();
|
||||
assert_update_guards();
|
||||
}
|
||||
|
||||
fn caps_with(shell_extra: &str, tab_extra: &str, site_extra: &str) -> Capabilities {
|
||||
|
|
|
|||
53
crates/waybetter-host/src/update_commands.rs
Normal file
53
crates/waybetter-host/src/update_commands.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
//! Native updates, for every app that sets [`crate::app::AppConfig::updates`]: the shell's four
|
||||
//! narrow commands ([`crate::commands::UPDATE`]) over [`crate::updater`] (signed native updates and
|
||||
//! the signed data-only hotfix channel, configured by the app's [`crate::updater::UpdaterConfig`]).
|
||||
//! Everything runs in Rust; site tabs get no updater IPC at all, only the verified hotfix data the
|
||||
//! app injects ([`crate::updater::hotfix_script`]).
|
||||
|
||||
use crate::app::config;
|
||||
use crate::host::shell_only;
|
||||
use crate::update::UpdateStatus;
|
||||
use crate::updater;
|
||||
use tauri::{AppHandle, Webview};
|
||||
|
||||
/// Runs the launch handshake, loads the cached hotfix and starts the update loop. Does nothing
|
||||
/// unless the app set [`crate::app::AppConfig::updates`].
|
||||
pub(crate) fn init(app: &AppHandle) {
|
||||
if let Some(updates) = config(app).updates {
|
||||
updater::init(app, updates);
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn update_status(webview: Webview, app: AppHandle) -> Result<UpdateStatus, String> {
|
||||
shell_only(&webview)?;
|
||||
updater::status(&app)
|
||||
}
|
||||
|
||||
/// "Check for updates" in the app menu: the same check the timer runs, once, now (the same updater
|
||||
/// endpoint; never queued, never retried). Returns the status when the check has finished.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn update_check(webview: Webview, app: AppHandle) -> Result<UpdateStatus, String> {
|
||||
shell_only(&webview)?;
|
||||
updater::check_now(&app).await
|
||||
}
|
||||
|
||||
/// Installs the downloaded update and restarts. On Windows the quiet NSIS installer replaces
|
||||
/// the app and relaunches it (`/UPDATE /R`), and this process exits inside `install`.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn apply_update(webview: Webview, app: AppHandle) -> Result<(), String> {
|
||||
shell_only(&webview)?;
|
||||
updater::apply(&app)
|
||||
}
|
||||
|
||||
/// The shell reached a working state: confirms a just-installed update and returns the status.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn app_ready(webview: Webview, app: AppHandle) -> Result<UpdateStatus, String> {
|
||||
shell_only(&webview)?;
|
||||
updater::ready(&app)
|
||||
}
|
||||
|
||||
/// The update commands ([`crate::commands::UPDATE`]).
|
||||
pub(crate) fn handler() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||
tauri::generate_handler![update_status, update_check, apply_update, app_ready]
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ const HOTFIX_SIG_FILE: &str = "hotfix.json.sig";
|
|||
|
||||
/// What differs between apps: the hotfix channel, the page names hotfix data is delivered under,
|
||||
/// the manual download page and what to do when an update is ready.
|
||||
#[derive(Debug)]
|
||||
pub struct UpdaterConfig {
|
||||
/// Hotfix data goes to this site's tabs.
|
||||
pub site: &'static SiteSpec,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue