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

Fix Edge Browser Deadlock issue for instantiation during a WebView Callback #1378

Merged
merged 1 commit into from
Sep 25, 2024

Conversation

amartya4256
Copy link
Contributor

@amartya4256 amartya4256 commented Aug 2, 2024

This contribution fixes Edge browser for win32 to handle the deadlock issue during the instantiation of a new edge browser object amidst a webview callback.

The problem with webview is that it can't execute tasks simultaneously and all the requests are serialized. Hence, a new request to the webview inside its callback leads to a deadlock since, both the tasks wait for each other to execute. The solution is to schedule the Webview calls during the callback asynchronously and let the control in the callback move on.

contributes to #669

Scope not covered

  • This implementation doesn't fix the evaluate calls during the webview callback and would be addressed in a separate issue.

Pre-requisites

Testing

  • Change the method org.eclipse.swt.browser.BrowserFactory.createWebBrowser to:
WebBrowser createWebBrowser (int style) {
	return new Edge ();
}

Copy link
Contributor

github-actions bot commented Aug 2, 2024

Test Results

   486 files  ±0     486 suites  ±0   6m 55s ⏱️ - 1m 22s
 4 159 tests +1   4 151 ✅ +1   8 💤 ±0  0 ❌ ±0 
16 390 runs  +4  16 298 ✅ +4  92 💤 ±0  0 ❌ ±0 

Results for commit 1f22a98. ± Comparison against base commit 3c8ed8e.

♻️ This comment has been updated with latest results.

@Phillipus
Copy link
Contributor

Phillipus commented Aug 2, 2024

The temp file is not deleted on exit. Temp files on Windows can accumulate over time and Windows never deletes them, so perhaps file.deleteOnExit() could be added?

@Phillipus
Copy link
Contributor

Phillipus commented Aug 2, 2024

This PR creates a temporary file and replaces the original implementation of Edge#setText() in all cases, even where Edge#setText() might not need it in the case of very simple HTML (no references to local resources). Would an additional setText method work rather than replacing the existing one?

And Edge#getText() doesn't return the correct contents. Run the following snippet:

import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

public class SWTBrowserTest {
    
    public static void main(String[] args) {
        final Display display = new Display();

        final Shell shell = new Shell(display);
        shell.setText("SWT Browser");
        shell.setLayout(new FillLayout());
        shell.setSize(1024, 768);
        
        String html = makeHTMLEntry();
        System.out.println(html);
        
        Browser browser = new Browser(shell, SWT.EDGE);
        browser.setText(html);
        
        System.out.println(browser.getText());

        shell.open();
        
        while(!shell.isDisposed()) {
            if(!display.readAndDispatch()) {
                display.sleep();
            }
        }

        display.dispose();
    }
    
    private static String makeHTMLEntry() {
        StringBuffer html = new StringBuffer();
        html.append("<html><head>");
        html.append("</head>");
        
        html.append("<body>");
        html.append("<p>Hello World!</p>");
        html.append("</body>");
        
        html.append("</html>");
        return html.toString();
    }
}

Output is as follows (first line is original HTML, second line is returned from getText):

<html><head></head><body><p>Hello World!</p></body></html>
<html><head></head><body></body></html>

@amartya4256 amartya4256 force-pushed the fix_edge branch 2 times, most recently from 7b7ea3a to 95d0757 Compare August 5, 2024 12:18
@Phillipus
Copy link
Contributor

If BASE_URL is only used by the Edge class should that portion of code be moved there instead of being in the Browser class? And does it need to be public, perhaps it's an internal thing?

@amartya4256
Copy link
Contributor Author

amartya4256 commented Aug 6, 2024

If BASE_URL is only used by the Edge class should that portion of code be moved there instead of being in the Browser class? And does it need to be public, perhaps it's an internal thing?

The idea to put it in Browser was because this is needed outside swt as well and having it in Edge would lead to problems for other platforms. Moreover, this approach in Edge is also a high level implementation which means that this could be used for other browsers as well, since this problem can occur with any modern browser. I am not sure if a similar issue exists with browsers in other platforms but if so, in future we can do something similar to them as well.

