GP-4292: Detect input-driven Model tree expansion and refresh.

This commit is contained in:
Dan 2024-02-14 16:01:22 -05:00
parent d8dae6d092
commit ed9297dd25
7 changed files with 184 additions and 15 deletions

View File

@ -277,6 +277,9 @@ def start_trace(name):
with STATE.trace.open_tx("Create Root Object"):
root = STATE.trace.create_root_object(schema_xml, 'Session')
root.set_value('_display', 'GNU gdb ' + util.GDB_VERSION.full)
STATE.trace.create_object(AVAILABLES_PATH).insert()
STATE.trace.create_object(BREAKPOINTS_PATH).insert()
STATE.trace.create_object(INFERIORS_PATH).insert()
gdb.set_convenience_variable('_ghidra_tracing', True)
@ -1057,7 +1060,7 @@ def put_available():
procobj = STATE.trace.create_object(ppath)
keys.append(AVAILABLE_KEY_PATTERN.format(pid=proc.pid))
procobj.set_value('_pid', proc.pid)
procobj.set_value('_display', '{} {}'.format(proc.pid, proc.name))
procobj.set_value('_display', '{} {}'.format(proc.pid, proc.name()))
procobj.insert()
STATE.trace.proxy_object_path(AVAILABLES_PATH).retain_values(keys)

View File

