Skip to content

fix: handle Windows shutdown cleanup#136

Merged
zouyonghe merged 3 commits into
AstrBotDevs:mainfrom
zouyonghe:fix/windows-shutdown-cleanup-upstream
Jun 3, 2026
Merged

fix: handle Windows shutdown cleanup#136
zouyonghe merged 3 commits into
AstrBotDevs:mainfrom
zouyonghe:fix/windows-shutdown-cleanup-upstream

Conversation

@zouyonghe
Copy link
Copy Markdown
Member

@zouyonghe zouyonghe commented Jun 3, 2026

Summary

Fixes #135.

  • register a Windows-only shutdown message hook for the main Tauri window
  • handle WM_QUERYENDSESSION by stopping the backend with a short shutdown timeout before Windows proceeds
  • handle WM_ENDSESSION cancellation by resetting the local shutdown cleanup guard
  • keep the existing 10s graceful backend stop behavior for normal app exits

Validation

  • cargo fmt --check
  • cargo check
  • cargo test (136 passed)

Notes

  • cargo check --target x86_64-pc-windows-msvc could not complete on this macOS host because ring requires an unavailable Windows C toolchain/header set (fatal error: assert.h file not found).

Co-Authored-By: Warp agent@warp.dev

Summary by Sourcery

Handle Windows OS shutdown events to stop the backend process quickly while preserving existing graceful shutdown behavior for normal exits.

New Features:

  • Add a Windows-specific shutdown handler that hooks into the main window message loop to manage system shutdown events.

Bug Fixes:

  • Ensure the backend is stopped with a shorter timeout during Windows system shutdown so the process can exit cleanly before the session ends.

Enhancements:

  • Introduce a configurable backend stop function that accepts a timeout duration, reusing it for both normal exits and Windows shutdown handling.

Build:

  • Add the windows-sys crate as a Windows-only dependency for interacting with Windows shutdown and window messaging APIs.

Co-Authored-By: Warp <agent@warp.dev>
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In call_previous_wndproc, if previous_wndproc is 0 you currently return 0; consider falling back to DefWindowProcW instead so you don’t accidentally swallow messages when no previous proc is installed.
  • In install, treating a SetWindowLongPtrW return value of 0 as a hard failure may conflate a real error with a valid (but rare) address; if you need to distinguish, you may want to check GetLastError before logging the install as failed.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `call_previous_wndproc`, if `previous_wndproc` is 0 you currently return 0; consider falling back to `DefWindowProcW` instead so you don’t accidentally swallow messages when no previous proc is installed.
- In `install`, treating a `SetWindowLongPtrW` return value of 0 as a hard failure may conflate a real error with a valid (but rare) address; if you need to distinguish, you may want to check `GetLastError` before logging the install as failed.

## Individual Comments

### Comment 1
<location path="src-tauri/src/windows_shutdown.rs" line_range="147-68" />
<code_context>
+        }
+    }
+
+    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;
+        }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Fall back to DefWindowProcW instead of returning 0 when no previous WndProc is available.

If `previous_wndproc` is ever 0 (e.g., due to initialization issues), this helper returns 0 and skips default window handling. To preserve normal behavior and make this more resilient to partial initialization or future hook changes, it would be better to call `DefWindowProcW` instead.

Suggested implementation:

```rust
        if previous == 0 {
            // Fall back to the default window procedure when no previous WndProc is available.
            return DefWindowProcW(hwnd, msg, wparam, lparam);
        }

```

1. Ensure `DefWindowProcW` is imported at the top of `src-tauri/src/windows_shutdown.rs` from the same Windows crate used for `HWND`, `WPARAM`, `LPARAM`, and `LRESULT`. For example, if you are using `windows-sys` elsewhere in this file, add:
   ```rust
   use windows_sys::Win32::UI::WindowsAndMessaging::DefWindowProcW;
   ```
   or, if using the `windows` crate:
   ```rust
   use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
   ```
2. Keep the import grouped consistently with existing Windows API imports in this file (respecting your current style/order).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src-tauri/src/windows_shutdown.rs Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Windows-specific shutdown handler to gracefully and quickly stop the backend process when a system shutdown is requested. The feedback focuses on improving the implementation by ensuring WM_QUERYENDSESSION is correctly propagated down the window procedure chain instead of returning early, and optimizing performance by replacing the mutex lock in the window procedure hot path with a global atomic variable (AtomicIsize).

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +83 to +86
WM_QUERYENDSESSION => {
handle_query_end_session();
1
}
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)
}

Comment on lines +23 to +30
#[derive(Default)]
struct ShutdownHookState {
app_handle: Option<AppHandle>,
previous_wndproc: isize,
cleanup_started: bool,
}

static SHUTDOWN_HOOK: OnceLock<Mutex<ShutdownHookState>> = OnceLock::new();
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);

Comment thread src-tauri/src/windows_shutdown.rs Outdated
Comment on lines +63 to +73
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");
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 the install function to use the new PREVIOUS_WNDPROC atomic variable instead of the mutex-protected previous_wndproc field.

Suggested change
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");
if PREVIOUS_WNDPROC.load(std::sync::atomic::Ordering::Relaxed) != 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;
}
PREVIOUS_WNDPROC.store(previous, std::sync::atomic::Ordering::Relaxed);
append_shutdown_log("Windows shutdown handler installed");

Comment on lines +153 to +156
let previous = SHUTDOWN_HOOK
.get()
.and_then(|hook| hook.lock().ok().map(|guard| guard.previous_wndproc))
.unwrap_or_default();
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);

Co-Authored-By: Warp <agent@warp.dev>
@zouyonghe
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In shutdown_wndproc, the WM_ENDSESSION branch with wparam != 0 calls std::process::exit(0) without forwarding the message to the previous window procedure, which may bypass any cleanup or state handling that Tauri or other hooks rely on; consider delegating to the previous WndProc before or after your cleanup.
  • The shutdown handling in handle_query_end_session synchronously calls stop_backend_with_timeout, which can block the UI thread for up to SYSTEM_SHUTDOWN_STOP_TIMEOUT_MS; consider performing the backend stop on a separate thread to avoid stalling the message loop during system shutdown.
  • When the SHUTDOWN_HOOK mutex is poisoned (both in install and cleanup paths), the code only logs and then bails; depending on how critical this hook is, you might want to recover by reinitializing the state or falling back to a safe default rather than silently skipping future shutdown handling.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `shutdown_wndproc`, the `WM_ENDSESSION` branch with `wparam != 0` calls `std::process::exit(0)` without forwarding the message to the previous window procedure, which may bypass any cleanup or state handling that Tauri or other hooks rely on; consider delegating to the previous WndProc before or after your cleanup.
- The shutdown handling in `handle_query_end_session` synchronously calls `stop_backend_with_timeout`, which can block the UI thread for up to `SYSTEM_SHUTDOWN_STOP_TIMEOUT_MS`; consider performing the backend stop on a separate thread to avoid stalling the message loop during system shutdown.
- When the `SHUTDOWN_HOOK` mutex is poisoned (both in `install` and cleanup paths), the code only logs and then bails; depending on how critical this hook is, you might want to recover by reinitializing the state or falling back to a safe default rather than silently skipping future shutdown handling.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Co-Authored-By: Warp <agent@warp.dev>
@zouyonghe
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@zouyonghe zouyonghe merged commit 8067943 into AstrBotDevs:main Jun 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Windows 关机时弹出 taskkill.exe 错误 (0xc0000142),阻断正常关机

1 participant