Merge remote-tracking branch 'origin/GT-2875-dragonmacher-task-launcher-deadlock'

This commit is contained in:
ghidravore 2019-05-21 16:42:32 -04:00
commit 1340268ffa
49 changed files with 1145 additions and 542 deletions

View file

@ -32,10 +32,10 @@ import docking.event.mouse.GMouseListenerAdapter;
import docking.menu.DockingToolbarButton;
import docking.util.*;
import docking.widgets.label.GDHtmlLabel;
import ghidra.generic.function.Callback;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import ghidra.util.task.*;
import utility.function.Callback;
/**
* Base class used for creating dialogs in Ghidra. Subclass this to create a dialog provider that has

View file

@ -15,8 +15,7 @@
*/
package docking.test;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.junit.Assert.*;
import java.awt.*;
import java.awt.datatransfer.*;
@ -1760,9 +1759,11 @@ public abstract class AbstractDockingTest extends AbstractGenericTest {
*/
public static void setErrorsExpected(boolean expected) {
if (expected) {
Msg.error(AbstractDockingTest.class, ">>>>>>>>>>>>>>>> Expected Exception");
ConcurrentTestExceptionHandler.disable();
}
else {
Msg.error(AbstractDockingTest.class, "<<<<<<<<<<<<<<<< End Expected Exception");
ConcurrentTestExceptionHandler.enable();
}
}

View file

@ -22,10 +22,10 @@ import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import ghidra.generic.function.Callback;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import utility.function.Callback;
/**
* A text field that is meant to be used in conjunction with tables that allow filter text. This

View file

@ -28,10 +28,10 @@ import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import docking.widgets.table.*;
import ghidra.framework.preferences.Preferences;
import ghidra.generic.function.Callback;
import ghidra.util.filechooser.GhidraFileChooserModel;
import ghidra.util.filechooser.GhidraFileFilter;
import resources.ResourceManager;
import utility.function.Callback;
/**
* Component that has a table to show pathnames; the panel includes buttons to control

View file

@ -40,7 +40,6 @@ import docking.widgets.table.columnfilter.ColumnBasedTableFilter;
import docking.widgets.table.columnfilter.ColumnFilterSaveManager;
import docking.widgets.table.constraint.dialog.ColumnFilterDialog;
import ghidra.framework.options.PreferenceState;
import ghidra.generic.function.Callback;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.datastruct.WeakDataStructureFactory;
@ -50,6 +49,7 @@ import ghidra.util.task.SwingUpdateManager;
import resources.Icons;
import resources.ResourceManager;
import utilities.util.reflection.ReflectionUtilities;
import utility.function.Callback;
/**
* This class is a panel that provides a label and text field that allows users to input text that

View file

@ -33,11 +33,11 @@ import docking.widgets.table.GTableFilterPanel;
import docking.widgets.table.RowObjectFilterModel;
import docking.widgets.table.columnfilter.*;
import docking.widgets.table.constrainteditor.ColumnConstraintEditor;
import ghidra.generic.function.Callback;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.layout.VerticalLayout;
import resources.ResourceManager;
import utility.function.Callback;
/**
* Dialog for creating and editing column table filters.

View file

@ -221,7 +221,8 @@ public abstract class DataLoadingConstraintEditor<T> extends AbstractColumnConst
reloadDataButton.setVisible(false);
Task task = new LoadDataTask();
task.addTaskListener(this);
new TaskLauncher(task, (TaskMonitor) taskMonitorComponent);
BackgroundThreadTaskLauncher launcher = new BackgroundThreadTaskLauncher(task);
launcher.run(taskMonitorComponent);
}
@Override

View file

@ -0,0 +1,53 @@
/* ###
* 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.Executor;
import generic.concurrent.GThreadPool;
import ghidra.util.Swing;
import ghidra.util.TaskUtilities;
/**
* Helper class to launch the given task in a background thread This helper will not
* show a task dialog.
*
* <p>This class is useful when you want to run the task and use a monitor that is embedded
* in some other component.
*
* <p>See {@link TaskLauncher}.
*/
public class BackgroundThreadTaskLauncher {
private Task task;
public BackgroundThreadTaskLauncher(Task task) {
this.task = task;
}
public 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();
GThreadPool pool = GThreadPool.getSharedThreadPool(Swing.GSWING_THREAD_POOL_NAME);
Executor executor = pool.getExecutor();
executor.execute(() -> {
Thread.currentThread().setName(name);
task.monitoredRun(monitor);
});
}
}

