Skip to content
Open
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
61 changes: 58 additions & 3 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,59 @@ fn set_main_window_hidden(hide: bool) {
);
}

/// Look up the main `WebviewWindow`, optionally waiting briefly on Windows
/// for the Tauri runtime to re-track the window after SW_SHOW.
///
/// Why this exists (OPENHUMAN-TAURI-3A): on Windows the close button routes
/// through [`set_main_window_hidden`] which uses raw-HWND `SW_HIDE`. CEF
/// treats the hidden host as gone and the Tauri runtime drops its
/// `WebviewWindow` record for `"main"` until the next event-loop tick after
/// SW_SHOW restores visibility. A tray "Show window" callback that runs
/// `set_main_window_hidden(false)` and then immediately calls
/// `app.get_webview_window("main")` can race the re-track step and observe
/// `None` even though the OS window is visible — Sentry sees a
/// `[tray] failed to show main window from menu: main window not found`
/// warn even though, from the user's perspective, the window came back.
///
/// Bounded retry budget: up to 5 lookups with 10 ms between attempts (≤ 50 ms
/// worst case). The tray menu is closed during this window, so the small
/// blocking delay is invisible. After the budget expires the original
/// error path still triggers, preserving the signal if the runtime never
/// re-tracks (which would indicate a real lifecycle bug, not a race).
///
/// Non-Windows platforms use a single lookup — the close-to-tray flow that
/// produces the race is Windows-specific (the macOS close button routes
/// through `app.hide()` per PR #2049, and Linux/X11 keeps the
/// `WebviewWindow` record across `WM_DELETE_WINDOW` handling).
fn get_main_webview_window_with_retry(
app: &AppHandle<AppRuntime>,
) -> Option<tauri::WebviewWindow<AppRuntime>> {
#[cfg(target_os = "windows")]
{
const ATTEMPTS: usize = 5;
const BACKOFF: std::time::Duration = std::time::Duration::from_millis(10);
for attempt in 0..ATTEMPTS {
if let Some(window) = app.get_webview_window("main") {
if attempt > 0 {
log::debug!(
"[show_main_window] runtime re-tracked main window after {} retries",
attempt
);
}
return Some(window);
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.

[major] std::thread::sleep blocks the current thread without pumping the Win32 message loop. The PR description says the runtime re-tracks the window "on the next event-loop tick" after SW_SHOW — but if the tray callback runs on the main thread (which is the thread driving the message pump), sleeping here prevents that tick from ever firing. The retry loop would exhaust all 5 attempts and still return None, making the fix a no-op in exactly the scenario it targets.

Could you verify which thread the tray on_menu_event handler dispatches on? If it's the main/UI thread, you'd need to pump messages between attempts instead of sleeping. Something like:

// Pump pending messages to let the runtime process the
// WM_SHOWWINDOW cascade that triggers re-tracking.
unsafe {
    let mut msg: MSG = std::mem::zeroed();
    while PeekMessageW(&mut msg, std::ptr::null_mut(), 0, 0, PM_REMOVE) != 0 {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
}

Alternatively, if the re-tracking is driven by CEF's internal thread (not the Win32 message pump), then std::thread::sleep would work fine — but the PR description's "next event-loop tick" wording suggests the main loop. Worth confirming either way, since if the sleep deadlocks the re-track the fix is invisible and OPENHUMAN-TAURI-3A stays live.

}
if attempt + 1 < ATTEMPTS {
std::thread::sleep(BACKOFF);
}
}
None
}
#[cfg(not(target_os = "windows"))]
{
app.get_webview_window("main")
}
}

fn show_main_window(app: &AppHandle<AppRuntime>) -> Result<(), String> {
// On Windows: surface the OS top-level Chrome_WidgetWin_1 frame BEFORE
// any Tauri lookups. After our close handler's SW_HIDE the runtime
Expand All @@ -1176,7 +1229,10 @@ fn show_main_window(app: &AppHandle<AppRuntime>) -> Result<(), String> {
// and the early `?` below would abort before SW_SHOW fires (#1607).
// EnumWindows + SW_SHOW operates directly on the OS HWND that
// survived independently, and the runtime re-tracks the window once
// it's visible again.
// it's visible again — but re-tracking lands on the next event-loop
// tick, not synchronously with SW_SHOW. `get_main_webview_window_with_retry`
// bounds the wait to ~50 ms total so the tray callback can pick up the
// re-tracked window without re-emitting OPENHUMAN-TAURI-3A.
#[cfg(target_os = "windows")]
{
set_main_window_hidden(false);
Expand All @@ -1185,8 +1241,7 @@ fn show_main_window(app: &AppHandle<AppRuntime>) -> Result<(), String> {
let _ = webview.set_focus();
}
}
let window = app
.get_webview_window("main")
let window = get_main_webview_window_with_retry(app)
.ok_or_else(|| "main window not found".to_string())?;
window
.show()
Expand Down
Loading