Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edge: Freeze in Browser#evaluate() if evaluated script calls into SWT #1771

Open
sratz opened this issue Jan 27, 2025 · 5 comments
Open

Edge: Freeze in Browser#evaluate() if evaluated script calls into SWT #1771

sratz opened this issue Jan 27, 2025 · 5 comments
Labels
edge Edge Browser

Comments

@sratz
Copy link
Member

sratz commented Jan 27, 2025

Describe the bug
With Edge, when using browser.evaluate(script) and script somehow causing a callback into SWT, the evaluate() freezes / hangs in 100% CPU.

To Reproduce

@Test
public void test_EvaluateCallingIntoSwt() throws Exception {
	AtomicBoolean initialLoad = new AtomicBoolean();
	AtomicBoolean openWindowListenerCalled = new AtomicBoolean();
	browser.addProgressListener(ProgressListener.completedAdapter(e -> initialLoad.set(true)));
	browser.addOpenWindowListener(event -> {
		event.required = true; // block default
		openWindowListenerCalled.set(true);
	});
	browser.setText("""
			<button id="button" onClick="window.open('https://eclipse.org');">open eclipse.org</button>
			""");
	waitForPassCondition(initialLoad::get);

	browser.evaluate("""
				document.getElementById("button").click();
			""");

	waitForPassCondition(openWindowListenerCalled::get);
}

Expected behavior
Test succeeds with IE and Edge.

Actual behavior
Test hangs with Edge, succeeds with IE.
Stack trace shows:

Thread [main] (Suspended)	
	org.eclipse.swt.internal.win32.OS.PeekMessage(org.eclipse.swt.internal.win32.MSG, long, int, int, int) line: not available [native method]	
	org.eclipse.swt.browser.Edge.processNextOSMessage() line: 371	
	org.eclipse.swt.browser.Edge.callAndWait(java.lang.String[], java.util.function.ToIntFunction<org.eclipse.swt.internal.ole.win32.IUnknown>) line: 284	
	org.eclipse.swt.browser.Edge.evaluate(java.lang.String) line: 746	
	org.eclipse.swt.browser.Edge(org.eclipse.swt.browser.WebBrowser).evaluate(java.lang.String, boolean) line: 406	
	org.eclipse.swt.browser.Browser.evaluate(java.lang.String, boolean) line: 665	
	org.eclipse.swt.browser.Browser.evaluate(java.lang.String) line: 614	
	org.eclipse.swt.tests.junit.Test_org_eclipse_swt_browser_Browser.test_EvaluateCallingIntoSwt() line: 2700	

Environment:

  1. Select the platform(s) on which the behavior is seen:
    • All OS
    • Windows / Edge
    • Linux
    • macOS
  1. Additional OS info (e.g. OS version, Linux Desktop, etc)

Additional context
This is a side-effect of #660, where inside Edge.callAndWait()
if (!display.readAndDispatch()) display.sleep();
was replaced with
processNextOSMessage();.

@HeikoKlare:
PR #660 suggest that this was to avoid a out-of-handles during initialization, i.e. inside createEnvironment(), but it also affects the evaluate() call negatively.
Do you think it would be reasonable to keep using display.readAndDispatch() in the latter?

@HeikoKlare
Copy link
Contributor