View file

@ -54,6 +54,19 @@ import util.CollectionUtils;
* .launchModal();
* </pre>
*
* Or,
*
* <pre>
* TaskBuilder.withTask(new AwesomeTask(awesomeStuff)).launchModal();
* </pre>
*
* Or,
*
* <pre>
* {@link TaskLauncher#launch(Task) TaskLauncher.launch}(new AwesomeTask(awesomeStuff));
* </pre>
*
*
* <p>Note: this class will check to see if it is in a headless environment before launching
* its task. This makes it safe to use this class in headed or headless environments.
*/
@ -84,6 +97,21 @@ public class TaskBuilder {
return new TaskBuilder(r);
}
/**
* A convenience method to start a builder using the given task. The
* {@link #setTitle(String) title} of the task will be the value of
* {@link Task#getTaskTitle()}.
*
* <p>This method allows for a more attractive fluent API usage than does the constructor
* (see the javadoc header).
*
* @param t the task
* @return this builder
*/
public static TaskBuilder withTask(Task t) {
return new TaskBuilder(t.getTaskTitle(), t);
}
private TaskBuilder(MonitoredRunnable r) {
this.runnable = Objects.requireNonNull(r);
}
@ -148,8 +176,8 @@ public class TaskBuilder {
/**
* Sets the amount of time that will pass before showing the dialog. The default is
* {@link TaskLauncher#INITIAL_DELAY} for non-modal tasks and
* {@link TaskLauncher#INITIAL_MODAL_DELAY} for modal tasks.
* {@link TaskLauncher#INITIAL_DELAY_MS} for non-modal tasks and
* {@link TaskLauncher#INITIAL_MODAL_DELAY_MS} for modal tasks.
*
* @param delay the delay time
* @return this builder
@ -239,9 +267,9 @@ public class TaskBuilder {
}
if (isModal) {
return TaskLauncher.INITIAL_MODAL_DELAY;
return TaskLauncher.INITIAL_MODAL_DELAY_MS;
}
return TaskLauncher.INITIAL_DELAY;
return TaskLauncher.INITIAL_DELAY_MS;
}
private class TaskBuilderTask extends Task {

View file

@ -16,24 +16,30 @@
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.Swing;
import ghidra.util.exception.UnableToSwingException;
/**
* Class to initiate a Task in a new Thread, and to show a progress dialog that indicates
* activity. The progress dialog will show an animation in the event that the task of this class
* cannot show progress.
* activity <b>if the task takes too long</b>. The progress dialog will show an
* animation in the event that the task of this class cannot show progress.
*
* <p>For complete control of how this class functions, use
* {@link #TaskLauncher(Task, Component, int, int)}. Alternatively, for simpler uses,
* see one of the many static convenience methods.
*
* <p><b>Important Usage Note:</b><br>
* For clients that are not on the Swing thread the behavior of this class is designed to
* prevent deadlocks. When called from a non-Swing thread, this class will attempt to show a
* modal dialog. However, if more than {@link #getSwingTimeoutInSeconds()} elapses while waiting
* for the Swing thread, then this class will <b>give up on using the Swing thread and will not
* create a background thread</b>. Instead, the client code will be run in the client thread.
*
* <a name="modal_usage"></a>
* <p><a name="modal_usage"></a>Most clients of this class should not be concerned with where
* <p><b><a name="modal_usage">Modal Usage</a></b><br>
* Most clients of this class should not be concerned with where
* the dialog used by this class will appear. By default, it will be shown over
* the active window, which is the desired
* behavior for most uses. If you should need a dialog to appear over a non-active window,
@ -93,7 +99,7 @@ public class TaskLauncher {
public void run(TaskMonitor monitor) {
runnable.monitoredRun(monitor);
}
}, null, INITIAL_DELAY);
}, null, INITIAL_DELAY_MS);
}
/**
@ -122,7 +128,7 @@ public class TaskLauncher {
public void run(TaskMonitor monitor) {
runnable.monitoredRun(monitor);
}
}, null, INITIAL_MODAL_DELAY);
}, null, INITIAL_MODAL_DELAY_MS);
}
/**
@ -163,33 +169,13 @@ public class TaskLauncher {
// End Static Launcher Methods
//==================================================================================================
static final int INITIAL_DELAY = 1000;// 1 second
static final int INITIAL_DELAY_MS = 1000;
/** 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;
}
return (parent.isVisible() ? parent : null);
}
static final int INITIAL_MODAL_DELAY_MS = 500;
/**
* Constructor for TaskLauncher.
* Constructor for TaskLauncher
*
* <p>This constructor assumes that if a progress dialog is needed, then it should appear
* over the active window. If you should need a dialog to appear over a non-active window,
@ -200,11 +186,11 @@ public class TaskLauncher {
*
*/
public TaskLauncher(Task task) {
this(task, null, task.isModal() ? INITIAL_MODAL_DELAY : INITIAL_DELAY);
this(task, null, task.isModal() ? INITIAL_MODAL_DELAY_MS : INITIAL_DELAY_MS);
}
/**
* Constructor for TaskLauncher.
* Constructor for TaskLauncher
*
* <p>See <a href="#modal_usage">notes on modal usage</a>
*
@ -212,121 +198,96 @@ public class TaskLauncher {
* @param parent component whose window to use to parent the dialog.
*/
public TaskLauncher(Task task, Component parent) {
this(task, getParent(parent), task.isModal() ? INITIAL_MODAL_DELAY : INITIAL_DELAY);
this(task, getParent(parent), task.isModal() ? INITIAL_MODAL_DELAY_MS : INITIAL_DELAY_MS);
}
/**
* Construct a new TaskLauncher.
* Construct a new 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 parent component whose window to use to parent the dialog; null centers the task
* dialog over the current window
* @param delay number of milliseconds to delay until the task monitor is displayed
* @param delayMs number of milliseconds to delay until the task monitor is displayed
*/
public TaskLauncher(Task task, Component parent, int delay) {
this(task, parent, delay, TaskDialog.DEFAULT_WIDTH);
public TaskLauncher(Task task, Component parent, int delayMs) {
this(task, parent, delayMs, TaskDialog.DEFAULT_WIDTH);
}
/**
* Construct a new TaskLauncher.
* Construct a new 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 parent component whose window to use to parent the dialog; null centers the task
* dialog over the current window
* @param delay number of milliseconds to delay until the task monitor is displayed
* @param delayMs number of milliseconds to delay until the task monitor is displayed
* @param dialogWidth The preferred width of the dialog (this allows clients to make a wider
* dialog, which better shows long messages).
*/
public TaskLauncher(Task task, Component parent, int delay, int dialogWidth) {
public TaskLauncher(Task task, Component parent, int delayMs, int dialogWidth) {
this.task = task;
this.taskDialog = buildTaskDialog(parent, dialogWidth);
startBackgroundThread(taskDialog);
taskDialog.show(Math.max(delay, 0));
waitForModalIfNotSwing();
try {
scheduleFromSwingThread(task, parent, delayMs, dialogWidth);
}
catch (UnableToSwingException e) {
runInThisBackgroundThread(task);
}
}
private void waitForModalIfNotSwing() {
if (SwingUtilities.isEventDispatchThread() || !task.isModal()) {
private void scheduleFromSwingThread(Task task, Component parent, int delayMs, int dialogWidth)
throws UnableToSwingException {
TaskRunner runner = createTaskRunner(task, parent, delayMs, dialogWidth);
if (Swing.isEventDispatchThread()) {
runner.run();
return;
}
try {
taskThread.join();
}
catch (InterruptedException e) {
Msg.debug(this, "Task Launcher unexpectedly interrupted waiting for task thread", e);
}
//
// Not on the Swing thread. Try to execute on the Swing thread, timing-out if it takes
// too long (this prevents deadlocks).
//
// This will throw an exception if we could not get the Swing lock. When that happens,
// the task was NOT run.
int timeout = getSwingTimeoutInSeconds();
Swing.runNow(() -> runner.run(), timeout, TimeUnit.SECONDS);
}
// template method to allow timeout change; used by tests
protected int getSwingTimeoutInSeconds() {
return 2;
}
// template method to allow task runner change; used by tests
protected TaskRunner createTaskRunner(Task task, Component parent, int delayMs,
int dialogWidth) {
return new TaskRunner(task, parent, delayMs, dialogWidth);
}
/**
* Constructor where an external taskMonitor is used. Normally, this class will provide
* the {@link TaskDialog} as the monitor. This constructor is useful when you want to run
* the task and use a monitor that is embedded in some other component.
*
* <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.
* Runs the given task in the current thread, which <b>cannot be the Swing thread</b>
*
* @param task the task to run
* @throws IllegalStateException if the given thread is the Swing thread
*/
public TaskLauncher(Task task, TaskMonitor taskMonitor) {
this.task = task;
startBackgroundThread(taskMonitor);
}
private TaskDialog buildTaskDialog(Component comp, int dialogWidth) {
taskDialog = createTaskDialog(comp);
taskDialog.setMinimumSize(dialogWidth, 0);
if (task.isInterruptible() || task.isForgettable()) {
taskDialog.addCancelledListener(monitorChangeListener);
protected void runInThisBackgroundThread(Task task) {
if (Swing.isEventDispatchThread()) {
throw new IllegalStateException("Must not call this method from the Swing thread");
}
taskDialog.setStatusJustification(task.getStatusTextAlignment());
return taskDialog;
task.monitoredRun(TaskMonitor.DUMMY);
}
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 parent = comp;
if (parent != null) {
parent = WindowUtilities.windowForComponent(comp);
}
private static Component getParent(Component parent) {
if (parent == null) {
return new TaskDialog(task);
return null;
}
return new TaskDialog(comp, task);
return (parent.isVisible() ? parent : null);
}
}

View file

@ -0,0 +1,112 @@
/* ###
* 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 java.util.concurrent.Executor;
import generic.concurrent.GThreadPool;
import generic.util.WindowUtilities;
import ghidra.util.Swing;
import ghidra.util.TaskUtilities;
/**
* 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 TaskRunner {
protected Task task;
private Component parent;
private int delayMs;
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
}
};
TaskRunner(Task task, Component parent, int delayMs, int dialogWidth) {
this.task = task;
this.parent = parent;
this.delayMs = delayMs;
this.dialogWidth = dialogWidth;
}
void run() {
// note: we need to be on the Swing thread to create our UI widgets
Swing.assertThisIsTheSwingThread(
"The Task runner is required to be run from the Swing thread");
this.taskDialog = buildTaskDialog(parent);
startBackgroundThread(taskDialog);
taskDialog.show(Math.max(delayMs, 0));
}
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).
//
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();
GThreadPool pool = GThreadPool.getSharedThreadPool(Swing.GSWING_THREAD_POOL_NAME);
Executor executor = pool.getExecutor();
executor.execute(() -> {
Thread.currentThread().setName(name);
task.monitoredRun(monitor);
taskDialog.taskProcessed();
});
}
private 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);
}
}