From a7f51b12d779d97f285c724dacfe1356d076dfd3 Mon Sep 17 00:00:00 2001 From: Heiko Klare Date: Wed, 1 Apr 2026 09:34:12 +0200 Subject: [PATCH] [Win32][Edge] Fix BrowserFunction race condition using AddScriptToExecuteOnDocumentCreated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Edge/WebView2, execute() is asynchronous. When new BrowserFunction() is called, the injection script is queued via ExecuteScript(), but if a page navigation completes before WebView2 processes that queued script, the function is unavailable in the new document. This race is most easily triggered when two Browser instances are created in quick succession. Fix: override createFunction() to also register the function script via AddScriptToExecuteOnDocumentCreated. This API guarantees that the script runs on every future document creation, before any page scripts — eliminating the race condition. The script ID returned by the async registration is stored so it can be cleaned up via RemoveScriptToExecuteOnDocumentCreated when the BrowserFunction is disposed. When createFunction() is called from within a WebView2 callback (inCallback>0), blocking on the registration callback would deadlock, so the persistent registration is skipped and the existing NavigationStarting re-injection remains as a fallback. Fixes https://github.com/eclipse-platform/eclipse.platform.swt/issues/20 --- .../win32/org/eclipse/swt/browser/Edge.java | 35 ++++++++ .../swt/internal/ole/win32/ICoreWebView2.java | 4 + .../Test_org_eclipse_swt_browser_Browser.java | 89 +++++++++++++++++++ 3 files changed, 128 insertions(+) diff --git a/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java b/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java index edf1de646f..cf5acab04d 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java @@ -85,6 +85,8 @@ public WebViewEnvironment(ICoreWebView2Environment environment) { boolean inNewWindow; private boolean inEvaluate; HashMap navigations = new HashMap<>(); + /** Maps BrowserFunction index to the script ID from AddScriptToExecuteOnDocumentCreated. */ + private final Map functionScriptIds = new HashMap<>(); private boolean ignoreGotFocus; private boolean ignoreFocusIn; private String lastCustomText; @@ -1836,4 +1838,37 @@ public boolean setUrl(String url, String postData, String[] headers) { return setWebpageData(url, postData, headers, null); } +/** + * Registers the function script persistently via AddScriptToExecuteOnDocumentCreated so it is + * injected on every future document creation before any page scripts run, avoiding the race + * condition between async function injection and navigation completion. + * See issue #20. + */ +@Override +public void createFunction(BrowserFunction function) { + super.createFunction(function); + if (inCallback > 0) { + // Cannot wait for a callback result while already inside a WebView2 callback; + // the existing NavigationStarting re-injection will handle future navigations. + return; + } + String[] scriptId = new String[1]; + callAndWait(scriptId, completion -> + webViewProvider.getWebView(true).AddScriptToExecuteOnDocumentCreated( + stringToWstr(function.functionString), completion.getAddress())); + if (scriptId[0] != null) { + functionScriptIds.put(function.index, scriptId[0]); + } +} + +@Override +void deregisterFunction(BrowserFunction function) { + super.deregisterFunction(function); + String scriptId = functionScriptIds.remove(function.index); + if (scriptId != null) { + webViewProvider.getWebView(true).RemoveScriptToExecuteOnDocumentCreated( + stringToWstr(scriptId)); + } +} + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java b/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java index 473a3d981d..842d585f4b 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java @@ -67,6 +67,10 @@ public int AddScriptToExecuteOnDocumentCreated(char[] javaScript, long handler) return COM.VtblCall(27, address, javaScript, handler); } +public int RemoveScriptToExecuteOnDocumentCreated(char[] id) { + return COM.VtblCall(28, address, id); +} + public int ExecuteScript(char[] javaScript, IUnknown handler) { return COM.VtblCall(29, address, javaScript, handler.address); } diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java index 595b49efc1..dbe78ed1da 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java @@ -2943,6 +2943,95 @@ public void completed(ProgressEvent event) { browser2.dispose(); } +/** + * Regression test for https://github.com/eclipse-platform/eclipse.platform.swt/issues/20 + * + *

A BrowserFunction registered before a navigation must be available when the new page's + * inline scripts execute (not just after the page finishes loading). In Edge/WebView2, + * function injection via {@code execute()} is asynchronous and can race with navigation + * completion. The fix uses {@code AddScriptToExecuteOnDocumentCreated} which guarantees + * injection before any page script runs. + */ +@Test +public void test_BrowserFunction_availableBeforePageScripts_issue20() { + assumeTrue(isEdge, "Race condition is specific to async Edge/WebView2 implementation"); + AtomicBoolean functionCalled = new AtomicBoolean(false); + AtomicBoolean pageLoadCompleted = new AtomicBoolean(false); + + new BrowserFunction(browser, "options") { + @Override + public Object function(Object[] arguments) { + functionCalled.set(true); + return null; + } + }; + + // Navigate to a page whose inline "); + + shell.open(); + assertTrue(waitForPassCondition(pageLoadCompleted::get), "Page did not finish loading"); + assertTrue(functionCalled.get(), + "BrowserFunction 'options' was not called by the page script — it was not available " + + "before page scripts ran (regression of https://github.com/eclipse-platform/eclipse.platform.swt/issues/20)"); +} + +/** + * Regression test for https://github.com/eclipse-platform/eclipse.platform.swt/issues/20 + * + *

BrowserFunctions must survive page navigations. When a second Browser instance is being + * initialized concurrently, event-loop processing for the first browser can cause its async + * function-injection script to race with navigation completion, making the function undefined + * on the newly loaded page. + */ +@Test +public void test_BrowserFunction_availableOnLoad_concurrentInstances_issue20() { + assumeTrue(isEdge, "Race condition is specific to async Edge/WebView2 implementation"); + AtomicBoolean browser1FuncAvailable = new AtomicBoolean(false); + AtomicBoolean browser2FuncAvailable = new AtomicBoolean(false); + + // Use new Browser() directly (not the createBrowser() helper that waits for + // initialization) so that both browsers are initializing concurrently, replicating + // the timing described in the bug report. + Browser b1 = new Browser(shell, SWT.NONE); + b1.setUrl("about:blank"); + new BrowserFunction(b1, "options") { + @Override + public Object function(Object[] arguments) { return null; } + }; + b1.addProgressListener(completedAdapter(e -> { + try { + b1.evaluate("options();"); + browser1FuncAvailable.set(true); + } catch (SWTException ignored) {} + })); + createdBroswers.add(b1); + + // Creating a second browser forces event-loop processing that can reveal the race. + Browser b2 = new Browser(shell, SWT.NONE); + b2.setUrl("about:blank"); + new BrowserFunction(b2, "options") { + @Override + public Object function(Object[] arguments) { return null; } + }; + b2.addProgressListener(completedAdapter(e -> { + try { + b2.evaluate("options();"); + browser2FuncAvailable.set(true); + } catch (SWTException ignored) {} + })); + createdBroswers.add(b2); + + shell.open(); + assertTrue( + waitForPassCondition(() -> browser1FuncAvailable.get() && browser2FuncAvailable.get()), + "BrowserFunction must be available when page load completes on both browsers " + + "(regression of https://github.com/eclipse-platform/eclipse.platform.swt/issues/20)"); +} + @Test @Disabled("Too fragile on CI, Display.getDefault().post(event) does not work reliably") public void test_TabTraversalOutOfBrowser() {