Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/app_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/app_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ fn configure_setup(builder: Builder<tauri::Wry>) -> Builder<tauri::Wry> {
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(())
Expand Down
12 changes: 6 additions & 6 deletions src-tauri/src/backend/process_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
))
}

Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
173 changes: 173 additions & 0 deletions src-tauri/src/windows_shutdown.rs
Original file line number Diff line number Diff line change
@@ -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<AppHandle>,
installed: bool,
previous_wndproc: isize,
cleanup_started: bool,
}

static SHUTDOWN_HOOK: OnceLock<Mutex<ShutdownHookState>> = OnceLock::new();
Comment on lines +23 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Locking the SHUTDOWN_HOOK mutex on every single window message in call_previous_wndproc introduces significant performance overhead on the main UI thread, especially during high-frequency events like mouse movement or rendering.

Since previous_wndproc is immutable once set, you can store it in a global AtomicIsize and load it using Ordering::Relaxed to completely avoid mutex locking in the hot path.

Suggested change
#[derive(Default)]
struct ShutdownHookState {
app_handle: Option<AppHandle>,
previous_wndproc: isize,
cleanup_started: bool,
}
static SHUTDOWN_HOOK: OnceLock<Mutex<ShutdownHookState>> = OnceLock::new();
#[derive(Default)]
struct ShutdownHookState {
app_handle: Option<AppHandle>,
cleanup_started: bool,
}
static SHUTDOWN_HOOK: OnceLock<Mutex<ShutdownHookState>> = OnceLock::new();
static PREVIOUS_WNDPROC: std::sync::atomic::AtomicIsize = std::sync::atomic::AtomicIsize::new(0);


fn lock_shutdown_hook<'a>(
hook: &'a Mutex<ShutdownHookState>,
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
}
Comment on lines +98 to +101

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In shutdown_wndproc, returning 1 directly for WM_QUERYENDSESSION bypasses the rest of the window procedure subclass chain (including Tauri's default window procedure and any other plugins). This can prevent other components from performing necessary cleanup or correctly handling the shutdown query.

Instead, you should call the previous window procedure and return its result so that the message propagates down the chain.

Suggested change
WM_QUERYENDSESSION => {
handle_query_end_session();
1
}
WM_QUERYENDSESSION => {
handle_query_end_session();
call_previous_wndproc(hwnd, msg, wparam, lparam)
}

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::<BackendState>();
// 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<AppHandle> {
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();
Comment on lines +157 to +160

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update call_previous_wndproc to load the previous window procedure from the PREVIOUS_WNDPROC atomic variable, completely removing the mutex lock from the hot path.

        let previous = PREVIOUS_WNDPROC.load(std::sync::atomic::Ordering::Relaxed);

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) {}