Skip to content

[Win32] Fix BrowserFunction missing on first page - #3479

Open
HeikoKlare wants to merge 2 commits into
eclipse-platform:masterfrom
HeikoKlare:fix/edge-browserfunction-concurrent-init-issue20
Open

[Win32] Fix BrowserFunction missing on first page#3479
HeikoKlare wants to merge 2 commits into
eclipse-platform:masterfrom
HeikoKlare:fix/edge-browserfunction-concurrent-init-issue20

Conversation

@HeikoKlare

Copy link
Copy Markdown
Contributor

Fixes #3370 — the sporadic failure of test_BrowserFunction_availableOnLoad_concurrentInstances_issue20 on the Edge/WebView2 backend.

Builds on top of #3478 (BrowserFunction dispose/redefine regression tests) and employs the regression tests added there — #3478 should be merged first.

Problem

A BrowserFunction created while an Edge browser is still initializing (with a navigation queued via setUrl/setText) could be missing on the first loaded page. AddScriptToExecuteOnDocumentCreated was issued only after the queued navigation and raced with document creation, so the function was sometimes not registered when the page's document was created — surfacing as SWTException: <function> is not defined.

Fix

Register the document-created scripts of all pending BrowserFunctions inside WebViewProvider.initializeWebView(), before completing the initialization future (which synchronously runs the queued navigation). This is the only point that is reliably "after init, before the first navigation", since the CompletableFuture navigation chain cannot be preempted retroactively.

Registration is now issued asynchronously: what makes a function land on a page is issuing the registration before the navigation is issued to WebView2, not waiting for its completion. This also removes the previous inCallback > 0 deferral (an async call cannot deadlock inside a WebView2 callback) and the separate synchronous registration method, leaving a single registration method and a single branch in createFunction. deregisterFunction stays correct for a registration whose script ID has not arrived yet (its async completion detects the removed function and removes the script itself).

Tests

Adds regression tests for function creation during concurrent initialization: creating a function from inside a callback, availability before the first page's inline scripts run, and multiple functions registered before initialization completes. These complement the dispose/redefine regression tests from #3478.

Reproducer

The underlying race is a sub-millisecond window and cannot be forced deterministically through the public API with a single browser. The following standalone reproducer makes it deterministic in practice by stressing concurrent initialization: before the fix it reports options is not defined within a few iterations; after the fix it completes all iterations cleanly.

Standalone reproducer (Windows/Edge)
/*******************************************************************************
 * Copyright (c) 2026 Contributors to the Eclipse Foundation.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.eclipse.swt.tests.junit;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.BrowserFunction;
import org.eclipse.swt.browser.ProgressAdapter;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

/**
 * Standalone reproducer for the Edge/WebView2 BrowserFunction registration race fixed by this change.
 *
 * <p>A {@link BrowserFunction} created while a {@link Browser} is still initializing (and while a
 * navigation queued via {@code setUrl}/{@code setText} is pending) must be available on the first
 * loaded page. Before the fix, {@code AddScriptToExecuteOnDocumentCreated} was issued <em>after</em>
 * the navigation, so it raced with document creation and the function was sometimes undefined,
 * producing {@code SWTException: options is not defined}.
 *
 * <p>The underlying race is a sub-millisecond timing window, so it cannot be forced deterministically
 * through the public SWT API with a single browser. This reproducer makes it <em>deterministic in
 * practice</em> by stressing the exact scenario: it creates several browsers that initialize
 * concurrently (their initialization pumps the shared event loop and perturbs each other's timing),
 * repeated over many iterations. Before the fix it reports failures within the first few iterations;
 * after the fix it completes all iterations with zero failures.
 *
 * <p>Run it standalone via {@link #main(String[])} on Windows (Edge backend), or call
 * {@link #reproduce(Display, int, int)} from a test.
 */
public final class BrowserFunctionConcurrentInitReproducer {

	private BrowserFunctionConcurrentInitReproducer() {
	}

