Merge remote-tracking branch 'origin/patch'

Conflicts:
	Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskDialog.java
	Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskRunner.java
	Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimer.java
This commit is contained in:
dragonmacher 2020-12-03 10:42:37 -05:00
commit fa18866fa8
19 changed files with 296 additions and 278 deletions

View file

@ -35,8 +35,7 @@ public class HexNumbersAction extends CompositeEditorTableAction implements Togg
private boolean isSelected;
public HexNumbersAction(CompositeEditorProvider provider) {
super(provider, EDIT_ACTION_PREFIX + ACTION_NAME, GROUP_NAME, PATH, PATH,
null);
super(provider, EDIT_ACTION_PREFIX + ACTION_NAME, GROUP_NAME, PATH, PATH, null);
setDescription(DESCRIPTION);
setEnabled(true);
setSelected(model.isShowingNumbersInHex());
@ -59,6 +58,9 @@ public class HexNumbersAction extends CompositeEditorTableAction implements Togg
@Override
public void setSelected(boolean newValue) {
if (isSelected == newValue) {
return;
}
isSelected = newValue;
firePropertyChanged(SELECTED_STATE_PROPERTY, !isSelected, isSelected);
}

View file

@ -20,7 +20,9 @@ import java.awt.Rectangle;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.swing.*;
@ -50,7 +52,6 @@ import ghidra.program.model.listing.Program;
import ghidra.util.*;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.CancelledException;
import ghidra.util.table.GhidraTableFilterPanel;
import ghidra.util.task.*;
import resources.ResourceManager;
@ -605,28 +606,34 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
}
void runScript(ResourceFile scriptFile, TaskListener listener) {
if (SystemUtilities.isEventDispatchThread()) {
new TaskLauncher(new Task("compiling script directory", true, false, true, true) {
@Override
public void run(TaskMonitor monitor) throws CancelledException {
doRunScript(scriptFile, listener);
}
}, plugin.getTool().getToolFrame(), 1000);
}
else {
doRunScript(scriptFile, listener);
}
lastRunScript = scriptFile;
GhidraScript script = doGetScriptInstance(scriptFile);
doRunScript(script, listener);
}
private void doRunScript(ResourceFile scriptFile, TaskListener listener) {
lastRunScript = scriptFile;
private GhidraScript doGetScriptInstance(ResourceFile scriptFile) {
ConsoleService console = plugin.getConsoleService();
GhidraScript script = getScriptInstance(scriptFile, console);
if (script == null) {
return;
Supplier<GhidraScript> scriptSupplier = () -> {
ConsoleService console = plugin.getConsoleService();
return getScriptInstance(scriptFile, console);
};
if (!Swing.isSwingThread()) {
return scriptSupplier.get();
}
AtomicReference<GhidraScript> ref = new AtomicReference<>();
TaskBuilder.withRunnable(monitor -> ref.set(scriptSupplier.get()))
.setTitle("Compiling Script Directory")
.setLaunchDelay(1000)
.launchModal();
return ref.get();
}
private void doRunScript(GhidraScript script, TaskListener listener) {
ConsoleService console = plugin.getConsoleService();
RunScriptTask task = new RunScriptTask(script, plugin.getCurrentState(), console);
runningScriptTaskSet.add(task);
task.addTaskListener(listener);
@ -811,13 +818,13 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
/*
Unusual Algorithm
The tree nodes represent categories, but do not contain nodes for individual
The tree nodes represent categories, but do not contain nodes for individual
scripts. We wish to remove any of the tree nodes that no longer represent script
categories. (This can happen when a script is deleted or its category is changed.)
This algorithm will assume that all nodes need to be deleted. Then, each script is
examined, using its category to mark a given node as 'safe'; that node's parents are
also marked as safe. Any nodes remaining unmarked have no reference script and
will be deleted.
also marked as safe. Any nodes remaining unmarked have no reference script and
will be deleted.
*/
// note: turn String[] to List<String> to use hashing

View file

@ -67,8 +67,10 @@ import utilities.util.FileUtilities;
public abstract class AbstractGhidraScriptMgrPluginTest
extends AbstractGhidraHeadedIntegrationTest {
protected static final int MAX_TIME = 4000;
protected static final int SCRIPT_TIMEOUT_SECS = 5;
// timeout for scripts run by invoking RunScriptTask directly
protected static final int TASK_RUN_SCRIPT_TIMEOUT_SECS = 5;
// timeout for scripts run indirectly through the GUI
protected static final int GUI_RUN_SCRIPT_TIMEOUT_MSECS = 6 * DEFAULT_WAIT_TIMEOUT;
protected TestEnv env;
protected CodeBrowserPlugin browser;
protected GhidraScriptMgrPlugin plugin;
@ -594,12 +596,6 @@ public abstract class AbstractGhidraScriptMgrPluginTest
waitForSwing();
}
protected void performGlobalRunLastScriptAction() {
// note: this action used to be different from the 'run last script'; currently they are
// the same
pressRunLastScriptButton();
}
protected KeyBindingInputDialog pressKeyBindingAction() {
DockingActionIf keyBindingAction = getAction(plugin, "Key Binding");
performAction(keyBindingAction, false);
@ -625,10 +621,17 @@ public abstract class AbstractGhidraScriptMgrPluginTest
waitForSwing();
}
protected String runScript(String scriptName) throws Exception {
/**
* Run the currently selected script by pressing the run button and return its output.
*
* @param taskName name for the task listener
* @return script output written to the console
* @throws Exception on failure, e.g. timeout
*/
protected String runSelectedScript(String taskName) throws Exception {
clearConsole();
TaskListenerFlag taskFlag = new TaskListenerFlag(scriptName);
TaskListenerFlag taskFlag = new TaskListenerFlag(taskName);
TaskUtilities.addTrackedTaskListener(taskFlag);
pressRunButton();
@ -639,8 +642,15 @@ public abstract class AbstractGhidraScriptMgrPluginTest
return output;
}
protected String runLastScript(String scriptName) throws Exception {
TaskListenerFlag taskFlag = new TaskListenerFlag(scriptName);
/**
* Run the last script by pressing the last script button and return output.
*
* @param taskName name for the task listener
* @return script output written to the console
* @throws Exception on failure, e.g. timeout
*/
protected String runLastScript(String taskName) throws Exception {
TaskListenerFlag taskFlag = new TaskListenerFlag(taskName);
TaskUtilities.addTrackedTaskListener(taskFlag);
pressRunLastScriptButton();
@ -651,18 +661,6 @@ public abstract class AbstractGhidraScriptMgrPluginTest
return output;
}
protected String runGlobalLastScriptAction(String scriptName) throws Exception {
TaskListenerFlag taskFlag = new TaskListenerFlag(scriptName);
TaskUtilities.addTrackedTaskListener(taskFlag);
performGlobalRunLastScriptAction();
waitForTaskEnd(taskFlag);
String output = getConsoleText();
clearConsole();
return output;
}
protected void deleteFile(ResourceFile file) {
assertTrue(file.delete());
}
@ -1031,15 +1029,9 @@ public abstract class AbstractGhidraScriptMgrPluginTest
protected void waitForTaskEnd(TaskListenerFlag flag) {
waitForSwing();
int waitCount = 0;
while (!flag.ended && waitCount < 401) {
try {
Thread.sleep(DEFAULT_WAIT_DELAY);
}
catch (InterruptedException e) {
// don't care; try again
}
waitCount++;
int totalTime = 0;
while (!flag.ended && totalTime <= GUI_RUN_SCRIPT_TIMEOUT_MSECS) {
totalTime += sleep(DEFAULT_WAIT_DELAY);
}
TaskUtilities.removeTrackedTaskListener(flag);
@ -1047,6 +1039,7 @@ public abstract class AbstractGhidraScriptMgrPluginTest
if (!flag.ended) {
Assert.fail("Task took too long to complete: " + flag);
}
Msg.debug(this, flag.taskName + " task ended in " + totalTime + " ms");
}
protected int getSelectedRow() {
@ -1165,11 +1158,11 @@ public abstract class AbstractGhidraScriptMgrPluginTest
pressButtonByText(window, "Cancel");
}
protected TestChangeProgramScript startCancellableScript() throws Exception {
protected TestChangeProgramScript startCancellableScriptTask() throws Exception {
TestChangeProgramScript script = new TestChangeProgramScript();
ResourceFile fakeFile = new ResourceFile(createTempFile(CANCELLABLE_SCRIPT_NAME, "java"));
script.setSourceFile(fakeFile);
runScript(script);
startRunScriptTask(script);
boolean success = script.waitForStart();
assertTrue("Test script did not get started!", success);
@ -1202,7 +1195,7 @@ public abstract class AbstractGhidraScriptMgrPluginTest
assertTrue("Timed-out waiting for cancelled script to complete", success);
}
protected void runScript(GhidraScript script) throws Exception {
protected void startRunScriptTask(GhidraScript script) throws Exception {
Task task = new RunScriptTask(script, plugin.getCurrentState(), console);
task.addTaskListener(provider.getTaskListener());
new TaskLauncher(task, plugin.getTool().getToolFrame());
@ -1213,10 +1206,10 @@ public abstract class AbstractGhidraScriptMgrPluginTest
GhidraScript script =
scriptProvider.getScriptInstance(scriptFile, new PrintWriter(System.err));
return runScriptAndGetOutput(script);
return runScriptTaskAndGetOutput(script);
}
protected String runScriptAndGetOutput(GhidraScript script) throws Exception {
protected String runScriptTaskAndGetOutput(GhidraScript script) throws Exception {
SpyConsole spyConsole = installSpyConsole();
Task task = new RunScriptTask(script, plugin.getCurrentState(), spyConsole);
@ -1238,7 +1231,7 @@ public abstract class AbstractGhidraScriptMgrPluginTest
TaskLauncher.launch(task);
latch.await(SCRIPT_TIMEOUT_SECS, TimeUnit.SECONDS);
latch.await(TASK_RUN_SCRIPT_TIMEOUT_SECS, TimeUnit.SECONDS);
String output = spyConsole.getApiOutput();
spyConsole.clear();
@ -1604,11 +1597,11 @@ public abstract class AbstractGhidraScriptMgrPluginTest
}
boolean waitForStart() throws Exception {
return startedLatch.await(SCRIPT_TIMEOUT_SECS, TimeUnit.SECONDS);
return startedLatch.await(TASK_RUN_SCRIPT_TIMEOUT_SECS, TimeUnit.SECONDS);
}
boolean waitForFinish() throws Exception {
return doneLatch.await(SCRIPT_TIMEOUT_SECS, TimeUnit.SECONDS);
return doneLatch.await(TASK_RUN_SCRIPT_TIMEOUT_SECS, TimeUnit.SECONDS);
}
}

View file

@ -104,11 +104,11 @@ public class BundleStatusManagerTest extends AbstractGhidraScriptMgrPluginTest {
assertTrue(status.isEnabled());
assertScriptInTable(scriptFile);
runScript(SCRIPT_NAME);
selectAndRunScript(SCRIPT_NAME);
cleanViaGUI(viewRow);
runScript(SCRIPT_NAME);
runSelectedScript(SCRIPT_NAME);
}
@Test
@ -181,7 +181,7 @@ public class BundleStatusManagerTest extends AbstractGhidraScriptMgrPluginTest {
addBundlesViaGUI(dir1, dir2);
String output = runScript(TEST_SCRIPT_NAME + ".java");
String output = selectAndRunScript(TEST_SCRIPT_NAME + ".java");
assertEquals(EXPECTED_OUTPUT, output);
int row1 = getBundleRow(dir1);
@ -392,11 +392,10 @@ public class BundleStatusManagerTest extends AbstractGhidraScriptMgrPluginTest {
testBundleHostListener.awaitActivation();
}
@Override
public String runScript(String scriptName) throws Exception {
public String selectAndRunScript(String scriptName) throws Exception {
env.getTool().showComponentProvider(provider, true);
selectScript(scriptName);
String output = super.runScript(scriptName);
String output = runSelectedScript(scriptName);
env.getTool().showComponentProvider(bundleStatusProvider, true);
return output;
}

View file

@ -46,7 +46,7 @@ public class GhidraScriptMgrPlugin1Test extends AbstractGhidraScriptMgrPluginTes
//
String initialScriptName = "HelloWorldScript.java";
selectScript(initialScriptName);
String fullOutput = runScript(initialScriptName);
String fullOutput = runSelectedScript(initialScriptName);
String expectedOutput = "Hello World";
assertTrue("Script did not run - output: " + fullOutput,
fullOutput.indexOf(expectedOutput) != -1);
@ -63,7 +63,7 @@ public class GhidraScriptMgrPlugin1Test extends AbstractGhidraScriptMgrPluginTes
//
String secondScriptName = "FormatExampleScript.java";
selectScript(secondScriptName);
fullOutput = runScript(secondScriptName);
fullOutput = runSelectedScript(secondScriptName);
expectedOutput = "jumped over the";
assertTrue("Script did not run - output: " + fullOutput,
fullOutput.indexOf(expectedOutput) != -1);
@ -84,7 +84,7 @@ public class GhidraScriptMgrPlugin1Test extends AbstractGhidraScriptMgrPluginTes
//
String scriptName = "HelloWorldScript.java";
selectScript(scriptName);
String fullOutput = runScript(scriptName);
String fullOutput = runSelectedScript(scriptName);
String expectedOutput = "Hello World";
assertTrue("Script did not run - output: " + fullOutput,
fullOutput.indexOf(expectedOutput) != -1);
@ -105,7 +105,7 @@ public class GhidraScriptMgrPlugin1Test extends AbstractGhidraScriptMgrPluginTes
//
String scriptName = "HelloWorldScript.java";
selectScript(scriptName);
String fullOutput = runScript(scriptName);
String fullOutput = runSelectedScript(scriptName);
String expectedOutput = "Hello World";
assertTrue("Script did not run - output: " + fullOutput,
fullOutput.indexOf(expectedOutput) != -1);
@ -115,7 +115,7 @@ public class GhidraScriptMgrPlugin1Test extends AbstractGhidraScriptMgrPluginTes
//
// Run the script again
//
fullOutput = runGlobalLastScriptAction(scriptName);
fullOutput = runLastScript(scriptName);
assertTrue("Did not rerun last run script", fullOutput.indexOf(expectedOutput) != -1);
}

View file

@ -97,7 +97,7 @@ public class GhidraScriptMgrPlugin3Test extends AbstractGhidraScriptMgrPluginTes
pressSaveButton();
String scriptOutput = runScript(script.getName());
String scriptOutput = runSelectedScript(script.getName());
assertTrue("Script output not generated",
scriptOutput.contains("> new scripts are neato!"));
@ -132,7 +132,7 @@ public class GhidraScriptMgrPlugin3Test extends AbstractGhidraScriptMgrPluginTes
pressSaveButton();
setTimestampToTheFuture(script);
String updatedScriptOutput = runScript(script.getName());
String updatedScriptOutput = runSelectedScript(script.getName());
assertTrue("Script output not updated with new script contents - did recompile work?",
StringUtilities.containsAll(updatedScriptOutput, "> new scripts are neato!",
@ -530,7 +530,7 @@ public class GhidraScriptMgrPlugin3Test extends AbstractGhidraScriptMgrPluginTes
@Test
public void testCancel() throws Exception {
TestChangeProgramScript script = startCancellableScript();
TestChangeProgramScript script = startCancellableScriptTask();
cancel();
@ -539,7 +539,7 @@ public class GhidraScriptMgrPlugin3Test extends AbstractGhidraScriptMgrPluginTes
@Test
public void testCancel_DoNotCancel() throws Exception {
TestChangeProgramScript script = startCancellableScript();
TestChangeProgramScript script = startCancellableScriptTask();
cancel();

View file

@ -1380,6 +1380,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
focusedPlaceholder.setSelected(false);
}
// Activating placeholders is done to help users find widgets hiding in plain sight.
// Assume that the user is no longer seeking a provider if they are clicking around.
activatedInfo.clear();
focusedPlaceholder = placeholder;
// put the last focused placeholder at the front of the list for restoring focus work later
@ -2294,5 +2298,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
}
lastCalledTimestamp = System.currentTimeMillis();
}
void clear() {
lastActivatedPlaceholder = null;
lastCalledTimestamp = 0;
}
}
}

View file

@ -444,8 +444,7 @@ public abstract class DockingAction implements DockingActionIf {
// menu path
if (menuBarData != null) {
buffer.append(" MENU PATH: ")
.append(
menuBarData.getMenuPathAsString());
.append(menuBarData.getMenuPathAsString());
buffer.append('\n');
buffer.append(" MENU GROUP: ").append(menuBarData.getMenuGroup());
buffer.append('\n');
@ -468,8 +467,7 @@ public abstract class DockingAction implements DockingActionIf {
// popup menu path
if (popupMenuData != null) {
buffer.append(" POPUP PATH: ")
.append(
popupMenuData.getMenuPathAsString());
.append(popupMenuData.getMenuPathAsString());
buffer.append('\n');
buffer.append(" POPUP GROUP: ").append(popupMenuData.getMenuGroup());
buffer.append('\n');
@ -535,6 +533,9 @@ public abstract class DockingAction implements DockingActionIf {
}
public void firePropertyChanged(String propertyName, Object oldValue, Object newValue) {
if (Objects.equals(oldValue, newValue)) {
return;
}
PropertyChangeEvent event = new PropertyChangeEvent(this, propertyName, oldValue, newValue);
for (PropertyChangeListener listener : propertyListeners) {
listener.propertyChange(event);

View file

@ -43,6 +43,9 @@ public abstract class ToggleDockingAction extends DockingAction implements Toggl
@Override
public void setSelected(boolean newValue) {
if (isSelected == newValue) {
return;
}
isSelected = newValue;
firePropertyChanged(SELECTED_STATE_PROPERTY, !isSelected, isSelected);
}

View file

@ -19,6 +19,7 @@ import java.awt.BorderLayout;
import java.awt.Component;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.JPanel;
@ -64,7 +65,7 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
}
};
private GTimerMonitor showTimer;
private GTimerMonitor showTimer = GTimerMonitor.DUMMY;
private CountDownLatch finished = new CountDownLatch(1);
private boolean supportsProgress;
@ -78,17 +79,20 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
private SwingUpdateManager messageUpdater =
new SwingUpdateManager(100, 250, () -> setStatusText(newMessage.getAndSet(null)));
private AtomicBoolean shown = new AtomicBoolean();
/**
* Constructor
*
* @param centerOnComp component to be centered over when shown,
* otherwise center over parent. If both centerOnComp and parent
* are null, dialog will be centered on screen.
* @param centerOnComp component to be centered over when shown, otherwise center over parent.
* If both centerOnComp and parent are null, dialog will be centered on screen.
* @param task the Task that this dialog will be associated with
* @param finished overrides the latch used by this dialog to know when the task is finished
*/
public TaskDialog(Component centerOnComp, Task task) {
TaskDialog(Component centerOnComp, Task task, CountDownLatch finished) {
this(centerOnComp, task.getTaskTitle(), task.isModal(), task.canCancel(),
task.hasProgress());
this.finished = finished;
}
/**
@ -209,14 +213,18 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
/**
* Called after the task has been executed
*/
public synchronized void taskProcessed() {
public void taskProcessed() {
finished.countDown();
monitorComponent.notifyChangeListeners();
Swing.runLater(closeDialog);
}
public synchronized boolean isCompleted() {
return finished.getCount() == 0;
/**
* Returns true if this dialog's task has completed normally or been cancelled
* @return true if this dialog's task has completed normally or been cancelled
*/
public boolean isCompleted() {
return finished.getCount() == 0 || isCancelled();
}
/**
@ -239,6 +247,14 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
}
}
/**
* Returns true if this dialog was ever made visible
* @return true if shown
*/
public boolean wasShown() {
return shown.get();
}
private void doShowModal(int delay) {
//
// Note: we must block, since we are modal. Clients want us to finish the task before
@ -272,6 +288,7 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
Swing.runIfSwingOrRunLater(() -> {
if (!isCompleted()) {
shown.set(true);
DockingWindowManager.showDialog(centerOnComponent, TaskDialog.this);
}
});
@ -289,17 +306,9 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
}
public void dispose() {
cancel();
showTimer.cancel();
messageUpdater.dispose();
Runnable disposeTask = () -> {
if (showTimer != null) {
showTimer.cancel();
showTimer = null;
}
};
Swing.runNow(disposeTask);
}
//==================================================================================================

View file

@ -17,8 +17,6 @@ package ghidra.util.task;
import java.awt.Component;
import ghidra.util.Swing;
/**
* Class to initiate a Task in a new Thread, and to show a progress dialog that indicates
* activity <b>if the task takes too long</b>. The progress dialog will show an
@ -229,20 +227,6 @@ public class TaskLauncher {
return new TaskRunner(task, parent, delayMs, dialogWidth);
}
/**
* 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
*/
protected void runInThisBackgroundThread(Task task) {
if (Swing.isSwingThread()) {
throw new IllegalStateException("Must not call this method from the Swing thread");
}
task.monitoredRun(TaskMonitor.DUMMY);
}
private static Component getParent(Component parent) {
if (parent == null) {
return null;

View file

@ -70,9 +70,21 @@ class TaskRunner {
}
// protected to allow for dependency injection
protected TaskDialog buildTaskDialog(Component comp, TaskMonitor monitor) {
protected TaskDialog buildTaskDialog() {
TaskDialog dialog = createTaskDialog(comp);
Component centerOverComponent = parent;
Component currentParent = centerOverComponent;
if (currentParent != null) {
currentParent = WindowUtilities.windowForComponent(parent);
}
if (currentParent == null) {
centerOverComponent = null;
}
// we pass in our 'finished' latch here to avoid relying on a Swing.runLater() callback
// (see taskFinished())
TaskDialog dialog = new TaskDialog(centerOverComponent, task, finished);
dialog.setMinimumSize(dialogWidth, 0);
dialog.setStatusJustification(task.getStatusTextAlignment());
return dialog;
@ -88,7 +100,6 @@ class TaskRunner {
Executor executor = pool.getExecutor();
executor.execute(() -> {
Thread.currentThread().setName(name);
try {
task.monitoredRun(monitor);
}
@ -98,33 +109,10 @@ class TaskRunner {
});
}
private TaskDialog createTaskDialog(Component comp) {
Component centerOverComponent = comp;
Component currentParent = centerOverComponent;
if (currentParent != null) {
currentParent = WindowUtilities.windowForComponent(comp);
}
if (currentParent == null) {
centerOverComponent = null;
}
return new TaskDialog(centerOverComponent, task) {
// note: we override this method here to help with the race condition where the
// TaskRunner does not yet know about the task dialog, but the background
// thread has actually finished the work.
@Override
public synchronized boolean isCompleted() {
return super.isCompleted() || isFinished();
}
};
}
private void showTaskDialog(WrappingTaskMonitor monitor) {
Swing.runIfSwingOrRunLater(() -> {
taskDialog = buildTaskDialog(parent, monitor);
taskDialog = buildTaskDialog();
monitor.setDelegate(taskDialog); // initialize the dialog to the current monitor state
taskDialog.show(Math.max(delayMs, 0));
});
@ -135,6 +123,11 @@ class TaskRunner {
}
private void taskFinished() {
// This will release the the task dialog. We passed this latch to the dialog at
// construction so that does not block until we notify it in the Swing.runLater() below.
// If we only rely on that notification, then the notification will be blocked when the
// dialog is waiting in the Swing thread.
finished.countDown();
// Do this later on the Swing thread to handle the race condition where the dialog

View file

@ -20,7 +20,6 @@ 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;
import ghidra.util.Swing;
@ -37,8 +36,7 @@ public class AbstractTaskTest extends AbstractDockingTest {
protected Deque<TDEvent> eventQueue = new LinkedBlockingDeque<>();
protected volatile TaskLauncherSpy taskLauncherSpy;
protected volatile TaskDialogSpy dialogSpy;
protected AtomicBoolean didRunInBackground = new AtomicBoolean();
protected volatile TaskDialog taskDialog;
protected void assertDidNotRunInSwing() {
for (TDEvent e : eventQueue) {
@ -46,11 +44,9 @@ public class AbstractTaskTest extends AbstractDockingTest {
}
}
protected void assertRanInSwingThread() {
assertFalse("Task was not run in the Swing thread", didRunInBackground.get());
}
protected void assertSwingThreadBlockedForTask() {
// if the last event is the swing thread, then we know the task blocked that thread, since
// the task delay would have made it run last had it not blocked
waitForSwing();
TDEvent lastEvent = eventQueue.peekLast();
boolean swingIsLast = lastEvent.getThreadName().contains("AWT");
@ -70,26 +66,35 @@ public class AbstractTaskTest extends AbstractDockingTest {
}
protected void assertNoDialogShown() {
if (dialogSpy == null) {
if (taskDialog == null) {
return; // not shown
}
assertFalse("Dialog should not have been shown.\nEvents: " + eventQueue,
dialogSpy.wasShown());
taskDialog.wasShown());
}
protected void assertDialogShown() {
assertTrue("Dialog should have been shown.\nEvents: " + eventQueue, dialogSpy.wasShown());
assertTrue("Dialog should have been shown.\nEvents: " + eventQueue, taskDialog.wasShown());
}
protected void waitForTask() throws Exception {
threadsFinished.await(2, TimeUnit.SECONDS);
}
/**
* Launches the task and waits until the dialog is shown
* @param task the task to launch
*/
protected void launchTask(Task task) {
launchTaskFromSwing(task);
}
protected void launchTaskWithoutBlocking(Task task) {
runSwing(() -> launchTaskFromSwing(task), false);
waitFor(() -> taskDialog != null);
}
protected void launchTaskFromSwing(Task task) {
runSwing(() -> {
@ -99,14 +104,30 @@ public class AbstractTaskTest extends AbstractDockingTest {
});
}
protected void launchTaskFromSwing(FastModalTask task, int dialogDelay) {
runSwing(() -> {
taskLauncherSpy = new TaskLauncherSpy(task, dialogDelay);
postEvent("After task launcher");
threadsFinished.countDown();
});
}
protected void postEvent(String message) {
eventQueue.add(new TDEvent(message));
}
//==================================================================================================
// Inner Classes
//==================================================================================================
protected class TaskLauncherSpy extends TaskLauncher {
public TaskLauncherSpy(Task task) {
super(task, null, DELAY_LAUNCHER);
this(task, DELAY_LAUNCHER);
}
public TaskLauncherSpy(Task task, int dialogDelay) {
super(task, null, dialogDelay);
}
@Override
@ -115,31 +136,12 @@ public class AbstractTaskTest extends AbstractDockingTest {
return new TaskRunner(task, parent, delay, dialogWidth) {
@Override
protected TaskDialog buildTaskDialog(Component comp, TaskMonitor monitor) {
dialogSpy = new TaskDialogSpy(task) {
@Override
public synchronized boolean isCompleted() {
return super.isCompleted() || isFinished();
}
};
return dialogSpy;
protected TaskDialog buildTaskDialog() {
taskDialog = super.buildTaskDialog();
return taskDialog;
}
};
}
@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 {
@ -203,6 +205,25 @@ public class AbstractTaskTest extends AbstractDockingTest {
}
}
protected class LatchedModalTask extends Task {
private CountDownLatch latch;
public LatchedModalTask(CountDownLatch latch) {
super("Latched Modal Task", true, true, true);
this.latch = latch;
}
@Override
public void run(TaskMonitor monitor) {
postEvent(getName() + " started...");
waitFor(latch);
sleep(DELAY_SLOW);
threadsFinished.countDown();
postEvent(getName() + " finished.");
}
}
protected class TDEvent {
protected String threadName = Thread.currentThread().getName();

View file

@ -1,36 +0,0 @@
/* ###
* 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,6 +17,8 @@ package ghidra.util.task;
import static org.junit.Assert.*;
import java.util.concurrent.CountDownLatch;
import org.junit.After;
import org.junit.Test;
@ -51,7 +53,7 @@ public class TaskDialogTest extends AbstractTaskTest {
waitForTask();
assertFalse(dialogSpy.wasShown());
assertFalse(taskDialog.wasShown());
assertSwingThreadBlockedForTask();
}
@ -76,7 +78,7 @@ public class TaskDialogTest extends AbstractTaskTest {
waitForTask();
assertFalse(dialogSpy.wasShown());
assertFalse(taskDialog.wasShown());
assertNoDialogShown();
}
@ -98,16 +100,16 @@ public class TaskDialogTest extends AbstractTaskTest {
*/
@Test
public void testTaskCancel() throws Exception {
SlowModalTask task = new SlowModalTask();
launchTask(task);
CountDownLatch latch = new CountDownLatch(1);
LatchedModalTask task = new LatchedModalTask(latch);
launchTaskWithoutBlocking(task);
dialogSpy.doShow();
taskDialog.doShow();
latch.countDown();
waitForTask();
assertFalse(dialogSpy.isCancelled());
dialogSpy.cancel();
assertTrue(dialogSpy.isCancelled());
assertFalse(taskDialog.isCancelled());
taskDialog.cancel();
assertTrue(taskDialog.isCancelled());
}
/*
@ -119,11 +121,11 @@ public class TaskDialogTest extends AbstractTaskTest {
SlowModalTask task = new SlowModalTask();
launchTask(task);
dialogSpy.doShow();
dialogSpy.setCancelEnabled(false);
taskDialog.doShow();
taskDialog.setCancelEnabled(false);
waitForTask();
assertFalse(dialogSpy.isCancelEnabled());
assertFalse(taskDialog.isCancelEnabled());
}
}

View file

@ -52,7 +52,7 @@ public class TaskLauncherTest extends AbstractTaskTest {
FastModalTask task = new FastModalTask();
launchTaskFromBackground(task);
waitForTask();
assertRanInSwingThread();
assertDidNotRunInSwing();
}
@Test
@ -93,11 +93,36 @@ public class TaskLauncherTest extends AbstractTaskTest {
assertDidNotRunInSwing();
}
@Test
public void testLaunchFromSwingThreadWithModalTaskDoesNotBlockForFullDelay() throws Exception {
//
// Tests that a short-lived task does not block for the full dialog delay
//
FastModalTask task = new FastModalTask();
int dialogDelay = 3000;
long start = System.nanoTime();
launchTaskFromSwing(task, dialogDelay);
waitForTask();
long end = System.nanoTime();
long totalTime = TimeUnit.NANOSECONDS.toMillis(end - start);
assertSwingThreadBlockedForTask();
assertTrue(
"Time waited is longer that the dialog delay: " + totalTime + " vs " + dialogDelay,
totalTime < dialogDelay);
}
//==================================================================================================
// Private Methods
//==================================================================================================
private int getWaitTimeoutInSeconds() {
return (int) TimeUnit.SECONDS.convert(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS) * 2;
}
protected void launchTaskFromBackground(Task task) throws InterruptedException {
private void launchTaskFromBackground(Task task) throws InterruptedException {
CountDownLatch start = new CountDownLatch(1);
new Thread("Test Task Launcher Background Client") {
@ -114,7 +139,7 @@ public class TaskLauncherTest extends AbstractTaskTest {
start.await(getWaitTimeoutInSeconds(), TimeUnit.SECONDS));
}
protected void launchTaskFromTask() throws InterruptedException {
private void launchTaskFromTask() throws InterruptedException {
TaskLaunchingTask task = new TaskLaunchingTask();
@ -133,6 +158,10 @@ public class TaskLauncherTest extends AbstractTaskTest {
start.await(getWaitTimeoutInSeconds(), TimeUnit.SECONDS));
}
//==================================================================================================
// Inner Classes
//==================================================================================================
private class TaskLaunchingTask extends Task {
public TaskLaunchingTask() {

View file

@ -27,7 +27,7 @@ import ghidra.util.Msg;
*/
public class GTimer {
private static Timer timer;
private static GTimerMonitor DO_NOTHING_MONITOR = new DoNothingMonitor();
private static GTimerMonitor DO_NOTHING_MONITOR = GTimerMonitor.DUMMY;
/**
* Schedules a runnable for execution after the specified delay. A delay value less than 0
@ -77,24 +77,6 @@ public class GTimer {
return timer;
}
static class DoNothingMonitor implements GTimerMonitor {
@Override
public boolean cancel() {
return false;
}
@Override
public boolean didRun() {
return false;
}
@Override
public boolean wasCancelled() {
return false;
}
}
static class GTimerTask extends TimerTask implements GTimerMonitor {
private final Runnable runnable;

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,9 +17,31 @@ package ghidra.util.timer;
/**
* Monitor object returned from a GTimer.schedule() call
*
*/
public interface GTimerMonitor {
/**
* A dummy implementation of this interface
*/
public static GTimerMonitor DUMMY =
new GTimerMonitor() {
@Override
public boolean wasCancelled() {
return false;
}
@Override
public boolean didRun() {
return false;
}
@Override
public boolean cancel() {
return false;
}
};
/**
* Cancels the scheduled runnable associated with this GTimerMonitor if it has not already run.
* @return true if the scheduled runnable was cancelled before it had a chance to execute.
@ -35,7 +56,7 @@ public interface GTimerMonitor {
/**
* Return true if the scheduled runnable was cancelled before it had a chance to run.
* @return
* @return true if the scheduled runnable was cancelled before it had a chance to run.
*/
public boolean wasCancelled();
}

View file

@ -509,7 +509,6 @@ public class ToolTaskManager implements Runnable {
toolTaskMonitor.dispose();
if (modalTaskDialog != null) {
modalTaskDialog.cancel();
modalTaskDialog.dispose();
}