GP-249 - Task Dialog - fixed issue with task dialog appearing over user input dialog; fixed spin/sleep code

This commit is contained in:
dragonmacher 2020-10-15 18:06:36 -04:00
parent 22fd0a24a3
commit dc7e45762d
8 changed files with 172 additions and 185 deletions

View file

@ -47,7 +47,7 @@ class LoadPdbTask extends Task {
LoadPdbTask(Program program, File pdbFile, boolean useMsDiaParser,
PdbApplicatorRestrictions restrictions, DataTypeManagerService service) {
super("Loading PDB...", true, false, false);
super("Load PDB", true, false, false);
this.program = program;
this.pdbFile = pdbFile;
this.useMsDiaParser = useMsDiaParser;
@ -88,12 +88,9 @@ class LoadPdbTask extends Task {
}
};
try {
AutoAnalysisManager.getAnalysisManager(program)
.scheduleWorker(worker, null, true,
monitor);
.scheduleWorker(worker, null, true, monitor);
if (log.hasMessages()) {
MultiLineMessageDialog dialog = new MultiLineMessageDialog("Load PDB File",
"There were warnings/errors loading the PDB file.", log.toString(),
@ -151,9 +148,8 @@ class LoadPdbTask extends Task {
pdbApplicatorOptions.setRestrictions(restrictions);
try (AbstractPdb pdb =
ghidra.app.util.bin.format.pdb2.pdbreader.PdbParser.parse(pdbFile.getAbsolutePath(),
pdbReaderOptions, monitor)) {
try (AbstractPdb pdb = ghidra.app.util.bin.format.pdb2.pdbreader.PdbParser
.parse(pdbFile.getAbsolutePath(), pdbReaderOptions, monitor)) {
monitor.setMessage("PDB: Parsing " + pdbFile + "...");
pdb.deserialize(monitor);
PdbApplicator applicator = new PdbApplicator(pdbFile.getAbsolutePath(), pdb);

View file

@ -20,6 +20,7 @@ import java.io.File;
import docking.action.MenuData;
import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import ghidra.app.CorePluginPackage;
import ghidra.app.context.ProgramActionContext;
import ghidra.app.context.ProgramContextAction;
@ -131,11 +132,14 @@ public class PdbPlugin extends Plugin {
return;
}
TaskLauncher
.launch(new LoadPdbTask(program, pdb, useMsDiaParser, restrictions, service));
// note: We intentionally use a 0-delay here. Our underlying task may show modal
// dialog prompts. We want the task progress dialog to be showing before any
// promts appear.
LoadPdbTask task = new LoadPdbTask(program, pdb, useMsDiaParser, restrictions, service);
new TaskLauncher(task, null, 0);
}
catch (Exception pe) {
Msg.showError(getClass(), null, "Error", pe.getMessage());
Msg.showError(getClass(), null, "Error Loading PDB", pe.getMessage(), pe);
}
}
@ -145,7 +149,7 @@ public class PdbPlugin extends Plugin {
pdbChooser = new GhidraFileChooser(tool.getToolFrame());
pdbChooser.setTitle("Select PDB file to load:");
pdbChooser.setApproveButtonText("Select PDB");
pdbChooser.setFileSelectionMode(GhidraFileChooser.FILES_ONLY);
pdbChooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY);
pdbChooser.setFileFilter(new ExtensionFileFilter(new String[] { "pdb", "xml" },
"Program Database Files and PDB XML Representations"));
}

View file

@ -17,19 +17,20 @@ package ghidra.util.task;
import java.awt.BorderLayout;
import java.awt.Component;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.*;
import javax.swing.JPanel;
import docking.DialogComponentProvider;
import docking.DockingWindowManager;
import docking.tool.ToolConstants;
import docking.widgets.OptionDialog;
import ghidra.util.HelpLocation;
import ghidra.util.Swing;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.timer.GTimer;
import ghidra.util.timer.GTimerMonitor;
/**
* Dialog that is displayed to show activity for a Task that is running outside of the
@ -48,25 +49,29 @@ import ghidra.util.timer.GTimer;
*/
public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
/** Timer used to give the task a chance to complete */
private static final int SLEEPY_TIME = 10;
/** Amount of time to wait before showing the monitor dialog */
private final static int MAX_DELAY = 200000;
public final static int DEFAULT_WIDTH = 275;
private Timer showTimer;
private AtomicInteger taskID = new AtomicInteger();
private Runnable closeDialog;
private Component centerOnComp;
private Runnable shouldCancelRunnable;
private boolean taskDone;
private Runnable closeDialog = () -> {
close();
dispose();
};
private Runnable verifyCancel = () -> {
if (promptToVerifyCancel()) {
cancel();
}
};
private GTimerMonitor showTimer;
private CountDownLatch finished = new CountDownLatch(1);
private boolean supportsProgress;
private JPanel mainPanel;
private JPanel activityPanel;
private TaskMonitorComponent monitorComponent;
private Component centerOnComponent;
/** If not null, then the value of the string has yet to be rendered */
private AtomicReference<String> newMessage = new AtomicReference<>();
@ -120,7 +125,7 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
private TaskDialog(Component centerOnComp, String title, boolean isModal, boolean canCancel,
boolean hasProgress) {
super(title, isModal, true, canCancel, true);
this.centerOnComp = centerOnComp;
this.centerOnComponent = centerOnComp;
this.supportsProgress = hasProgress;
setup(canCancel);
}
@ -133,19 +138,6 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
setRememberLocation(false);
setRememberSize(false);
setTransient(true);
closeDialog = () -> {
close();
dispose();
};
shouldCancelRunnable = () -> {
int currentTaskID = taskID.get();
boolean doCancel = promptToVerifyCancel();
if (doCancel && currentTaskID == taskID.get()) {
cancel();
}
};
mainPanel = new JPanel(new BorderLayout());
addWorkPanel(mainPanel);
@ -161,7 +153,6 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
addCancelButton();
}
// SPLIT the help for this dialog should not be in the front end plugin.
setHelpLocation(new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "TaskDialog"));
}
@ -199,22 +190,9 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
});
}
@Override
protected void dialogShown() {
// our task may have completed while we were queued up to be shown
if (isCompleted()) {
close();
}
}
@Override
protected void dialogClosed() {
close();
}
@Override
protected void cancelCallback() {
SwingUtilities.invokeLater(shouldCancelRunnable);
Swing.runLater(verifyCancel);
}
@Override
@ -228,19 +206,17 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
return monitorComponent.isCancelEnabled();
}
/**
* Called after the task has been executed
*/
public synchronized void taskProcessed() {
taskDone = true;
finished.countDown();
monitorComponent.notifyChangeListeners();
SwingUtilities.invokeLater(closeDialog);
}
public synchronized void reset() {
taskDone = false;
taskID.incrementAndGet();
Swing.runLater(closeDialog);
}
public synchronized boolean isCompleted() {
return taskDone;
return finished.getCount() == 0;
}
/**
@ -256,7 +232,6 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
else {
doShowNonModal(delay);
}
}
private void doShowModal(int delay) {
@ -278,7 +253,8 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
// Note: we must not block, as we are not modal. Clients want control back. Our job is
// only to show a progress dialog if enough time has elapsed.
//
GTimer.scheduleRunnable(delay, () -> {
int waitTime = Math.min(delay, MAX_DELAY);
showTimer = GTimer.scheduleRunnable(waitTime, () -> {
if (isCompleted()) {
return;
}
@ -288,31 +264,32 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
}
protected void doShow() {
Swing.runIfSwingOrRunLater(() -> {
DockingWindowManager.showDialog(centerOnComp, TaskDialog.this);
if (!isCompleted()) {
DockingWindowManager.showDialog(centerOnComponent, TaskDialog.this);
}
});
}
private void giveTheTaskThreadAChanceToComplete(int delay) {
delay = Math.min(delay, MAX_DELAY);
int elapsedTime = 0;
while (!isCompleted() && elapsedTime < delay) {
int waitTime = Math.min(delay, MAX_DELAY);
try {
Thread.sleep(SLEEPY_TIME);
finished.await(waitTime, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
// don't care; we will try again
}
elapsedTime += SLEEPY_TIME;
Msg.debug(this, "Interrupted waiting for task '" + getTitle() + "'", e);
}
}
public void dispose() {
messageUpdater.dispose();
Runnable disposeTask = () -> {
if (showTimer != null) {
showTimer.stop();
showTimer.cancel();
showTimer = null;
}
};

View file

@ -49,8 +49,7 @@ class TaskRunner {
BasicTaskMonitor internalMonitor = new BasicTaskMonitor();
WrappingTaskMonitor monitor = new WrappingTaskMonitor(internalMonitor);
startTaskThread(monitor);
Swing.runIfSwingOrRunLater(() -> showTaskDialog(monitor));
showTaskDialog(monitor);
waitForModalTask();
}
@ -124,11 +123,11 @@ class TaskRunner {
private void showTaskDialog(WrappingTaskMonitor monitor) {
Swing.assertSwingThread("Must be on the Swing thread build the Task Dialog");
Swing.runIfSwingOrRunLater(() -> {
taskDialog = buildTaskDialog(parent, monitor);
monitor.setDelegate(taskDialog); // initialize the dialog to the current state of the monitor
monitor.setDelegate(taskDialog); // initialize the dialog to the current monitor state
taskDialog.show(Math.max(delayMs, 0));
});
}
/*testing*/ boolean isFinished() {

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.
@ -16,50 +15,53 @@
*/
package generic.cache;
import java.util.*;
import ghidra.util.timer.GTimer;
import ghidra.util.timer.GTimerMonitor;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A thread-safe pool class that knows how to create instances as needed. When clients are done
* with the pooled item they then call {@link #release(Object)}.
* A thread-safe pool that knows how to create instances as needed. When clients are done
* with the pooled item they then call {@link #release(Object)}, thus enabling them to be
* re-used in the future.
*
* <p>Calling {@link #setCleanupTimeout(long)} with a non-negative value will start a timer when
* {@link #release(Object)} is called to {@link BasicFactory#dispose(Object)} any objects in the
* pool. By default, the cleanup timer does not run.
*
* <p>Once {@link #dispose()} has been called on this class, items created or released will no
* longer be pooled.
*
* @param <T> the type of object to pool
*/
public class CachingPool<T> {
private static final long TIMEOUT = 0;
// Use -1 to signal the cleanup timer should not be used
private static final long TIMEOUT = -1;
private AtomicBoolean isDisposed = new AtomicBoolean(false);
private boolean isDisposed;
private BasicFactory<T> factory;
private Deque<T> cache = new ArrayDeque<T>();
private long disposeTimeout = TIMEOUT;
private GTimerMonitor timerMonitor;
private Runnable cleanupRunnable = new Runnable() {
@Override
public void run() {
synchronized (CachingPool.this) {
for (T t : cache) {
factory.dispose(t);
}
}
}
};
/**
* Creates a new pool that uses the given factory to create new items as needed
*
* @param factory the factory used to create new items
*/
public CachingPool(BasicFactory<T> factory) {
if (factory == null) {
throw new IllegalArgumentException("factory cannot be null");
}
this.factory = factory;
this.factory = Objects.requireNonNull(factory);
}
/**
* Sets the time to wait for released items to be automatically disposed. The
* default is {@link #TIMEOUT}.
* Sets the time to wait for released items to be disposed by this pool by calling
* {@link BasicFactory#dispose(Object)}. A negative timeout value signals to disable
* the cleanup task.
*
* <p>When clients call {@link #get()}, the timer will not be running. It will be restarted
* again once {@link #release(Object)} has been called.
*
* @param timeout the new timeout.
*/
@ -68,47 +70,60 @@ public class CachingPool<T> {
}
/**
* Returns a cached or new {@link T}
* Returns a cached or new {@code T}
*
* @return a cached or new {@link T}
* @return a cached or new {@code T}
* @throws Exception if there is a problem instantiating a new instance
*/
public synchronized T get() throws Exception {
cancel();
if (cache.isEmpty()) {
stopCleanupTimer();
if (cache.isEmpty() || isDisposed) {
return factory.create();
}
return cache.pop();
}
/**
* Signals that the given object is no longer being used. The object will be placed back into
* the pool until it is disposed via the cleanup timer, if it is running.
* @param t the item to release
*/
public synchronized void release(T t) {
restart();
if (isDisposed.get()) {
restartCleanupTimer();
if (isDisposed) {
factory.dispose(t);
return;
}
cache.push(t);
}
/**
* Triggers all pooled object to be disposed via this pool's factory. Future calls to
* {@link #get()} will still create new objects, but the internal cache will no longer be used.
*/
public synchronized void dispose() {
cancel();
isDisposed.set(true);
stopCleanupTimer();
isDisposed = true;
disposeCachedItems();
}
private synchronized void disposeCachedItems() {
for (T t : cache) {
factory.dispose(t);
}
}
private void cancel() {
private void stopCleanupTimer() {
if (timerMonitor != null) {
timerMonitor.cancel();
}
}
private void restart() {
private void restartCleanupTimer() {
if (timerMonitor != null) {
timerMonitor.cancel();
}
timerMonitor = GTimer.scheduleRunnable(disposeTimeout, cleanupRunnable);
timerMonitor = GTimer.scheduleRunnable(disposeTimeout, this::disposeCachedItems);
}
}

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.
@ -16,23 +15,32 @@
*/
package ghidra.util.timer;
import ghidra.util.Msg;
import java.util.Timer;
import java.util.TimerTask;
import ghidra.util.Msg;
/**
* A class to schedule {@link Runnable}s to run after some delay, optionally repeating. This class
* uses a {@link Timer} internally to schedule work. Clients of this class are given a monitor
* that allows them to check on the state of the runnable, as well as to cancel the runnable.
*/
public class GTimer {
private static Timer timer;
private static GTimerMonitor DO_NOTHING_MONITOR = new DoNothingMonitor();
/**
* Schedules a runnable for execution after the specified delay.
* @param delay the time (in milliseconds) to wait before executing the runnable.
* Schedules a runnable for execution after the specified delay. A delay value less than 0
* will cause this timer to schedule nothing. This allows clients to use this timer class
* with no added logic for managing timer enablement.
*
* @param delay the time (in milliseconds) to wait before executing the runnable. A negative
* value signals not to run the timer--the callback will not be executed
* @param callback the runnable to be executed.
* @return a GTimerMonitor which allows the caller to cancel the timer and check its status.
*/
public static GTimerMonitor scheduleRunnable(long delay, Runnable callback) {
if (delay <= 0) {
if (delay < 0) {
return DO_NOTHING_MONITOR;
}
GTimerTask gTimerTask = new GTimerTask(callback);
@ -41,14 +49,22 @@ public class GTimer {
}
/**
* Schedules a runnable for <b>repeated</b> execution after the specified delay.
* Schedules a runnable for <b>repeated</b> execution after the specified delay. A delay value
* less than 0 will cause this timer to schedule nothing. This allows clients to use this
* timer class with no added logic for managing timer enablement.
*
* @param delay the time (in milliseconds) to wait before executing the runnable.
* @param period time in milliseconds between successive runnable executions.
* @param callback the runnable to be executed.
* @return a GTimerMonitor which allows the caller to cancel the timer and check its status.
* @param delay the time (in milliseconds) to wait before executing the runnable. A negative
* value signals not to run the timer--the callback will not be executed
* @param period time in milliseconds between successive runnable executions
* @param callback the runnable to be executed
* @return a GTimerMonitor which allows the caller to cancel the timer and check its status
* @throws IllegalArgumentException if {@code period <= 0}
*/
public static GTimerMonitor scheduleRepeatingRunnable(long delay, long period, Runnable callback) {
public static GTimerMonitor scheduleRepeatingRunnable(long delay, long period,
Runnable callback) {
if (delay < 0) {
return DO_NOTHING_MONITOR;
}
GTimerTask gTimerTask = new GTimerTask(callback);
getTimer().schedule(gTimerTask, delay, period);
return gTimerTask;

View file

@ -34,12 +34,13 @@ class LockingTaskMonitor implements TaskMonitor {
private MyTaskDialog taskDialog;
/**
* Constructs a locking task handler for a locked dobj. The setCompleted() method must be
* invoked to dispose this object and release the lock. This should
* be done in a try/finally block to avoid accidentally locking the
* Constructs a locking task handler for a locked domain object. The
* {@link #releaseLock()} method must be invoked to dispose this object and release the
* lock. This should be done in a try/finally block to avoid accidentally locking the
* domain object indefinitely.
*
* @param dobj domain object
* @param hasProgress true if this monitorhas progress
* @param hasProgress true if this monitor has progress
* @param title task title
*/
LockingTaskMonitor(DomainObjectAdapterDB dobj, boolean hasProgress, String title) {
@ -84,9 +85,6 @@ class LockingTaskMonitor implements TaskMonitor {
}
}
/*
* @see ghidra.util.task.TaskMonitor#isCancelled()
*/
@Override
public synchronized boolean isCancelled() {
return taskDialog != null ? taskDialog.isCancelled() : isCanceled;
@ -108,9 +106,6 @@ class LockingTaskMonitor implements TaskMonitor {
notifyAll();
}
/*
* @see ghidra.util.task.TaskMonitor#setMessage(java.lang.String)
*/
@Override
public synchronized void setMessage(String msg) {
this.msg = msg;
@ -124,9 +119,6 @@ class LockingTaskMonitor implements TaskMonitor {
return msg;
}
/*
* @see ghidra.util.task.TaskMonitor#setProgress(int)
*/
@Override
public synchronized void setProgress(long value) {
this.curProgress = value;
@ -175,9 +167,6 @@ class LockingTaskMonitor implements TaskMonitor {
return indeterminate;
}
/*
* @see ghidra.util.task.TaskMonitor#setCancelEnabled(boolean)
*/
@Override
public synchronized void setCancelEnabled(boolean enable) {
this.cancelEnabled = enable;
@ -186,17 +175,11 @@ class LockingTaskMonitor implements TaskMonitor {
}
}
/**
* @see ghidra.util.task.TaskMonitor#isCancelEnabled()
*/
@Override
public synchronized boolean isCancelEnabled() {
return taskDialog != null ? taskDialog.isCancelEnabled() : cancelEnabled;
}
/*
* @see ghidra.util.task.TaskMonitor#cancel()
*/
@Override
public synchronized void cancel() {
this.isCanceled = true;
@ -205,9 +188,6 @@ class LockingTaskMonitor implements TaskMonitor {
}
}
/*
* @see ghidra.util.task.TaskMonitor#clearCanceled()
*/
@Override
public void clearCanceled() {
this.isCanceled = false;