From 1410c927106c49f08daf910209bf6399a91e99fd Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 21 Jul 2026 18:43:50 +0800 Subject: [PATCH] fix(desktop): allow native quit before frontend ready --- .../src/composables/useCloseActionPrompt.ts | 8 ++- src-tauri/src/commands/app_settings.rs | 8 +++ src-tauri/src/lib.rs | 49 +++++++++++++++++-- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/composables/useCloseActionPrompt.ts b/apps/desktop/src/composables/useCloseActionPrompt.ts index f128f7ce91..43478e7e4c 100644 --- a/apps/desktop/src/composables/useCloseActionPrompt.ts +++ b/apps/desktop/src/composables/useCloseActionPrompt.ts @@ -2,6 +2,7 @@ import { ref } from "vue"; import { useSettingsStore } from "@/stores/settingsStore"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import * as api from "@/lib/backend/api"; +import { invoke } from "@tauri-apps/api/core"; export type AppCloseAction = "quit" | "hide"; type AppCloseRequestTarget = "settings" | "quit"; @@ -62,7 +63,12 @@ export function useCloseActionPrompt(options: { requestClose: (action: AppCloseA void import("@tauri-apps/api/event").then(({ listen }) => { listen("dbx-app-close-requested", (event: AppCloseRequestPayload) => { handleCloseRequest(event.payload === "quit" ? "quit" : "settings"); - }).then((unlisten) => unlistenHandles.push(unlisten)); + }).then((unlisten) => { + unlistenHandles.push(unlisten); + // Rust falls back to native Quit until this listener is installed, so a + // failed WebView2 startup cannot leave the tray request waiting forever. + void invoke("mark_frontend_ready"); + }); }); } diff --git a/src-tauri/src/commands/app_settings.rs b/src-tauri/src/commands/app_settings.rs index 1dcc01a49f..0b76e808dd 100644 --- a/src-tauri/src/commands/app_settings.rs +++ b/src-tauri/src/commands/app_settings.rs @@ -59,6 +59,14 @@ pub async fn complete_app_close(app: AppHandle, window: Window, action: String) Ok(()) } +#[tauri::command] +pub fn mark_frontend_ready(app: AppHandle) -> Result<(), String> { + let state = + app.try_state::().ok_or_else(|| "close behavior state is unavailable".to_string())?; + state.set_frontend_ready(true); + Ok(()) +} + #[tauri::command] pub async fn request_app_close_from_window_controls(app: AppHandle) -> Result<(), String> { request_app_close(&app, "settings"); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0660c2f180..5d100a12f2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -15,6 +15,7 @@ use std::time::{Duration, Instant}; use tauri::menu::Menu; #[cfg(target_os = "macos")] use tauri::menu::{AboutMetadata, MenuItem, PredefinedMenuItem, Submenu}; +use tauri::webview::PageLoadEvent; use tauri::RunEvent; use tauri::{ menu::MenuBuilder, @@ -35,11 +36,12 @@ const APP_MENU_COPY_SUPPORT_INFO_ID: &str = "app-menu-copy-support-info"; pub struct CloseBehaviorState { confirmed_exit: AtomicBool, + frontend_ready: AtomicBool, } impl CloseBehaviorState { fn new() -> Self { - Self { confirmed_exit: AtomicBool::new(false) } + Self { confirmed_exit: AtomicBool::new(false), frontend_ready: AtomicBool::new(false) } } pub(crate) fn allow_next_exit(&self) { @@ -49,6 +51,14 @@ impl CloseBehaviorState { fn take_confirmed_exit(&self) -> bool { self.confirmed_exit.swap(false, Ordering::Relaxed) } + + pub(crate) fn set_frontend_ready(&self, ready: bool) { + self.frontend_ready.store(ready, Ordering::Release); + } + + fn is_frontend_ready(&self) -> bool { + self.frontend_ready.load(Ordering::Acquire) + } } #[cfg(target_os = "macos")] const MACOS_TRAY_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/tray-macos-template.png"); @@ -103,6 +113,10 @@ fn should_confirm_app_exit_request(target_os: &str, exit_code: Option, conf should_hide_window_on_close(target_os) && exit_code != Some(tauri::RESTART_EXIT_CODE) && !confirmed_exit } +fn should_fallback_to_native_quit(target: &str, frontend_ready: bool) -> bool { + target == "quit" && !frontend_ready +} + fn native_window_decorations_override(target_os: &str) -> Option { match target_os { "windows" | "linux" => Some(false), @@ -436,6 +450,16 @@ pub(crate) fn hide_main_window_for_close(app: &tauri::AppHand } pub(crate) fn request_app_close(app: &tauri::AppHandle, target: &str) { + let frontend_ready = app.try_state::().is_some_and(|state| state.is_frontend_ready()); + if should_fallback_to_native_quit(target, frontend_ready) { + // A missing WebView2 runtime can prevent the frontend listener from ever + // loading. Only the explicit tray Quit fallback bypasses the prompt. + if let Some(state) = app.try_state::() { + state.allow_next_exit(); + } + app.exit(0); + return; + } show_main_window(app); let _ = app.emit(APP_CLOSE_REQUESTED_EVENT, target); } @@ -589,9 +613,9 @@ mod tests { use super::{ linux_appimage_system_gtk_immodules_cache, linux_appimage_wayland_backend_override, linux_nvidia_driver_from_state, linux_selected_drm_render_device, linux_webkit_rendering_workarounds, - native_window_decorations_override, should_confirm_app_exit_request, should_hide_window_on_close, - should_setup_desktop_tray, should_show_main_window_after_setup, uses_application_level_icon, - LinuxDrmRenderDevice, LinuxNvidiaDriver, + native_window_decorations_override, should_confirm_app_exit_request, should_fallback_to_native_quit, + should_hide_window_on_close, should_setup_desktop_tray, should_show_main_window_after_setup, + uses_application_level_icon, LinuxDrmRenderDevice, LinuxNvidiaDriver, }; use std::ffi::OsStr; use std::path::{Path, PathBuf}; @@ -665,6 +689,13 @@ mod tests { assert!(!should_confirm_app_exit_request("linux", Some(0), false)); } + #[test] + fn only_quit_uses_native_fallback_before_frontend_ready() { + assert!(should_fallback_to_native_quit("quit", false)); + assert!(!should_fallback_to_native_quit("quit", true)); + assert!(!should_fallback_to_native_quit("settings", false)); + } + #[test] fn overrides_native_window_decorations_for_desktop_platforms() { assert_eq!(native_window_decorations_override("windows"), Some(false)); @@ -898,6 +929,14 @@ pub fn run() { }); builder + .manage(CloseBehaviorState::new()) + .on_page_load(|webview, payload| { + if payload.event() == PageLoadEvent::Started { + if let Some(state) = webview.app_handle().try_state::() { + state.set_frontend_ready(false); + } + } + }) .setup(move |app| { let setup_start = Instant::now(); eprintln!("[STARTUP] plugins registered in {:?}", startup_begin.elapsed()); @@ -968,7 +1007,6 @@ pub fn run() { app.manage(commands::external_sql::ExternalSqlOpenState::default()); app.manage(commands::external_db::ExternalDbOpenState::default()); app.manage(commands::deep_link::DeepLinkOpenState::default()); - app.manage(CloseBehaviorState::new()); app.manage(commands::update::PendingUpdateState::default()); #[cfg(target_os = "macos")] macos_app_delegate::install_dock_quit_handler(app.handle()); @@ -1038,6 +1076,7 @@ pub fn run() { commands::app_settings::load_desktop_settings, commands::app_settings::save_desktop_settings, commands::app_settings::complete_app_close, + commands::app_settings::mark_frontend_ready, commands::app_settings::request_app_close_from_window_controls, commands::window_controls::set_macos_traffic_light_position, commands::app_settings::set_driver_store_dir,