But for now, we cant use Edge.BASE_URL outside SWT, since it is windows specific. We need the BASE_URL in 2 places: jdt.ui and eclipse.platform, these PRs would be follow up after this PR, as mentioned in the description. However I can make the static block to only create a file if Edge is the default, if necessary.

@amartya4256
Copy link
Contributor Author

This PR creates a temporary file and replaces the original implementation of Edge#setText() in all cases, even where Edge#setText() might not need it in the case of very simple HTML (no references to local resources). Would an additional setText method work rather than replacing the existing one?

And Edge#getText() doesn't return the correct contents. Run the following snippet:

import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

public class SWTBrowserTest {
    
    public static void main(String[] args) {
        final Display display = new Display();

        final Shell shell = new Shell(display);
        shell.setText("SWT Browser");
        shell.setLayout(new FillLayout());
        shell.setSize(1024, 768);
        
        String html = makeHTMLEntry();
        System.out.println(html);
        
        Browser browser = new Browser(shell, SWT.EDGE);
        browser.setText(html);
        
        System.out.println(browser.getText());

        shell.open();
        
        while(!shell.isDisposed()) {
            if(!display.readAndDispatch()) {
                display.sleep();
            }
        }

        display.dispose();
    }
    
    private static String makeHTMLEntry() {
        StringBuffer html = new StringBuffer();
        html.append("<html><head>");
        html.append("</head>");
        
        html.append("<body>");
        html.append("<p>Hello World!</p>");
        html.append("</body>");
        
        html.append("</html>");
        return html.toString();
    }
}

Output is as follows (first line is original HTML, second line is returned from getText):

<html><head></head><body><p>Hello World!</p></body></html>
<html><head></head><body></body></html>

This issue was because of the asynchronous behaviour. I have fixed it now.

@Phillipus
Copy link
Contributor

However I can make the static block to only create a file if Edge is the default, if necessary.

Perhaps create it lazily when setText is called?

@amartya4256 amartya4256 force-pushed the fix_edge branch 2 times, most recently from 50bd0bc to b3a9b1f Compare August 6, 2024 15:29
Copy link
Contributor

@fedejeanne fedejeanne left a comment

Choose a reason for hiding this comment

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

Looks far better now but I know this PR is going to be changed so please @Phillipus / @HeikoKlare review too.

I'm out of office the next 2 weeks.

@amartya4256 amartya4256 changed the title Fix Edge Browser for Win32 Fix Edge Browser Deadlock issue for instantiation during a WebView Callback Aug 8, 2024
@amartya4256
Copy link
Contributor Author

Split the PR into 2 and moved the set text fix to #1395

@fedejeanne
Copy link
Contributor

Split the PR into 2 and moved the set text fix to #1395

You still have 2 commits in this PR. This one is probably the one that you want to move to #1395 : 9635dff

@amartya4256
Copy link
Contributor Author

Split the PR into 2 and moved the set text fix to #1395

You still have 2 commits in this PR. This one is probably the one that you want to move to #1395 : 9635dff

This PR is based on #1395. Once 1395 is merged, it will not be visible. The other commit is from #1395. As mentioned in the description, only the last commit is relevant for this PR.

@fedejeanne fedejeanne dismissed their stale review September 12, 2024 11:26

My 2 findings need to be addressed in a separate PR

Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

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

The code looks much better structured now. I still see some easy opportunities for improving the WebViewProvider from a rather data-wrapping class to a better encapsulated provider class via moving more of the initialization logic inside that class and reducing the methods to be called from the consumer.

I also have one remark regarding a potential regressions, but most of the comments concern design and documentation improvements.

private CompletableFuture<ICoreWebView2_11> webView_11Future = new CompletableFuture<>();