@ -428,7 +428,8 @@ public class TraceRmiTarget extends AbstractTarget {
@Override
protected Map<String, ActionEntry> collectRefreshActions(ActionContext context) {
return collectByName(ActionName.REFRESH, context);
return collectFromMethods(connection.getMethods().getByAction(ActionName.REFRESH), context,
true, false, false);
}
@Override

View File

@ -141,6 +141,7 @@ public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleServ
* For testing: get the number of rows having a given class of action context
*
* @param ctxCls the context class
* @return the number of rows
*/
public long getRowCount(Class<? extends ActionContext> ctxCls) {
return provider.getRowCount(ctxCls);

View File

@ -15,6 +15,8 @@
*/
package ghidra.app.plugin.core.debug.gui.control;
import java.util.concurrent.CompletableFuture;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.services.DebuggerConsoleService;
import ghidra.app.services.ProgressService;
@ -25,25 +27,78 @@ import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
/**
* A task for executing a target {@link ActionEntry}.
*
* <p>
* This also has some static convenience methods for scheduling this and other types of tasks in the
* Debugger tool.
*/
public class TargetActionTask extends Task {
public static void executeTask(PluginTool tool, Task task) {
/**
* Execute a task
*
* <p>
* If available, this simply delegates to {@link ProgressService#execute(Task)}. If not, then
* this falls back to {@link PluginTool#execute(Task)}.
*
* @param tool the tool in which to execute
* @param task the task to execute
* @return a future that completes (perhaps exceptionally) when the task is finished or
* cancelled
*/
public static CompletableFuture<Void> executeTask(PluginTool tool, Task task) {
ProgressService progressService = tool.getService(ProgressService.class);
if (progressService != null) {
progressService.execute(task);
}
else {
tool.execute(task);
return progressService.execute(task);
}
CompletableFuture<Void> future = new CompletableFuture<>();
tool.execute(
new Task(task.getTaskTitle(), task.canCancel(), task.hasProgress(), task.isModal(),
task.getWaitForTaskCompleted()) {
@Override
public void run(TaskMonitor monitor) throws CancelledException {
try {
task.run(monitor);
future.complete(null);
}
catch (CancelledException e) {
future.cancel(false);
}
catch (Throwable e) {
future.completeExceptionally(e);
}
}
});
tool.execute(task);
return future;
}
public static void runAction(PluginTool tool, String title, ActionEntry entry) {
executeTask(tool, new TargetActionTask(tool, title, entry));
/**
* Execute an {@link ActionEntry}
*
* @param tool the tool in which to execute
* @param title the title, often {@link ActionEntry#display()}
* @param entry the action to execute
* @return a future that completes (perhaps exceptionally) when the task is finished or
* cancelled
*/
public static CompletableFuture<Void> runAction(PluginTool tool, String title,
ActionEntry entry) {
return executeTask(tool, new TargetActionTask(tool, title, entry));
}
private final PluginTool tool;
private final ActionEntry entry;
/**
* Construct a task fore the given action
*
* @param tool the plugin tool
* @param title the title, often {@link ActionEntry#display()}
* @param entry the action to execute
*/
public TargetActionTask(PluginTool tool, String title, ActionEntry entry) {
super(title, false, false, false);
this.tool = tool;

View File

@ -18,13 +18,16 @@ package ghidra.app.plugin.core.debug.gui.model;
import java.awt.*;
import java.awt.event.*;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.swing.*;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.tree.TreePath;
import docking.*;
import docking.action.DockingAction;
@ -39,12 +42,17 @@ import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.CloneWindowAction;
import ghidra.app.plugin.core.debug.gui.MultiProviderSaveBehavior.SaveableProvider;
import ghidra.app.plugin.core.debug.gui.control.TargetActionTask;
import ghidra.app.plugin.core.debug.gui.model.AbstractQueryTablePanel.CellActivationListener;
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ObjectRow;
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.AbstractNode;
import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.*;
import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.RootNode;
import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.Target.ActionEntry;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
@ -501,11 +509,26 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable
setPath(value == null ? TraceObjectKeyPath.of() : value.getCanonicalPath(),
objectsTreePanel, EventOrigin.INTERNAL_GENERATED);
});
objectsTreePanel.tree.addTreeExpansionListener(new TreeExpansionListener() {
@Override
public void treeExpanded(TreeExpansionEvent expEvent) {
if (EventQueue.getCurrentEvent() instanceof InputEvent event &&
!event.isShiftDown()) {
refreshTargetChildren(expEvent.getPath());
}
}
@Override
public void treeCollapsed(TreeExpansionEvent event) {
// I don't care
}
});
objectsTreePanel.tree.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) {
activateObjectSelectedInTree();
e.consume();
}
}
});
@ -572,6 +595,54 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable
rebuildPanels();
}
private TraceObjectValue getObject(TreePath path) {
if (path.getLastPathComponent() instanceof CanonicalNode node) {
return node.getValue();
}
if (path.getLastPathComponent() instanceof RootNode node) {
return node.getValue();
}
return null;
}
private void refreshTargetChildren(TreePath path) {
TraceObjectValue value = getObject(path);
if (value == null) {
return;
}
DebuggerCoordinates current = traceManager.getCurrentFor(value.getTrace());
if (!current.isAliveAndPresent() && limitToSnap) {
return;
}
Target target = current.getTarget();
if (target == null) {
return;
}
Map<String, ActionEntry> actions = target.collectActions(ActionName.REFRESH,
new DebuggerObjectActionContext(List.of(value), this, objectsTreePanel));
for (ActionEntry ent : actions.values()) {
if (ent.requiresPrompt()) {
continue;
}
if (path.getLastPathComponent() instanceof AbstractNode node) {
/**
* This pending node does not duplicate what the lazy node already does. For all
* it's concerned, once it has loaded the entries from the database, it is done.
* This task asks the target to update that database, so it needs its own indicator.
*/
PendingNode pending = new PendingNode();
node.addNode(0, pending);
CompletableFuture<Void> future =
TargetActionTask.runAction(plugin.getTool(), ent.display(), ent);
future.handle((__, ex) -> {
node.removeNode(pending);
return null;
});
return;
}
}
}
private void activateObjectSelectedInTree() {
List<AbstractNode> sel = objectsTreePanel.getSelectedItems();
if (sel.size() != 1) {
@ -580,7 +651,7 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable
return;
}
TraceObjectValue value = sel.get(0).getValue();
if (value.getValue() instanceof TraceObject child) {
if (value != null && value.getValue() instanceof TraceObject child) {
activatePath(child.getCanonicalPath());
}
}

View File

@ -22,6 +22,7 @@ import javax.swing.Icon;
import docking.widgets.tree.GTreeLazyNode;
import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.dbg.target.*;
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
@ -35,6 +36,7 @@ import ghidra.util.datastruct.WeakValueHashMap;
import utilities.util.IDKeyed;
public class ObjectTreeModel implements DisplaysModified {
public static final GIcon ICON_PENDING = new GIcon("icon.pending");
class ListenerForChanges extends TraceDomainObjectListener
implements DomainObjectClosedListener {
@ -193,6 +195,38 @@ public class ObjectTreeModel implements DisplaysModified {
}
}
public static class PendingNode extends GTreeLazyNode {
@Override
public String getName() {
return ""; // Want it sorted to the front
}
@Override
public String getDisplayText() {
return "Refreshing...";
}
@Override
public Icon getIcon(boolean expanded) {
return ICON_PENDING;
}
@Override
public boolean isLeaf() {
return true;
}
@Override
protected List<GTreeNode> generateChildren() {
return List.of();
}
@Override
public String getToolTip() {
return null;
}
}
public abstract class AbstractNode extends GTreeLazyNode {
public abstract TraceObjectValue getValue();
@ -332,7 +366,7 @@ public class ObjectTreeModel implements DisplaysModified {
}
}
class RootNode extends AbstractNode {
public class RootNode extends AbstractNode {
@Override
public TraceObjectValue getValue() {
if (trace == null) {

View File

@ -398,7 +398,11 @@ public class ObjectsTreePanel extends JPanel {
protected <R, A> R getItemsFromPaths(TreePath[] paths,
Collector<? super AbstractNode, A, R> collector) {
return Stream.of(paths)
.map(p -> (AbstractNode) p.getLastPathComponent())
.<AbstractNode> mapMulti((path, consumer) -> {
if (path.getLastPathComponent() instanceof AbstractNode node) {
consumer.accept(node);
}
})
.collect(collector);
}