mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +02:00
Merge remote-tracking branch 'origin/GT-3457-dragonmacher-gtree-cannot-find-new-node'
This commit is contained in:
commit
060b06b688
11 changed files with 400 additions and 93 deletions
|
@ -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,14 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.app.plugin.core.datamgr.actions;
|
package ghidra.app.plugin.core.datamgr.actions;
|
||||||
|
|
||||||
import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin;
|
|
||||||
import ghidra.app.plugin.core.datamgr.DataTypesActionContext;
|
|
||||||
import ghidra.app.plugin.core.datamgr.archive.Archive;
|
|
||||||
import ghidra.app.plugin.core.datamgr.tree.*;
|
|
||||||
import ghidra.program.model.data.Category;
|
|
||||||
import ghidra.program.model.data.DataTypeManager;
|
|
||||||
import ghidra.util.InvalidNameException;
|
|
||||||
|
|
||||||
import javax.swing.tree.TreePath;
|
import javax.swing.tree.TreePath;
|
||||||
|
|
||||||
import docking.ActionContext;
|
import docking.ActionContext;
|
||||||
|
@ -31,6 +22,13 @@ import docking.action.DockingAction;
|
||||||
import docking.action.MenuData;
|
import docking.action.MenuData;
|
||||||
import docking.widgets.tree.GTree;
|
import docking.widgets.tree.GTree;
|
||||||
import docking.widgets.tree.GTreeNode;
|
import docking.widgets.tree.GTreeNode;
|
||||||
|
import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin;
|
||||||
|
import ghidra.app.plugin.core.datamgr.DataTypesActionContext;
|
||||||
|
import ghidra.app.plugin.core.datamgr.archive.Archive;
|
||||||
|
import ghidra.app.plugin.core.datamgr.tree.*;
|
||||||
|
import ghidra.program.model.data.Category;
|
||||||
|
import ghidra.program.model.data.DataTypeManager;
|
||||||
|
import ghidra.util.InvalidNameException;
|
||||||
|
|
||||||
public class CreateCategoryAction extends DockingAction {
|
public class CreateCategoryAction extends DockingAction {
|
||||||
|
|
||||||
|
@ -89,12 +87,12 @@ public class CreateCategoryAction extends DockingAction {
|
||||||
ArchiveNode archiveNode = node.getArchiveNode();
|
ArchiveNode archiveNode = node.getArchiveNode();
|
||||||
Archive archive = archiveNode.getArchive();
|
Archive archive = archiveNode.getArchive();
|
||||||
DataTypeManager dataTypeManager = archive.getDataTypeManager();
|
DataTypeManager dataTypeManager = archive.getDataTypeManager();
|
||||||
|
|
||||||
|
String newNodeName = null;
|
||||||
int transactionID = dataTypeManager.startTransaction("Create Category");
|
int transactionID = dataTypeManager.startTransaction("Create Category");
|
||||||
try {
|
try {
|
||||||
final String newNodeName = getUniqueCategoryName(category);
|
newNodeName = getUniqueCategoryName(category);
|
||||||
category.createCategory(newNodeName);
|
category.createCategory(newNodeName);
|
||||||
dataTypeManager.flushEvents();
|
|
||||||
gtree.startEditing(node, newNodeName);
|
|
||||||
}
|
}
|
||||||
catch (InvalidNameException ie) {
|
catch (InvalidNameException ie) {
|
||||||
// can't happen since we created a unique valid name.
|
// can't happen since we created a unique valid name.
|
||||||
|
@ -102,6 +100,9 @@ public class CreateCategoryAction extends DockingAction {
|
||||||
finally {
|
finally {
|
||||||
dataTypeManager.endTransaction(transactionID, true);
|
dataTypeManager.endTransaction(transactionID, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataTypeManager.flushEvents();
|
||||||
|
gtree.startEditing(node, newNodeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getUniqueCategoryName(Category parent) {
|
private String getUniqueCategoryName(Category parent) {
|
||||||
|
|
|
@ -26,7 +26,6 @@ import ghidra.app.plugin.core.datamgr.DataTypesActionContext;
|
||||||
import ghidra.app.plugin.core.datamgr.tree.*;
|
import ghidra.app.plugin.core.datamgr.tree.*;
|
||||||
import ghidra.program.model.data.*;
|
import ghidra.program.model.data.*;
|
||||||
import ghidra.util.StringUtilities;
|
import ghidra.util.StringUtilities;
|
||||||
import ghidra.util.SystemUtilities;
|
|
||||||
|
|
||||||
public class CreateTypeDefAction extends AbstractTypeDefAction {
|
public class CreateTypeDefAction extends AbstractTypeDefAction {
|
||||||
|
|
||||||
|
@ -130,7 +129,7 @@ public class CreateTypeDefAction extends AbstractTypeDefAction {
|
||||||
|
|
||||||
GTreeNode finalParentNode = info.getParentNode();
|
GTreeNode finalParentNode = info.getParentNode();
|
||||||
String newNodeName = newTypeDef.getName();
|
String newNodeName = newTypeDef.getName();
|
||||||
SystemUtilities.runSwingLater(() -> gTree.startEditing(finalParentNode, newNodeName));
|
gTree.startEditing(finalParentNode, newNodeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getBaseName(DataType dt) {
|
private static String getBaseName(DataType dt) {
|
||||||
|
|
|
@ -339,11 +339,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
|
||||||
sb.append("Parent namespace " + namespace.getName() +
|
sb.append("Parent namespace " + namespace.getName() +
|
||||||
" contains namespace named " + symbol.getName() + "\n");
|
" contains namespace named " + symbol.getName() + "\n");
|
||||||
}
|
}
|
||||||
catch (InvalidInputException e) {
|
catch (InvalidInputException | CircularDependencyException e) {
|
||||||
sb.append("Could not change parent namespace for " + symbol.getName() + ": " +
|
|
||||||
e.getMessage() + "\n");
|
|
||||||
}
|
|
||||||
catch (CircularDependencyException e) {
|
|
||||||
sb.append("Could not change parent namespace for " + symbol.getName() + ": " +
|
sb.append("Could not change parent namespace for " + symbol.getName() + ": " +
|
||||||
e.getMessage() + "\n");
|
e.getMessage() + "\n");
|
||||||
}
|
}
|
||||||
|
@ -408,7 +404,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
|
||||||
private void addTask(GTreeTask task) {
|
private void addTask(GTreeTask task) {
|
||||||
// Note: if we want to call this method from off the Swing thread, then we have to
|
// Note: if we want to call this method from off the Swing thread, then we have to
|
||||||
// synchronize on the list that we are adding to here.
|
// synchronize on the list that we are adding to here.
|
||||||
SystemUtilities.assertThisIsTheSwingThread(
|
Swing.assertThisIsTheSwingThread(
|
||||||
"Adding tasks must be done on the Swing thread," +
|
"Adding tasks must be done on the Swing thread," +
|
||||||
"since they are put into a list that is processed on the Swing thread. ");
|
"since they are put into a list that is processed on the Swing thread. ");
|
||||||
|
|
||||||
|
@ -631,6 +627,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
|
||||||
private class SymbolTreeProviderDomainObjectListener implements DomainObjectListener {
|
private class SymbolTreeProviderDomainObjectListener implements DomainObjectListener {
|
||||||
@Override
|
@Override
|
||||||
public void domainObjectChanged(DomainObjectChangedEvent event) {
|
public void domainObjectChanged(DomainObjectChangedEvent event) {
|
||||||
|
|
||||||
if (!tool.isVisible(SymbolTreeProvider.this)) {
|
if (!tool.isVisible(SymbolTreeProvider.this)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import java.util.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.Timer;
|
import javax.swing.Timer;
|
||||||
|
@ -44,6 +45,7 @@ import docking.widgets.tree.internal.*;
|
||||||
import docking.widgets.tree.support.*;
|
import docking.widgets.tree.support.*;
|
||||||
import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin;
|
import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin;
|
||||||
import docking.widgets.tree.tasks.*;
|
import docking.widgets.tree.tasks.*;
|
||||||
|
import generic.timer.ExpiringSwingTimer;
|
||||||
import ghidra.util.*;
|
import ghidra.util.*;
|
||||||
import ghidra.util.exception.AssertException;
|
import ghidra.util.exception.AssertException;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
|
@ -978,23 +980,45 @@ public class GTree extends JPanel implements BusyListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addGTModelListener(TreeModelListener listener) {
|
public void addGTModelListener(TreeModelListener listener) {
|
||||||
tree.getModel().addTreeModelListener(listener);
|
model.addTreeModelListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeGTModelListener(TreeModelListener listener) {
|
public void removeGTModelListener(TreeModelListener listener) {
|
||||||
tree.getModel().removeTreeModelListener(listener);
|
model.removeTreeModelListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEditable(boolean editable) {
|
public void setEditable(boolean editable) {
|
||||||
tree.setEditable(editable);
|
tree.setEditable(editable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startEditing(final GTreeNode parent, final String childName) {
|
/**
|
||||||
|
* Requests that the node with the given name, in the given parent, be edited. <b>This
|
||||||
|
* operation (as with many others on this tree) is asynchronous.</b> This request will be
|
||||||
|
* buffered as needed to wait for the given node to be added to the parent, up to a timeout
|
||||||
|
* period.
|
||||||
|
*
|
||||||
|
* @param parent the parent node
|
||||||
|
* @param childName the child node name
|
||||||
|
*/
|
||||||
|
public void startEditing(GTreeNode parent, final String childName) {
|
||||||
|
|
||||||
// we call this here, even though the JTree will do this for us, so that we will trigger
|
// we call this here, even though the JTree will do this for us, so that we will trigger
|
||||||
// a load call before this task is run, in case lazy nodes are involved in this tree,
|
// a load call before this task is run, in case lazy nodes are involved in this tree,
|
||||||
// which must be loaded before we can edit
|
// which must be loaded before we can edit
|
||||||
expandPath(parent);
|
expandPath(parent);
|
||||||
runTask(new GTreeStartEditingTask(GTree.this, tree, parent, childName));
|
|
||||||
|
//
|
||||||
|
// The request to edit the node may be for a node that has not yet been added to this
|
||||||
|
// tree. Further, some clients will buffer events, which means that the node the client
|
||||||
|
// wishes to edit may not yet be in the parent node even if we run this request later on
|
||||||
|
// the Swing thread. To deal with this, we use a construct that will run our request
|
||||||
|
// once the given node has been added to the parent.
|
||||||
|
//
|
||||||
|
BooleanSupplier isReady = () -> parent.getChild(childName) != null;
|
||||||
|
int expireMs = 3000;
|
||||||
|
ExpiringSwingTimer.runWhen(isReady, expireMs, () -> {
|
||||||
|
runTask(new GTreeStartEditingTask(GTree.this, tree, parent, childName));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -28,6 +28,7 @@ import docking.widgets.tree.*;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.SystemUtilities;
|
import ghidra.util.SystemUtilities;
|
||||||
import ghidra.util.exception.AssertException;
|
import ghidra.util.exception.AssertException;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
import util.CollectionUtils;
|
import util.CollectionUtils;
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ public class GTreeStartEditingTask extends GTreeTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(final TaskMonitor monitor) {
|
public void run(final TaskMonitor monitor) throws CancelledException {
|
||||||
runOnSwingThread(() -> {
|
runOnSwingThread(() -> {
|
||||||
if (monitor.isCancelled()) {
|
if (monitor.isCancelled()) {
|
||||||
return; // we can be cancelled while waiting for Swing to run us
|
return; // we can be cancelled while waiting for Swing to run us
|
||||||
|
@ -58,18 +59,21 @@ public class GTreeStartEditingTask extends GTreeTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void edit() {
|
private void edit() {
|
||||||
final GTreeNode child = parent.getChild(childName);
|
|
||||||
if (child == null) {
|
GTreeNode editNode = parent.getChild(childName);
|
||||||
|
if (editNode == null) {
|
||||||
if (tree.isFiltered()) {
|
if (tree.isFiltered()) {
|
||||||
Msg.showWarn(getClass(), tree, "Cannot Edit Tree Node",
|
Msg.showWarn(getClass(), tree, "Cannot Edit Tree Node",
|
||||||
"Cannot edit tree node \"" + childName + "\" while tree is filtered.");
|
"Can't edit tree node \"" + childName + "\" while tree is filtered.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Msg.debug(this,
|
||||||
|
"Can't find node \"" + childName + "\" to edit.");
|
||||||
}
|
}
|
||||||
Msg.debug(this,
|
|
||||||
"Can't find node for \"" + childName + "\". Perhaps it is filtered out?");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TreePath path = child.getTreePath();
|
TreePath path = editNode.getTreePath();
|
||||||
final Set<GTreeNode> childrenBeforeEdit = new HashSet<>(parent.getChildren());
|
final Set<GTreeNode> childrenBeforeEdit = new HashSet<>(parent.getChildren());
|
||||||
|
|
||||||
final CellEditor cellEditor = tree.getCellEditor();
|
final CellEditor cellEditor = tree.getCellEditor();
|
||||||
|
@ -95,7 +99,7 @@ public class GTreeStartEditingTask extends GTreeTask {
|
||||||
* has finished and been applied.
|
* has finished and been applied.
|
||||||
*/
|
*/
|
||||||
private void reselectNode() {
|
private void reselectNode() {
|
||||||
String newName = child.getName();
|
String newName = editNode.getName();
|
||||||
GTreeNode newChild = parent.getChild(newName);
|
GTreeNode newChild = parent.getChild(newName);
|
||||||
if (newChild == null) {
|
if (newChild == null) {
|
||||||
throw new AssertException("Unable to find new node by name: " + newName);
|
throw new AssertException("Unable to find new node by name: " + newName);
|
||||||
|
@ -140,7 +144,7 @@ public class GTreeStartEditingTask extends GTreeTask {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tree.setNodeEditable(child);
|
tree.setNodeEditable(editNode);
|
||||||
jTree.startEditingAtPath(path);
|
jTree.startEditingAtPath(path);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,13 @@
|
||||||
*/
|
*/
|
||||||
package generic.test;
|
package generic.test;
|
||||||
|
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.*;
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
@ -360,6 +360,17 @@ public abstract class AbstractGTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for the given AtomicBoolean to return true. This is a convenience method for
|
||||||
|
* {@link #waitFor(BooleanSupplier)}.
|
||||||
|
*
|
||||||
|
* @param ab the atomic boolean
|
||||||
|
* @throws AssertionFailedError if the condition is not met within the timeout period
|
||||||
|
*/
|
||||||
|
public static void waitFor(AtomicBoolean ab) throws AssertionFailedError {
|
||||||
|
waitForCondition(() -> ab.get());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for the given condition to return true
|
* Waits for the given condition to return true
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
/* ###
|
||||||
|
* 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 generic.timer;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class allows clients to run swing action at some point in the future, when the given
|
||||||
|
* condition is met, allowing for the task to timeout. While this class implements the
|
||||||
|
* {@link GhidraTimer} interface, it is really meant to be used to execute a code snippet one
|
||||||
|
* time at some point in the future.
|
||||||
|
*
|
||||||
|
* <p>Both the call to check for readiness and the actual client code will be run on the Swing
|
||||||
|
* thread.
|
||||||
|
*/
|
||||||
|
public class ExpiringSwingTimer extends GhidraSwingTimer {
|
||||||
|
|
||||||
|
private long startMs = System.currentTimeMillis();
|
||||||
|
private int expireMs;
|
||||||
|
private BooleanSupplier isReady;
|
||||||
|
private ExpiringTimerCallback expiringTimerCallback = new ExpiringTimerCallback();
|
||||||
|
private TimerCallback clientCallback;
|
||||||
|
private AtomicBoolean didRun = new AtomicBoolean();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given client runnable when the given condition returns true. The returned timer
|
||||||
|
* will be running.
|
||||||
|
*
|
||||||
|
* <p>Once the timer has performed the work, any calls to start the returned timer will
|
||||||
|
* not perform any work. You can check {@link #didRun()} to see if the work has been completed.
|
||||||
|
*
|
||||||
|
* @param isReady true if the code should be run
|
||||||
|
* @param expireMs the amount of time past which the code will not be run
|
||||||
|
* @param runnable the code to run
|
||||||
|
* @return the timer object that is running, which will execute the given code when ready
|
||||||
|
*/
|
||||||
|
public static ExpiringSwingTimer runWhen(BooleanSupplier isReady,
|
||||||
|
int expireMs,
|
||||||
|
Runnable runnable) {
|
||||||
|
|
||||||
|
// Note: we could let the client specify the period, but that would add an extra argument
|
||||||
|
// to this method. For now, just use something reasonable.
|
||||||
|
int delay = 250;
|
||||||
|
ExpiringSwingTimer timer =
|
||||||
|
new ExpiringSwingTimer(delay, expireMs, isReady, runnable);
|
||||||
|
timer.start();
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* <p>Note: this class sets the parent's initial delay to 0. This is to allow the client
|
||||||
|
* code to be executed without delay when the ready condition is true.
|
||||||
|
*
|
||||||
|
* @param delay the delay between calls to check <code>isReady</code>
|
||||||
|
* @param isReady true if the code should be run
|
||||||
|
* @param expireMs the amount of time past which the code will not be run
|
||||||
|
* @param runnable the code to run
|
||||||
|
*/
|
||||||
|
public ExpiringSwingTimer(int delay, int expireMs, BooleanSupplier isReady,
|
||||||
|
Runnable runnable) {
|
||||||
|
super(0, delay, null);
|
||||||
|
this.expireMs = expireMs;
|
||||||
|
this.isReady = isReady;
|
||||||
|
this.clientCallback = () -> runnable.run();
|
||||||
|
super.setTimerCallback(expiringTimerCallback);
|
||||||
|
setRepeats(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the client runnable was run
|
||||||
|
* @return true if the client runnable was run
|
||||||
|
*/
|
||||||
|
public boolean didRun() {
|
||||||
|
return didRun.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
if (didRun() || isExpired()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true the initial expiration period has passed
|
||||||
|
* @return true if expired
|
||||||
|
*/
|
||||||
|
public boolean isExpired() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
int elapsed = (int) (now - startMs);
|
||||||
|
return elapsed > expireMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTimerCallback(TimerCallback callback) {
|
||||||
|
// overridden to ensure clients cannot overwrite out wrapping callback
|
||||||
|
this.clientCallback = Objects.requireNonNull(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExpiringTimerCallback implements TimerCallback {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void timerFired() {
|
||||||
|
|
||||||
|
if (isReady.getAsBoolean()) {
|
||||||
|
clientCallback.timerFired();
|
||||||
|
didRun.set(true);
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (isExpired()) {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
@ -22,67 +21,78 @@ import java.awt.event.ActionListener;
|
||||||
import javax.swing.Timer;
|
import javax.swing.Timer;
|
||||||
|
|
||||||
public class GhidraSwingTimer implements GhidraTimer, ActionListener {
|
public class GhidraSwingTimer implements GhidraTimer, ActionListener {
|
||||||
private Timer timer;
|
|
||||||
|
Timer timer;
|
||||||
private TimerCallback callback;
|
private TimerCallback callback;
|
||||||
|
|
||||||
public GhidraSwingTimer() {
|
public GhidraSwingTimer() {
|
||||||
this(100, null);
|
this(100, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GhidraSwingTimer(int delay, TimerCallback callback) {
|
public GhidraSwingTimer(int delay, TimerCallback callback) {
|
||||||
this(delay,delay,callback);
|
this(delay, delay, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GhidraSwingTimer(int initialDelay, int delay, TimerCallback callback) {
|
public GhidraSwingTimer(int initialDelay, int delay, TimerCallback callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
timer = new Timer(delay, this);
|
timer = new Timer(delay, this);
|
||||||
timer.setInitialDelay(delay);
|
timer.setInitialDelay(initialDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void actionPerformed(ActionEvent e) {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.timerFired();
|
callback.timerFired();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getDelay() {
|
public int getDelay() {
|
||||||
return timer.getDelay();
|
return timer.getDelay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getInitialDelay() {
|
public int getInitialDelay() {
|
||||||
return timer.getInitialDelay();
|
return timer.getInitialDelay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isRepeats() {
|
public boolean isRepeats() {
|
||||||
return timer.isRepeats();
|
return timer.isRepeats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void setDelay(int delay) {
|
public void setDelay(int delay) {
|
||||||
timer.setDelay(delay);
|
timer.setDelay(delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void setInitialDelay(int initialDelay) {
|
public void setInitialDelay(int initialDelay) {
|
||||||
timer.setInitialDelay(initialDelay);
|
timer.setInitialDelay(initialDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void setRepeats(boolean repeats) {
|
public void setRepeats(boolean repeats) {
|
||||||
timer.setRepeats(repeats);
|
timer.setRepeats(repeats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void setTimerCallback(TimerCallback callback) {
|
public void setTimerCallback(TimerCallback callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void start() {
|
public void start() {
|
||||||
timer.start();
|
timer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
timer.stop();
|
timer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isRunning() {
|
public boolean isRunning() {
|
||||||
return timer.isRunning();
|
return timer.isRunning();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.util.task;
|
package ghidra.util.task;
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
import javax.swing.Timer;
|
import javax.swing.Timer;
|
||||||
|
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
@ -50,13 +48,8 @@ import utilities.util.reflection.ReflectionUtilities;
|
||||||
*
|
*
|
||||||
* <P> This class is safe to use in a multi-threaded environment. State variables are guarded
|
* <P> This class is safe to use in a multi-threaded environment. State variables are guarded
|
||||||
* via synchronization on this object. The Swing thread is used to perform updates, which
|
* via synchronization on this object. The Swing thread is used to perform updates, which
|
||||||
* guarantees that only one update will happen at a time. There is one state variable,
|
* guarantees that only one update will happen at a time.
|
||||||
* the {@link #workCount}, that is changed both in the synchronized blocks and the Swing thread
|
|
||||||
* which is an atomic variable. This variable must be updated/incremented when the
|
|
||||||
* synchronized variables are cleared to prevent {@link #isBusy()} from returning false when
|
|
||||||
* there is a gap between 'work posted' and 'work execution'.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class SwingUpdateManager {
|
public class SwingUpdateManager {
|
||||||
private static final long NONE = 0;
|
private static final long NONE = 0;
|
||||||
public static final int DEFAULT_MAX_DELAY = 30000;
|
public static final int DEFAULT_MAX_DELAY = 30000;
|
||||||
|
@ -78,11 +71,13 @@ public class SwingUpdateManager {
|
||||||
private long bufferingStartTime;
|
private long bufferingStartTime;
|
||||||
private boolean disposed = false;
|
private boolean disposed = false;
|
||||||
|
|
||||||
// this is the number of times we will be calling work
|
// This is true when work has begun and is not finished. This is only mutated on the
|
||||||
private AtomicInteger workCount = new AtomicInteger();
|
// Swing thread, but is read by other threads.
|
||||||
|
private boolean isWorking;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new SwingUpdateManager.
|
* Constructs a new SwingUpdateManager with default values for min and max delay. See
|
||||||
|
* {@link #DEFAULT_MIN_DELAY} and {@value #DEFAULT_MAX_DELAY}.
|
||||||
*
|
*
|
||||||
* @param r the runnable that performs the client work.
|
* @param r the runnable that performs the client work.
|
||||||
*/
|
*/
|
||||||
|
@ -171,7 +166,7 @@ public class SwingUpdateManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestTime = System.currentTimeMillis();
|
requestTime = System.currentTimeMillis();
|
||||||
bufferingStartTime = bufferingStartTime == 0 ? requestTime : bufferingStartTime;
|
bufferingStartTime = bufferingStartTime == NONE ? requestTime : bufferingStartTime;
|
||||||
scheduleCheckForWork();
|
scheduleCheckForWork();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,15 +214,15 @@ public class SwingUpdateManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if any work is being performed or if there is buffered work.
|
* Returns true if any work is being performed or if there is buffered work
|
||||||
* @return true if any work is being performed or if there is buffered work.
|
* @return true if any work is being performed or if there is buffered work
|
||||||
*/
|
*/
|
||||||
public synchronized boolean isBusy() {
|
public synchronized boolean isBusy() {
|
||||||
if (disposed) {
|
if (disposed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestTime != NONE || workCount.get() != 0;
|
return requestTime != NONE || isWorking;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void dispose() {
|
public synchronized void dispose() {
|
||||||
|
@ -253,21 +248,25 @@ public class SwingUpdateManager {
|
||||||
"\tname: " + name + "\n" +
|
"\tname: " + name + "\n" +
|
||||||
"\tcreator: " + inceptionInformation + " ("+System.identityHashCode(this)+")\n" +
|
"\tcreator: " + inceptionInformation + " ("+System.identityHashCode(this)+")\n" +
|
||||||
"\trequest time: "+requestTime + "\n" +
|
"\trequest time: "+requestTime + "\n" +
|
||||||
"\twork count: " + workCount.get() + "\n" +
|
"\twork count: " + isWorking + "\n" +
|
||||||
"}";
|
"}";
|
||||||
//@formatter:on
|
//@formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// note: this is called on the Swing thread
|
||||||
private void checkForWork() {
|
private void checkForWork() {
|
||||||
|
|
||||||
if (shouldDoWork()) {
|
if (shouldDoWork()) {
|
||||||
doWork();
|
doWork();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// note: this is called on the Swing thread
|
||||||
private synchronized boolean shouldDoWork() {
|
private synchronized boolean shouldDoWork() {
|
||||||
|
|
||||||
// If no pending request, exit without restarting timer.
|
// If no pending request, exit without restarting timer
|
||||||
if (requestTime == NONE) {
|
if (requestTime == NONE) {
|
||||||
|
bufferingStartTime = NONE; // The timer has fired and there is no pending work
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,7 +274,7 @@ public class SwingUpdateManager {
|
||||||
if (isTimeToWork(now)) {
|
if (isTimeToWork(now)) {
|
||||||
bufferingStartTime = now;
|
bufferingStartTime = now;
|
||||||
requestTime = NONE;
|
requestTime = NONE;
|
||||||
workCount.incrementAndGet();
|
isWorking = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,6 +303,7 @@ public class SwingUpdateManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// note: this is called on the Swing thread
|
||||||
private void doWork() {
|
private void doWork() {
|
||||||
try {
|
try {
|
||||||
clientRunnable.run();
|
clientRunnable.run();
|
||||||
|
@ -313,9 +313,11 @@ public class SwingUpdateManager {
|
||||||
Msg.showError(this, null, "Unexpected Exception",
|
Msg.showError(this, null, "Unexpected Exception",
|
||||||
"Unexpected exception in Swing Update Manager", t);
|
"Unexpected exception in Swing Update Manager", t);
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
workCount.decrementAndGet();
|
isWorking = false;
|
||||||
}
|
|
||||||
|
// we need to clear the buffering flag after the minDelay has passed, so start the timer
|
||||||
|
scheduleCheckForWork();
|
||||||
}
|
}
|
||||||
|
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
/* ###
|
||||||
|
* 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 generic.timer;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import generic.test.AbstractGenericTest;
|
||||||
|
|
||||||
|
public class ExpiringSwingTimerTest extends AbstractGenericTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRunWhenReady() {
|
||||||
|
|
||||||
|
int waitCount = 2;
|
||||||
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
BooleanSupplier isReady = () -> {
|
||||||
|
return counter.incrementAndGet() > waitCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
AtomicInteger runCount = new AtomicInteger();
|
||||||
|
Runnable r = () -> {
|
||||||
|
runCount.incrementAndGet();
|
||||||
|
};
|
||||||
|
ExpiringSwingTimer.runWhen(isReady, 10000, r);
|
||||||
|
|
||||||
|
waitFor(() -> runCount.get() > 0);
|
||||||
|
assertTrue("Timer did not wait for the condition to be true", counter.get() > waitCount);
|
||||||
|
assertEquals("Client code was run more than once", 1, runCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRunWhenReady_Timeout() {
|
||||||
|
|
||||||
|
BooleanSupplier isReady = () -> {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
AtomicBoolean didRun = new AtomicBoolean();
|
||||||
|
Runnable r = () -> didRun.set(true);
|
||||||
|
ExpiringSwingTimer timer = ExpiringSwingTimer.runWhen(isReady, 500, r);
|
||||||
|
|
||||||
|
waitFor(() -> !timer.isRunning());
|
||||||
|
|
||||||
|
assertFalse(didRun.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWorkOnlyHappensOnce() {
|
||||||
|
|
||||||
|
BooleanSupplier isReady = () -> {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
AtomicInteger runCount = new AtomicInteger();
|
||||||
|
Runnable r = () -> {
|
||||||
|
runCount.incrementAndGet();
|
||||||
|
};
|
||||||
|
|
||||||
|
ExpiringSwingTimer timer = ExpiringSwingTimer.runWhen(isReady, 10000, r);
|
||||||
|
waitFor(() -> !timer.isRunning());
|
||||||
|
assertEquals(1, runCount.get());
|
||||||
|
|
||||||
|
timer.start();
|
||||||
|
waitFor(() -> !timer.isRunning());
|
||||||
|
assertEquals(1, runCount.get());
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,11 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
manager = createUpdateManager(MIN_DELAY, MAX_DELAY);
|
manager = createUpdateManager(MIN_DELAY, MAX_DELAY);
|
||||||
|
|
||||||
|
// must turn this on to get the expected results, as in headless mode the update manager
|
||||||
|
// will run it's Swing work immediately on the test thread, which is not true to the
|
||||||
|
// default behavior
|
||||||
|
System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.FALSE.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -160,20 +165,16 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
|
||||||
// before the update runnable is called.
|
// before the update runnable is called.
|
||||||
//
|
//
|
||||||
|
|
||||||
// must turn this on to get the expected results, as in headless mode the update manager
|
CountDownLatch startLatch = new CountDownLatch(1);
|
||||||
// will run it's Swing work immediately
|
CountDownLatch endLatch = new CountDownLatch(1);
|
||||||
System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.FALSE.toString());
|
AtomicBoolean exception = new AtomicBoolean();
|
||||||
|
|
||||||
final CountDownLatch startGate = new CountDownLatch(1);
|
|
||||||
final CountDownLatch endGate = new CountDownLatch(1);
|
|
||||||
final AtomicBoolean exception = new AtomicBoolean();
|
|
||||||
|
|
||||||
runSwing(new Runnable() {
|
runSwing(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
startGate.countDown();
|
startLatch.countDown();
|
||||||
try {
|
try {
|
||||||
endGate.await(10, TimeUnit.SECONDS);
|
endLatch.await(10, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
catch (InterruptedException e) {
|
catch (InterruptedException e) {
|
||||||
exception.set(true);
|
exception.set(true);
|
||||||
|
@ -181,13 +182,13 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
// This will cause the swing thread to block until will countdown the endGate latch
|
// This will cause the swing thread to block until we countdown the end latch
|
||||||
startGate.await(10, TimeUnit.SECONDS);
|
startLatch.await(10, TimeUnit.SECONDS);
|
||||||
|
|
||||||
manager.update();
|
manager.update();
|
||||||
assertTrue("Manager not busy after requesting an update", manager.isBusy());
|
assertTrue("Manager not busy after requesting an update", manager.isBusy());
|
||||||
|
|
||||||
endGate.countDown();
|
endLatch.countDown();
|
||||||
|
|
||||||
waitForManager();
|
waitForManager();
|
||||||
assertTrue("Manager still busy after waiting for update", !manager.isBusy());
|
assertTrue("Manager still busy after waiting for update", !manager.isBusy());
|
||||||
|
@ -195,9 +196,61 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
|
||||||
assertFalse("Interrupted waiting for CountDowLatch", exception.get());
|
assertFalse("Interrupted waiting for CountDowLatch", exception.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCallToUpdateWhileAnUpdateIsWorking() throws Exception {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Test that an update call from a non-swing thread will still get processed if the
|
||||||
|
// manager is actively processing an update on the swing thread.
|
||||||
|
//
|
||||||
|
|
||||||
|
CountDownLatch startLatch = new CountDownLatch(1);
|
||||||
|
CountDownLatch endLatch = new CountDownLatch(1);
|
||||||
|
AtomicBoolean exception = new AtomicBoolean();
|
||||||
|
|
||||||
|
Runnable r = () -> {
|
||||||
|
runnableCalled++;
|
||||||
|
|
||||||
|
startLatch.countDown();
|
||||||
|
try {
|
||||||
|
endLatch.await(10, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
exception.set(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// start the update manager and have it wait for us
|
||||||
|
manager = new SwingUpdateManager(MIN_DELAY, MAX_DELAY, r);
|
||||||
|
manager.update();
|
||||||
|
|
||||||
|
// have the swing thread block until we countdown the end latch
|
||||||
|
startLatch.await(10, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
// post the second update request now that the manager is actively processing
|
||||||
|
manager.update();
|
||||||
|
|
||||||
|
// let the update manager finish; verify 2 work items total
|
||||||
|
endLatch.countDown();
|
||||||
|
waitForManager();
|
||||||
|
assertEquals("Expected exactly 2 callbacks", 2, runnableCalled);
|
||||||
|
}
|
||||||
|
|
||||||
//==============================================================================================
|
//==============================================================================================
|
||||||
// Private Methods
|
// Private Methods
|
||||||
//==============================================================================================
|
//==============================================================================================
|
||||||
|
private void waitForManager() {
|
||||||
|
|
||||||
|
// let all swing updates finish, which may trigger the update manager
|
||||||
|
waitForSwing();
|
||||||
|
|
||||||
|
while (manager.isBusy()) {
|
||||||
|
sleep(DEFAULT_WAIT_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// let any resulting swing events finish
|
||||||
|
waitForSwing();
|
||||||
|
}
|
||||||
|
|
||||||
private SwingUpdateManager createUpdateManager(int min, int max) {
|
private SwingUpdateManager createUpdateManager(int min, int max) {
|
||||||
return new SwingUpdateManager(min, max, new Runnable() {
|
return new SwingUpdateManager(min, max, new Runnable() {
|
||||||
|
@ -209,19 +262,4 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void waitForManager() {
|
|
||||||
|
|
||||||
// let all swing updates finish, which may trigger the update manager
|
|
||||||
waitForPostedSwingRunnables();
|
|
||||||
|
|
||||||
while (manager.isBusy()) {
|
|
||||||
sleep(DEFAULT_WAIT_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// let any resulting swing events finish
|
|
||||||
waitForPostedSwingRunnables();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue