Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,11 @@ ICoreWebView2 initializeWebView(ICoreWebView2Controller controller) {
webViewWrapper.webView_11 = initializeWebView_11(webView);
webViewWrapper.webView_12 = initializeWebView_12(webView);
webViewWrapper.webView_13 = initializeWebView_13(webView);
// Register the scripts of all BrowserFunctions created during initialization
// before completing the future below, as that synchronously runs queued
// navigation tasks (e.g. from setUrl()/setText()) and thus may already create
// the first document
registerPendingFunctionScripts(webView);
boolean success = webViewWrapperFuture.complete(webViewWrapper);
// Release the webViews if the webViewWrapperFuture has already timed out and completed exceptionally
if(!success && webViewWrapperFuture.isCompletedExceptionally()) {
Expand All @@ -399,6 +404,10 @@ private void abortInitialization() {
webViewWrapperFuture.cancel(true);
}

boolean isInitialized() {
return webViewWrapperFuture.isDone();
}

void releaseWebView() {
getWebViewWrapper().releaseWebViews();
}
Expand Down Expand Up @@ -1839,40 +1848,69 @@ public boolean setUrl(String url, String postData, String[] headers) {
}

/**
* 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.
* If called while inside a WebView2 callback, the persistent registration is deferred via
* {@link Display#asyncExec(Runnable)} so it completes once the callback returns.
* Registers a BrowserFunction persistently via AddScriptToExecuteOnDocumentCreated so it is
* injected on every future document creation before any page scripts run.
* <p>
* The registration is issued immediately, but without waiting for its (asynchronous) completion:
* what makes a function available on a page is <em>issuing</em> the registration before the
* navigation that creates the document is issued to WebView2 - not waiting for the registration to
* complete. Since nothing is blocked on the completion, this can also safely be issued from within
* a WebView2 callback without risking a deadlock.
* <p>
* If the browser is not yet initialized when the function is created, the registration is issued by
* {@link WebViewProvider#initializeWebView(ICoreWebView2Controller)} (via
* {@link #registerPendingFunctionScripts(ICoreWebView2)}) before the first navigation, so functions
* created concurrently with initialization are available on the first loaded page.
* See <a href="https://github.com/eclipse-platform/eclipse.platform.swt/issues/20">issue #20</a>.
*/
@Override
public void createFunction(BrowserFunction function) {
// If the browser is not yet initialized, initializeWebView() registers all pending
// functions (including this one) before completing the initialization future. Do not
// register again in that case - and in particular do not call getWebView() below, as
// that would pump the event loop until initialization completes and thus create a
// duplicate registration.
boolean alreadyInitialized = webViewProvider.isInitialized();
super.createFunction(function);
int functionIndex = function.index;
String functionString = function.functionString;
if (inCallback > 0) {
// Cannot wait for a callback result while already inside a WebView2 callback;
// defer the persistent registration to after the callback completes.
browser.getDisplay().asyncExec(() -> {
if (browser.isDisposed() || !functions.containsKey(functionIndex)) return;
registerFunctionScript(functionIndex, functionString);
});
return;
if (alreadyInitialized) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have a race condition here?

when super.createFunction() returns, we might be initialized in the meantime as the future is completed from an asynchronous callback.

registerFunctionScript(webViewProvider.getWebView(false), function.index, function.functionString);
}
registerFunctionScript(functionIndex, functionString);
}

private void registerFunctionScript(int functionIndex, String functionString) {
String[] scriptId = new String[1];
callAndWait(scriptId, completion ->
webViewProvider.getWebView(false).AddScriptToExecuteOnDocumentCreated(
stringToWstr(functionString), completion.getAddress()));
if (scriptId[0] != null) {
functionScriptIds.put(functionIndex, scriptId[0]);
private void registerPendingFunctionScripts(ICoreWebView2 webView) {
for (Map.Entry<Integer, BrowserFunction> entry : functions.entrySet()) {
BrowserFunction function = entry.getValue();
if (function.functionString != null) {
registerFunctionScript(webView, entry.getKey(), function.functionString);
}
}
}

/**
* Issues the registration of a function's document-created script on the given WebView without
* blocking for the asynchronous completion. The resulting script ID is stored once the completion
* callback fires; if the function was deregistered again in the meantime, the script is removed
* right away instead, so an immediately following deregistration does not leak the script.
*/
private void registerFunctionScript(ICoreWebView2 webView, int functionIndex, String functionString) {
IUnknown completion = newCallback((result, scriptIdPointer) -> {
if ((int) result == COM.S_OK) {
String scriptId = wstrToString(scriptIdPointer, false);
if (functions.containsKey(functionIndex)) {
functionScriptIds.put(functionIndex, scriptId);
} else if (!browser.isDisposed()) {
webView.RemoveScriptToExecuteOnDocumentCreated(stringToWstr(scriptId));
}
}
return COM.S_OK;
});
int hr = webView.AddScriptToExecuteOnDocumentCreated(stringToWstr(functionString), completion.getAddress());
if (hr != OS.S_OK) {
System.err.println("Registering browser function failed with result " + hr + " for function: " + functionString);
}
completion.Release();
}

@Override
void deregisterFunction(BrowserFunction function) {
super.deregisterFunction(function);
Expand All @@ -1881,6 +1919,8 @@ void deregisterFunction(BrowserFunction function) {
webViewProvider.getWebView(true).RemoveScriptToExecuteOnDocumentCreated(
stringToWstr(scriptId));
}
// If scriptId == null, an asynchronous registration has not stored its ID yet; its completion
// callback detects the now-removed function (via the functions map) and removes the script itself.
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3045,6 +3045,133 @@ public void test_BrowserFunction_availableOnLoad_concurrentInstances_issue20() {
assertTrue(browser2FuncAvailable.get(), "BrowserFunction for second browser missing when page load completed");
}

/**
* Regression test: a BrowserFunction created from <em>inside</em> another BrowserFunction's
* callback must be registered and available on a page that is navigated to from within that same
* callback.
* <p>
* On the Edge/WebView2 backend this exercises function creation while a WebView2 callback is on the
* stack. Registration must be issued (before the navigation queued in the same callback) without
* blocking, since blocking inside a callback would deadlock.
*/
@Test
public void test_BrowserFunction_createFunctionInsideCallback() {
assumeTrue(isEdge, "BrowserFunction availability before the page's inline scripts is specific to the Edge/WebView2 implementation");
AtomicBoolean innerCalled = new AtomicBoolean(false);

// 'inner' is only created when 'outer' is invoked from JavaScript, i.e. inside a callback.
class Inner extends BrowserFunction {
Inner() {
super(browser, "inner");
}
@Override
public Object function(Object[] arguments) {
innerCalled.set(true);
return null;
}
}
class Outer extends BrowserFunction {
Outer() {
super(browser, "outer");
}
@Override
public Object function(Object[] arguments) {
new Inner(); // create a new BrowserFunction from inside a callback
// Navigate to a page whose inline script calls the just-created function.
browser.setText("<html><body><script>inner();</script></body></html>");
return null;
}
}
new Outer();

// Trigger outer() once, after the first page has loaded.
AtomicBoolean outerTriggered = new AtomicBoolean(false);
browser.addProgressListener(completedAdapter(e -> {
if (outerTriggered.compareAndSet(false, true)) {
browser.execute("outer();");
}
}));
browser.setText("<html><body>first page</body></html>");

shell.open();
assertTrue(waitForPassCondition(innerCalled::get),
"BrowserFunction created inside a callback was not available on the page navigated to from that callback");
}

/**
* Regression test for issue #20: a BrowserFunction created while the browser is still initializing
* must be available <em>before</em> the first loaded page's own inline scripts run - not merely
* after the page finished loading. This combines concurrent initialization (the browser is not
* awaited) with a page whose inline script immediately calls the function.
*/
@Test
public void test_BrowserFunction_availableBeforePageScripts_concurrentInit_issue20() {
assumeTrue(isEdge, "BrowserFunction availability before the page's inline scripts is specific to the Edge/WebView2 implementation");
AtomicBoolean functionCalled = new AtomicBoolean(false);

// Use new Browser() directly (not the createBrowser() helper that waits for initialization) so
// the browser is still initializing while we navigate and register the function.
Browser b = new Browser(shell, SWT.NONE);
createdBroswers.add(b);
// Mirror the bug's order: request the navigation first, then create the function - both before
// initialization completes.
b.setText("<html><body><script>options();</script></body></html>");
new BrowserFunction(b, "options") {
@Override
public Object function(Object[] arguments) {
functionCalled.set(true);
return null;
}
};

shell.open();
assertTrue(waitForPassCondition(functionCalled::get),
"BrowserFunction 'options' was not available before the first page's inline script ran during concurrent initialization");
}

/**
* Regression test for issue #20: when multiple BrowserFunctions are created while the browser is
* still initializing, all of them must be available on the first loaded page.
*/
@Test
public void test_BrowserFunction_multipleFunctionsDuringConcurrentInit_issue20() {
assumeFalse(SwtTestUtil.isCocoa, "BrowserFunction availability during concurrent initialization is not reliable on Cocoa");
AtomicReference<Object> result = new AtomicReference<>();
AtomicReference<SWTException> failure = new AtomicReference<>();

Browser b = new Browser(shell, SWT.NONE);
createdBroswers.add(b);
b.setUrl("about:blank");
new BrowserFunction(b, "f1") {
@Override
public Object function(Object[] arguments) {
return 1;
}
};
new BrowserFunction(b, "f2") {
@Override
public Object function(Object[] arguments) {
return 2;
}
};
b.addProgressListener(completedAdapter(e -> {
try {
result.set(b.evaluate("return f1() + f2();"));
} catch (SWTException ex) {
failure.set(ex);
}
}));

shell.open();
waitForPassCondition(() -> result.get() != null || failure.get() != null);
if (failure.get() != null) {
throw failure.get();
}
assertNotNull(result.get(), "Neither BrowserFunction was available on the first loaded page");
assertEquals(3.0, ((Number) result.get()).doubleValue(),
"Both BrowserFunctions created during concurrent initialization must be available on the first page");
}

/**
* Regression test: a disposed BrowserFunction must no longer be available (re-injected) after a
* subsequent navigation. This verifies that deregistration removes the persistent document-created
Expand Down
Loading