	/** Number of browsers initialized concurrently in a single iteration. */
	private static final int DEFAULT_BROWSERS_PER_ITERATION = 8;
	/** Number of iterations to run when stressing the scenario. */
	private static final int DEFAULT_ITERATIONS = 50;
	/** Maximum time to wait for all browsers of one iteration to resolve. */
	private static final int ITERATION_TIMEOUT_MILLIS = 30_000;

	/** Result of a single browser within an iteration. */
	private enum Result {
		PENDING, AVAILABLE, MISSING
	}

	public static void main(String[] args) {
		Display display = new Display();
		try {
			Shell shell = new Shell(display);
			shell.setLayout(new FillLayout());
			Browser probe = new Browser(shell, SWT.NONE);
			boolean isEdge = "edge".equals(probe.getBrowserType());
			probe.dispose();
			if (!isEdge) {
				System.out.println("This reproducer targets the Edge/WebView2 backend; current backend is '"
						+ probe.getBrowserType() + "'. Nothing to reproduce.");
				return;
			}

			int failures = reproduce(display, DEFAULT_ITERATIONS, DEFAULT_BROWSERS_PER_ITERATION);
			if (failures > 0) {
				System.out.println("\nREPRODUCED: 'options' was missing in " + failures
						+ " case(s). This indicates the AddScriptToExecuteOnDocumentCreated race (Finding 1).");
				System.exit(1);
			} else {
				System.out.println("\nNo failures observed across " + DEFAULT_ITERATIONS + " iterations of "
						+ DEFAULT_BROWSERS_PER_ITERATION + " concurrent browsers. The race appears fixed.");
			}
		} finally {
			display.dispose();
		}
	}

	/**
	 * Runs the concurrent-initialization scenario and returns the number of browsers for which the
	 * BrowserFunction was <em>not</em> available on the loaded page (i.e. reproductions of the bug).
	 *
	 * @param display            the display to run on (must be the calling thread's display)
	 * @param iterations         number of iterations to run
	 * @param browsersPerIteration number of browsers initialized concurrently per iteration
	 * @return the total number of reproductions observed
	 */
	public static int reproduce(Display display, int iterations, int browsersPerIteration) {
		int totalFailures = 0;
		for (int iteration = 0; iteration < iterations; iteration++) {
			totalFailures += runSingleIteration(display, iteration, browsersPerIteration);
			if (totalFailures > 0) {
				// Fail fast: one reproduction is enough to demonstrate the defect.
				break;
			}
		}
		return totalFailures;
	}

	private static int runSingleIteration(Display display, int iteration, int browsersPerIteration) {
		Shell shell = new Shell(display);
		shell.setLayout(new FillLayout());
		List<Browser> browsers = new ArrayList<>();
		List<Result[]> results = new ArrayList<>();
		List<AtomicReference<SWTException>> exceptions = new ArrayList<>();

		try {
			for (int i = 0; i < browsersPerIteration; i++) {
				final Result[] result = { Result.PENDING };
				final AtomicReference<SWTException> exception = new AtomicReference<>();
				results.add(result);
				exceptions.add(exception);

				// Deliberately do NOT wait for initialization: keep all browsers initializing
				// concurrently, which is the timing that exposes the race (issue #20).
				final Browser browser = new Browser(shell, SWT.NONE);
				browser.setUrl("about:blank");
				new BrowserFunction(browser, "options") {
					@Override
					public Object function(Object[] arguments) {
						return null;
					}
				};
				browser.addProgressListener(new ProgressAdapter() {
					@Override
					public void completed(ProgressEvent event) {
						try {
							browser.evaluate("options();");
							result[0] = Result.AVAILABLE;
						} catch (SWTException ex) {
							result[0] = Result.MISSING;
							exception.set(ex);
						}
					}
				});
				browsers.add(browser);
			}

			shell.open();
			waitUntilResolved(display, results);

			int failures = 0;
			for (int i = 0; i < results.size(); i++) {
				Result r = results.get(i)[0];
				if (r != Result.AVAILABLE) {
					failures++;
					SWTException ex = exceptions.get(i).get();
					System.out.println("Iteration " + iteration + ", browser " + i + ": " + r
							+ (ex != null ? " (" + ex.getMessage() + ")" : " (timed out)"));
				}
			}
			if (failures == 0) {
				System.out.println("Iteration " + iteration + ": OK (" + browsersPerIteration + " browsers)");
			}
			return failures;
		} finally {
			for (Browser browser : browsers) {
				if (!browser.isDisposed()) {
					browser.dispose();
				}
			}
			shell.dispose();
			// Let Edge process pending disposal work between iterations.
			while (display.readAndDispatch()) {
				// drain
			}
		}
	}

