From fb01a322183daf0eabbfc0d0940f6c38a29127e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Wed, 3 Jun 2026 15:20:58 +0900 Subject: [PATCH 1/3] fix: handle Windows shutdown cleanup Co-Authored-By: Warp --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 7 + src-tauri/src/app_constants.rs | 2 + src-tauri/src/app_runtime.rs | 1 + src-tauri/src/backend/process_lifecycle.rs | 12 +- src-tauri/src/main.rs | 1 + src-tauri/src/windows_shutdown.rs | 169 +++++++++++++++++++++ 7 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 src-tauri/src/windows_shutdown.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 78dd7c85..71c3ecba 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -73,6 +73,7 @@ dependencies = [ "tauri-plugin-updater", "tempfile", "url", + "windows-sys 0.59.0", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f0455ca6..320c0990 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,6 +26,13 @@ tauri-plugin-single-instance = "2.0" tauri-plugin-updater = "2.0" url = "2.5" +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.59", features = [ + "Win32_Foundation", + "Win32_System_Threading", + "Win32_UI_WindowsAndMessaging", +] } + [features] default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/src/app_constants.rs b/src-tauri/src/app_constants.rs index e7de6dd9..fc8ec743 100644 --- a/src-tauri/src/app_constants.rs +++ b/src-tauri/src/app_constants.rs @@ -8,6 +8,8 @@ pub(crate) const GRACEFUL_RESTART_REQUEST_TIMEOUT_MS: u64 = 2_500; pub(crate) const GRACEFUL_RESTART_START_TIME_TIMEOUT_MS: u64 = 1_800; pub(crate) const GRACEFUL_RESTART_POLL_INTERVAL_MS: u64 = 350; pub(crate) const GRACEFUL_STOP_TIMEOUT_MS: u64 = 10_000; +#[cfg(target_os = "windows")] +pub(crate) const SYSTEM_SHUTDOWN_STOP_TIMEOUT_MS: u64 = 2_000; pub(crate) const DEFAULT_BACKEND_READY_POLL_INTERVAL_MS: u64 = 300; pub(crate) const BACKEND_READY_POLL_INTERVAL_MIN_MS: u64 = 50; pub(crate) const BACKEND_READY_POLL_INTERVAL_MAX_MS: u64 = 10_000; diff --git a/src-tauri/src/app_runtime.rs b/src-tauri/src/app_runtime.rs index 1aa8286c..5b18de4f 100644 --- a/src-tauri/src/app_runtime.rs +++ b/src-tauri/src/app_runtime.rs @@ -117,6 +117,7 @@ fn configure_setup(builder: Builder) -> Builder { if let Err(error) = tray::setup::setup_tray(&app_handle) { append_startup_log(&format!("failed to initialize tray: {error}")); } + crate::windows_shutdown::install(&app_handle); startup_task::spawn_startup_task(app_handle.clone(), append_startup_log); Ok(()) diff --git a/src-tauri/src/backend/process_lifecycle.rs b/src-tauri/src/backend/process_lifecycle.rs index e90c6f9c..950db264 100644 --- a/src-tauri/src/backend/process_lifecycle.rs +++ b/src-tauri/src/backend/process_lifecycle.rs @@ -17,6 +17,10 @@ use crate::{ impl BackendState { pub(crate) fn stop_backend(&self) -> Result<(), String> { + self.stop_backend_with_timeout(Duration::from_millis(GRACEFUL_STOP_TIMEOUT_MS)) + } + + pub(crate) fn stop_backend_with_timeout(&self, timeout: Duration) -> Result<(), String> { self.stop_backend_log_rotation_worker(); let mut guard = self .child @@ -27,18 +31,14 @@ impl BackendState { return Ok(()); }; - if process_control::stop_child_process_gracefully( - child, - Duration::from_millis(GRACEFUL_STOP_TIMEOUT_MS), - append_desktop_log, - ) { + if process_control::stop_child_process_gracefully(child, timeout, append_desktop_log) { *guard = None; return Ok(()); } Err(format!( "Backend process did not exit after {}ms graceful stop timeout.", - GRACEFUL_STOP_TIMEOUT_MS + timeout.as_millis() )) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fe7d3d44..d29ec7be 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -28,6 +28,7 @@ mod ui_dispatch; mod update_channel; mod webui_paths; mod window; +mod windows_shutdown; pub(crate) use app_constants::*; pub(crate) use app_helpers::{ diff --git a/src-tauri/src/windows_shutdown.rs b/src-tauri/src/windows_shutdown.rs new file mode 100644 index 00000000..15366faf --- /dev/null +++ b/src-tauri/src/windows_shutdown.rs @@ -0,0 +1,169 @@ +#[cfg(target_os = "windows")] +mod platform { + use std::{ + mem, + sync::{Mutex, OnceLock}, + time::Duration, + }; + + use tauri::{AppHandle, Manager}; + use windows_sys::Win32::{ + Foundation::{HWND, LPARAM, LRESULT, WPARAM}, + System::Threading::SetProcessShutdownParameters, + UI::WindowsAndMessaging::{ + CallWindowProcW, SetWindowLongPtrW, GWLP_WNDPROC, WM_ENDSESSION, WM_QUERYENDSESSION, + WNDPROC, + }, + }; + + use crate::{append_shutdown_log, BackendState, SYSTEM_SHUTDOWN_STOP_TIMEOUT_MS}; + + const SHUTDOWN_PRIORITY_EARLY: u32 = 0x100; + + #[derive(Default)] + struct ShutdownHookState { + app_handle: Option, + previous_wndproc: isize, + cleanup_started: bool, + } + + static SHUTDOWN_HOOK: OnceLock> = OnceLock::new(); + + pub(crate) fn install(app_handle: &AppHandle) { + unsafe { + if SetProcessShutdownParameters(SHUTDOWN_PRIORITY_EARLY, 0) == 0 { + append_shutdown_log("failed to set Windows shutdown priority"); + } + } + + let Some(window) = app_handle.get_webview_window("main") else { + append_shutdown_log("Windows shutdown handler skipped: main window not found"); + return; + }; + let hwnd = match window.hwnd() { + Ok(hwnd) => hwnd.0, + Err(error) => { + append_shutdown_log(&format!("Windows shutdown handler skipped: {error}")); + return; + } + }; + + let hook = SHUTDOWN_HOOK.get_or_init(|| Mutex::new(ShutdownHookState::default())); + let mut guard = match hook.lock() { + Ok(guard) => guard, + Err(error) => { + append_shutdown_log(&format!( + "Windows shutdown handler lock poisoned during install: {error}" + )); + return; + } + }; + + guard.app_handle = Some(app_handle.clone()); + if guard.previous_wndproc != 0 { + return; + } + + let previous = unsafe { SetWindowLongPtrW(hwnd, GWLP_WNDPROC, shutdown_wndproc as isize) }; + if previous == 0 { + append_shutdown_log("Windows shutdown handler install returned empty previous WndProc"); + return; + } + guard.previous_wndproc = previous; + append_shutdown_log("Windows shutdown handler installed"); + } + + unsafe extern "system" fn shutdown_wndproc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + match msg { + WM_QUERYENDSESSION => { + handle_query_end_session(); + 1 + } + WM_ENDSESSION => { + if wparam != 0 { + append_shutdown_log("Windows end session confirmed, exiting desktop process"); + std::process::exit(0); + } + reset_shutdown_cleanup(); + call_previous_wndproc(hwnd, msg, wparam, lparam) + } + _ => call_previous_wndproc(hwnd, msg, wparam, lparam), + } + } + + fn handle_query_end_session() { + let Some(app_handle) = take_shutdown_app_handle_for_cleanup() else { + append_shutdown_log("Windows shutdown cleanup skipped: app handle unavailable"); + return; + }; + + append_shutdown_log("Windows shutdown requested, stopping backend quickly"); + let state = app_handle.state::(); + if let Err(error) = + state.stop_backend_with_timeout(Duration::from_millis(SYSTEM_SHUTDOWN_STOP_TIMEOUT_MS)) + { + append_shutdown_log(&format!("backend stop on Windows shutdown failed: {error}")); + } + } + + fn take_shutdown_app_handle_for_cleanup() -> Option { + let hook = SHUTDOWN_HOOK.get()?; + let mut guard = match hook.lock() { + Ok(guard) => guard, + Err(error) => { + append_shutdown_log(&format!( + "Windows shutdown handler lock poisoned during cleanup: {error}" + )); + return None; + } + }; + if guard.cleanup_started { + return None; + } + guard.cleanup_started = true; + guard.app_handle.clone() + } + + fn reset_shutdown_cleanup() { + let Some(hook) = SHUTDOWN_HOOK.get() else { + return; + }; + match hook.lock() { + Ok(mut guard) => { + guard.cleanup_started = false; + append_shutdown_log("Windows shutdown canceled, cleanup flag reset"); + } + Err(error) => append_shutdown_log(&format!( + "Windows shutdown handler lock poisoned while resetting cleanup: {error}" + )), + } + } + + unsafe fn call_previous_wndproc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + let previous = SHUTDOWN_HOOK + .get() + .and_then(|hook| hook.lock().ok().map(|guard| guard.previous_wndproc)) + .unwrap_or_default(); + if previous == 0 { + return 0; + } + let previous: WNDPROC = mem::transmute(previous); + CallWindowProcW(previous, hwnd, msg, wparam, lparam) + } +} + +#[cfg(target_os = "windows")] +pub(crate) use platform::install; + +#[cfg(not(target_os = "windows"))] +pub(crate) fn install(_app_handle: &tauri::AppHandle) {} From 6b0047a796ae15b8fe88e8a2f30588b32421b2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Wed, 3 Jun 2026 20:31:46 +0900 Subject: [PATCH 2/3] fix: harden Windows shutdown hook fallback Co-Authored-By: Warp --- src-tauri/src/windows_shutdown.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/windows_shutdown.rs b/src-tauri/src/windows_shutdown.rs index 15366faf..3ac071a5 100644 --- a/src-tauri/src/windows_shutdown.rs +++ b/src-tauri/src/windows_shutdown.rs @@ -8,11 +8,11 @@ mod platform { use tauri::{AppHandle, Manager}; use windows_sys::Win32::{ - Foundation::{HWND, LPARAM, LRESULT, WPARAM}, + Foundation::{GetLastError, SetLastError, HWND, LPARAM, LRESULT, WPARAM}, System::Threading::SetProcessShutdownParameters, UI::WindowsAndMessaging::{ - CallWindowProcW, SetWindowLongPtrW, GWLP_WNDPROC, WM_ENDSESSION, WM_QUERYENDSESSION, - WNDPROC, + CallWindowProcW, DefWindowProcW, SetWindowLongPtrW, GWLP_WNDPROC, WM_ENDSESSION, + WM_QUERYENDSESSION, WNDPROC, }, }; @@ -23,6 +23,7 @@ mod platform { #[derive(Default)] struct ShutdownHookState { app_handle: Option, + installed: bool, previous_wndproc: isize, cleanup_started: bool, } @@ -60,15 +61,22 @@ mod platform { }; guard.app_handle = Some(app_handle.clone()); - if guard.previous_wndproc != 0 { + if guard.installed { return; } - let previous = unsafe { SetWindowLongPtrW(hwnd, GWLP_WNDPROC, shutdown_wndproc as isize) }; - if previous == 0 { - append_shutdown_log("Windows shutdown handler install returned empty previous WndProc"); + let previous = unsafe { + SetLastError(0); + SetWindowLongPtrW(hwnd, GWLP_WNDPROC, shutdown_wndproc as isize) + }; + let last_error = unsafe { GetLastError() }; + if previous == 0 && last_error != 0 { + append_shutdown_log(&format!( + "Windows shutdown handler install failed: error={last_error}" + )); return; } + guard.installed = true; guard.previous_wndproc = previous; append_shutdown_log("Windows shutdown handler installed"); } @@ -155,7 +163,7 @@ mod platform { .and_then(|hook| hook.lock().ok().map(|guard| guard.previous_wndproc)) .unwrap_or_default(); if previous == 0 { - return 0; + return DefWindowProcW(hwnd, msg, wparam, lparam); } let previous: WNDPROC = mem::transmute(previous); CallWindowProcW(previous, hwnd, msg, wparam, lparam) From 33bf2375a53c5d45a505373515ba34b8274fa485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Wed, 3 Jun 2026 20:40:48 +0900 Subject: [PATCH 3/3] fix: forward Windows end session messages Co-Authored-By: Warp --- src-tauri/src/windows_shutdown.rs | 56 ++++++++++++++----------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src-tauri/src/windows_shutdown.rs b/src-tauri/src/windows_shutdown.rs index 3ac071a5..5a316859 100644 --- a/src-tauri/src/windows_shutdown.rs +++ b/src-tauri/src/windows_shutdown.rs @@ -2,7 +2,7 @@ mod platform { use std::{ mem, - sync::{Mutex, OnceLock}, + sync::{Mutex, MutexGuard, OnceLock}, time::Duration, }; @@ -30,6 +30,21 @@ mod platform { static SHUTDOWN_HOOK: OnceLock> = OnceLock::new(); + fn lock_shutdown_hook<'a>( + hook: &'a Mutex, + context: &str, + ) -> MutexGuard<'a, ShutdownHookState> { + match hook.lock() { + Ok(guard) => guard, + Err(error) => { + append_shutdown_log(&format!( + "Windows shutdown handler lock poisoned {context}: {error}" + )); + error.into_inner() + } + } + } + pub(crate) fn install(app_handle: &AppHandle) { unsafe { if SetProcessShutdownParameters(SHUTDOWN_PRIORITY_EARLY, 0) == 0 { @@ -50,15 +65,7 @@ mod platform { }; let hook = SHUTDOWN_HOOK.get_or_init(|| Mutex::new(ShutdownHookState::default())); - let mut guard = match hook.lock() { - Ok(guard) => guard, - Err(error) => { - append_shutdown_log(&format!( - "Windows shutdown handler lock poisoned during install: {error}" - )); - return; - } - }; + let mut guard = lock_shutdown_hook(hook, "during install"); guard.app_handle = Some(app_handle.clone()); if guard.installed { @@ -93,12 +100,13 @@ mod platform { 1 } WM_ENDSESSION => { + let previous_result = call_previous_wndproc(hwnd, msg, wparam, lparam); if wparam != 0 { append_shutdown_log("Windows end session confirmed, exiting desktop process"); std::process::exit(0); } reset_shutdown_cleanup(); - call_previous_wndproc(hwnd, msg, wparam, lparam) + previous_result } _ => call_previous_wndproc(hwnd, msg, wparam, lparam), } @@ -112,6 +120,8 @@ mod platform { append_shutdown_log("Windows shutdown requested, stopping backend quickly"); let state = app_handle.state::(); + // Keep this bounded wait inside WM_QUERYENDSESSION so taskkill is issued + // before Windows advances to the final session-ending phase. if let Err(error) = state.stop_backend_with_timeout(Duration::from_millis(SYSTEM_SHUTDOWN_STOP_TIMEOUT_MS)) { @@ -121,15 +131,7 @@ mod platform { fn take_shutdown_app_handle_for_cleanup() -> Option { let hook = SHUTDOWN_HOOK.get()?; - let mut guard = match hook.lock() { - Ok(guard) => guard, - Err(error) => { - append_shutdown_log(&format!( - "Windows shutdown handler lock poisoned during cleanup: {error}" - )); - return None; - } - }; + let mut guard = lock_shutdown_hook(hook, "during cleanup"); if guard.cleanup_started { return None; } @@ -141,15 +143,9 @@ mod platform { let Some(hook) = SHUTDOWN_HOOK.get() else { return; }; - match hook.lock() { - Ok(mut guard) => { - guard.cleanup_started = false; - append_shutdown_log("Windows shutdown canceled, cleanup flag reset"); - } - Err(error) => append_shutdown_log(&format!( - "Windows shutdown handler lock poisoned while resetting cleanup: {error}" - )), - } + let mut guard = lock_shutdown_hook(hook, "while resetting cleanup"); + guard.cleanup_started = false; + append_shutdown_log("Windows shutdown canceled, cleanup flag reset"); } unsafe fn call_previous_wndproc( @@ -160,7 +156,7 @@ mod platform { ) -> LRESULT { let previous = SHUTDOWN_HOOK .get() - .and_then(|hook| hook.lock().ok().map(|guard| guard.previous_wndproc)) + .map(|hook| lock_shutdown_hook(hook, "while forwarding message").previous_wndproc) .unwrap_or_default(); if previous == 0 { return DefWindowProcW(hwnd, msg, wparam, lparam);