diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 78dd7c8..71c3ecb 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 f0455ca..320c099 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 e7de6dd..fc8ec74 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 1aa8286..5b18de4 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 e90c6f9..950db26 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 fe7d3d4..d29ec7b 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 0000000..5a31685 --- /dev/null +++ b/src-tauri/src/windows_shutdown.rs @@ -0,0 +1,173 @@ +#[cfg(target_os = "windows")] +mod platform { + use std::{ + mem, + sync::{Mutex, MutexGuard, OnceLock}, + time::Duration, + }; + + use tauri::{AppHandle, Manager}; + use windows_sys::Win32::{ + Foundation::{GetLastError, SetLastError, HWND, LPARAM, LRESULT, WPARAM}, + System::Threading::SetProcessShutdownParameters, + UI::WindowsAndMessaging::{ + CallWindowProcW, DefWindowProcW, 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, + installed: bool, + previous_wndproc: isize, + cleanup_started: bool, + } + + 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 { + 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 = lock_shutdown_hook(hook, "during install"); + + guard.app_handle = Some(app_handle.clone()); + if guard.installed { + return; + } + + 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"); + } + + 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 => { + 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(); + previous_result + } + _ => 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::(); + // 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)) + { + 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 = lock_shutdown_hook(hook, "during cleanup"); + 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; + }; + 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( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + let previous = SHUTDOWN_HOOK + .get() + .map(|hook| lock_shutdown_hook(hook, "while forwarding message").previous_wndproc) + .unwrap_or_default(); + if previous == 0 { + return DefWindowProcW(hwnd, msg, wparam, lparam); + } + 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) {}