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

View file

@ -20,6 +20,7 @@ import java.io.File;
import docking.action.MenuData; import docking.action.MenuData;
import docking.widgets.OptionDialog; import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import ghidra.app.CorePluginPackage; import ghidra.app.CorePluginPackage;
import ghidra.app.context.ProgramActionContext; import ghidra.app.context.ProgramActionContext;
import ghidra.app.context.ProgramContextAction; import ghidra.app.context.ProgramContextAction;
@ -131,11 +132,14 @@ public class PdbPlugin extends Plugin {
return; return;
} }
TaskLauncher // note: We intentionally use a 0-delay here. Our underlying task may show modal
.launch(new LoadPdbTask(program, pdb, useMsDiaParser, restrictions, service)); // 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) { 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 = new GhidraFileChooser(tool.getToolFrame());
pdbChooser.setTitle("Select PDB file to load:"); pdbChooser.setTitle("Select PDB file to load:");
pdbChooser.setApproveButtonText("Select PDB"); pdbChooser.setApproveButtonText("Select PDB");
pdbChooser.setFileSelectionMode(GhidraFileChooser.FILES_ONLY); pdbChooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY);
pdbChooser.setFileFilter(new ExtensionFileFilter(new String[] { "pdb", "xml" }, pdbChooser.setFileFilter(new ExtensionFileFilter(new String[] { "pdb", "xml" },
"Program Database Files and PDB XML Representations")); "Program Database Files and PDB XML Representations"));
} }

View file