	private static void waitUntilResolved(Display display, List<Result[]> results) {
		long deadline = System.currentTimeMillis() + ITERATION_TIMEOUT_MILLIS;
		// Periodically wake the display so the deadline is honored even if no browser event arrives.
		Runnable[] wake = new Runnable[1];
		wake[0] = () -> {
			if (!display.isDisposed()) {
				display.timerExec(100, wake[0]);
			}
		};
		display.timerExec(100, wake[0]);
		try {
			while (System.currentTimeMillis() < deadline && !allResolved(results)) {
				if (!display.readAndDispatch()) {
					display.sleep();
				}
			}
		} finally {
			display.timerExec(-1, wake[0]);
		}
	}

	private static boolean allResolved(List<Result[]> results) {
		for (Result[] result : results) {
			if (result[0] == Result.PENDING) {
				return false;
			}
		}
		return true;
	}
}

Add two backend-agnostic regression tests for BrowserFunction lifecycle:
- a disposed BrowserFunction must not be re-injected on a subsequently
loaded page
- redefining a function with the same name takes effect and survives
navigation, and the previous definition is not resurrected

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Test Results

  212 files  ± 0    212 suites  ±0   30m 59s ⏱️ +54s
4 900 tests +10  4 874 ✅ + 8   26 💤 + 2  0 ❌ ±0 
7 083 runs  +30  6 903 ✅ +18  180 💤 +12  0 ❌ ±0 

Results for commit 336e925. ± Comparison against base commit 5ac8459.

♻️ This comment has been updated with latest results.

A BrowserFunction created while an Edge/WebView2 browser is still
initializing (with a navigation queued via setUrl/setText) could be
missing on the first loaded page: AddScriptToExecuteOnDocumentCreated
was issued only after the queued navigation and raced with document
creation, so the function was sometimes not yet registered when the
page's document was created.

With the change, we register the document-created scripts of all pending
BrowserFunctions inside WebViewProvider.initializeWebView(), before
completing the initialization future. Completing the future
synchronously runs the queued navigation, so registering beforehand
guarantees the functions are in place before the first document is
created. This is the only point that is reliably "after init, before the
first navigation", since a CompletableFuture navigation chain cannot be
preempted retroactively.

Registration is now issued asynchronously (fire-and-forget). What makes
a function land on a page is issuing the registration before the
navigation is issued to WebView2, not waiting for its completion. As a
result the previous inCallback>0 deferral is no longer needed (an async
call cannot deadlock inside a WebView2 callback) and the separate
synchronous registration method is removed, leaving a single
registration method and a single branch in createFunction.

deregisterFunction stays correct for a registration whose script ID has
not arrived yet: the async completion callback detects the removed
function via the functions map and removes the just-registered script
itself.

Fixes eclipse-platform#3370

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HeikoKlare
HeikoKlare force-pushed the fix/edge-browserfunction-concurrent-init-issue20 branch from 762e863 to 336e925 Compare July 31, 2026 16:41
@HeikoKlare
HeikoKlare marked this pull request as ready for review July 31, 2026 17:01
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.

Test_org_eclipse_swt_browser_Browser.test_BrowserFunction_availableOnLoad_concurrentInstances_issue20 failed in I-build

1 participant