Skip to content

Commit a7f51b1

Browse files
committed
[Win32][Edge] Fix BrowserFunction race condition using
AddScriptToExecuteOnDocumentCreated 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 #20
1 parent e476728 commit a7f51b1

File tree

3 files changed

+128
-0
lines changed

3 files changed

+128
-0
lines changed

bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ public WebViewEnvironment(ICoreWebView2Environment environment) {
8585
boolean inNewWindow;
8686
private boolean inEvaluate;
8787
HashMap<Long, LocationEvent> navigations = new HashMap<>();
88+
/** Maps BrowserFunction index to the script ID from AddScriptToExecuteOnDocumentCreated. */
89+
private final Map<Integer, String> functionScriptIds = new HashMap<>();
8890
private boolean ignoreGotFocus;
8991
private boolean ignoreFocusIn;
9092
private String lastCustomText;
@@ -1836,4 +1838,37 @@ public boolean setUrl(String url, String postData, String[] headers) {
18361838
return setWebpageData(url, postData, headers, null);
18371839
}
18381840

1841+
/**
1842+
* Registers the function script persistently via AddScriptToExecuteOnDocumentCreated so it is
1843+
* injected on every future document creation before any page scripts run, avoiding the race
1844+
* condition between async function injection and navigation completion.
1845+
* See <a href="https://github.com/eclipse-platform/eclipse.platform.swt/issues/20">issue #20</a>.
1846+
*/
1847+
@Override
1848+
public void createFunction(BrowserFunction function) {
1849+
super.createFunction(function);
1850+
if (inCallback > 0) {
1851+
// Cannot wait for a callback result while already inside a WebView2 callback;
1852+
// the existing NavigationStarting re-injection will handle future navigations.
1853+
return;
1854+
}
1855+
String[] scriptId = new String[1];
1856+
callAndWait(scriptId, completion ->
1857+
webViewProvider.getWebView(true).AddScriptToExecuteOnDocumentCreated(
1858+
stringToWstr(function.functionString), completion.getAddress()));
1859+
if (scriptId[0] != null) {
1860+
functionScriptIds.put(function.index, scriptId[0]);
1861+
}
1862+
}
1863+
1864+
@Override
1865+
void deregisterFunction(BrowserFunction function) {
1866+
super.deregisterFunction(function);
1867+
String scriptId = functionScriptIds.remove(function.index);
1868+
if (scriptId != null) {
1869+
webViewProvider.getWebView(true).RemoveScriptToExecuteOnDocumentCreated(
1870+
stringToWstr(scriptId));
1871+
}
1872+
}
1873+
18391874
}

bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ public int AddScriptToExecuteOnDocumentCreated(char[] javaScript, long handler)
6767
return COM.VtblCall(27, address, javaScript, handler);
6868
}
6969

70+
public int RemoveScriptToExecuteOnDocumentCreated(char[] id) {
71+
return COM.VtblCall(28, address, id);
72+
}
73+
7074
public int ExecuteScript(char[] javaScript, IUnknown handler) {
7175
return COM.VtblCall(29, address, javaScript, handler.address);
7276
}

tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2943,6 +2943,95 @@ public void completed(ProgressEvent event) {
29432943
browser2.dispose();
29442944
}
29452945

