diff --git a/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters b/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters index 75d51ab81da..b41fbd2ee7c 100644 --- a/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters @@ -1,32 +1,38 @@ - - + + - - + + + + + + + + - + - + - + + - + - - + - + diff --git a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/BusyIndicator.java b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/BusyIndicator.java index eb143853535..254777969c3 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/BusyIndicator.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/BusyIndicator.java @@ -17,6 +17,7 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; +import java.util.function.*; import org.eclipse.swt.*; import org.eclipse.swt.graphics.*; @@ -33,6 +34,14 @@ public class BusyIndicator { private static final AtomicInteger nextBusyId = new AtomicInteger(); static final String BUSYID_NAME = "SWT BusyIndicator"; //$NON-NLS-1$ static final String BUSY_CURSOR = "SWT BusyIndicator Cursor"; //$NON-NLS-1$ + /** + * @noreference This field is not intended to be referenced by clients. + */ + public static Runnable onWake; + /** + * @noreference This field is not intended to be referenced by clients. + */ + public static Consumer onWakeError; /** * Runs the given Runnable while providing @@ -111,8 +120,14 @@ public static void showWhile(Future future) { stage.handle((nil1, nil2) -> { if (!display.isDisposed()) { try { + if (onWake!=null) { + onWake.run(); + } display.wake(); } catch (SWTException e) { + if (onWakeError!=null) { + onWakeError.accept(e); + } // ignore then, this can happen due to the async nature between our check for // disposed and the actual call to wake the display can be disposed } diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_BusyIndicator.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_BusyIndicator.java index 2add7920c7d..0f93912fe43 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_BusyIndicator.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_BusyIndicator.java @@ -17,12 +17,18 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.BusyIndicator; @@ -34,63 +40,263 @@ public class Test_org_eclipse_swt_custom_BusyIndicator { + private static final long LOOP_TIMEOUT_SECONDS = 5; + private static final long WATCHDOG_TIMEOUT_SECONDS = 25; + + /** + * Lightweight tracker that uses the public BusyIndicator hooks to track + * wake() calls without using reflection, to avoid influencing JVM timing. + */ + private static class WakeTracker { + final CopyOnWriteArrayList eventLog = new CopyOnWriteArrayList<>(); + final AtomicInteger wakeCallCount = new AtomicInteger(0); + final AtomicInteger wakeErrorCount = new AtomicInteger(0); + final AtomicInteger asyncExecScheduledCount = new AtomicInteger(0); + final AtomicInteger asyncExecRunCount = new AtomicInteger(0); + + WakeTracker() { + BusyIndicator.onWake = () -> { + wakeCallCount.incrementAndGet(); + logEvent("BusyIndicator triggered display.wake()"); + }; + BusyIndicator.onWakeError = e -> { + wakeErrorCount.incrementAndGet(); + logEvent("BusyIndicator wake error: " + e); + }; + logEvent("WakeTracker initialized"); + } + + void logEvent(String event) { + long ts = System.currentTimeMillis() % 100000; + String threadName = Thread.currentThread().getName(); + eventLog.add(String.format("[%05d] [%-20s] %s", ts, threadName, event)); + } + + void recordAsyncExecScheduled(String description) { + asyncExecScheduledCount.incrementAndGet(); + logEvent("asyncExec SCHEDULED: " + description); + } + + void recordAsyncExecRun(String description) { + asyncExecRunCount.incrementAndGet(); + logEvent("asyncExec RUNNING: " + description); + } + + String getSummary() { + StringBuilder sb = new StringBuilder(); + sb.append("=== WAKE TRACKER STATE ===\n"); + sb.append(" BusyIndicator.onWake call count: ").append(wakeCallCount.get()).append("\n"); + sb.append(" BusyIndicator.onWakeError count: ").append(wakeErrorCount.get()).append("\n"); + sb.append(" asyncExec scheduled count: ").append(asyncExecScheduledCount.get()).append("\n"); + sb.append(" asyncExec actually ran count: ").append(asyncExecRunCount.get()).append("\n"); + sb.append("=== END WAKE TRACKER STATE ==="); + return sb.toString(); + } + + void printEventLog() { + System.err.println("=== EVENT LOG (" + eventLog.size() + " entries) ==="); + for (String event : eventLog) { + System.err.println(event); + } + System.err.println("=== END EVENT LOG ==="); + } + + void cleanup() { + BusyIndicator.onWake = null; + BusyIndicator.onWakeError = null; + } + } + + private static class TestState { + final Thread testThread = Thread.currentThread(); + final AtomicBoolean latchWaitEntered = new AtomicBoolean(false); + final AtomicBoolean latchNestedWaitEntered = new AtomicBoolean(false); + final AtomicBoolean showWhileStarted = new AtomicBoolean(false); + final AtomicBoolean showWhileCompleted = new AtomicBoolean(false); + final AtomicBoolean eventLoopDraining = new AtomicBoolean(false); + final AtomicBoolean futureCompleted = new AtomicBoolean(false); + final AtomicBoolean futureNestedCompleted = new AtomicBoolean(false); + volatile String currentStage = "initialization"; + volatile WakeTracker wakeTracker = null; + + // Track asyncExec callback execution + final AtomicBoolean asyncExec1Ran = new AtomicBoolean(false); + final AtomicBoolean asyncExec2Ran = new AtomicBoolean(false); + final AtomicBoolean asyncExec3Ran = new AtomicBoolean(false); + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("TestState[\n"); + sb.append(" stage=").append(currentStage).append("\n"); + sb.append(" testThread=").append(testThread.getName()).append("\n"); + sb.append(" latchWaitEntered=").append(latchWaitEntered.get()).append("\n"); + sb.append(" latchNestedWaitEntered=").append(latchNestedWaitEntered.get()).append("\n"); + sb.append(" showWhileStarted=").append(showWhileStarted.get()).append("\n"); + sb.append(" showWhileCompleted=").append(showWhileCompleted.get()).append("\n"); + sb.append(" eventLoopDraining=").append(eventLoopDraining.get()).append("\n"); + sb.append(" futureCompleted=").append(futureCompleted.get()).append("\n"); + sb.append(" futureNestedCompleted=").append(futureNestedCompleted.get()).append("\n"); + sb.append(" asyncExec1Ran=").append(asyncExec1Ran.get()).append("\n"); + sb.append(" asyncExec2Ran=").append(asyncExec2Ran.get()).append("\n"); + sb.append(" asyncExec3Ran=").append(asyncExec3Ran.get()).append("\n"); + sb.append("]"); + return sb.toString(); + } + } + + private static ScheduledFuture startWatchdog(ScheduledExecutorService watchdogExecutor, TestState state) { + return watchdogExecutor.schedule(() -> { + System.err.println("=== WATCHDOG FIRED ==="); + System.err.println("Test state: " + state); + System.err.println(); + + // Print wake tracker state + if (state.wakeTracker != null) { + System.err.println(state.wakeTracker.getSummary()); + System.err.println(); + state.wakeTracker.printEventLog(); + System.err.println(); + } + + System.err.println("=== THREAD DUMP ==="); + Map allStackTraces = Thread.getAllStackTraces(); + for (Map.Entry entry : allStackTraces.entrySet()) { + Thread thread = entry.getKey(); + StackTraceElement[] stackTrace = entry.getValue(); + System.err.println("Thread: " + thread.getName() + " (state=" + thread.getState() + ", id=" + + thread.threadId() + ")"); + for (StackTraceElement element : stackTrace) { + System.err.println(" at " + element); + } + System.err.println(); + } + System.err.println("=== END THREAD DUMP ==="); +// Runtime.getRuntime().exit(-1000); + }, WATCHDOG_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + private static void drainEventQueue(Display display, TestState state) { + state.eventLoopDraining.set(true); + if (state.wakeTracker != null) { + state.wakeTracker.logEvent("drainEventQueue() started"); + } + long startTime = System.nanoTime(); + long timeoutNanos = TimeUnit.SECONDS.toNanos(LOOP_TIMEOUT_SECONDS); + while (!display.isDisposed() && display.readAndDispatch()) { + if (System.nanoTime() - startTime > timeoutNanos) { + throw new IllegalStateException( + "Event queue not empty after " + LOOP_TIMEOUT_SECONDS + " seconds. State: " + state); + } + } + if (state.wakeTracker != null) { + state.wakeTracker.logEvent("drainEventQueue() completed"); + } + state.eventLoopDraining.set(false); + } + @Test @Timeout(value = 30) public void testShowWhile() { - // Executors.newSingleThreadExecutor() hangs on some Linux configurations - try (ExecutorService executor = Executors.newFixedThreadPool(2)){ - Shell shell = new Shell(); - Display display = shell.getDisplay(); - Cursor busyCursor = display.getSystemCursor(SWT.CURSOR_WAIT); - CountDownLatch latch = new CountDownLatch(1); - CompletableFuture future = CompletableFuture.runAsync(() -> { - try { - latch.await(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - } - }, executor); + TestState state = new TestState(); + try (ExecutorService executor = Executors.newFixedThreadPool(2); + ScheduledExecutorService watchdogExecutor = Executors.newSingleThreadScheduledExecutor()) { + ScheduledFuture watchdog = startWatchdog(watchdogExecutor, state); + WakeTracker tracker = new WakeTracker(); + state.wakeTracker = tracker; + try { + state.currentStage = "creating shell and display"; + Shell shell = new Shell(); + Display display = shell.getDisplay(); - CountDownLatch latchNested = new CountDownLatch(1); - CompletableFuture futureNested = CompletableFuture.runAsync(() -> { - try { - latchNested.await(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - } - }, executor); - - assertNotEquals(busyCursor, shell.getCursor()); - - // This it proves that events on the display are executed - display.asyncExec(() -> { - // This will happen during the showWhile(future) from below. - BusyIndicator.showWhile(futureNested); - }); - - Cursor[] cursorInAsync = new Cursor[2]; - - // this serves two purpose: - // 1) it proves that events on the display are executed - // 2) it checks that the shell has the busy cursor during the nest showWhile. - display.asyncExec(() -> { - cursorInAsync[0] = shell.getCursor(); - latchNested.countDown(); - }); - - // this serves two purpose: - // 1) it proves that events on the display are executed - // 2) it checks that the shell has the busy cursor even after the termination of - // the nested showWhile. - display.asyncExec(() -> { - cursorInAsync[1] = shell.getCursor(); - latch.countDown(); - }); - - BusyIndicator.showWhile(future); - assertTrue(future.isDone()); - assertEquals(busyCursor, cursorInAsync[0]); - assertEquals(busyCursor, cursorInAsync[1]); - shell.dispose(); - while (!display.isDisposed() && display.readAndDispatch()) { + Cursor busyCursor = display.getSystemCursor(SWT.CURSOR_WAIT); + CountDownLatch latch = new CountDownLatch(1); + + state.currentStage = "creating main future"; + tracker.logEvent("Creating main future"); + CompletableFuture future = CompletableFuture.runAsync(() -> { + tracker.logEvent("Main future task STARTED, entering latch.await()"); + state.latchWaitEntered.set(true); + try { + boolean completed = latch.await(10, TimeUnit.SECONDS); + tracker.logEvent("Main future latch.await() returned: " + completed); + } catch (InterruptedException e) { + tracker.logEvent("Main future latch.await() INTERRUPTED"); + } + state.futureCompleted.set(true); + tracker.logEvent("Main future task COMPLETED"); + }, executor); + + CountDownLatch latchNested = new CountDownLatch(1); + state.currentStage = "creating nested future"; + tracker.logEvent("Creating nested future"); + CompletableFuture futureNested = CompletableFuture.runAsync(() -> { + tracker.logEvent("Nested future task STARTED, entering latch.await()"); + state.latchNestedWaitEntered.set(true); + try { + boolean completed = latchNested.await(10, TimeUnit.SECONDS); + tracker.logEvent("Nested future latch.await() returned: " + completed); + } catch (InterruptedException e) { + tracker.logEvent("Nested future latch.await() INTERRUPTED"); + } + state.futureNestedCompleted.set(true); + tracker.logEvent("Nested future task COMPLETED"); + }, executor); + + assertNotEquals(busyCursor, shell.getCursor()); + + // asyncExec #1: This will call nested showWhile + tracker.recordAsyncExecScheduled("#1: BusyIndicator.showWhile(futureNested)"); + display.asyncExec(() -> { + tracker.recordAsyncExecRun("#1: BusyIndicator.showWhile(futureNested)"); + state.asyncExec1Ran.set(true); + BusyIndicator.showWhile(futureNested); + tracker.logEvent("asyncExec #1: showWhile(futureNested) returned"); + }); + + Cursor[] cursorInAsync = new Cursor[2]; + + // asyncExec #2: Check cursor and release nested latch + tracker.recordAsyncExecScheduled("#2: check cursor + latchNested.countDown()"); + display.asyncExec(() -> { + tracker.recordAsyncExecRun("#2: check cursor + latchNested.countDown()"); + state.asyncExec2Ran.set(true); + cursorInAsync[0] = shell.getCursor(); + tracker.logEvent("asyncExec #2: cursor=" + cursorInAsync[0] + ", calling latchNested.countDown()"); + latchNested.countDown(); + }); + + // asyncExec #3: Check cursor and release main latch + tracker.recordAsyncExecScheduled("#3: check cursor + latch.countDown()"); + display.asyncExec(() -> { + tracker.recordAsyncExecRun("#3: check cursor + latch.countDown()"); + state.asyncExec3Ran.set(true); + cursorInAsync[1] = shell.getCursor(); + tracker.logEvent("asyncExec #3: cursor=" + cursorInAsync[1] + ", calling latch.countDown()"); + latch.countDown(); + }); + + state.currentStage = "calling showWhile"; + state.showWhileStarted.set(true); + tracker.logEvent("About to call BusyIndicator.showWhile(future)"); + + BusyIndicator.showWhile(future); + + tracker.logEvent("BusyIndicator.showWhile(future) returned"); + state.showWhileCompleted.set(true); + assertTrue(future.isDone()); + assertEquals(busyCursor, cursorInAsync[0]); + assertEquals(busyCursor, cursorInAsync[1]); + shell.dispose(); + state.currentStage = "draining event queue"; + drainEventQueue(display, state); + state.currentStage = "completed"; + tracker.logEvent("Test completed successfully"); + } finally { + watchdog.cancel(false); + tracker.printEventLog(); + tracker.cleanup(); } } } @@ -98,38 +304,76 @@ public void testShowWhile() { @Test @Timeout(value = 30) public void testShowWhileWithFuture() { - try (ExecutorService executor = Executors.newSingleThreadExecutor()){ - Shell shell = new Shell(); - Display display = shell.getDisplay(); - Cursor busyCursor = display.getSystemCursor(SWT.CURSOR_WAIT); - Cursor[] cursorInAsync = new Cursor[1]; - CountDownLatch latch = new CountDownLatch(1); - Future future = executor.submit(() -> { - try { - latch.await(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - } - }); - // this serves two purpose: - // 1) it proves that events on the display are executed - // 2) it checks that the shell has the busy cursor during the nest showWhile. - display.asyncExec(() -> { - cursorInAsync[0] = shell.getCursor(); - latch.countDown(); - }); - //External trigger for minimal latency as advised in the javadoc - executor.submit(()->{ - try { - future.get(); - } catch (Exception e) { - } - display.wake(); - }); - BusyIndicator.showWhile(future); - assertTrue(future.isDone()); - assertEquals(busyCursor, cursorInAsync[0]); - shell.dispose(); - while (!display.isDisposed() && display.readAndDispatch()) { + TestState state = new TestState(); + try (ExecutorService executor = Executors.newSingleThreadExecutor(); + ScheduledExecutorService watchdogExecutor = Executors.newSingleThreadScheduledExecutor()) { + ScheduledFuture watchdog = startWatchdog(watchdogExecutor, state); + WakeTracker tracker = new WakeTracker(); + state.wakeTracker = tracker; + try { + state.currentStage = "creating shell and display"; + Shell shell = new Shell(); + Display display = shell.getDisplay(); + + Cursor busyCursor = display.getSystemCursor(SWT.CURSOR_WAIT); + Cursor[] cursorInAsync = new Cursor[1]; + CountDownLatch latch = new CountDownLatch(1); + + state.currentStage = "creating future"; + tracker.logEvent("Creating future"); + Future future = executor.submit(() -> { + tracker.logEvent("Future task STARTED, entering latch.await()"); + state.latchWaitEntered.set(true); + try { + boolean completed = latch.await(10, TimeUnit.SECONDS); + tracker.logEvent("Future latch.await() returned: " + completed); + } catch (InterruptedException e) { + tracker.logEvent("Future latch.await() INTERRUPTED"); + } + state.futureCompleted.set(true); + tracker.logEvent("Future task COMPLETED"); + }); + + // asyncExec: Check cursor and release latch + tracker.recordAsyncExecScheduled("#1: check cursor + latch.countDown()"); + display.asyncExec(() -> { + tracker.recordAsyncExecRun("#1: check cursor + latch.countDown()"); + state.asyncExec1Ran.set(true); + cursorInAsync[0] = shell.getCursor(); + tracker.logEvent("asyncExec #1: cursor=" + cursorInAsync[0] + ", calling latch.countDown()"); + latch.countDown(); + }); + + // External trigger for minimal latency as advised in the javadoc + executor.submit(() -> { + tracker.logEvent("Wake trigger task: waiting for future.get()"); + try { + future.get(); + tracker.logEvent("Wake trigger task: future.get() returned, calling display.wake()"); + } catch (Exception e) { + tracker.logEvent("Wake trigger task: future.get() threw " + e.getClass().getSimpleName()); + } + display.wake(); + }); + + state.currentStage = "calling showWhile"; + state.showWhileStarted.set(true); + tracker.logEvent("About to call BusyIndicator.showWhile(future)"); + BusyIndicator.showWhile(future); + + tracker.logEvent("BusyIndicator.showWhile(future) returned"); + state.showWhileCompleted.set(true); + assertTrue(future.isDone()); + assertEquals(busyCursor, cursorInAsync[0]); + shell.dispose(); + state.currentStage = "draining event queue"; + drainEventQueue(display, state); + state.currentStage = "completed"; + tracker.logEvent("Test completed successfully"); + } finally { + watchdog.cancel(false); + tracker.printEventLog(); + tracker.cleanup(); } } }