fix(desktop): allow native quit before frontend ready

This commit is contained in:
t8y2
2026-07-21 18:45:02 +08:00
parent c153e93d54
commit 1410c92710
3 changed files with 59 additions and 6 deletions
@@ -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<AppCloseRequestTarget>("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");
});
});
}
+8
View File
@@ -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::<CloseBehaviorState>().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");
+44 -5
View File
@@ -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<i32>, 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<bool> {
match target_os {
"windows" | "linux" => Some(false),
@@ -436,6 +450,16 @@ pub(crate) fn hide_main_window_for_close<R: tauri::Runtime>(app: &tauri::AppHand
}
pub(crate) fn request_app_close<R: tauri::Runtime>(app: &tauri::AppHandle<R>, target: &str) {
let frontend_ready = app.try_state::<CloseBehaviorState>().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::<CloseBehaviorState>() {
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::<CloseBehaviorState>() {
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,