mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-01 13:38:01 +00:00
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
This commit is contained in:
@@ -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;
|
||||
|
||||
+20
-1
@@ -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"))]
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user