@ -1757,7 +1757,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
This method seeks to accomplish 2 goals: This method seeks to accomplish 2 goals:
1) find a suitable component over which to center, and 1) find a suitable component over which to center, and
2) ensure that the chosen component is in the parent hierarchy 2) ensure that the chosen component is in the parent hierarchy
*/ */
Component bestComponent = centeredOnComponent; Component bestComponent = centeredOnComponent;
if (SwingUtilities.isDescendingFrom(parent, bestComponent)) { if (SwingUtilities.isDescendingFrom(parent, bestComponent)) {
@ -1791,7 +1791,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
/* /*
Note: Which window should be the parent of the dialog when the user does not specify? Note: Which window should be the parent of the dialog when the user does not specify?
Some use cases; a dialog is shown from: Some use cases; a dialog is shown from:
1) A toolbar action 1) A toolbar action
2) A component provider's code 2) A component provider's code
@ -1799,7 +1799,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
4) A background thread 4) A background thread
5) The help window 5) The help window
6) A modal password dialog appears over the splash screen 6) A modal password dialog appears over the splash screen
It seems like the parent should be the active window for 1-2. It seems like the parent should be the active window for 1-2.
Case 3 should probably use the window of the dialog provider. Case 3 should probably use the window of the dialog provider.
Case 4 should probably use the main tool frame, since the user may be Case 4 should probably use the main tool frame, since the user may be
@ -1807,12 +1807,12 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
active window, we can default to the tool's frame. active window, we can default to the tool's frame.
Case 5 should use the help window. Case 5 should use the help window.
Case 6 should use the splash screen as the parent. Case 6 should use the splash screen as the parent.
We have not yet solidified how we should parent. This documentation is meant to We have not yet solidified how we should parent. This documentation is meant to
move us towards clarity as we find Use Cases that don't make sense. (Once we move us towards clarity as we find Use Cases that don't make sense. (Once we
finalize our understanding, we should update the javadoc to list exactly where finalize our understanding, we should update the javadoc to list exactly where
the given Dialog Component will be shown.) the given Dialog Component will be shown.)
Use Case Use Case
A -The user presses an action on a toolbar from a window on screen 1, while the A -The user presses an action on a toolbar from a window on screen 1, while the
main tool frame is on screen 2. We want the popup window to appear on screen main tool frame is on screen 2. We want the popup window to appear on screen
@ -1824,8 +1824,8 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
-modal - Java handles this correctly, allowing the new dialog to be used -modal - Java handles this correctly, allowing the new dialog to be used
-non-modal - Java prevents the non-modal from being editing if not parented -non-modal - Java prevents the non-modal from being editing if not parented
correctly correctly
For now, the easiest mental model to use is to always prefer the active window so For now, the easiest mental model to use is to always prefer the active window so
that a dialog will appear in the user's view. If we find a case where this is that a dialog will appear in the user's view. If we find a case where this is
not desired, then document it here. not desired, then document it here.

View file

@ -17,65 +17,70 @@ package ghidra.util.task;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.Component; 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 java.util.concurrent.atomic.AtomicReference;
import javax.swing.*; import javax.swing.JPanel;
import docking.DialogComponentProvider; import docking.DialogComponentProvider;
import docking.DockingWindowManager; import docking.DockingWindowManager;
import docking.tool.ToolConstants; import docking.tool.ToolConstants;
import docking.widgets.OptionDialog; import docking.widgets.OptionDialog;
import ghidra.util.HelpLocation; import ghidra.util.*;
import ghidra.util.Swing;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.timer.GTimer; 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 * Dialog that is displayed to show activity for a Task that is running outside of the
* Swing Thread. * Swing Thread.
* *
* <p>Implementation note: * <p>Implementation note:
* if this class is constructed with a {@code hasProgress} value of {@code false}, * if this class is constructed with a {@code hasProgress} value of {@code false},
* then an activity component will be shown, not a progress monitor. Any calls to update * then an activity component will be shown, not a progress monitor. Any calls to update
* progress will not affect the display. However, the display can be converted to use progress * progress will not affect the display. However, the display can be converted to use progress
* by first calling {@link #setIndeterminate(boolean)} with a {@code false} value and then calling * by first calling {@link #setIndeterminate(boolean)} with a {@code false} value and then calling
* {@link #initialize(long)}. Once this has happened, this dialog will no longer use the * {@link #initialize(long)}. Once this has happened, this dialog will no longer use the
* activity display--the progress bar is in effect for the duration of this dialog's usage. * activity display--the progress bar is in effect for the duration of this dialog's usage.
* *
* <p>This dialog can be toggled between indeterminate mode and progress mode via calls to * <p>This dialog can be toggled between indeterminate mode and progress mode via calls to
* {@link #setIndeterminate(boolean)}. * {@link #setIndeterminate(boolean)}.
*/ */
public class TaskDialog extends DialogComponentProvider implements TaskMonitor { 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 */ /** Amount of time to wait before showing the monitor dialog */
private final static int MAX_DELAY = 200000; private final static int MAX_DELAY = 200000;
public final static int DEFAULT_WIDTH = 275; public final static int DEFAULT_WIDTH = 275;
private Timer showTimer; private Runnable closeDialog = () -> {
private AtomicInteger taskID = new AtomicInteger(); close();
private Runnable closeDialog; dispose();
private Component centerOnComp; };
private Runnable shouldCancelRunnable; private Runnable verifyCancel = () -> {
private boolean taskDone; if (promptToVerifyCancel()) {
cancel();
}
};
private GTimerMonitor showTimer;
private CountDownLatch finished = new CountDownLatch(1);
private boolean supportsProgress; private boolean supportsProgress;
private JPanel mainPanel; private JPanel mainPanel;
private JPanel activityPanel; private JPanel activityPanel;
private TaskMonitorComponent monitorComponent; private TaskMonitorComponent monitorComponent;
private Component centerOnComponent;
/** If not null, then the value of the string has yet to be rendered */ /** If not null, then the value of the string has yet to be rendered */
private AtomicReference<String> newMessage = new AtomicReference<>(); private AtomicReference<String> newMessage = new AtomicReference<>();
private SwingUpdateManager messageUpdater = private SwingUpdateManager messageUpdater =
new SwingUpdateManager(100, 250, () -> setStatusText(newMessage.getAndSet(null))); new SwingUpdateManager(100, 250, () -> setStatusText(newMessage.getAndSet(null)));
/** /**
* Constructor * Constructor
* *
* @param centerOnComp component to be centered over when shown, * @param centerOnComp component to be centered over when shown,
* otherwise center over parent. If both centerOnComp and parent * otherwise center over parent. If both centerOnComp and parent
* are null, dialog will be centered on screen. * are null, dialog will be centered on screen.
@ -88,7 +93,7 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
/** /**
* Constructor * Constructor
* *
* @param task the Task that this dialog will be associated with * @param task the Task that this dialog will be associated with
*/ */
public TaskDialog(Task task) { public TaskDialog(Task task) {
@ -97,7 +102,7 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
/** /**
* Constructor * Constructor
* *
* @param title title for the dialog * @param title title for the dialog
* @param canCancel true if the task can be canceled * @param canCancel true if the task can be canceled
* @param isModal true if the dialog should be modal * @param isModal true if the dialog should be modal
@ -109,8 +114,8 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
/** /**
* Constructor * Constructor
* *
* @param centerOnComp component to be centered over when shown, otherwise center over * @param centerOnComp component to be centered over when shown, otherwise center over
* parent. If both centerOnComp is null, then the active window will be used * parent. If both centerOnComp is null, then the active window will be used
* @param title title for the dialog * @param title title for the dialog
* @param isModal true if the dialog should be modal * @param isModal true if the dialog should be modal
@ -120,7 +125,7 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
private TaskDialog(Component centerOnComp, String title, boolean isModal, boolean canCancel, private TaskDialog(Component centerOnComp, String title, boolean isModal, boolean canCancel,
boolean hasProgress) { boolean hasProgress) {
super(title, isModal, true, canCancel, true); super(title, isModal, true, canCancel, true);
this.centerOnComp = centerOnComp; this.centerOnComponent = centerOnComp;
this.supportsProgress = hasProgress; this.supportsProgress = hasProgress;
setup(canCancel); setup(canCancel);
} }
@ -133,19 +138,6 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
setRememberLocation(false); setRememberLocation(false);
setRememberSize(false); setRememberSize(false);
setTransient(true); setTransient(true);
closeDialog = () -> {
close();
dispose();
};
shouldCancelRunnable = () -> {
int currentTaskID = taskID.get();
boolean doCancel = promptToVerifyCancel();
if (doCancel && currentTaskID == taskID.get()) {
cancel();
}
};
mainPanel = new JPanel(new BorderLayout()); mainPanel = new JPanel(new BorderLayout());
addWorkPanel(mainPanel); addWorkPanel(mainPanel);
@ -161,13 +153,12 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
addCancelButton(); addCancelButton();
} }
// SPLIT the help for this dialog should not be in the front end plugin.
setHelpLocation(new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "TaskDialog")); setHelpLocation(new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "TaskDialog"));
} }
/** /**
* Shows a dialog asking the user if they really, really want to cancel the task * Shows a dialog asking the user if they really, really want to cancel the task
* *
* @return true if the task should be cancelled * @return true if the task should be cancelled
*/ */
private boolean promptToVerifyCancel() { private boolean promptToVerifyCancel() {
@ -188,7 +179,7 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
} }
/** /**
* Adds the panel that contains the activity panel (e.g., the eating bits animation) to the * Adds the panel that contains the activity panel (e.g., the eating bits animation) to the
* dialog. This should only be called if the dialog has no need to display progress. * dialog. This should only be called if the dialog has no need to display progress.
*/ */
private void installActivityDisplay() { private void installActivityDisplay() {
@ -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 @Override
protected void cancelCallback() { protected void cancelCallback() {
SwingUtilities.invokeLater(shouldCancelRunnable); Swing.runLater(verifyCancel);
} }
@Override @Override
@ -228,19 +206,17 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
return monitorComponent.isCancelEnabled(); return monitorComponent.isCancelEnabled();
} }
/**
* Called after the task has been executed
*/
public synchronized void taskProcessed() { public synchronized void taskProcessed() {
taskDone = true; finished.countDown();
monitorComponent.notifyChangeListeners(); monitorComponent.notifyChangeListeners();
SwingUtilities.invokeLater(closeDialog); Swing.runLater(closeDialog);
}
public synchronized void reset() {
taskDone = false;
taskID.incrementAndGet();
} }
public synchronized boolean isCompleted() { public synchronized boolean isCompleted() {
return taskDone; return finished.getCount() == 0;
} }
/** /**
@ -256,7 +232,6 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
else { else {
doShowNonModal(delay); doShowNonModal(delay);
} }
} }
private void doShowModal(int 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 // 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. // 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()) { if (isCompleted()) {
return; return;
} }
@ -288,31 +264,32 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor {
} }
protected void doShow() { protected void doShow() {
Swing.runIfSwingOrRunLater(() -> { Swing.runIfSwingOrRunLater(() -> {
DockingWindowManager.showDialog(centerOnComp, TaskDialog.this); if (!isCompleted()) {
DockingWindowManager.showDialog(centerOnComponent, TaskDialog.this);
}
}); });
} }
private void giveTheTaskThreadAChanceToComplete(int delay) { private void giveTheTaskThreadAChanceToComplete(int delay) {
delay = Math.min(delay, MAX_DELAY); int waitTime = Math.min(delay, MAX_DELAY);
int elapsedTime = 0; try {
while (!isCompleted() && elapsedTime < delay) { finished.await(waitTime, TimeUnit.MILLISECONDS);
try { }
Thread.sleep(SLEEPY_TIME); catch (InterruptedException e) {
} Msg.debug(this, "Interrupted waiting for task '" + getTitle() + "'", e);
catch (InterruptedException e) {
// don't care; we will try again
}
elapsedTime += SLEEPY_TIME;
} }
} }
public void dispose() { public void dispose() {
messageUpdater.dispose();
Runnable disposeTask = () -> { Runnable disposeTask = () -> {
if (showTimer != null) { if (showTimer != null) {
showTimer.stop(); showTimer.cancel();
showTimer = null; showTimer = null;
} }
}; };

View file

@ -24,7 +24,7 @@ import generic.util.WindowUtilities;
import ghidra.util.*; import ghidra.util.*;
/** /**
* Helper class to launch the given task in a background thread, showing a task dialog if * Helper class to launch the given task in a background thread, showing a task dialog if
* this task takes to long. See {@link TaskLauncher}. * this task takes to long. See {@link TaskLauncher}.
*/ */
class TaskRunner { class TaskRunner {
@ -49,8 +49,7 @@ class TaskRunner {
BasicTaskMonitor internalMonitor = new BasicTaskMonitor(); BasicTaskMonitor internalMonitor = new BasicTaskMonitor();
WrappingTaskMonitor monitor = new WrappingTaskMonitor(internalMonitor); WrappingTaskMonitor monitor = new WrappingTaskMonitor(internalMonitor);
startTaskThread(monitor); startTaskThread(monitor);
showTaskDialog(monitor);
Swing.runIfSwingOrRunLater(() -> showTaskDialog(monitor));
waitForModalTask(); waitForModalTask();
} }
@ -112,7 +111,7 @@ class TaskRunner {
return new TaskDialog(centerOverComponent, task) { return new TaskDialog(centerOverComponent, task) {
// note: we override this method here to help with the race condition where the // 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 // TaskRunner does not yet know about the task dialog, but the background
// thread has actually finished the work. // thread has actually finished the work.
@Override @Override
@ -124,11 +123,11 @@ class TaskRunner {
private void showTaskDialog(WrappingTaskMonitor monitor) { private void showTaskDialog(WrappingTaskMonitor monitor) {
Swing.assertSwingThread("Must be on the Swing thread build the Task Dialog"); Swing.runIfSwingOrRunLater(() -> {
taskDialog = buildTaskDialog(parent, monitor);
taskDialog = buildTaskDialog(parent, monitor); monitor.setDelegate(taskDialog); // initialize the dialog to the current monitor state
monitor.setDelegate(taskDialog); // initialize the dialog to the current state of the monitor taskDialog.show(Math.max(delayMs, 0));
taskDialog.show(Math.max(delayMs, 0)); });
} }
/*testing*/ boolean isFinished() { /*testing*/ boolean isFinished() {
@ -138,7 +137,7 @@ class TaskRunner {
private void taskFinished() { private void taskFinished() {
finished.countDown(); finished.countDown();
// Do this later on the Swing thread to handle the race condition where the dialog // Do this later on the Swing thread to handle the race condition where the dialog
// did not exist at the time of this call, but was in the process of being created // did not exist at the time of this call, but was in the process of being created
Swing.runLater(() -> { Swing.runLater(() -> {
if (taskDialog != null) { if (taskDialog != null) {

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,51 +15,54 @@
*/ */
package generic.cache; package generic.cache;
import java.util.*;
import ghidra.util.timer.GTimer; import ghidra.util.timer.GTimer;
import ghidra.util.timer.GTimerMonitor; 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 * 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)}. * 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 * @param <T> the type of object to pool
*/ */
public class CachingPool<T> { 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 BasicFactory<T> factory;
private Deque<T> cache = new ArrayDeque<T>(); private Deque<T> cache = new ArrayDeque<T>();
private long disposeTimeout = TIMEOUT; private long disposeTimeout = TIMEOUT;
private GTimerMonitor timerMonitor; 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) { public CachingPool(BasicFactory<T> factory) {
if (factory == null) { this.factory = Objects.requireNonNull(factory);
throw new IllegalArgumentException("factory cannot be null");
}
this.factory = factory;
} }
/** /**
* Sets the time to wait for released items to be automatically disposed. The * Sets the time to wait for released items to be disposed by this pool by calling
* default is {@link #TIMEOUT}. * {@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. * @param timeout the new timeout.
*/ */
public void setCleanupTimeout(long timeout) { public void setCleanupTimeout(long 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 * @throws Exception if there is a problem instantiating a new instance
*/ */
public synchronized T get() throws Exception { public synchronized T get() throws Exception {
cancel(); stopCleanupTimer();
if (cache.isEmpty()) { if (cache.isEmpty() || isDisposed) {
return factory.create(); return factory.create();
} }
return cache.pop(); 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) { public synchronized void release(T t) {
restart(); restartCleanupTimer();
if (isDisposed.get()) { if (isDisposed) {
factory.dispose(t); factory.dispose(t);
return; return;
} }
cache.push(t); 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() { public synchronized void dispose() {
cancel(); stopCleanupTimer();
isDisposed.set(true); isDisposed = true;
disposeCachedItems();
}
private synchronized void disposeCachedItems() {
for (T t : cache) { for (T t : cache) {
factory.dispose(t); factory.dispose(t);
} }
} }
private void cancel() { private void stopCleanupTimer() {
if (timerMonitor != null) { if (timerMonitor != null) {
timerMonitor.cancel(); timerMonitor.cancel();
} }
} }
private void restart() { private void restartCleanupTimer() {
if (timerMonitor != null) { if (timerMonitor != null) {
timerMonitor.cancel(); timerMonitor.cancel();
} }
timerMonitor = GTimer.scheduleRunnable(disposeTimeout, cleanupRunnable); timerMonitor = GTimer.scheduleRunnable(disposeTimeout, this::disposeCachedItems);
} }
} }

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,23 +15,32 @@
*/ */
package ghidra.util.timer; package ghidra.util.timer;
import ghidra.util.Msg;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; 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 { public class GTimer {
private static Timer timer; private static Timer timer;
private static GTimerMonitor DO_NOTHING_MONITOR = new DoNothingMonitor(); private static GTimerMonitor DO_NOTHING_MONITOR = new DoNothingMonitor();
/** /**
* Schedules a runnable for execution after the specified delay. * Schedules a runnable for execution after the specified delay. A delay value less than 0
* @param delay the time (in milliseconds) to wait before executing the runnable. * 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. * @param callback the runnable to be executed.
* @return a GTimerMonitor which allows the caller to cancel the timer and check its status. * @return a GTimerMonitor which allows the caller to cancel the timer and check its status.
*/ */
public static GTimerMonitor scheduleRunnable(long delay, Runnable callback) { public static GTimerMonitor scheduleRunnable(long delay, Runnable callback) {
if (delay <= 0) { if (delay < 0) {
return DO_NOTHING_MONITOR; return DO_NOTHING_MONITOR;
} }
GTimerTask gTimerTask = new GTimerTask(callback); 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
* @param delay the time (in milliseconds) to wait before executing the runnable. * timer class with no added logic for managing timer enablement.
* @param period time in milliseconds between successive runnable executions. *
* @param callback the runnable to be executed. * @param delay the time (in milliseconds) to wait before executing the runnable. A negative
* @return a GTimerMonitor which allows the caller to cancel the timer and check its status. * 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); GTimerTask gTimerTask = new GTimerTask(callback);
getTimer().schedule(gTimerTask, delay, period); getTimer().schedule(gTimerTask, delay, period);
return gTimerTask; return gTimerTask;

View file

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