2946+
/**
2947+
* Regression test for https://github.com/eclipse-platform/eclipse.platform.swt/issues/20
2948+
*
2949+
* <p>A BrowserFunction registered before a navigation must be available when the new page's
2950+
* inline scripts execute (not just after the page finishes loading). In Edge/WebView2,
2951+
* function injection via {@code execute()} is asynchronous and can race with navigation
2952+
* completion. The fix uses {@code AddScriptToExecuteOnDocumentCreated} which guarantees
2953+
* injection before any page script runs.
2954+
*/
2955+
@Test
2956+
public void test_BrowserFunction_availableBeforePageScripts_issue20() {
2957+
assumeTrue(isEdge, "Race condition is specific to async Edge/WebView2 implementation");
2958+
AtomicBoolean functionCalled = new AtomicBoolean(false);
2959+
AtomicBoolean pageLoadCompleted = new AtomicBoolean(false);
2960+
2961+
new BrowserFunction(browser, "options") {
2962+
@Override
2963+
public Object function(Object[] arguments) {
2964+
functionCalled.set(true);
2965+
return null;
2966+
}
2967+
};
2968+
2969+
// Navigate to a page whose inline <script> calls options() immediately.
2970+
// With the fix, options() is injected before any page scripts via
2971+
// AddScriptToExecuteOnDocumentCreated and is therefore guaranteed to be defined.
2972+
browser.addProgressListener(completedAdapter(e -> pageLoadCompleted.set(true)));
2973+
browser.setText("<html><body><script>options();</script></body></html>");
2974+
2975+
shell.open();
2976+
assertTrue(waitForPassCondition(pageLoadCompleted::get), "Page did not finish loading");
2977+
assertTrue(functionCalled.get(),
2978+
"BrowserFunction 'options' was not called by the page script — it was not available "
2979+
+ "before page scripts ran (regression of https://github.com/eclipse-platform/eclipse.platform.swt/issues/20)");
2980+
}
2981+
2982+
/**
2983+
* Regression test for https://github.com/eclipse-platform/eclipse.platform.swt/issues/20
2984+
*
2985+
* <p>BrowserFunctions must survive page navigations. When a second Browser instance is being
2986+
* initialized concurrently, event-loop processing for the first browser can cause its async
2987+
* function-injection script to race with navigation completion, making the function undefined
2988+
* on the newly loaded page.
2989+
*/
2990+
@Test
2991+
public void test_BrowserFunction_availableOnLoad_concurrentInstances_issue20() {
2992+
assumeTrue(isEdge, "Race condition is specific to async Edge/WebView2 implementation");
2993+
AtomicBoolean browser1FuncAvailable = new AtomicBoolean(false);
2994+
AtomicBoolean browser2FuncAvailable = new AtomicBoolean(false);
2995+
2996+
// Use new Browser() directly (not the createBrowser() helper that waits for
2997+
// initialization) so that both browsers are initializing concurrently, replicating
2998+
// the timing described in the bug report.
2999+
Browser b1 = new Browser(shell, SWT.NONE);
3000+
b1.setUrl("about:blank");
3001+
new BrowserFunction(b1, "options") {
3002+
@Override
3003+
public Object function(Object[] arguments) { return null; }
3004+
};
3005+
b1.addProgressListener(completedAdapter(e -> {
3006+
try {
3007+
b1.evaluate("options();");
3008+
browser1FuncAvailable.set(true);
3009+
} catch (SWTException ignored) {}
3010+
}));
3011+
createdBroswers.add(b1);
3012+
3013+
// Creating a second browser forces event-loop processing that can reveal the race.
3014+
Browser b2 = new Browser(shell, SWT.NONE);
3015+
b2.setUrl("about:blank");
3016+
new BrowserFunction(b2, "options") {
3017+
@Override
3018+
public Object function(Object[] arguments) { return null; }
3019+
};
3020+
b2.addProgressListener(completedAdapter(e -> {
3021+
try {
3022+
b2.evaluate("options();");
3023+
browser2FuncAvailable.set(true);
3024+
} catch (SWTException ignored) {}
3025+
}));
3026+
createdBroswers.add(b2);
3027+
3028+
shell.open();
3029+
assertTrue(
3030+
waitForPassCondition(() -> browser1FuncAvailable.get() && browser2FuncAvailable.get()),
3031+
"BrowserFunction must be available when page load completes on both browsers "
3032+
+ "(regression of https://github.com/eclipse-platform/eclipse.platform.swt/issues/20)");
3033+
}
3034+
29463035
@Test
29473036
@Disabled("Too fragile on CI, Display.getDefault().post(event) does not work reliably")
29483037
public void test_TabTraversalOutOfBrowser() {

0 commit comments

Comments
 (0)