GT-2875 - Unswingable - Code to allow task launching to give-up on the

Swing thread to prevent deadlocks
This commit is contained in:
dragonmacher 2019-05-16 15:42:24 -04:00
parent 88cf9c0f80
commit 8dffd377fb
10 changed files with 776 additions and 253 deletions

View file

@ -0,0 +1,43 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.task;
import ghidra.util.TaskUtilities;
/**
* Helper class to launch the given task in a background thread This helper will not
* show a task dialog. See {@link TaskLauncher}.
*/
class BackgroundThreadTaskLauncher {
private Task task;
BackgroundThreadTaskLauncher(Task task) {
this.task = task;
}
void run(TaskMonitor monitor) {
// add the task here, so we can track it before it is actually started by the thread
TaskUtilities.addTrackedTask(task, monitor);
String name = "Task - " + task.getTaskTitle();
Thread taskThread = new Thread(() -> {
task.monitoredRun(monitor);
}, name);
taskThread.setPriority(Thread.MIN_PRIORITY);
taskThread.start();
}
}

View file

@ -0,0 +1,32 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.task;
/**
* Helper class to launch the given task in the current thread. See {@link TaskLauncher}.
*/
class CurrentThreadTaskLauncher {
private Task task;
CurrentThreadTaskLauncher(Task task) {
this.task = task;
}
void run() {
task.monitoredRun(TaskMonitor.DUMMY);
}
}

View file

@ -0,0 +1,128 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.task;
import java.awt.Component;
import javax.swing.SwingUtilities;
import generic.util.WindowUtilities;
import ghidra.util.*;
/**
* Helper class to launch the given task in a background thread, showing a task dialog if
* this task takes to long. See {@link TaskLauncher}.
*/
class SwingTaskLauncher {
protected Task task;
private Component parent;
private int delay;
private int dialogWidth;
private TaskDialog taskDialog;
private Thread taskThread;
private CancelledListener monitorChangeListener = () -> {
if (task.isInterruptible()) {
taskThread.interrupt();
}
if (task.isForgettable()) {
taskDialog.close(); // close the dialog and forget about the task
}
};
SwingTaskLauncher(Task task, Component parent, int delay, int dialogWidth) {
this.task = task;
this.parent = parent;
this.delay = delay;
this.dialogWidth = dialogWidth;
}
void run() {
this.taskDialog = buildTaskDialog(parent);
startBackgroundThread(taskDialog);
taskDialog.show(Math.max(delay, 0));
waitIfNotSwing();
}
private void waitIfNotSwing() {
if (SwingUtilities.isEventDispatchThread() || !task.isModal()) {
return;
}
try {
taskThread.join();
}
catch (InterruptedException e) {
Msg.debug(this, "Task Launcher unexpectedly interrupted waiting for task thread", e);
}
}
protected TaskDialog buildTaskDialog(Component comp) {
//
// This class may be used by background threads. Make sure that our GUI creation is
// on the Swing thread to prevent exceptions while painting (as seen when using the
// Nimbus Look and Feel).
//
SystemUtilities.runSwingNow(() -> {
taskDialog = createTaskDialog(comp);
taskDialog.setMinimumSize(dialogWidth, 0);
});
if (task.isInterruptible() || task.isForgettable()) {
taskDialog.addCancelledListener(monitorChangeListener);
}
taskDialog.setStatusJustification(task.getStatusTextAlignment());
return taskDialog;
}
private void startBackgroundThread(TaskMonitor monitor) {
// add the task here, so we can track it before it is actually started by the thread
TaskUtilities.addTrackedTask(task, monitor);
String name = "Task - " + task.getTaskTitle();
taskThread = new Thread(() -> {
task.monitoredRun(monitor);
taskProcessed();
}, name);
taskThread.setPriority(Thread.MIN_PRIORITY);
taskThread.start();
}
private void taskProcessed() {
if (taskDialog != null) {
taskDialog.taskProcessed();
}
}
protected TaskDialog createTaskDialog(Component comp) {
Component currentParent = comp;
if (currentParent != null) {
currentParent = WindowUtilities.windowForComponent(comp);
}
if (currentParent == null) {
return new TaskDialog(task);
}
return new TaskDialog(comp, task);
}
}

