Skip to content

Commit 7ad097a

Browse files
HeikoKlarefedejeanne
authored andcommitted
[Win32] Avoid blocking operations on Edge due to OS message processing
The Edge browser implementation currently spins the event queue without any possibility to escape in case no OS message or not the expected OS message to proceed occurs and is processed. In order to avoid that operations called on Edge deadlock, this change introduces timeouts for OS event processing inside Edge: - The existing processNextOSMessage() method is replaced with processOSMessagesUntil(condition) that spins the event queue until the condition is met or a timeout occurred - CompletableFutures used for the queuing events on the WebView instance are extended to wake the display in order to avoid that the OS message processing is unnecessary sleeping until a timeout wakes it up
1 parent 9aa7973 commit 7ad097a

File tree

2 files changed

+68
-61
lines changed

2 files changed

+68
-61
lines changed

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

+60-57
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.time.*;
2222
import java.util.*;
2323
import java.util.concurrent.*;
24+
import java.util.concurrent.atomic.*;
2425
import java.util.function.*;
2526

2627
import org.eclipse.swt.*;
@@ -42,6 +43,7 @@ class Edge extends WebBrowser {
4243
static final String APPLOCAL_DIR_KEY = "org.eclipse.swt.internal.win32.appLocalDir";
4344
static final String EDGE_USER_DATA_FOLDER = "org.eclipse.swt.internal.win32.Edge.userDataFolder";
4445
static final String EDGE_USE_DARK_PREFERED_COLOR_SCHEME = "org.eclipse.swt.internal.win32.Edge.useDarkPreferedColorScheme"; //$NON-NLS-1$
46+
static final String WEB_VIEW_OPERATION_TIMEOUT = "org.eclipse.swt.internal.win32.Edge.timeout"; //$NON-NLS-1$
4547

4648
// System.getProperty() keys
4749
static final String BROWSER_DIR_PROP = "org.eclipse.swt.browser.EdgeDir";
@@ -58,6 +60,7 @@ class Edge extends WebBrowser {
5860
private static final String ABOUT_BLANK = "about:blank";
5961

6062
private static final int MAXIMUM_CREATION_RETRIES = 5;
63+
private static final Duration MAXIMUM_OPERATION_TIME = Duration.ofMillis(Integer.getInteger(WEB_VIEW_OPERATION_TIMEOUT, 5_000));
6164

6265
private record WebViewEnvironment(ICoreWebView2Environment environment, ArrayList<Edge> instances) {
6366
public WebViewEnvironment(ICoreWebView2Environment environment) {
@@ -260,14 +263,12 @@ static int callAndWait(long[] ppv, ToIntFunction<IUnknown> callable) {
260263
phr[0] = callable.applyAsInt(completion);
261264
// "completion" callback may be called asynchronously,
262265
// so keep processing next OS message that may call it
263-
while (phr[0] == COM.S_OK && ppv[0] == 0) {
264-
processNextOSMessage();
265-
}
266+
processOSMessagesUntil(() -> phr[0] != COM.S_OK || ppv[0] != 0, Display.getCurrent());
266267
completion.Release();
267268
return phr[0];
268269
}
269270

270-
static int callAndWait(String[] pstr, ToIntFunction<IUnknown> callable) {
271+
int callAndWait(String[] pstr, ToIntFunction<IUnknown> callable) {
271272
int[] phr = new int[1];
272273
IUnknown completion = newCallback((result, pszJson) -> {
273274
phr[0] = (int)result;
@@ -280,9 +281,7 @@ static int callAndWait(String[] pstr, ToIntFunction<IUnknown> callable) {
280281
phr[0] = callable.applyAsInt(completion);
281282
// "completion" callback may be called asynchronously,
282283
// so keep processing next OS message that may call it
283-
while (phr[0] == COM.S_OK && pstr[0] == null) {
284-
processNextOSMessage();
285-
}
284+
processOSMessagesUntil(() -> phr[0] != COM.S_OK || pstr[0] != null, browser.getDisplay());
286285
completion.Release();
287286
return phr[0];
288287
}
@@ -298,7 +297,7 @@ class WebViewWrapper {
298297

299298
class WebViewProvider {
300299

301-
private CompletableFuture<WebViewWrapper> webViewWrapperFuture = new CompletableFuture<>();
300+
private CompletableFuture<WebViewWrapper> webViewWrapperFuture = wakeDisplayAfterFuture(new CompletableFuture<>());
302301
private CompletableFuture<Void> lastWebViewTask = webViewWrapperFuture.thenRun(() -> {});;
303302

304303
ICoreWebView2 initializeWebView(ICoreWebView2Controller controller) {
@@ -365,95 +364,91 @@ private ICoreWebView2_13 initializeWebView_13(ICoreWebView2 webView) {
365364
return null;
366365
}
367366

368-
ICoreWebView2 getWebView(boolean waitForPendingWebviewTasksToFinish) {
367+
private WebViewWrapper getWebViewWrapper(boolean waitForPendingWebviewTasksToFinish) {
369368
if(waitForPendingWebviewTasksToFinish) {
370-
waitForFutureToFinish(lastWebViewTask);
369+
processOSMessagesUntil(lastWebViewTask::isDone, browser.getDisplay());
371370
}
372-
return webViewWrapperFuture.join().webView;
371+
return webViewWrapperFuture.join();
372+
}
373+
374+
private WebViewWrapper getWebViewWrapper() {
375+
processOSMessagesUntil(webViewWrapperFuture::isDone, browser.getDisplay());
376+
return webViewWrapperFuture.join();
377+
}
378+
379+
ICoreWebView2 getWebView(boolean waitForPendingWebviewTasksToFinish) {
380+
return getWebViewWrapper(waitForPendingWebviewTasksToFinish).webView;
373381
}
374382

375383
ICoreWebView2_2 getWebView_2(boolean waitForPendingWebviewTasksToFinish) {
376-
if(waitForPendingWebviewTasksToFinish) {
377-
waitForFutureToFinish(lastWebViewTask);
378-
}
379-
return webViewWrapperFuture.join().webView_2;
384+
return getWebViewWrapper(waitForPendingWebviewTasksToFinish).webView_2;
380385
}
381386

382387
boolean isWebView_2Available() {
383-
waitForFutureToFinish(webViewWrapperFuture);
384-
return webViewWrapperFuture.join().webView_2 != null;
388+
return getWebViewWrapper().webView_2 != null;
385389
}
386390

387391
ICoreWebView2_10 getWebView_10(boolean waitForPendingWebviewTasksToFinish) {
388-
if(waitForPendingWebviewTasksToFinish) {
389-
waitForFutureToFinish(lastWebViewTask);
390-
}
391-
return webViewWrapperFuture.join().webView_10;
392+
return getWebViewWrapper(waitForPendingWebviewTasksToFinish).webView_10;
392393
}
393394

394395
boolean isWebView_10Available() {
395-
waitForFutureToFinish(webViewWrapperFuture);
396-
return webViewWrapperFuture.join().webView_10 != null;
396+
return getWebViewWrapper().webView_10 != null;
397397
}
398398

399399
ICoreWebView2_11 getWebView_11(boolean waitForPendingWebviewTasksToFinish) {
400-
if(waitForPendingWebviewTasksToFinish) {
401-
waitForFutureToFinish(lastWebViewTask);
402-
}
403-
return webViewWrapperFuture.join().webView_11;
400+
return getWebViewWrapper(waitForPendingWebviewTasksToFinish).webView_11;
404401
}
405402

406403
boolean isWebView_11Available() {
407-
waitForFutureToFinish(webViewWrapperFuture);
408-
return webViewWrapperFuture.join().webView_11 != null;
404+
return getWebViewWrapper().webView_11 != null;
409405
}
410406

411407
ICoreWebView2_12 getWebView_12(boolean waitForPendingWebviewTasksToFinish) {
412-
if(waitForPendingWebviewTasksToFinish) {
413-
waitForFutureToFinish(lastWebViewTask);
414-
}
415-
return webViewWrapperFuture.join().webView_12;
408+
return getWebViewWrapper(waitForPendingWebviewTasksToFinish).webView_12;
416409
}
417410

418411
boolean isWebView_12Available() {
419-
waitForFutureToFinish(webViewWrapperFuture);
420-
return webViewWrapperFuture.join().webView_12 != null;
412+
return getWebViewWrapper().webView_12 != null;
421413
}
422414

423415
ICoreWebView2_13 getWebView_13(boolean waitForPendingWebviewTasksToFinish) {
424-
if(waitForPendingWebviewTasksToFinish) {
425-
waitForFutureToFinish(lastWebViewTask);
426-
}
427-
return webViewWrapperFuture.join().webView_13;
416+
return getWebViewWrapper(waitForPendingWebviewTasksToFinish).webView_13;
428417
}
429418

430419
boolean isWebView_13Available() {
431-
waitForFutureToFinish(webViewWrapperFuture);
432-
return webViewWrapperFuture.join().webView_13 != null;
420+
return getWebViewWrapper().webView_13 != null;
433421
}
434422

435423
/*
436424
* Schedule a given runnable in a queue to execute when the webView is free and
437425
* has finished all the pending tasks queued before it.
438426
*/
439427
void scheduleWebViewTask(Runnable action) {
440-
lastWebViewTask = lastWebViewTask.thenRun(() -> {
441-
action.run();
428+
lastWebViewTask = wakeDisplayAfterFuture(lastWebViewTask.thenRun(action::run));
429+
}
430+
431+
private <T> CompletableFuture<T> wakeDisplayAfterFuture(CompletableFuture<T> future) {
432+
return future.handle((nil1, nil2) -> {
433+
Display display = browser.getDisplay();
434+
if (!display.isDisposed()) {
435+
try {
436+
display.wake();
437+
} catch (SWTException e) {
438+
// ignore then, this can happen due to the async nature between our check for
439+
// disposed and the actual call to wake the display can be disposed
440+
}
441+
}
442+
return null;
442443
});
443444
}
444-
445-
private <T> void waitForFutureToFinish(CompletableFuture<T> future) {
446-
while(!future.isDone()) {
447-
processNextOSMessage();
448-
}
449-
}
450-
451445
}
452446

453447
/**
454-
* Processes a single OS message using {@link Display#readAndDispatch()}. This
448+
* Processes single OS messages using {@link Display#readAndDispatch()}. This
455449
* is required for processing the OS events during browser initialization, since
456-
* Edge browser initialization happens asynchronously.
450+
* Edge browser initialization happens asynchronously. Messages are processed
451+
* until the given condition is fulfilled or a timeout occurs.
457452
* <p>
458453
* {@link Display#readAndDispatch()} also processes events scheduled for
459454
* asynchronous execution via {@link Display#asyncExec(Runnable)}. This may
@@ -462,13 +457,21 @@ private <T> void waitForFutureToFinish(CompletableFuture<T> future) {
462457
* events for initialization. Thus, this method does not implement an ordinary
463458
* readAndDispatch loop, but waits for an OS event to be processed.
464459
*/
465-
private static void processNextOSMessage() {
466-
Display display = Display.getCurrent();
460+
private static void processOSMessagesUntil(Supplier<Boolean> condition, Display display) {
467461
MSG msg = new MSG();
468-
while (!OS.PeekMessage (msg, 0, 0, 0, OS.PM_NOREMOVE)) {
469-
display.sleep();
462+
AtomicBoolean timeoutOccurred = new AtomicBoolean();
463+
// The timer call also wakes up the display to avoid being stuck in display.sleep()
464+
display.timerExec((int) MAXIMUM_OPERATION_TIME.toMillis(), () -> timeoutOccurred.set(true));
465+
while (!display.isDisposed() && !condition.get() && !timeoutOccurred.get()) {
466+
if (OS.PeekMessage(msg, 0, 0, 0, OS.PM_NOREMOVE)) {
467+
display.readAndDispatch();
468+
} else {
469+
display.sleep();
470+
}
471+
}
472+
if (!condition.get()) {
473+
SWT.error(SWT.ERROR_UNSPECIFIED, null, " Waiting for Edge operation to terminate timed out");
470474
}
471-
display.readAndDispatch();
472475
}
473476

474477
static ICoreWebView2CookieManager getCookieManager() {

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

+8-4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.nio.file.Files;
4040
import java.nio.file.Path;
4141
import java.nio.file.Paths;
42+
import java.time.Duration;
4243
import java.time.Instant;
4344
import java.util.ArrayList;
4445
import java.util.Collection;
@@ -101,12 +102,16 @@
101102
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
102103
public class Test_org_eclipse_swt_browser_Browser extends Test_org_eclipse_swt_widgets_Composite {
103104

105+
// TODO Reduce to reasonable value
106+
private static Duration MAXIMUM_BROWSER_CREATION_TIME = Duration.ofSeconds(90);
107+
104108
static {
105109
try {
106110
printSystemEnv();
107111
} catch (Exception e) {
108112
e.printStackTrace();
109113
}
114+
System.setProperty("org.eclipse.swt.internal.win32.Edge.timeout", Long.toString(MAXIMUM_BROWSER_CREATION_TIME.toMillis()));
110115
}
111116

112117
// CONFIG
@@ -296,14 +301,13 @@ private int reportOpenedDescriptors() {
296301
}
297302

298303
private Browser createBrowser(Shell s, int flags) {
299-
long maximumBrowserCreationMilliseconds = 90_000;
300-
long createStartTime = System.currentTimeMillis();
304+
Instant createStartTime = Instant.now();
301305
Browser b = new Browser(s, flags);
302306
// Wait for asynchronous initialization via getting URL
303307
b.getUrl();
304308
createdBroswers.add(b);
305-
long createDuration = System.currentTimeMillis() - createStartTime;
306-
assertTrue("creating browser took too long: " + createDuration + "ms", createDuration < maximumBrowserCreationMilliseconds);
309+
Duration createDuration = Duration.between(createStartTime, Instant.now());
310+
assertTrue("creating browser took too long: " + createDuration.toMillis() + "ms", createDuration.minus(MAXIMUM_BROWSER_CREATION_TIME).isNegative());
307311
return b;
308312
}
309313

0 commit comments

Comments
 (0)