From 0c65837360b33f239431b74df3209574a72f5cb4 Mon Sep 17 00:00:00 2001 From: NimBold Date: Thu, 27 Aug 2026 15:27:04 +0330 Subject: [PATCH] fix(startup): prevent Windows WebView2 focus crash (#37) - Create the main Windows WebView without requesting focus until the native window exists.\n- Keep hidden Properties WebViews unfocused until their reveal path.\n- Make packaged smoke checks observe startup stability and clean up lingering Unix helpers.\n- Refs #37 --- scripts/smoke-packaged-app.js | 71 +++++++++++++++++++++++++----- src-tauri/src/lib.rs | 21 ++++++++- src-tauri/src/properties_window.rs | 3 ++ 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/scripts/smoke-packaged-app.js b/scripts/smoke-packaged-app.js index 742dea0..3d48b3f 100755 --- a/scripts/smoke-packaged-app.js +++ b/scripts/smoke-packaged-app.js @@ -17,6 +17,12 @@ if (!executableArg) { const executable = path.resolve(executableArg); const assertNoVisibleChildWindows = process.argv.includes('--assert-no-visible-child-windows'); const assertPortableData = process.argv.includes('--assert-portable-data'); +const MAX_STABILITY_MS = 60_000; +const MAX_CONSECUTIVE_STABILITY_FAILURES = 3; +const stabilityMsValue = Number.parseInt(argValue('--stability-ms') || '5000', 10); +const stabilityMs = Number.isFinite(stabilityMsValue) && stabilityMsValue >= 0 + ? Math.min(stabilityMsValue, MAX_STABILITY_MS) + : 5000; const READY_PORT_TIMEOUT_MS = 500; const child = spawn(executable, [], { cwd: process.env.RUNNER_TEMP || process.env.TMPDIR || process.cwd(), @@ -83,6 +89,55 @@ async function findReadyPort() { } } +async function checkReadyPort() { + if (readyPort === null) return false; + + try { + const response = await fetch(`http://127.0.0.1:${readyPort}/ping`, { + signal: AbortSignal.timeout(READY_PORT_TIMEOUT_MS), + }); + const matchesChild = response.headers.get('x-firelink-server') === '1' + && response.headers.get('x-firelink-smoke-process-id') === String(child.pid); + await response.body?.cancel(); + return matchesChild; + } catch { + return false; + } +} + +async function assertStableReady() { + const deadline = Date.now() + stabilityMs; + let consecutiveFailures = 0; + while (Date.now() < deadline) { + if (spawnError) { + throw new Error(`Packaged Firelink failed during stability check: ${spawnError.message}`); + } + if (childExit) { + throw new Error( + `Packaged Firelink exited during stability check with code ${childExit.code} signal ${childExit.signal}.`, + ); + } + if (await checkReadyPort()) { + consecutiveFailures = 0; + } else { + consecutiveFailures += 1; + if (consecutiveFailures >= MAX_CONSECUTIVE_STABILITY_FAILURES) { + throw new Error('Packaged Firelink stopped exposing its extension ping endpoint during stability check.'); + } + } + await sleep(Math.min(250, Math.max(1, deadline - Date.now()))); + } + + if (childExit) { + throw new Error( + `Packaged Firelink exited during stability check with code ${childExit.code} signal ${childExit.signal}.`, + ); + } + if (!await checkReadyPort() && !await checkReadyPort()) { + throw new Error('Packaged Firelink was not healthy at the end of its stability check.'); + } +} + function assertNoVisibleWindows(rootPid) { if (process.platform !== 'win32') { return; @@ -277,18 +332,16 @@ async function terminateChild() { } } } - if (await waitForChildExit(5000) && await waitForProcessGroupExit(child.pid, 5000)) { + const childExited = await waitForChildExit(5000); + const processGroupExited = await waitForProcessGroupExit(child.pid, 5000); + if (childExited && processGroupExited) { return true; } - if (!childWasRunning || childExit) { - return false; - } - try { process.kill(-child.pid, 'SIGKILL'); } catch { - if (!childExit) { + if (!childExited) { child.kill('SIGKILL'); } } @@ -338,11 +391,9 @@ try { await assertPortableStorage(); } - if (childExit) { - throw new Error(`Packaged Firelink exited after exposing extension ping endpoint with code ${childExit.code} signal ${childExit.signal}.`); - } + await assertStableReady(); - console.log(`Packaged Firelink smoke passed on 127.0.0.1:${readyPort}`); + console.log(`Packaged Firelink smoke passed on 127.0.0.1:${readyPort} with ${stabilityMs}ms stability`); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 75cfba8..ce42028 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -18512,9 +18512,28 @@ pub fn run() { main_window_builder = main_window_builder .inner_size(startup_size.width as f64, startup_size.height as f64) .prevent_overflow(); - main_window_builder + #[cfg(target_os = "windows")] + { + // WebView2 can return E_INVALIDARG from MoveFocus while the + // newly-created host window is not focusable yet. Wry + // propagates that error from WebviewWindowBuilder::build, + // which destroys the native window and makes a release build + // look like it started headlessly before exiting. Focus the + // window only after WebView2 has been created successfully. + main_window_builder = main_window_builder.focused(false); + } + let main_window = main_window_builder .build() .map_err(|error| format!("failed to create main window: {error}"))?; + #[cfg(target_os = "windows")] + if let Err(error) = main_window.set_focus() { + // The window is already usable if the OS declines this + // best-effort activation request. A later user interaction + // can focus it without risking startup failure. + log::warn!("could not focus the main window after startup: {error}"); + } + #[cfg(not(target_os = "windows"))] + let _ = main_window; restore_pending_main_window(app.handle()); #[cfg(any(target_os = "windows", target_os = "linux"))] diff --git a/src-tauri/src/properties_window.rs b/src-tauri/src/properties_window.rs index bbae442..a7939d7 100644 --- a/src-tauri/src/properties_window.rs +++ b/src-tauri/src/properties_window.rs @@ -437,6 +437,9 @@ pub fn open_download_properties_window( // native window becomes visible. Showing an opaque native surface // here exposes the webview's unpainted white background. .visible(false) + // A hidden WebView2 must not request focus during construction. The + // native reveal path focuses it after the window is visible. + .focused(false) .transparent(true); #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] let builder = builder.decorations(false);