View file

@ -16,12 +16,12 @@
package ghidra.util.task;
import java.awt.Component;
import java.util.concurrent.TimeUnit;
import javax.swing.SwingUtilities;
import generic.util.WindowUtilities;
import ghidra.util.Msg;
import ghidra.util.TaskUtilities;
import ghidra.util.SystemUtilities;
import ghidra.util.exception.UnableToSwingException;
/**
* Class to initiate a Task in a new Thread, and to show a progress dialog that indicates
@ -168,18 +168,6 @@ public class TaskLauncher {
/** The time, for modal tasks, to try and run before blocking and showing a dialog */
public static final int INITIAL_MODAL_DELAY = 500;
protected Task task;
private TaskDialog taskDialog;
private Thread taskThread;
private CancelledListener monitorChangeListener = () -> {
if (task.isInterruptible()) {
taskThread.interrupt();
}
if (task.isForgettable()) {
taskDialog.close(); // close the dialog and forget about the task
}
};
private static Component getParent(Component parent) {
if (parent == null) {
return null;
@ -243,26 +231,11 @@ public class TaskLauncher {
*/
public TaskLauncher(Task task, Component parent, int delay, int dialogWidth) {
this.task = task;
this.taskDialog = buildTaskDialog(parent, dialogWidth);
startBackgroundThread(taskDialog);
taskDialog.show(Math.max(delay, 0));
waitForModalIfNotSwing();
}
private void waitForModalIfNotSwing() {
if (SwingUtilities.isEventDispatchThread() || !task.isModal()) {
return;
}
try {
taskThread.join();
runSwing(task, parent, delay, dialogWidth);
}
catch (InterruptedException e) {
Msg.debug(this, "Task Launcher unexpectedly interrupted waiting for task thread", e);
catch (UnableToSwingException e) {
runInThisBackgroundThread(task);
}
}
@ -274,59 +247,50 @@ public class TaskLauncher {
* <p>See <a href="#modal_usage">notes on modal usage</a>
*
* @param task task to run in another thread (other than the Swing Thread)
* @param taskMonitor the monitor to use while running the task.
* @param monitor the monitor to use while running the task.
*/
public TaskLauncher(Task task, TaskMonitor taskMonitor) {
this.task = task;
startBackgroundThread(taskMonitor);
public TaskLauncher(Task task, TaskMonitor monitor) {
BackgroundThreadTaskLauncher runner = new BackgroundThreadTaskLauncher(task);
runner.run(monitor);
}
private TaskDialog buildTaskDialog(Component comp, int dialogWidth) {
private void runSwing(Task task, Component parent, int delay, int dialogWidth)
throws UnableToSwingException {
taskDialog = createTaskDialog(comp);
taskDialog.setMinimumSize(dialogWidth, 0);
if (task.isInterruptible() || task.isForgettable()) {
taskDialog.addCancelledListener(monitorChangeListener);
SwingTaskLauncher swinger = buildSwingLauncher(task, parent, delay, dialogWidth);
if (SwingUtilities.isEventDispatchThread()) {
swinger.run();
return;
}
taskDialog.setStatusJustification(task.getStatusTextAlignment());
//
// Not on the Swing thread. Try to execute on the Swing thread, timing-out if it takes
// too long (this prevents deadlocks).
//
return taskDialog;
// This will throw an exception if we could not get the Swing lock. When that happens,
// the task was NOT run.
int timeout = getSwingTimeoutInSeconds();
SystemUtilities.runSwingNow(() -> {
swinger.run();
}, timeout, TimeUnit.SECONDS);
}
private void startBackgroundThread(TaskMonitor monitor) {
// add the task here, so we can track it before it is actually started by the thread
TaskUtilities.addTrackedTask(task, monitor);
String name = "Task - " + task.getTaskTitle();
taskThread = new Thread(() -> {
task.monitoredRun(monitor);
taskProcessed();
}, name);
taskThread.setPriority(Thread.MIN_PRIORITY);
taskThread.start();
protected int getSwingTimeoutInSeconds() {
return 2;
}
private void taskProcessed() {
if (taskDialog != null) {
taskDialog.taskProcessed();
}
protected SwingTaskLauncher buildSwingLauncher(Task task, Component parent, int delay,
int dialogWidth) {
return new SwingTaskLauncher(task, parent, delay, dialogWidth);
}
protected TaskDialog createTaskDialog(Component comp) {
Component parent = comp;
if (parent != null) {
parent = WindowUtilities.windowForComponent(comp);
protected void runInThisBackgroundThread(Task task) {
if (SwingUtilities.isEventDispatchThread()) {
throw new IllegalStateException("Must not call this method from the Swing thread");
}
if (parent == null) {
return new TaskDialog(task);
}
return new TaskDialog(comp, task);
CurrentThreadTaskLauncher runner = new CurrentThreadTaskLauncher(task);
runner.run();
}
}

View file

@ -0,0 +1,207 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.task;
import static org.junit.Assert.*;
import java.awt.Component;
import java.util.Deque;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import docking.test.AbstractDockingTest;
public class AbstractTaskTest extends AbstractDockingTest {
protected static final int DELAY_FAST = 10;
protected static final int DELAY_SLOW = 100;
protected static final int DELAY_LAUNCHER = DELAY_FAST * 2;
// 2 - 1 for the task itself; 1 for the launcher
protected CountDownLatch threadsFinished = new CountDownLatch(2);
protected Deque<TDEvent> eventQueue = new LinkedBlockingDeque<>();
protected volatile TaskLauncherSpy taskLauncherSpy;
protected volatile TaskDialogSpy dialogSpy;
protected AtomicBoolean didRunInBackground = new AtomicBoolean();
protected void assertDidNotRunInSwing() {
for (TDEvent e : eventQueue) {
assertFalse(e.getThreadName().contains("AWT"));
}
}
protected void assertRanInSwingThread() {
assertFalse("Task was not run in the Swing thread", didRunInBackground.get());
}
protected void assertSwingThreadBlockedForTask() {
TDEvent lastEvent = eventQueue.peekLast();
boolean swingIsLast = lastEvent.getThreadName().contains("AWT");
if (!swingIsLast) {
fail("The Swing thread did not block until the task finished");
}
}
protected void assertSwingThreadFinishedBeforeTask() {
TDEvent lastEvent = eventQueue.peekLast();
boolean swingIsLast = lastEvent.getThreadName().contains("AWT");
if (swingIsLast) {
fail("The Swing thread blocked until the task finished");
}
}
protected void waitForTask() throws Exception {
threadsFinished.await(2, TimeUnit.SECONDS);
}
protected void launchTask(Task task) {
launchTaskFromSwing(task);
}
protected void launchTaskFromSwing(Task task) {
runSwing(() -> {
taskLauncherSpy = new TaskLauncherSpy(task);
postEvent("After task launcher");
threadsFinished.countDown();
});
}
protected void postEvent(String message) {
eventQueue.add(new TDEvent(message));
}
protected class TaskLauncherSpy extends TaskLauncher {
public TaskLauncherSpy(Task task) {
super(task, null, DELAY_LAUNCHER);
}
@Override
protected SwingTaskLauncher buildSwingLauncher(Task task, Component parent, int delay,
int dialogWidth) {
return new SwingTaskLauncher(task, parent, delay, dialogWidth) {
@Override
protected TaskDialog buildTaskDialog(Component comp) {
dialogSpy = new TaskDialogSpy(task);
return dialogSpy;
}
};
}
@Override
protected int getSwingTimeoutInSeconds() {
return 1; // speed-up for tests
}
@Override
protected void runInThisBackgroundThread(Task task) {
didRunInBackground.set(true);
super.runInThisBackgroundThread(task);
}
TaskDialogSpy getDialogSpy() {
return dialogSpy;
}
boolean didRunInBackground() {
return didRunInBackground.get();
}
}
protected class FastModalTask extends Task {
public FastModalTask() {
super("Fast Modal Task", true, true, true);
}
@Override
public void run(TaskMonitor monitor) {
postEvent(getName() + " started...");
sleep(DELAY_FAST);
threadsFinished.countDown();
postEvent(getName() + " finished.");
}
}
protected class FastNonModalTask extends Task {
public FastNonModalTask() {
super("Fast Non-modal Task", true, true, false);
}
@Override
public void run(TaskMonitor monitor) {
postEvent(getName() + " started...");
sleep(DELAY_FAST);
postEvent(getName() + " finished.");
threadsFinished.countDown();
}
}
protected class SlowModalTask extends Task {
public SlowModalTask() {
super("Slow Modal Task", true, true, true);
}
@Override
public void run(TaskMonitor monitor) {
postEvent(getName() + " started...");
sleep(DELAY_SLOW);
threadsFinished.countDown();
postEvent(getName() + " finished.");
}
}
protected class SlowNonModalTask extends Task {
public SlowNonModalTask() {
super("Slow Non-modal Task", true, true, false);
}
@Override
public void run(TaskMonitor monitor) {
postEvent(getName() + " started...");
sleep(DELAY_SLOW);
threadsFinished.countDown();
postEvent(getName() + " finished.");
}
}
protected class TDEvent {
protected String threadName = Thread.currentThread().getName();
protected String message;
TDEvent(String message) {
this.message = message;
// Msg.out(message + " from " + threadName);
}
String getThreadName() {
return threadName;
}
@Override
public String toString() {
return message + " - thread [" + threadName + ']';
}
}
}

View file

@ -0,0 +1,36 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.task;
import java.util.concurrent.atomic.AtomicBoolean;
public class TaskDialogSpy extends TaskDialog {
private AtomicBoolean shown = new AtomicBoolean();
public TaskDialogSpy(Task task) {
super(task);
}
@Override
protected void doShow() {
shown.set(true);
super.doShow();
}
boolean wasShown() {
return shown.get();
}
}

View file

@ -17,26 +17,10 @@ package ghidra.util.task;
import static org.junit.Assert.*;
import java.awt.Component;
import java.util.Deque;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Test;
import docking.test.AbstractDockingTest;
public class TaskDialogTest extends AbstractDockingTest {
private static final int DELAY_FAST = 10;
private static final int DELAY_SLOW = 100;
private static final int DELAY_LAUNCHER = DELAY_FAST * 2;
private CountDownLatch threadsFinished = new CountDownLatch(2);
private Deque<TDEvent> eventQueue = new LinkedBlockingDeque<>();
public class TaskDialogTest extends AbstractTaskTest {
@After
public void tearDown() {
@ -48,7 +32,7 @@ public class TaskDialogTest extends AbstractDockingTest {
FastModalTask task = new FastModalTask();
TaskDialogSpy dialogSpy = launchTask(task);
launchTask(task);
waitForTask();
@ -60,7 +44,7 @@ public class TaskDialogTest extends AbstractDockingTest {
public void testModalDialog_SlowTask_Dialog() throws Exception {
SlowModalTask task = new SlowModalTask();
TaskDialogSpy dialogSpy = launchTask(task);
launchTask(task);
waitForTask();
@ -73,7 +57,7 @@ public class TaskDialogTest extends AbstractDockingTest {
FastNonModalTask task = new FastNonModalTask();
TaskDialogSpy dialogSpy = launchTask(task);
launchTask(task);
waitForTask();
@ -86,7 +70,7 @@ public class TaskDialogTest extends AbstractDockingTest {
SlowNonModalTask task = new SlowNonModalTask();
TaskDialogSpy dialogSpy = launchTask(task);
launchTask(task);
waitForTask();
@ -100,7 +84,7 @@ public class TaskDialogTest extends AbstractDockingTest {
@Test
public void testTaskCancel() throws Exception {
SlowModalTask task = new SlowModalTask();
TaskDialogSpy dialogSpy = launchTask(task);
launchTask(task);
dialogSpy.doShow();
@ -118,7 +102,7 @@ public class TaskDialogTest extends AbstractDockingTest {
@Test
public void testTaskNoCancel() throws Exception {
SlowModalTask task = new SlowModalTask();
TaskDialogSpy dialogSpy = launchTask(task);
launchTask(task);
dialogSpy.doShow();
dialogSpy.setCancelEnabled(false);
@ -128,159 +112,4 @@ public class TaskDialogTest extends AbstractDockingTest {
assertFalse(dialogSpy.isCancelEnabled());
}
private void assertSwingThreadBlockedForTask() {
TDEvent lastEvent = eventQueue.peekLast();
boolean swingIsLast = lastEvent.getThreadName().contains("AWT");
if (!swingIsLast) {
fail("The Swing thread did not block until the task finished");
}
}
private void assertSwingThreadFinishedBeforeTask() {
TDEvent lastEvent = eventQueue.peekLast();
boolean swingIsLast = lastEvent.getThreadName().contains("AWT");
if (swingIsLast) {
fail("The Swing thread blocked until the task finished");
}
}
private void waitForTask() throws Exception {
threadsFinished.await(2, TimeUnit.SECONDS);
}
private TaskDialogSpy launchTask(Task task) {
AtomicReference<TaskDialogSpy> ref = new AtomicReference<>();
runSwing(() -> {
TaskLauncherSpy launcherSpy = new TaskLauncherSpy(task);
postEvent("After task launcher");
TaskDialogSpy dialogSpy = launcherSpy.getDialogSpy();
ref.set(dialogSpy);
threadsFinished.countDown();
});
return ref.get();
}
private void postEvent(String message) {
eventQueue.add(new TDEvent(message));
}
private class TaskLauncherSpy extends TaskLauncher {
private TaskDialogSpy dialogSpy;
public TaskLauncherSpy(Task task) {
super(task, null, DELAY_LAUNCHER);
}
@Override
protected TaskDialog createTaskDialog(Component comp) {
dialogSpy = new TaskDialogSpy(task);
return dialogSpy;
}
TaskDialogSpy getDialogSpy() {
return dialogSpy;
}
}
private class TaskDialogSpy extends TaskDialog {
private AtomicBoolean shown = new AtomicBoolean();
public TaskDialogSpy(Task task) {
super(task);
}
@Override
protected void doShow() {
shown.set(true);
super.doShow();
}
boolean wasShown() {
return shown.get();
}
}
private class FastModalTask extends Task {
public FastModalTask() {
super("Fast Modal Task", true, true, true);
}
@Override
public void run(TaskMonitor monitor) {
postEvent(" started...");
sleep(DELAY_FAST);
threadsFinished.countDown();
postEvent(" finished.");
}
}
private class FastNonModalTask extends Task {
public FastNonModalTask() {
super("Fast Non-modal Task", true, true, false);
}
@Override
public void run(TaskMonitor monitor) {
postEvent(" started...");
sleep(DELAY_FAST);
postEvent(" finished.");
threadsFinished.countDown();
}
}
private class SlowModalTask extends Task {
public SlowModalTask() {
super("Slow Modal Task", true, true, true);
}
@Override
public void run(TaskMonitor monitor) {
postEvent(" started...");
sleep(DELAY_SLOW);
threadsFinished.countDown();
postEvent(" finished.");
}
}
private class SlowNonModalTask extends Task {
public SlowNonModalTask() {
super("Slow Non-modal Task", true, true, false);
}
@Override
public void run(TaskMonitor monitor) {
postEvent(" started...");
sleep(DELAY_SLOW);
threadsFinished.countDown();
postEvent(" finished.");
}
}
private class TDEvent {
private String threadName = Thread.currentThread().getName();
private String message;
TDEvent(String message) {
this.message = message;
// Msg.out(message + " from " + threadName);
}
String getThreadName() {
return threadName;
}
@Override
public String toString() {
return message + " - thread [" + threadName + ']';
}
}
}

View file

@ -0,0 +1,177 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.task;
import static org.junit.Assert.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.*;
public class TaskLauncherTest extends AbstractTaskTest {
private Thread swingThread;
@Before
public void setUp() {
runSwing(() -> swingThread = Thread.currentThread());
}
@After
public void tearDown() {
// release any blockers
swingThread.interrupt();
}
@Test
public void testLaunchFromSwing() throws Exception {
FastModalTask task = new FastModalTask();
launchTaskFromSwing(task);
waitForTask();
assertSwingThreadBlockedForTask();
}
@Test
public void testLaunchFromBackground() throws Exception {
FastModalTask task = new FastModalTask();
launchTaskFromBackground(task);
waitForTask();
assertRanInSwingThread();
}
@Test
public void testLaunchFromBackgroundWithBusySwing() throws Exception {
SwingBlocker blocker = new SwingBlocker();
runSwing(blocker, false);
blocker.waitForStart();
FastModalTask task = new FastModalTask();
launchTaskFromBackground(task);
waitForTask();
assertDidNotRunInSwing();
}
@Test
public void testLaunchFromInsideOfAnotherTaskThread() throws Exception {
SwingBlocker blocker = new SwingBlocker();
runSwing(blocker, false);
blocker.waitForStart();
// 4 - 2 per task
threadsFinished = new CountDownLatch(4);
launchTaskFromTask();
waitForTask();
assertDidNotRunInSwing();
}
@Test
public void testLaunchFromInsideOfAnotherTaskThreadWithBusySwingThread() throws Exception {
// 4 - 2 per task
threadsFinished = new CountDownLatch(4);
launchTaskFromTask();
waitForTask();
assertDidNotRunInSwing();
}
private int getWaitTimeoutInSeconds() {
return (int) TimeUnit.SECONDS.convert(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS) * 2;
}
protected void launchTaskFromBackground(Task task) throws InterruptedException {
CountDownLatch start = new CountDownLatch(1);
new Thread("Test Task Launcher Background Client") {
@Override
public void run() {
taskLauncherSpy = new TaskLauncherSpy(task);
start.countDown();
postEvent("After task launcher");
threadsFinished.countDown();
}
}.start();
assertTrue("Background thread did not start in " + getWaitTimeoutInSeconds() + " seconds",
start.await(getWaitTimeoutInSeconds(), TimeUnit.SECONDS));
}
protected void launchTaskFromTask() throws InterruptedException {
TaskLaunchingTask task = new TaskLaunchingTask();
CountDownLatch start = new CountDownLatch(1);
new Thread("Nested Test Task Launcher Background Client") {
@Override
public void run() {
taskLauncherSpy = new TaskLauncherSpy(task);
start.countDown();
postEvent("After task launcher");
threadsFinished.countDown();
}
}.start();
assertTrue("Background thread did not start in " + getWaitTimeoutInSeconds() + " seconds",
start.await(getWaitTimeoutInSeconds(), TimeUnit.SECONDS));
}
private class TaskLaunchingTask extends Task {
public TaskLaunchingTask() {
super("Slow Modal Task", true, true, true);
}
@Override
public void run(TaskMonitor monitor) {
postEvent(getName() + " started...");
sleep(DELAY_FAST);
try {
launchTaskFromBackground(new FastModalTask());
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
threadsFinished.countDown();
postEvent(getName() + " finished.");
}
}
private class SwingBlocker implements Runnable {
private static final long REALLY_LONG_SLEEP_THAT_DOESNT_FINISH_MS = 20000;
private CountDownLatch started = new CountDownLatch(1);
@Override
public void run() {
started.countDown();
sleep(REALLY_LONG_SLEEP_THAT_DOESNT_FINISH_MS);
}
void waitForStart() throws InterruptedException {
assertTrue("Swing blocker did not start",
started.await(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS));
}
}
}

View file

@ -22,12 +22,16 @@ import java.net.URL;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import javax.swing.SwingUtilities;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.UnableToSwingException;
import utilities.util.reflection.ReflectionUtilities;
/**
@ -257,7 +261,80 @@ public class SystemUtilities {
* @see #runSwingNow(Supplier) if you need to return a value from the Swing thread.
*/
public static void runSwingNow(Runnable r) {
runSwing(r, true, SWING_RUN_ERROR_MSG);
try {
// not sure what a reasonable wait is for a background thread; we can make this larger
// if we find that a really slow system UI causes this to fail
int maxWait = 10;
runSwingNow(r, maxWait, TimeUnit.SECONDS);
}
catch (UnableToSwingException e) {
throw new RuntimeException("Timed-out waiting to run a Swing task--potential deadlock!",
e);
}
}
/**
* Calls the given runnable on the Swing thread.
*
* <p>This method will throw an exception if the Swing thread is not available within the
* given timeout. This method is useful for preventing deadlocks.
*
* @param r the runnable
* @param timeout the timeout value
* @param unit the time unit of the timeout value
* @throws UnableToSwingException if the timeout was reach waiting for the Swing thead
* @see #runSwingNow(Supplier) if you need to return a value from the Swing thread.
*/
public static void runSwingNow(Runnable r, long timeout, TimeUnit unit)
throws UnableToSwingException {
if (isInHeadlessMode() || SystemUtilities.isEventDispatchThread()) {
doRunSwing(r, true, SWING_RUN_ERROR_MSG);
return;
}
CountDownLatch start = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(1);
AtomicBoolean timedOut = new AtomicBoolean();
doRunSwing(() -> {
start.countDown();
try {
if (timedOut.get()) {
// timed-out waiting for Swing lock, but eventually did get the lock; too late now
return;
}
r.run();
}
finally {
end.countDown();
}
}, false, SWING_RUN_ERROR_MSG);
try {
timedOut.set(!start.await(timeout, unit));
}
catch (InterruptedException e) {
// handled below
}
if (timedOut.get()) {
throw new UnableToSwingException(
"Timed-out waiting for Swing thread lock in " + timeout + " " + unit);
}
// we've started!
try {
end.await(); // wait FOREVER!
}
catch (InterruptedException e) {
// we sometimes interrupt our tasks intentionally, so don't report it
}
}
/**
@ -267,7 +344,7 @@ public class SystemUtilities {
* @param r the runnable
*/
public static void runSwingLater(Runnable r) {
runSwing(r, false, SWING_RUN_ERROR_MSG);
doRunSwing(r, false, SWING_RUN_ERROR_MSG);
}
public static void runIfSwingOrPostSwingLater(Runnable r) {
@ -284,7 +361,7 @@ public class SystemUtilities {
}
}
private static void runSwing(Runnable r, boolean wait, String errorMessage) {
private static void doRunSwing(Runnable r, boolean wait, String errorMessage) {
if (isInHeadlessMode()) {
r.run();
return;

View file

@ -0,0 +1,30 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.exception;
import javax.swing.SwingUtilities;
/**
* Signals that a background thread attempted to {@link SwingUtilities#invokeAndWait(Runnable)}
* operation that timed-out because the Swing thread was busy. This can be a sign of
* a deadlock.
*/
public class UnableToSwingException extends Exception {
public UnableToSwingException(String message) {
super(message);
}
}