private CompletableFuture<Void> lastWebViewTask = webViewFuture.thenCompose(__ -> {
return CompletableFuture.allOf(webView_2Future.handle((result, ex) -> null), webView_11Future.handle((result, ex) -> null));
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not understand the purpose of this line. Maybe you can wrap the initialization of lastWebViewTask into a method that better explains (by its name or, if not possible, by documentation) what this is supposed to do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll extract it to a method. This one bascially creates a future which will complete after webview is completed and after that webview_2 and webview_11 are either completed or cancelled (depending on the execution in setup task), just to make sure once all the webviews are ready before we start executing the other scheduled tasks

What should I name it? getWaitForWebViewInitializationFuture?

Copy link
Contributor

Choose a reason for hiding this comment

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

I do not understand the purpose of the calls to the method handle.

Anyway, why not simply complete the webViewFuture after the other web view interface have been initialized, i.e., in the initializeWebView method, replace this:

		webViewFuture.complete(new ICoreWebView2(ppv[0]));
		initializeWebView_2();
		initializeWebView_11();

with something like this:

		ICoreWebView2 webView = new ICoreWebView2(ppv[0]);
		initializeWebView_2(webView);
		initializeWebView_11(webView);
		webViewFuture.complete(webView);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

handle basically chains the 2 futures telling that we dont care if they are cancelled or completed but it the lastWebViewTask would only be completed after both the webView_2 and webView_11 are done after webView itself. But I like the approach. I'll create a Completablefuture and complete it following the initialization.

@@ -312,12 +407,13 @@ static ICoreWebView2CookieManager getCookieManager() {
SWT.error(SWT.ERROR_NOT_IMPLEMENTED, null, " [WebView2: cookie access requires a Browser instance]");
}
Edge instance = environmentWrapper.instances().get(0);
if (instance.webView_2 == null) {
instance.webViewProvider.waitForPendingWebviewTasksToFinish();
Copy link
Contributor

Choose a reason for hiding this comment

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

Now all except this calls to waitForPendingWebiewTaskToFinish() are encapsulated within the WebViewProvider. I propose to keep that functionality completely hidden in the WebViewProvider. I guess calls to isWebView*Unavailable() should probably block until initialization has finished, as otherwise the method will return unreasonable resonable in case the web view has not been initialized yet anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

cant put waitForPendingWebiewTaskToFinish inside isWebView*Unavailable cause its used in callbacks and calling it inside that would cause a deadlock

Copy link
Contributor

Choose a reason for hiding this comment

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

But actually that's not even what you want to do here, is it? You want to wait for webView being initialized, not for all tasks being finished. That is something that should be done in the isWebView*Unavailable() methods anyway in order to avoid that they return unreasonable results when called while the web view has not been initialized yet.

In any case, hiding the need to wait for pending tasks in all but one case is a design flaw for which we need to have a solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved it inside the method and tested it. It seems fine and it isnt causing any deadlock. Seems good.

private CompletableFuture<ICoreWebView2_11> webView_11Future = new CompletableFuture<>();

private CompletableFuture<Void> lastWebViewTask = webViewFuture.thenCompose(__ -> {
return CompletableFuture.allOf(webView_2Future.handle((result, ex) -> null), webView_11Future.handle((result, ex) -> null));
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not understand the purpose of the calls to the method handle.

Anyway, why not simply complete the webViewFuture after the other web view interface have been initialized, i.e., in the initializeWebView method, replace this:

		webViewFuture.complete(new ICoreWebView2(ppv[0]));
		initializeWebView_2();
		initializeWebView_11();

with something like this:

		ICoreWebView2 webView = new ICoreWebView2(ppv[0]);
		initializeWebView_2(webView);
		initializeWebView_11(webView);
		webViewFuture.complete(webView);

This contribution fixes Edge browser Deadlock issue on instantiating a
new Edge Browser object during a webview callback using async execution.
The Implementation follows the pattern similar to how WebKit has been
implemented imitating the async behaviour in a sync fashion using
Futures and callbacks.

contributes to eclipse-platform#669
Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

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

Looks like a rather clean solution now. Locally executing the Browser tests for Edge is successful. In some manual testing, I also did not find any issues so far.
Let's give this a try.

@HeikoKlare HeikoKlare merged commit 1d12c5f into eclipse-platform:master Sep 25, 2024
14 checks passed
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.

5 participants