Thank you for this report, @sratz! We are currently investigating potential reasons for Edge blocking the UI thread (according to eclipse-platform/eclipse.platform.ui#2734) starting with this PR:

I also pointed to the calls to processNextOSMessage() there (#1752 (review)) as they might be blocking the UI thread. Now it's nice to have an actual reproducer of a potential issue from you!

In my opinion, we could/should replace all the calls to processNextOSMessage() and just do the previous if (!display.readAndDispatch()) display.sleep() again, as in every case (in particular the initialization) this is now run inside a loop anyway (as the initialization logic has changed and been made completely asynchronous since back then). So there is no need for peeking a message anymore.

@HeikoKlare
Copy link
Contributor

I've created a draft PR with the idea for the improvement:

HeikoKlare pushed a commit to vi-eclipse/eclipse.platform.swt that referenced this issue Jan 28, 2025
HeikoKlare pushed a commit to vi-eclipse/eclipse.platform.swt that referenced this issue Jan 28, 2025
HeikoKlare added a commit to vi-eclipse/eclipse.platform.swt that referenced this issue Jan 29, 2025
…ipse-platform#1771

Before recent improvements of the Edge implementation, it was possible
that a browser instance was disposed during WebView initialization,
which made the initialization fail exceptionally. This has been
worked around by not processing all kinds of asynchronously scheduled
events (like a disposal) while waiting for WebView initialization but
only those being processes by the OS event queue. This was still
necessary to process the OS callbacks for WebView initialization and
other operations.

In some cases, this may lead to an Edge browser instance blocking the UI
thread, as some asynchronously scheduled tasks need to be processed but
are not. In addition, enhancements of the Edge implementation made the
initialization happen asynchronously anyway, such that the browser
instantiation itself cannot throw an error anymore, but the
asynchronously happening initialization is now capable of processing
asynchronous browser disposals.

This change simplifies the event processing inside Edge to not only
process the next OS message but to just ordinarily spin the event loop.
This reduces the risk of operations executed on an Edge browser to block
execution.

eclipse-platform#1771
HeikoKlare pushed a commit to vi-eclipse/eclipse.platform.swt that referenced this issue Jan 29, 2025
HeikoKlare added a commit to vi-eclipse/eclipse.platform.swt that referenced this issue Jan 29, 2025
…ipse-platform#1771

Before recent improvements of the Edge implementation, it was possible
that a browser instance was disposed during WebView initialization,
which made the initialization fail exceptionally. This has been
worked around by not processing all kinds of asynchronously scheduled
events (like a disposal) while waiting for WebView initialization but
only those being processes by the OS event queue. This was still
necessary to process the OS callbacks for WebView initialization and
other operations.

In some cases, this may lead to an Edge browser instance blocking the UI
thread, as some asynchronously scheduled tasks need to be processed but
are not. In addition, enhancements of the Edge implementation made the
initialization happen asynchronously anyway, such that the browser
instantiation itself cannot throw an error anymore, but the
asynchronously happening initialization is now capable of processing
asynchronous browser disposals.

This change simplifies the event processing inside Edge to not only
process the next OS message but to just ordinarily spin the event loop.
This reduces the risk of operations executed on an Edge browser to block
execution.

eclipse-platform#1771
HeikoKlare pushed a commit to vi-eclipse/eclipse.platform.swt that referenced this issue Jan 29, 2025
HeikoKlare added a commit to vi-eclipse/eclipse.platform.swt that referenced this issue Jan 29, 2025
…ipse-platform#1771

Before recent improvements of the Edge implementation, it was possible
that a browser instance was disposed during WebView initialization,
which made the initialization fail exceptionally. This has been
worked around by not processing all kinds of asynchronously scheduled
events (like a disposal) while waiting for WebView initialization but
only those being processes by the OS event queue. This was still
necessary to process the OS callbacks for WebView initialization and
other operations.

In some cases, this may lead to an Edge browser instance blocking the UI
thread, as some asynchronously scheduled tasks need to be processed but
are not. In addition, enhancements of the Edge implementation made the
initialization happen asynchronously anyway, such that the browser
instantiation itself cannot throw an error anymore, but the
asynchronously happening initialization is now capable of processing
asynchronous browser disposals.

This change simplifies the event processing inside Edge to not only
process the next OS message but to just ordinarily spin the event loop.
This reduces the risk of operations executed on an Edge browser to block
execution.

eclipse-platform#1771
HeikoKlare pushed a commit to vi-eclipse/eclipse.platform.swt that referenced this issue Jan 29, 2025
HeikoKlare added a commit to vi-eclipse/eclipse.platform.swt that referenced this issue Jan 29, 2025
…ipse-platform#1771

Before recent improvements of the Edge implementation, it was possible
that a browser instance was disposed during WebView initialization,
which made the initialization fail exceptionally. This has been
worked around by not processing all kinds of asynchronously scheduled
events (like a disposal) while waiting for WebView initialization but
only those being processes by the OS event queue. This was still
necessary to process the OS callbacks for WebView initialization and
other operations.

In some cases, this may lead to an Edge browser instance blocking the UI
thread, as some asynchronously scheduled tasks need to be processed but
are not. In addition, enhancements of the Edge implementation made the
initialization happen asynchronously anyway, such that the browser
instantiation itself cannot throw an error anymore, but the
asynchronously happening initialization is now capable of processing
asynchronous browser disposals.

This change simplifies the event processing inside Edge to not only
process the next OS message but to just ordinarily spin the event loop.
This reduces the risk of operations executed on an Edge browser to block
execution.

eclipse-platform#1771
HeikoKlare pushed a commit to vi-eclipse/eclipse.platform.swt that referenced this issue Jan 29, 2025
@laeubi
Copy link
Contributor

laeubi commented Jan 29, 2025

@sratz @HeikoKlare I think the reproducer violates the basic contract of evaluate that is:

If document-defined functions or properties are accessed by the script then this method should not be invoked until the document has finished loading (ProgressListener.completed() gives notification of this).

As you invoke the evaluate synchronously with construction but access document.getElementById this is actually invalid and can at best lead to undefined value.

So that it works here is more maybe the bug, the browser should most likley fire the event of document load inside an asnyc exec and your code should look like this:

browser.addProgressListener(ProgressListener.completedAdapter(e ->{

	browser.evaluate("""
				document.getElementById("button").click();
			""");

}));

I can't really think of any (non deadlock prone) way to support async javascript loading and synchronous instantiation of a browsers document, and this is also the reason why this will never happen in a real web application and completed event is mandatory in JS to be used before accessing the dom.

@HeikoKlare
Copy link
Contributor

Some more insights on the underlying cause of this: when opening a new window, the OS callback does not directly process this creation but, again, creates an async task to do it:

int handleNewWindowRequested(long pView, long pArgs) {
ICoreWebView2NewWindowRequestedEventArgs args = new ICoreWebView2NewWindowRequestedEventArgs(pArgs);
args.AddRef();
long[] ppv = new long[1];
args.GetDeferral(ppv);
ICoreWebView2Deferral deferral = new ICoreWebView2Deferral(ppv[0]);
inNewWindow = true;
browser.getDisplay().asyncExec(() -> {
try {
if (browser.isDisposed()) return;
WindowEvent openEvent = new WindowEvent(browser);
openEvent.display = browser.getDisplay();
openEvent.widget = browser;
openEvent.required = true;
for (OpenWindowListener openListener : openWindowListeners) {
openListener.open(openEvent);
if (browser.isDisposed()) return;
}
if (openEvent.browser != null && !openEvent.browser.isDisposed()) {
WebBrowser other = openEvent.browser.webBrowser;
args.put_Handled(true);
if (other instanceof Edge otherEdge) {
args.put_NewWindow(otherEdge.webViewProvider.getWebView(true).getAddress());
// Send show event to the other browser.
WindowEvent showEvent = new WindowEvent (other.browser);
showEvent.display = browser.getDisplay();
showEvent.widget = other.browser;
updateWindowFeatures(args, showEvent);
for (VisibilityWindowListener showListener : other.visibilityWindowListeners) {
showListener.show(showEvent);
if (other.browser.isDisposed()) return;
}
}
} else if (openEvent.required) {
args.put_Handled(true);
}
} finally {
deferral.Complete();
deferral.Release();
args.Release();
inNewWindow = false;
}
});
return COM.S_OK;
}

Since our callAndWait() only processes OS events but does not process tasks scheduled via Display#asyncExec(), the actual window creation will never terminate. While being stuck in callAndWait() as in the original issue report, you see this event pending in the display's synchronizer:
Image

This is the reason why spinning the event loop "solved" the problem, as it will execute that asynchronous task.

Simply performing the execution of that callback synchronously without creating an async task does not work either, as then other use cases (specified as tests) will get stuck.

@laeubi
Copy link
Contributor

laeubi commented Jan 29, 2025

As mentioned here:

I think it is not appropriate to "join" a future unconditionally on the event thread (in any case) without special considerations/handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
edge Edge Browser
Projects
None yet
Development

No branches or pull requests

3 participants