mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 10:19:23 +02:00
GT-2875 - Unswingable - Code to allow task launching to give-up on the
Swing thread to prevent deadlocks
This commit is contained in:
parent
88cf9c0f80
commit
8dffd377fb
10 changed files with 776 additions and 253 deletions
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 + ']';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 + ']';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue