From 0d7cb0dd505befdfecd4e9106fa1763306f80a5f Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Wed, 2 Nov 2022 10:31:19 -0400 Subject: [PATCH] GP-2555: Refactor StackProvider for object-mode trace. --- .../model/impl/GdbModelTargetStackFrame.java | 2 +- .../gui/control/DebuggerControlPlugin.java | 6 +- .../gui/model/AbstractQueryTableModel.java | 8 +- .../gui/model/AbstractQueryTablePanel.java | 29 +- .../core/debug/gui/model/ModelQuery.java | 100 +++- .../debug/gui/model/ObjectTableModel.java | 109 +++- .../core/debug/gui/model/ObjectTreeModel.java | 26 +- .../debug/gui/model/ObjectsTablePanel.java | 14 +- .../core/debug/gui/model/PathsTablePanel.java | 4 +- .../model/columns/TraceValueKeyColumn.java | 8 + .../TraceValueObjectAttributeColumn.java | 9 +- .../gui/stack/DebuggerLegacyStackPanel.java | 480 +++++++++++++++ .../debug/gui/stack/DebuggerStackPanel.java | 179 ++++++ .../debug/gui/stack/DebuggerStackPlugin.java | 36 +- .../gui/stack/DebuggerStackProvider.java | 493 +++------------ .../core/debug/gui/stack/StackFrameRow.java | 38 +- .../core/debug/gui/model/ModelQueryTest.java | 27 + .../DebuggerStackProviderLegacyTest.java | 508 ++++++++++++++++ .../DebuggerStackProviderObjectTest.java | 124 ---- .../gui/stack/DebuggerStackProviderTest.java | 566 +++++++++++------- .../dbg/target/schema/TargetObjectSchema.java | 7 + .../table/GDynamicColumnTableModel.java | 20 +- 22 files changed, 1911 insertions(+), 882 deletions(-) create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerLegacyStackPanel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPanel.java create mode 100644 Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderLegacyTest.java delete mode 100644 Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderObjectTest.java diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrame.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrame.java index 01f287f4a2..452e705fad 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrame.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrame.java @@ -120,7 +120,7 @@ public class GdbModelTargetStackFrame return impl.gateFuture(frame.setActive(false)); } - @TargetAttributeType(name = FUNC_ATTRIBUTE_NAME) + @TargetAttributeType(name = FUNC_ATTRIBUTE_NAME, hidden = true) public String getFunction() { return func; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java index a88cb86d54..d56d31723c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java @@ -107,7 +107,11 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin if (object != null) { return iface.cast(recorder.getTargetObject(object)); } - return recorder.getFocus().getCachedSuitable(iface); + TargetObject focus = recorder.getFocus(); + if (focus == null) { + return null; + } + return focus.getCachedSuitable(iface); } abstract boolean isEnabledForObject(T t); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java index 29c98851f8..315da4bf89 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java @@ -45,14 +45,14 @@ public abstract class AbstractQueryTableModel extends ThreadedTableModel extends ThreadedTableModel extends JPanel { +public abstract class AbstractQueryTablePanel> + extends JPanel { - protected final AbstractQueryTableModel tableModel; + protected final M tableModel; protected final GhidraTable table; protected final GhidraTableFilterPanel filterPanel; @@ -53,7 +56,7 @@ public abstract class AbstractQueryTablePanel extends JPanel { add(filterPanel, BorderLayout.SOUTH); } - protected abstract AbstractQueryTableModel createModel(Plugin plugin); + protected abstract M createModel(Plugin plugin); public void goToCoordinates(DebuggerCoordinates coords) { if (DebuggerCoordinates.equalsIgnoreRecorderAndView(current, coords)) { @@ -184,6 +187,26 @@ public abstract class AbstractQueryTablePanel extends JPanel { return filterPanel.getSelectedItem(); } + public List getAllItems() { + return List.copyOf(tableModel.getModelData()); + } + + @SuppressWarnings("unchecked") + public DynamicTableColumn getColumnByNameAndType(String name, Class type) { + int count = tableModel.getColumnCount(); + for (int i = 0; i < count; i++) { + DynamicTableColumn column = tableModel.getColumn(i); + if (!name.equals(column.getColumnName())) { + continue; + } + if (column.getColumnClass() != type) { + continue; + } + return (DynamicTableColumn) column; + } + return null; + } + public void setDiffColor(Color diffColor) { tableModel.setDiffColor(diffColor); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java index ac949ede52..0a7c7c8d54 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java @@ -15,19 +15,20 @@ */ package ghidra.app.plugin.core.debug.gui.model; -import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; +import ghidra.dbg.target.schema.EnumerableTargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; import ghidra.dbg.util.*; -import ghidra.trace.database.DBTraceUtils; import ghidra.trace.model.Lifespan; import ghidra.trace.model.Trace; import ghidra.trace.model.target.*; public class ModelQuery { + public static final ModelQuery EMPTY = new ModelQuery(PathPredicates.EMPTY); // TODO: A more capable query language, e.g., with WHERE clauses. // Could also want math expressions for the conditionals... Hmm. // They need to be user enterable, so just a Java API won't suffice. @@ -112,6 +113,23 @@ public class ModelQuery { return trace.getObjectManager().getValuePaths(span, predicates).map(p -> p); } + public List computeSchemas(Trace trace) { + TargetObjectSchema rootSchema = trace.getObjectManager().getRootSchema(); + return predicates.getPatterns() + .stream() + .map(p -> rootSchema.getSuccessorSchema(p.asPath())) + .distinct() + .collect(Collectors.toList()); + } + + public TargetObjectSchema computeSingleSchema(Trace trace) { + List schemas = computeSchemas(trace); + if (schemas.size() != 1) { + return EnumerableTargetObjectSchema.OBJECT; + } + return schemas.get(0); + } + /** * Compute the named attributes for resulting objects, according to the schema * @@ -122,15 +140,31 @@ public class ModelQuery { * @return the list of attributes */ public Stream computeAttributes(Trace trace) { - TraceObjectManager objects = trace.getObjectManager(); - TargetObjectSchema schema = - objects.getRootSchema().getSuccessorSchema(predicates.getSingletonPattern().asPath()); + TargetObjectSchema schema = computeSingleSchema(trace); return schema.getAttributeSchemas() .values() .stream() .filter(as -> !"".equals(as.getName())); } + protected static boolean includes(Lifespan span, PathPattern pattern, TraceObjectValue value) { + List asPath = pattern.asPath(); + if (asPath.isEmpty()) { + // If the pattern is the root, then only match the "root value" + return value.getParent() == null; + } + if (!PathPredicates.keyMatches(PathUtils.getKey(asPath), value.getEntryKey())) { + return false; + } + TraceObject parent = value.getParent(); + if (parent == null) { + // Value is the root. We would already have matched above + return false; + } + return parent.getAncestors(span, pattern.removeRight(1)) + .anyMatch(v -> v.getSource(parent).isRoot()); + } + /** * Determine whether this query would include the given value in its result * @@ -144,21 +178,57 @@ public class ModelQuery { * @return true if the value would be accepted */ public boolean includes(Lifespan span, TraceObjectValue value) { - List path = predicates.getSingletonPattern().asPath(); - if (path.isEmpty()) { - return value.getParent() == null; - } - if (!PathPredicates.keyMatches(PathUtils.getKey(path), value.getEntryKey())) { - return false; - } if (!span.intersects(value.getLifespan())) { return false; } + for (PathPattern pattern : predicates.getPatterns()) { + if (includes(span, pattern, value)) { + return true; + } + } + return false; + } + + protected static boolean involves(Lifespan span, PathPattern pattern, TraceObjectValue value) { TraceObject parent = value.getParent(); + // Every query involves the root if (parent == null) { + return true; + } + + // Check if any of the value's paths could be an ancestor of a result + List asPath = new ArrayList<>(pattern.asPath()); + // Destroy the pattern from the right, thus iterating each ancestor + while (!asPath.isEmpty()) { + // The value's key much match somewhere in the pattern to be involved + if (!PathPredicates.keyMatches(PathUtils.getKey(asPath), value.getEntryKey())) { + asPath.remove(asPath.size() - 1); + continue; + } + // If it does, then check if any path to the value's parent matches the rest + asPath.remove(asPath.size() - 1); + if (parent.getAncestors(span, new PathPattern(asPath)) + .anyMatch(v -> v.getSource(parent).isRoot())) { + return true; + } + } + return false; + } + + /** + * Determine whether the query results could depend on the given value + * + * @return true if the query results depend on the given value + */ + public boolean involves(Lifespan span, TraceObjectValue value) { + if (!span.intersects(value.getLifespan())) { return false; } - return parent.getAncestors(span, predicates.removeRight(1)) - .anyMatch(v -> v.getSource(parent).isRoot()); + for (PathPattern pattern : predicates.getPatterns()) { + if (involves(span, pattern, value)) { + return true; + } + } + return false; } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java index c77bc468c1..2efa069b55 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java @@ -37,9 +37,6 @@ import ghidra.trace.model.target.TraceObjectValue; import ghidra.util.HTMLUtilities; public class ObjectTableModel extends AbstractQueryTableModel { - /** Initialized in {@link #createTableColumnDescriptor()}, which precedes this. */ - private TraceValueValColumn valueColumn; - private TraceValueLifePlotColumn lifePlotColumn; protected static Stream distinctCanonical( Stream stream) { @@ -272,6 +269,19 @@ public class ObjectTableModel extends AbstractQueryTableModel { } } + static class AutoAttributeColumn extends TraceValueObjectAttributeColumn { + public static TraceValueObjectAttributeColumn fromSchema(SchemaContext ctx, + AttributeSchema attributeSchema) { + String name = attributeSchema.getName(); + Class type = computeColumnType(ctx, attributeSchema); + return new AutoAttributeColumn(name, type); + } + + public AutoAttributeColumn(String attributeName, Class attributeType) { + super(attributeName, attributeType); + } + } + // TODO: Save and restore these between sessions, esp., their settings private Map columnCache = new HashMap<>(); @@ -307,7 +317,13 @@ public class ObjectTableModel extends AbstractQueryTableModel { protected void updateTimelineMax() { Long max = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap(); Lifespan fullRange = Lifespan.span(0L, max == null ? 1 : max + 1); - lifePlotColumn.setFullRange(fullRange); + int count = getColumnCount(); + for (int i = 0; i < count; i++) { + DynamicTableColumn column = getColumn(i); + if (column instanceof TraceValueLifePlotColumn plotCol) { + plotCol.setFullRange(fullRange); + } + } } protected List computeAttributeSchemas() { @@ -337,7 +353,6 @@ public class ObjectTableModel extends AbstractQueryTableModel { else { SchemaContext ctx = trace.getObjectManager().getRootSchema().getContext(); attributes = query.computeAttributes(trace) - .filter(a -> isShowHidden() || !a.isHidden()) .filter(a -> !ctx.getSchema(a.getSchema()).isCanonicalContainer()) .collect(Collectors.toList()); } @@ -346,6 +361,9 @@ public class ObjectTableModel extends AbstractQueryTableModel { protected Set> computeAttributeColumns( Collection attributes) { + if (attributes == null) { + return Set.of(); + } Trace trace = getTrace(); if (trace == null) { return Set.of(); @@ -357,25 +375,31 @@ public class ObjectTableModel extends AbstractQueryTableModel { SchemaContext ctx = rootSchema.getContext(); return attributes.stream() .map(as -> columnCache.computeIfAbsent(ColKey.fromSchema(ctx, as), - ck -> TraceValueObjectAttributeColumn.fromSchema(ctx, as))) + ck -> AutoAttributeColumn.fromSchema(ctx, as))) .collect(Collectors.toSet()); } protected void resyncAttributeColumns(Collection attributes) { - Set> columns = - new HashSet<>(computeAttributeColumns(attributes)); + Map> byVisible = attributes == null ? Map.of() + : attributes.stream() + .collect(Collectors.groupingBy(a -> !a.isHidden() || isShowHidden())); + Set> visibleColumns = + new HashSet<>(computeAttributeColumns(byVisible.get(true))); + Set> hiddenColumns = + new HashSet<>(computeAttributeColumns(byVisible.get(false))); Set> toRemove = new HashSet<>(); for (int i = 0; i < getColumnCount(); i++) { DynamicTableColumn exists = getColumn(i); - if (!(exists instanceof TraceValueObjectAttributeColumn)) { + if (!(exists instanceof AutoAttributeColumn)) { continue; } - if (!columns.remove(exists)) { + if (!visibleColumns.remove(exists) && !hiddenColumns.remove(exists)) { toRemove.add(exists); } } removeTableColumns(toRemove); - addTableColumns(columns); + addTableColumns(visibleColumns, true); + addTableColumns(hiddenColumns, false); } @Override @@ -389,9 +413,9 @@ public class ObjectTableModel extends AbstractQueryTableModel { protected TableColumnDescriptor createTableColumnDescriptor() { TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); descriptor.addVisibleColumn(new TraceValueKeyColumn()); - descriptor.addVisibleColumn(valueColumn = new TraceValueValColumn()); + descriptor.addVisibleColumn(new TraceValueValColumn()); descriptor.addVisibleColumn(new TraceValueLifeColumn()); - descriptor.addHiddenColumn(lifePlotColumn = new TraceValueLifePlotColumn()); + descriptor.addHiddenColumn(new TraceValueLifePlotColumn()); return descriptor; } @@ -405,9 +429,38 @@ public class ObjectTableModel extends AbstractQueryTableModel { return null; } + /** + * Find the row whose object is the canonical ancestor to the given object + * + * @param successor the given object + * @return the row or null + */ + public ValueRow findTraceObjectAncestor(TraceObject successor) { + for (ValueRow row : getModelData()) { + TraceObjectValue value = row.getValue(); + if (!value.isObject()) { + continue; + } + if (!value.getChild().getCanonicalPath().isAncestor(successor.getCanonicalPath())) { + continue; + } + return row; + } + return null; + } + @Override public void setDiffColor(Color diffColor) { - valueColumn.setDiffColor(diffColor); + int count = getColumnCount(); + for (int i = 0; i < count; i++) { + DynamicTableColumn column = getColumn(i); + if (column instanceof TraceValueObjectAttributeColumn attrCol) { + attrCol.setDiffColor(diffColor); + } + else if (column instanceof TraceValueValColumn valCol) { + valCol.setDiffColor(diffColor); + } + } for (TraceValueObjectAttributeColumn column : columnCache.values()) { column.setDiffColor(diffColor); } @@ -415,7 +468,16 @@ public class ObjectTableModel extends AbstractQueryTableModel { @Override public void setDiffColorSel(Color diffColorSel) { - valueColumn.setDiffColorSel(diffColorSel); + int count = getColumnCount(); + for (int i = 0; i < count; i++) { + DynamicTableColumn column = getColumn(i); + if (column instanceof TraceValueObjectAttributeColumn attrCol) { + attrCol.setDiffColorSel(diffColorSel); + } + else if (column instanceof TraceValueValColumn valCol) { + valCol.setDiffColorSel(diffColorSel); + } + } for (TraceValueObjectAttributeColumn column : columnCache.values()) { column.setDiffColorSel(diffColorSel); } @@ -424,11 +486,24 @@ public class ObjectTableModel extends AbstractQueryTableModel { @Override protected void snapChanged() { super.snapChanged(); - lifePlotColumn.setSnap(getSnap()); + long snap = getSnap(); + int count = getColumnCount(); + for (int i = 0; i < count; i++) { + DynamicTableColumn column = getColumn(i); + if (column instanceof TraceValueLifePlotColumn plotCol) { + plotCol.setSnap(snap); + } + } } @Override public void addSeekListener(SeekListener listener) { - lifePlotColumn.addSeekListener(listener); + int count = getColumnCount(); + for (int i = 0; i < count; i++) { + DynamicTableColumn column = getColumn(i); + if (column instanceof TraceValueLifePlotColumn plotCol) { + plotCol.addSeekListener(listener); + } + } } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java index f27ac897ba..774f0dda2c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java @@ -24,6 +24,7 @@ import docking.widgets.tree.GTreeLazyNode; import docking.widgets.tree.GTreeNode; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.dbg.target.*; +import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator; import ghidra.framework.model.DomainObjectClosedListener; import ghidra.trace.model.*; import ghidra.trace.model.Trace.TraceObjectChangeType; @@ -189,6 +190,19 @@ public class ObjectTreeModel implements DisplaysModified { public abstract class AbstractNode extends GTreeLazyNode { public abstract TraceObjectValue getValue(); + @Override + public int compareTo(GTreeNode node) { + return TargetObjectKeyComparator.CHILD.compare(this.getName(), node.getName()); + } + + @Override + public String getName() { + return getValue().getEntryKey(); + } + + @Override + public abstract String getDisplayText(); + protected void childCreated(TraceObjectValue value) { if (getParent() == null || !isLoaded()) { return; @@ -249,6 +263,11 @@ public class ObjectTreeModel implements DisplaysModified { @Override public String getName() { + return "Root"; + } + + @Override + public String getDisplayText() { if (trace == null) { return "No trace is active"; } @@ -332,7 +351,7 @@ public class ObjectTreeModel implements DisplaysModified { } @Override - public String getName() { + public String getDisplayText() { String html = HTMLUtilities.escapeHTML( value.getEntryKey() + ": " + display.getPrimitiveValueDisplay(value.getValue())); return "" + html; @@ -380,7 +399,7 @@ public class ObjectTreeModel implements DisplaysModified { } @Override - public String getName() { + public String getDisplayText() { return "" + HTMLUtilities.escapeHTML(value.getEntryKey()) + ": " + HTMLUtilities.escapeHTML(display.getObjectLinkDisplay(value)) + ""; } @@ -422,7 +441,7 @@ public class ObjectTreeModel implements DisplaysModified { } @Override - public String getName() { + public String getDisplayText() { return "" + HTMLUtilities.escapeHTML(display.getObjectDisplay(value)); } @@ -606,6 +625,7 @@ public class ObjectTreeModel implements DisplaysModified { List result = ObjectTableModel .distinctCanonical(object.getValues().stream().filter(this::isValueVisible)) .map(v -> nodeCache.getOrCreateNode(v)) + .sorted() .collect(Collectors.toList()); return result; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java index 5bdb24ebe2..49bba8b77f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java @@ -17,14 +17,24 @@ package ghidra.app.plugin.core.debug.gui.model; import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; import ghidra.framework.plugintool.Plugin; +import ghidra.trace.model.target.TraceObject; -public class ObjectsTablePanel extends AbstractQueryTablePanel { +public class ObjectsTablePanel extends AbstractQueryTablePanel { public ObjectsTablePanel(Plugin plugin) { super(plugin); } @Override - protected AbstractQueryTableModel createModel(Plugin plugin) { + protected ObjectTableModel createModel(Plugin plugin) { return new ObjectTableModel(plugin); } + + public boolean trySelectAncestor(TraceObject successor) { + ValueRow row = tableModel.findTraceObjectAncestor(successor); + if (row == null) { + return false; + } + setSelectedItem(row); + return true; + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java index f3d9cf4a3a..7d61f053b6 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java @@ -18,13 +18,13 @@ package ghidra.app.plugin.core.debug.gui.model; import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; import ghidra.framework.plugintool.Plugin; -public class PathsTablePanel extends AbstractQueryTablePanel { +public class PathsTablePanel extends AbstractQueryTablePanel { public PathsTablePanel(Plugin plugin) { super(plugin); } @Override - protected AbstractQueryTableModel createModel(Plugin plugin) { + protected PathTableModel createModel(Plugin plugin) { return new PathTableModel(plugin); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java index 875e01f877..5da678d0a3 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java @@ -15,8 +15,11 @@ */ package ghidra.app.plugin.core.debug.gui.model.columns; +import java.util.Comparator; + import docking.widgets.table.AbstractDynamicTableColumn; import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.ServiceProvider; import ghidra.trace.model.Trace; @@ -32,4 +35,9 @@ public class TraceValueKeyColumn extends AbstractDynamicTableColumn getComparator() { + return TargetObjectKeyComparator.CHILD; + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java index dc313d38dc..ec7bb3015a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java @@ -105,13 +105,6 @@ public class TraceValueObjectAttributeColumn return type; } - public static TraceValueObjectAttributeColumn fromSchema(SchemaContext ctx, - AttributeSchema attributeSchema) { - String name = attributeSchema.getName(); - Class type = computeColumnType(ctx, attributeSchema); - return new TraceValueObjectAttributeColumn(name, type); - } - private final String attributeName; private final Class attributeType; private final AttributeRenderer renderer = new AttributeRenderer(); @@ -155,7 +148,7 @@ public class TraceValueObjectAttributeColumn new ColumnRenderedValueBackupComparator<>(model, columnIndex)); } - protected Object getAttributeValue(ValueRow row) { + public Object getAttributeValue(ValueRow row) { TraceObjectValue edge = row.getAttribute(attributeName); return edge == null ? null : edge.getValue(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerLegacyStackPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerLegacyStackPanel.java new file mode 100644 index 0000000000..1d59f734cc --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerLegacyStackPanel.java @@ -0,0 +1,480 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.stack; + +import java.awt.BorderLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.*; +import java.util.function.*; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import docking.widgets.table.CustomToStringCellRenderer; +import docking.widgets.table.DefaultEnumeratedColumnTableModel; +import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.services.*; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.target.TargetStackFrame; +import ghidra.framework.plugintool.AutoService; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.Program; +import ghidra.trace.model.*; +import ghidra.trace.model.Trace.TraceMemoryBytesChangeType; +import ghidra.trace.model.Trace.TraceStackChangeType; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.stack.TraceStack; +import ghidra.trace.model.stack.TraceStackFrame; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.util.TraceAddressSpace; +import ghidra.trace.util.TraceRegisterUtils; +import ghidra.util.Swing; +import ghidra.util.table.GhidraTable; +import ghidra.util.table.GhidraTableFilterPanel; +import utilities.util.SuppressableCallback; +import utilities.util.SuppressableCallback.Suppression; + +public class DebuggerLegacyStackPanel extends JPanel { + + protected enum StackTableColumns + implements EnumeratedTableColumn { + LEVEL("Level", Integer.class, StackFrameRow::getFrameLevel), + PC("PC", Address.class, StackFrameRow::getProgramCounter), + FUNCTION("Function", ghidra.program.model.listing.Function.class, StackFrameRow::getFunction), + COMMENT("Comment", String.class, StackFrameRow::getComment, StackFrameRow::setComment, StackFrameRow::isCommentable); + + private final String header; + private final Function getter; + private final BiConsumer setter; + private final Predicate editable; + private final Class cls; + + @SuppressWarnings("unchecked") + StackTableColumns(String header, Class cls, Function getter, + BiConsumer setter, Predicate editable) { + this.header = header; + this.cls = cls; + this.getter = getter; + this.setter = (BiConsumer) setter; + this.editable = editable; + } + + StackTableColumns(String header, Class cls, Function getter) { + this(header, cls, getter, null, null); + } + + @Override + public Class getValueClass() { + return cls; + } + + @Override + public Object getValueOf(StackFrameRow row) { + return getter.apply(row); + } + + @Override + public void setValueOf(StackFrameRow row, Object value) { + setter.accept(row, value); + } + + @Override + public String getHeader() { + return header; + } + + @Override + public boolean isEditable(StackFrameRow row) { + return setter != null && editable.test(row); + } + } + + protected static class StackTableModel + extends DefaultEnumeratedColumnTableModel { + + public StackTableModel(PluginTool tool) { + super(tool, "Stack", StackTableColumns.class); + } + + @Override + public List defaultSortOrder() { + return List.of(StackTableColumns.LEVEL); + } + } + + class ForStackListener extends TraceDomainObjectListener { + public ForStackListener() { + listenFor(TraceStackChangeType.ADDED, this::stackAdded); + listenFor(TraceStackChangeType.CHANGED, this::stackChanged); + listenFor(TraceStackChangeType.DELETED, this::stackDeleted); + + listenFor(TraceMemoryBytesChangeType.CHANGED, this::bytesChanged); + } + + private void stackAdded(TraceStack stack) { + if (stack.getSnap() != current.getViewSnap()) { + return; + } + TraceThread curThread = current.getThread(); + if (curThread != stack.getThread()) { + return; + } + loadStack(); + } + + private void stackChanged(TraceStack stack) { + if (currentStack != stack) { + return; + } + updateStack(); + } + + private void stackDeleted(TraceStack stack) { + if (currentStack != stack) { + return; + } + loadStack(); + } + + // For updating a synthetic frame + private void bytesChanged(TraceAddressSpace space, TraceAddressSnapRange range) { + TraceThread curThread = current.getThread(); + if (space.getThread() != curThread || space.getFrameLevel() != 0) { + return; + } + if (!current.getView().getViewport().containsAnyUpper(range.getLifespan())) { + return; + } + List stackData = stackTableModel.getModelData(); + if (stackData.isEmpty() || !(stackData.get(0) instanceof StackFrameRow.Synthetic)) { + return; + } + StackFrameRow.Synthetic frameRow = (StackFrameRow.Synthetic) stackData.get(0); + Trace trace = current.getTrace(); + Register pc = trace.getBaseLanguage().getProgramCounter(); + if (!TraceRegisterUtils.rangeForRegister(pc).intersects(range.getRange())) { + return; + } + TraceMemorySpace regs = + trace.getMemoryManager().getMemoryRegisterSpace(curThread, false); + RegisterValue value = regs.getViewValue(current.getViewSnap(), pc); + Address address = trace.getBaseLanguage() + .getDefaultSpace() + .getAddress(value.getUnsignedValue().longValue()); + frameRow.updateProgramCounter(address); + stackTableModel.fireTableDataChanged(); + } + } + + class ForFunctionsListener implements DebuggerStaticMappingChangeListener { + @Override + public void mappingsChanged(Set affectedTraces, Set affectedPrograms) { + Trace curTrace = current.getTrace(); + if (curTrace == null || !affectedTraces.contains(curTrace)) { + return; + } + Swing.runIfSwingOrRunLater(() -> stackTableModel.fireTableDataChanged()); + } + } + + final DebuggerStackProvider provider; + + @AutoServiceConsumed + private DebuggerTraceManagerService traceManager; + // @AutoServiceConsumed by method + private DebuggerModelService modelService; + // @AutoServiceConsumed via method + DebuggerStaticMappingService mappingService; + @AutoServiceConsumed + private DebuggerListingService listingService; // TODO: Goto pc on double-click + @AutoServiceConsumed + private MarkerService markerService; // TODO: Mark non-current frame PCs, too. (separate plugin?) + @SuppressWarnings("unused") + private final AutoService.Wiring autoServiceWiring; + + // Table rows access this for function name resolution + DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; + private Trace currentTrace; // Copy for transition + + private TraceStack currentStack; + + private ForStackListener forStackListener = new ForStackListener(); + private ForFunctionsListener forFunctionsListener = new ForFunctionsListener(); + + private final SuppressableCallback cbFrameSelected = new SuppressableCallback<>(); + + protected final StackTableModel stackTableModel; + protected GhidraTable stackTable; + protected GhidraTableFilterPanel stackFilterPanel; + + private DebuggerStackActionContext myActionContext; + + public DebuggerLegacyStackPanel(DebuggerStackPlugin plugin, DebuggerStackProvider provider) { + super(new BorderLayout()); + this.provider = provider; + stackTableModel = new StackTableModel(provider.getTool()); + + this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); + + stackTable = new GhidraTable(stackTableModel); + add(new JScrollPane(stackTable)); + stackFilterPanel = new GhidraTableFilterPanel<>(stackTable, stackTableModel); + add(stackFilterPanel, BorderLayout.SOUTH); + + stackTable.getSelectionModel().addListSelectionListener(evt -> { + if (evt.getValueIsAdjusting()) { + return; + } + contextChanged(); + activateSelectedFrame(); + }); + + stackTable.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() < 2 || e.getButton() != MouseEvent.BUTTON1) { + return; + } + if (listingService == null) { + return; + } + if (myActionContext == null) { + return; + } + Address pc = myActionContext.getFrame().getProgramCounter(); + if (pc == null) { + return; + } + listingService.goTo(pc, true); + } + + @Override + public void mouseReleased(MouseEvent e) { + int selectedRow = stackTable.getSelectedRow(); + StackFrameRow row = stackTableModel.getRowObject(selectedRow); + rowActivated(row); + } + }); + + // TODO: Adjust default column widths? + TableColumnModel columnModel = stackTable.getColumnModel(); + + TableColumn levelCol = columnModel.getColumn(StackTableColumns.LEVEL.ordinal()); + levelCol.setPreferredWidth(25); + TableColumn baseCol = columnModel.getColumn(StackTableColumns.PC.ordinal()); + baseCol.setCellRenderer(CustomToStringCellRenderer.MONO_OBJECT); + } + + protected void contextChanged() { + StackFrameRow row = stackFilterPanel.getSelectedItem(); + myActionContext = + row == null ? null : new DebuggerStackActionContext(provider, row, stackTable); + provider.contextChanged(); + } + + protected void activateSelectedFrame() { + // TODO: Action to toggle sync? + if (myActionContext == null) { + return; + } + if (traceManager == null) { + return; + } + cbFrameSelected.invoke(() -> { + traceManager.activateFrame(myActionContext.getFrame().getFrameLevel()); + }); + } + + private void rowActivated(StackFrameRow row) { + if (row == null) { + return; + } + TraceStackFrame frame = row.frame; + if (frame == null) { + return; + } + TraceThread thread = frame.getStack().getThread(); + Trace trace = thread.getTrace(); + TraceRecorder recorder = modelService.getRecorder(trace); + if (recorder == null) { + return; + } + TargetStackFrame targetFrame = recorder.getTargetStackFrame(thread, frame.getLevel()); + if (targetFrame == null || !targetFrame.isValid()) { + return; + } + DebugModelConventions.requestActivation(targetFrame); + } + + protected void updateStack() { + Set toAdd = new LinkedHashSet<>(currentStack.getFrames(current.getSnap())); + for (Iterator it = stackTableModel.getModelData().iterator(); it + .hasNext();) { + StackFrameRow row = it.next(); + if (!toAdd.remove(row.frame)) { + it.remove(); + } + else { + row.update(); + } + } + + for (TraceStackFrame frame : toAdd) { + stackTableModel.add(new StackFrameRow(this, frame)); + } + + stackTableModel.fireTableDataChanged(); + + selectCurrentFrame(); + } + + protected void doSetCurrentStack(TraceStack stack) { + if (stack == null) { + currentStack = null; + stackTableModel.clear(); + contextChanged(); + return; + } + if (currentStack == stack && stack.hasFixedFrames()) { + stackTableModel.fireTableDataChanged(); + return; + } + currentStack = stack; + stackTableModel.clear(); + for (TraceStackFrame frame : currentStack.getFrames(current.getSnap())) { + stackTableModel.add(new StackFrameRow(this, frame)); + } + } + + /** + * Synthesize a stack with only one frame, taking PC from the registers + */ + protected void doSetSyntheticStack() { + stackTableModel.clear(); + currentStack = null; + + Trace curTrace = current.getTrace(); + TraceMemorySpace regs = + curTrace.getMemoryManager().getMemoryRegisterSpace(current.getThread(), false); + if (regs == null) { + contextChanged(); + return; + } + Register pc = curTrace.getBaseLanguage().getProgramCounter(); + if (pc == null) { + contextChanged(); + return; + } + RegisterValue value = regs.getViewValue(current.getViewSnap(), pc); + if (value == null) { + contextChanged(); + return; + } + Address address = curTrace.getBaseLanguage() + .getDefaultSpace() + .getAddress(value.getUnsignedValue().longValue(), true); + stackTableModel.add(new StackFrameRow.Synthetic(this, address)); + } + + protected void loadStack() { + TraceThread curThread = current.getThread(); + if (curThread == null) { + doSetCurrentStack(null); + return; + } + // TODO: getLatestViewStack? Conventionally, I don't expect any scratch stacks, yet. + TraceStack stack = + current.getTrace().getStackManager().getLatestStack(curThread, current.getViewSnap()); + if (stack == null) { + doSetSyntheticStack(); + } + else { + doSetCurrentStack(stack); + } + selectCurrentFrame(); + } + + private void removeOldListeners() { + if (currentTrace == null) { + return; + } + currentTrace.removeListener(forStackListener); + } + + private void addNewListeners() { + if (currentTrace == null) { + return; + } + currentTrace.addListener(forStackListener); + } + + private void doSetTrace(Trace trace) { + if (currentTrace == trace) { + return; + } + removeOldListeners(); + this.currentTrace = trace; + addNewListeners(); + } + + public void coordinatesActivated(DebuggerCoordinates coordinates) { + current = coordinates; + doSetTrace(current.getTrace()); + loadStack(); + } + + protected void selectCurrentFrame() { + try (Suppression supp = cbFrameSelected.suppress(null)) { + StackFrameRow row = + stackTableModel.findFirst(r -> r.getFrameLevel() == current.getFrame()); + if (row == null) { + // Strange + stackTable.clearSelection(); + } + else { + stackFilterPanel.setSelectedItem(row); + } + } + } + + public DebuggerStackActionContext getActionContext() { + return myActionContext; + } + + @AutoServiceConsumed + public void setModelService(DebuggerModelService modelService) { + this.modelService = modelService; + } + + @AutoServiceConsumed + private void setMappingService(DebuggerStaticMappingService mappingService) { + if (this.mappingService != null) { + this.mappingService.removeChangeListener(forFunctionsListener); + } + this.mappingService = mappingService; + if (this.mappingService != null) { + this.mappingService.addChangeListener(forFunctionsListener); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPanel.java new file mode 100644 index 0000000000..404b4f9f71 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPanel.java @@ -0,0 +1,179 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.stack; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import docking.ActionContext; +import docking.widgets.table.AbstractDynamicTableColumn; +import docking.widgets.table.TableColumnDescriptor; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.gui.model.*; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.app.plugin.core.debug.gui.model.columns.TraceValueKeyColumn; +import ghidra.app.plugin.core.debug.gui.model.columns.TraceValueObjectAttributeColumn; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.dbg.target.TargetStack; +import ghidra.dbg.target.TargetStackFrame; +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.util.PathMatcher; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.*; +import utilities.util.SuppressableCallback; +import utilities.util.SuppressableCallback.Suppression; + +public class DebuggerStackPanel extends ObjectsTablePanel implements ListSelectionListener { + + private static class FrameLevelColumn extends TraceValueKeyColumn { + @Override + public String getColumnName() { + return "Level"; + } + + @Override + public int getColumnPreferredWidth() { + return 48; + } + } + + private static class FramePcColumn extends TraceValueObjectAttributeColumn { + public FramePcColumn() { + super(TargetStackFrame.PC_ATTRIBUTE_NAME, Address.class); + } + + @Override + public String getColumnName() { + return "PC"; + } + } + + private class FrameFunctionColumn + extends AbstractDynamicTableColumn { + + @Override + public String getColumnName() { + return "Function"; + } + + @Override + public Function getValue(ValueRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + TraceObjectValue value = rowObject.getAttribute(TargetStackFrame.PC_ATTRIBUTE_NAME); + return value == null ? null : provider.getFunction((Address) value.getValue()); + } + } + + private class StackTableModel extends ObjectTableModel { + protected StackTableModel(Plugin plugin) { + super(plugin); + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + descriptor.addVisibleColumn(new FrameLevelColumn()); + descriptor.addVisibleColumn(new FramePcColumn()); + descriptor.addVisibleColumn(new FrameFunctionColumn()); + // TODO: Comment column? + return descriptor; + } + } + + private final DebuggerStackProvider provider; + + @AutoServiceConsumed + protected DebuggerTraceManagerService traceManager; + @SuppressWarnings("unused") + private final AutoService.Wiring autoServiceWiring; + + private final SuppressableCallback cbFrameSelected = new SuppressableCallback<>(); + + private DebuggerObjectActionContext myActionContext; + + public DebuggerStackPanel(Plugin plugin, DebuggerStackProvider provider) { + super(plugin); + this.provider = provider; + + this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); + + setLimitToSnap(true); + setShowHidden(false); + + addSelectionListener(this); + } + + @Override + protected ObjectTableModel createModel(Plugin plugin) { + return new StackTableModel(plugin); + } + + public ActionContext getActionContext() { + return myActionContext; + } + + protected ModelQuery computeQuery(TraceObject object) { + if (object == null) { + return ModelQuery.EMPTY; + } + TargetObjectSchema rootSchema = object.getRoot() + .getTargetSchema(); + List stackPath = rootSchema + .searchForSuitable(TargetStack.class, object.getCanonicalPath().getKeyList()); + if (stackPath == null) { + return ModelQuery.EMPTY; + } + TargetObjectSchema stackSchema = rootSchema.getSuccessorSchema(stackPath); + PathMatcher matcher = stackSchema.searchFor(TargetStackFrame.class, stackPath, true); + return new ModelQuery(matcher); + } + + public void coordinatesActivated(DebuggerCoordinates coordinates) { + TraceObject object = coordinates.getObject(); + setQuery(computeQuery(object)); + goToCoordinates(coordinates); + + if (object != null) { + try (Suppression supp = cbFrameSelected.suppress(null)) { + trySelectAncestor(object); + } + } + } + + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getValueIsAdjusting()) { + return; + } + List sel = getSelectedItems(); + if (!sel.isEmpty()) { + myActionContext = new DebuggerObjectActionContext( + sel.stream().map(r -> r.getValue()).collect(Collectors.toList()), provider, this); + } + ValueRow item = getSelectedItem(); + if (item != null) { + cbFrameSelected.invoke(() -> traceManager.activateObject(item.getValue().getChild())); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPlugin.java index 3a4f8186f4..08a6858ad8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackPlugin.java @@ -22,20 +22,24 @@ import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.*; +import ghidra.trace.model.thread.TraceThread; -@PluginInfo( // - shortDescription = "Debugger stack view", // - description = "GUI to navigate the stack", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { - TraceActivatedPluginEvent.class, // - TraceClosedPluginEvent.class, // - }, // - servicesRequired = { // - } // -) +@PluginInfo( + shortDescription = "Debugger stack view", + description = "GUI to navigate the stack", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { + TraceActivatedPluginEvent.class, + TraceClosedPluginEvent.class, + }, + servicesRequired = { + }) public class DebuggerStackPlugin extends AbstractDebuggerPlugin { protected DebuggerStackProvider provider; @@ -58,9 +62,11 @@ public class DebuggerStackPlugin extends AbstractDebuggerPlugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceActivatedPluginEvent) { - TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; + if (event instanceof TraceActivatedPluginEvent ev) { provider.coordinatesActivated(ev.getActiveCoordinates()); } + else if (event instanceof TraceClosedPluginEvent ev) { + provider.traceClosed(ev.getTrace()); + } } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java index 5542c58c4f..ccfda07617 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProvider.java @@ -16,115 +16,31 @@ package ghidra.app.plugin.core.debug.gui.stack; import java.awt.BorderLayout; -import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.util.*; -import java.util.function.*; +import java.util.Objects; -import javax.swing.*; -import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; +import javax.swing.JComponent; +import javax.swing.JPanel; + +import org.apache.commons.lang3.ArrayUtils; import docking.ActionContext; import docking.WindowPosition; -import docking.widgets.table.CustomToStringCellRenderer; -import docking.widgets.table.DefaultEnumeratedColumnTableModel; -import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.services.*; -import ghidra.dbg.DebugModelConventions; -import ghidra.dbg.target.TargetStackFrame; -import ghidra.framework.plugintool.*; +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.framework.plugintool.AutoService; +import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.program.model.address.Address; -import ghidra.program.model.lang.Register; -import ghidra.program.model.lang.RegisterValue; -import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.Function; +import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; -import ghidra.trace.model.Trace.TraceMemoryBytesChangeType; -import ghidra.trace.model.Trace.TraceStackChangeType; -import ghidra.trace.model.memory.TraceMemorySpace; -import ghidra.trace.model.stack.TraceStack; -import ghidra.trace.model.stack.TraceStackFrame; import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.util.TraceAddressSpace; -import ghidra.trace.util.TraceRegisterUtils; -import ghidra.util.Swing; -import ghidra.util.table.GhidraTable; -import ghidra.util.table.GhidraTableFilterPanel; -import utilities.util.SuppressableCallback; -import utilities.util.SuppressableCallback.Suppression; public class DebuggerStackProvider extends ComponentProviderAdapter { - protected enum StackTableColumns - implements EnumeratedTableColumn { - LEVEL("Level", Integer.class, StackFrameRow::getFrameLevel), - PC("PC", Address.class, StackFrameRow::getProgramCounter), - FUNCTION("Function", ghidra.program.model.listing.Function.class, StackFrameRow::getFunction), - COMMENT("Comment", String.class, StackFrameRow::getComment, StackFrameRow::setComment, StackFrameRow::isCommentable); - - private final String header; - private final Function getter; - private final BiConsumer setter; - private final Predicate editable; - private final Class cls; - - @SuppressWarnings("unchecked") - StackTableColumns(String header, Class cls, Function getter, - BiConsumer setter, Predicate editable) { - this.header = header; - this.cls = cls; - this.getter = getter; - this.setter = (BiConsumer) setter; - this.editable = editable; - } - - StackTableColumns(String header, Class cls, Function getter) { - this(header, cls, getter, null, null); - } - - @Override - public Class getValueClass() { - return cls; - } - - @Override - public Object getValueOf(StackFrameRow row) { - return getter.apply(row); - } - - @Override - public void setValueOf(StackFrameRow row, Object value) { - setter.accept(row, value); - } - - @Override - public String getHeader() { - return header; - } - - @Override - public boolean isEditable(StackFrameRow row) { - return setter != null && editable.test(row); - } - } - - protected static class StackTableModel - extends DefaultEnumeratedColumnTableModel { - - public StackTableModel(PluginTool tool) { - super(tool, "Stack", StackTableColumns.class); - } - - @Override - public List defaultSortOrder() { - return List.of(StackTableColumns.LEVEL); - } - } - protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) { if (!Objects.equals(a.getTrace(), b.getTrace())) { return false; @@ -141,119 +57,23 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { return true; } - class ForStackListener extends TraceDomainObjectListener { - public ForStackListener() { - listenFor(TraceStackChangeType.ADDED, this::stackAdded); - listenFor(TraceStackChangeType.CHANGED, this::stackChanged); - listenFor(TraceStackChangeType.DELETED, this::stackDeleted); + private final DebuggerStackPlugin plugin; - listenFor(TraceMemoryBytesChangeType.CHANGED, this::bytesChanged); - } - - private void stackAdded(TraceStack stack) { - if (stack.getSnap() != current.getViewSnap()) { - return; - } - TraceThread curThread = current.getThread(); - if (curThread != stack.getThread()) { - return; - } - loadStack(); - } - - private void stackChanged(TraceStack stack) { - if (currentStack != stack) { - return; - } - updateStack(); - } - - private void stackDeleted(TraceStack stack) { - if (currentStack != stack) { - return; - } - loadStack(); - } - - // For updating a synthetic frame - private void bytesChanged(TraceAddressSpace space, TraceAddressSnapRange range) { - TraceThread curThread = current.getThread(); - if (space.getThread() != curThread || space.getFrameLevel() != 0) { - return; - } - if (!current.getView().getViewport().containsAnyUpper(range.getLifespan())) { - return; - } - List stackData = stackTableModel.getModelData(); - if (stackData.isEmpty() || !(stackData.get(0) instanceof StackFrameRow.Synthetic)) { - return; - } - StackFrameRow.Synthetic frameRow = (StackFrameRow.Synthetic) stackData.get(0); - Trace trace = current.getTrace(); - Register pc = trace.getBaseLanguage().getProgramCounter(); - if (!TraceRegisterUtils.rangeForRegister(pc).intersects(range.getRange())) { - return; - } - TraceMemorySpace regs = - trace.getMemoryManager().getMemoryRegisterSpace(curThread, false); - RegisterValue value = regs.getViewValue(current.getViewSnap(), pc); - Address address = trace.getBaseLanguage() - .getDefaultSpace() - .getAddress(value.getUnsignedValue().longValue()); - frameRow.updateProgramCounter(address); - stackTableModel.fireTableDataChanged(); - } - } - - class ForFunctionsListener implements DebuggerStaticMappingChangeListener { - @Override - public void mappingsChanged(Set affectedTraces, Set affectedPrograms) { - Trace curTrace = current.getTrace(); - if (curTrace == null || !affectedTraces.contains(curTrace)) { - return; - } - Swing.runIfSwingOrRunLater(() -> stackTableModel.fireTableDataChanged()); - } - } - - //private final DebuggerStackPlugin plugin; - - // Table rows access these for function name resolution DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; - private Trace currentTrace; // Copy for transition - private TraceStack currentStack; - - @AutoServiceConsumed - private DebuggerTraceManagerService traceManager; - // @AutoServiceConsumed by method - private DebuggerModelService modelService; - // @AutoServiceConsumed via method + @AutoServiceConsumed // TODO: Add listener for mapping changes to refresh table DebuggerStaticMappingService mappingService; - @AutoServiceConsumed - private DebuggerListingService listingService; // TODO: Goto pc on double-click - @AutoServiceConsumed - private MarkerService markerService; // TODO: Mark non-current frame PCs, too. (separate plugin?) @SuppressWarnings("unused") private final AutoService.Wiring autoServiceWiring; - private ForStackListener forStackListener = new ForStackListener(); - private ForFunctionsListener forFunctionsListener = new ForFunctionsListener(); + private final JPanel mainPanel = new JPanel(new BorderLayout()); - private final SuppressableCallback cbFrameSelected = new SuppressableCallback<>(); - - protected final StackTableModel stackTableModel; - protected GhidraTable stackTable; - protected GhidraTableFilterPanel stackFilterPanel; - - private JPanel mainPanel = new JPanel(new BorderLayout()); - - private DebuggerStackActionContext myActionContext; + /*testing*/ DebuggerStackPanel panel; + /*testing*/ DebuggerLegacyStackPanel legacyPanel; public DebuggerStackProvider(DebuggerStackPlugin plugin) { super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_STACK, plugin.getName()); - stackTableModel = new StackTableModel(tool); - + this.plugin = plugin; this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); setTitle(DebuggerResources.TITLE_PROVIDER_STACK); @@ -267,99 +87,12 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { createActions(); setVisible(true); - contextChanged(); } protected void buildMainPanel() { - stackTable = new GhidraTable(stackTableModel); - mainPanel.add(new JScrollPane(stackTable)); - stackFilterPanel = new GhidraTableFilterPanel<>(stackTable, stackTableModel); - mainPanel.add(stackFilterPanel, BorderLayout.SOUTH); - - stackTable.getSelectionModel().addListSelectionListener(evt -> { - if (evt.getValueIsAdjusting()) { - return; - } - contextChanged(); - activateSelectedFrame(); - }); - - stackTable.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() < 2 || e.getButton() != MouseEvent.BUTTON1) { - return; - } - if (listingService == null) { - return; - } - if (myActionContext == null) { - return; - } - Address pc = myActionContext.getFrame().getProgramCounter(); - if (pc == null) { - return; - } - listingService.goTo(pc, true); - } - - @Override - public void mouseReleased(MouseEvent e) { - int selectedRow = stackTable.getSelectedRow(); - StackFrameRow row = stackTableModel.getRowObject(selectedRow); - rowActivated(row); - } - }); - - // TODO: Adjust default column widths? - TableColumnModel columnModel = stackTable.getColumnModel(); - - TableColumn levelCol = columnModel.getColumn(StackTableColumns.LEVEL.ordinal()); - levelCol.setPreferredWidth(25); - TableColumn baseCol = columnModel.getColumn(StackTableColumns.PC.ordinal()); - baseCol.setCellRenderer(CustomToStringCellRenderer.MONO_OBJECT); - } - - @Override - public void contextChanged() { - StackFrameRow row = stackFilterPanel.getSelectedItem(); - myActionContext = - row == null ? null : new DebuggerStackActionContext(this, row, stackTable); - super.contextChanged(); - } - - protected void activateSelectedFrame() { - // TODO: Action to toggle sync? - if (myActionContext == null) { - return; - } - if (traceManager == null) { - return; - } - cbFrameSelected.invoke(() -> { - traceManager.activateFrame(myActionContext.getFrame().getFrameLevel()); - }); - } - - private void rowActivated(StackFrameRow row) { - if (row == null) { - return; - } - TraceStackFrame frame = row.frame; - if (frame == null) { - return; - } - TraceThread thread = frame.getStack().getThread(); - Trace trace = thread.getTrace(); - TraceRecorder recorder = modelService.getRecorder(trace); - if (recorder == null) { - return; - } - TargetStackFrame targetFrame = recorder.getTargetStackFrame(thread, frame.getLevel()); - if (targetFrame == null || !targetFrame.isValid()) { - return; - } - DebugModelConventions.requestActivation(targetFrame); + panel = new DebuggerStackPanel(plugin, this); + mainPanel.add(panel); + legacyPanel = new DebuggerLegacyStackPanel(plugin, this); } protected void createActions() { @@ -371,100 +104,20 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { return mainPanel; } + protected static boolean isLegacy(Trace trace) { + return trace != null && trace.getObjectManager().getRootSchema() == null; + } + @Override public ActionContext getActionContext(MouseEvent event) { - if (myActionContext == null) { - return super.getActionContext(event); + if (isLegacy(current.getTrace())) { + return legacyPanel.getActionContext(); } - return myActionContext; - } - - protected void updateStack() { - Set toAdd = new LinkedHashSet<>(currentStack.getFrames(current.getSnap())); - for (Iterator it = stackTableModel.getModelData().iterator(); it - .hasNext();) { - StackFrameRow row = it.next(); - if (!toAdd.remove(row.frame)) { - it.remove(); - } - else { - row.update(); - } + ActionContext context = panel.getActionContext(); + if (context != null) { + return context; } - - for (TraceStackFrame frame : toAdd) { - stackTableModel.add(new StackFrameRow(this, frame)); - } - - stackTableModel.fireTableDataChanged(); - - selectCurrentFrame(); - } - - protected void doSetCurrentStack(TraceStack stack) { - if (stack == null) { - currentStack = null; - stackTableModel.clear(); - contextChanged(); - return; - } - if (currentStack == stack && stack.hasFixedFrames()) { - stackTableModel.fireTableDataChanged(); - return; - } - currentStack = stack; - stackTableModel.clear(); - for (TraceStackFrame frame : currentStack.getFrames(current.getSnap())) { - stackTableModel.add(new StackFrameRow(this, frame)); - } - } - - /** - * Synthesize a stack with only one frame, taking PC from the registers - */ - protected void doSetSyntheticStack() { - stackTableModel.clear(); - currentStack = null; - - Trace curTrace = current.getTrace(); - TraceMemorySpace regs = - curTrace.getMemoryManager().getMemoryRegisterSpace(current.getThread(), false); - if (regs == null) { - contextChanged(); - return; - } - Register pc = curTrace.getBaseLanguage().getProgramCounter(); - if (pc == null) { - contextChanged(); - return; - } - RegisterValue value = regs.getViewValue(current.getViewSnap(), pc); - if (value == null) { - contextChanged(); - return; - } - Address address = curTrace.getBaseLanguage() - .getDefaultSpace() - .getAddress(value.getUnsignedValue().longValue(), true); - stackTableModel.add(new StackFrameRow.Synthetic(this, address)); - } - - protected void loadStack() { - TraceThread curThread = current.getThread(); - if (curThread == null) { - doSetCurrentStack(null); - return; - } - // TODO: getLatestViewStack? Conventionally, I don't expect any scratch stacks, yet. - TraceStack stack = - current.getTrace().getStackManager().getLatestStack(curThread, current.getViewSnap()); - if (stack == null) { - doSetSyntheticStack(); - } - else { - doSetCurrentStack(stack); - } - selectCurrentFrame(); + return super.getActionContext(event); } protected String computeSubTitle() { @@ -476,69 +129,59 @@ public class DebuggerStackProvider extends ComponentProviderAdapter { setSubTitle(computeSubTitle()); } - private void removeOldListeners() { - if (currentTrace == null) { - return; - } - currentTrace.removeListener(forStackListener); - } - - private void addNewListeners() { - if (currentTrace == null) { - return; - } - currentTrace.addListener(forStackListener); - } - - private void doSetTrace(Trace trace) { - if (currentTrace == trace) { - return; - } - removeOldListeners(); - this.currentTrace = trace; - addNewListeners(); - } - public void coordinatesActivated(DebuggerCoordinates coordinates) { if (sameCoordinates(current, coordinates)) { current = coordinates; return; } + current = coordinates; - doSetTrace(current.getTrace()); - - loadStack(); + if (isLegacy(coordinates.getTrace())) { + panel.coordinatesActivated(DebuggerCoordinates.NOWHERE); + legacyPanel.coordinatesActivated(coordinates); + if (ArrayUtils.indexOf(mainPanel.getComponents(), legacyPanel) == -1) { + mainPanel.remove(panel); + mainPanel.add(legacyPanel); + mainPanel.validate(); + } + } + else { + legacyPanel.coordinatesActivated(DebuggerCoordinates.NOWHERE); + panel.coordinatesActivated(coordinates); + if (ArrayUtils.indexOf(mainPanel.getComponents(), panel) == -1) { + mainPanel.remove(legacyPanel); + mainPanel.add(panel); + mainPanel.validate(); + } + } updateSubTitle(); } - protected void selectCurrentFrame() { - try (Suppression supp = cbFrameSelected.suppress(null)) { - StackFrameRow row = - stackTableModel.findFirst(r -> r.getFrameLevel() == current.getFrame()); - if (row == null) { - // Strange - stackTable.clearSelection(); - } - else { - stackFilterPanel.setSelectedItem(row); - } + public void traceClosed(Trace trace) { + if (trace == current.getTrace()) { + panel.coordinatesActivated(DebuggerCoordinates.NOWHERE); + legacyPanel.coordinatesActivated(DebuggerCoordinates.NOWHERE); } } - @AutoServiceConsumed - public void setModelService(DebuggerModelService modelService) { - this.modelService = modelService; - } - - @AutoServiceConsumed - private void setMappingService(DebuggerStaticMappingService mappingService) { - if (this.mappingService != null) { - this.mappingService.removeChangeListener(forFunctionsListener); + public Function getFunction(Address pc) { + if (pc == null) { + return null; } - this.mappingService = mappingService; - if (this.mappingService != null) { - this.mappingService.addChangeListener(forFunctionsListener); + if (mappingService == null) { + return null; } + TraceThread curThread = current.getThread(); + if (curThread == null) { + return null; + } + TraceLocation dloc = new DefaultTraceLocation(curThread.getTrace(), + curThread, Lifespan.at(current.getSnap()), pc); + ProgramLocation sloc = mappingService.getOpenMappedLocation(dloc); + if (sloc == null) { + return null; + } + return sloc.getProgram().getFunctionManager().getFunctionContaining(sloc.getAddress()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/StackFrameRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/StackFrameRow.java index adafd37492..2a7716ae84 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/StackFrameRow.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/stack/StackFrameRow.java @@ -17,18 +17,15 @@ package ghidra.app.plugin.core.debug.gui.stack; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Function; -import ghidra.program.util.ProgramLocation; -import ghidra.trace.model.*; import ghidra.trace.model.stack.TraceStackFrame; -import ghidra.trace.model.thread.TraceThread; import ghidra.util.database.UndoableTransaction; public class StackFrameRow { public static class Synthetic extends StackFrameRow { private Address pc; - public Synthetic(DebuggerStackProvider provider, Address pc) { - super(provider); + public Synthetic(DebuggerLegacyStackPanel panel, Address pc) { + super(panel); this.pc = pc; } @@ -42,19 +39,19 @@ public class StackFrameRow { } } - private final DebuggerStackProvider provider; + private final DebuggerLegacyStackPanel panel; final TraceStackFrame frame; private int level; - public StackFrameRow(DebuggerStackProvider provider, TraceStackFrame frame) { - this.provider = provider; + public StackFrameRow(DebuggerLegacyStackPanel panel, TraceStackFrame frame) { + this.panel = panel; this.frame = frame; this.level = frame.getLevel(); } - private StackFrameRow(DebuggerStackProvider provider) { - this.provider = provider; + private StackFrameRow(DebuggerLegacyStackPanel panel) { + this.panel = panel; this.frame = null; this.level = 0; } @@ -64,7 +61,7 @@ public class StackFrameRow { } public long getSnap() { - return provider.current.getSnap(); + return panel.current.getSnap(); } public Address getProgramCounter() { @@ -87,24 +84,7 @@ public class StackFrameRow { } public Function getFunction() { - if (provider.mappingService == null) { - return null; - } - TraceThread curThread = provider.current.getThread(); - if (curThread == null) { - return null; - } - Address pc = getProgramCounter(); - if (pc == null) { - return null; - } - TraceLocation dloc = new DefaultTraceLocation(curThread.getTrace(), - curThread, Lifespan.at(getSnap()), pc); - ProgramLocation sloc = provider.mappingService.getOpenMappedLocation(dloc); - if (sloc == null) { - return null; - } - return sloc.getProgram().getFunctionManager().getFunctionContaining(sloc.getAddress()); + return panel.provider.getFunction(getProgramCounter()); } protected void update() { diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/ModelQueryTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/ModelQueryTest.java index 0857539e3a..ad4096dc7e 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/ModelQueryTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/ModelQueryTest.java @@ -57,4 +57,31 @@ public class ModelQueryTest extends AbstractGhidraHeadedDebuggerGUITest { assertFalse(threadQuery.includes(Lifespan.before(0), thread0Val)); } } + + @Test + public void testInvolves() throws Throwable { + createTrace(); + + ModelQuery rootQuery = ModelQuery.parse(""); + ModelQuery threadQuery = ModelQuery.parse("Processes[].Threads[]"); + + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceObjectManager objects = tb.trace.getObjectManager(); + + TraceObjectValue rootVal = + objects.createRootObject(CTX.getSchema(new SchemaName("Session"))); + + TraceObjectValue thread0Val = + objects.createObject(TraceObjectKeyPath.parse("Processes[0].Threads[0]")) + .insert(Lifespan.nowOn(0), ConflictResolution.DENY) + .getLastEntry(); + + assertTrue(rootQuery.involves(Lifespan.ALL, rootVal)); + assertFalse(rootQuery.involves(Lifespan.ALL, thread0Val)); + + assertTrue(threadQuery.involves(Lifespan.ALL, rootVal)); + assertTrue(threadQuery.involves(Lifespan.ALL, thread0Val)); + assertFalse(threadQuery.involves(Lifespan.before(0), thread0Val)); + } + } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderLegacyTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderLegacyTest.java new file mode 100644 index 0000000000..7d5eec1788 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderLegacyTest.java @@ -0,0 +1,508 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.stack; + +import static org.junit.Assert.*; + +import java.awt.event.MouseEvent; +import java.math.BigInteger; +import java.util.List; + +import org.junit.*; + +import generic.Unique; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingServicePlugin; +import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingUtils; +import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.Function; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.*; +import ghidra.trace.model.memory.TraceMemoryFlag; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.stack.TraceStack; +import ghidra.trace.model.stack.TraceStackFrame; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.exception.DuplicateNameException; +import ghidra.util.task.TaskMonitor; + +public class DebuggerStackProviderLegacyTest extends AbstractGhidraHeadedDebuggerGUITest { + protected DebuggerStackPlugin stackPlugin; + protected DebuggerStackProvider stackProvider; + protected DebuggerStaticMappingService mappingService; + + protected Register pc; + + @Before + public void setUpStackProviderTest() throws Exception { + stackPlugin = addPlugin(tool, DebuggerStackPlugin.class); + stackProvider = waitForComponentProvider(DebuggerStackProvider.class); + + mappingService = addPlugin(tool, DebuggerStaticMappingServicePlugin.class); + + pc = getToyBE64Language().getProgramCounter(); + } + + protected TraceThread addThread(String n) throws DuplicateNameException { + try (UndoableTransaction tid = tb.startTransaction()) { + return tb.trace.getThreadManager().createThread(n, 0); + } + } + + protected void addRegVals(TraceThread thread) { + try (UndoableTransaction tid = tb.startTransaction()) { + TraceMemorySpace regs = + tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, true); + regs.setValue(0, new RegisterValue(pc, new BigInteger("00400123", 16))); + } + } + + protected TraceStack addStack(TraceThread thread, int snap) { + try (UndoableTransaction tid = tb.startTransaction()) { + return tb.trace.getStackManager().getStack(thread, snap, true); + } + } + + protected TraceStack addStack(TraceThread thread) { + return addStack(thread, 0); + } + + protected void addStackFrames(TraceStack stack) { + try (UndoableTransaction tid = tb.startTransaction()) { + stack.setDepth(2, false); + + TraceStackFrame frame = stack.getFrame(0, false); + frame.setProgramCounter(Lifespan.ALL, tb.addr(0x00400100)); + frame.setComment(stack.getSnap(), "Hello"); + + frame = stack.getFrame(1, false); + frame.setProgramCounter(Lifespan.ALL, tb.addr(0x00400200)); + frame.setComment(stack.getSnap(), "World"); + } + } + + protected void assertProviderEmpty() { + List framesDisplayed = + stackProvider.legacyPanel.stackTableModel.getModelData(); + assertTrue(framesDisplayed.isEmpty()); + } + + protected void assertProviderPopulatedSynthetic() { + List framesDisplayed = + stackProvider.legacyPanel.stackTableModel.getModelData(); + StackFrameRow row = Unique.assertOne(framesDisplayed); + + assertNull(row.frame); + assertEquals(0x00400123, row.getProgramCounter().getOffset()); + } + + protected void assertTableSize(int size) { + assertEquals(size, stackProvider.legacyPanel.stackTableModel.getModelData().size()); + } + + protected void assertRow(int level, Address pcVal, String comment, Function func) { + StackFrameRow row = stackProvider.legacyPanel.stackTableModel.getModelData().get(level); + assertEquals(level, row.getFrameLevel()); + assertNotNull(row.frame); + assertEquals(pcVal, row.getProgramCounter()); + assertEquals(comment, row.getComment()); + assertEquals(func, row.getFunction()); + } + + protected void assertProviderPopulated() { + assertTableSize(2); + assertRow(0, tb.addr(0x00400100), "Hello", null); + assertRow(1, tb.addr(0x00400200), "World", null); + } + + @Test + public void testEmpty() throws Exception { + waitForSwing(); + assertProviderEmpty(); + } + + @Test + public void testActivateTraceNoThreadEmpty() throws Exception { + createAndOpenTrace(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertProviderEmpty(); + } + + @Test + public void testActivateThreadNoStackNoRegsEmpty() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderEmpty(); + } + + @Test + public void testActivateThreadNoStackRegsSynthetic() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + addRegVals(thread); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulatedSynthetic(); + } + + @Test + public void testActivateThreadRegsThenAddEmptyStackEmpty() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + addRegVals(thread); + addStack(thread); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderEmpty(); + } + + @Test + public void testActivateThreadThenAddStackPopulatesProvider() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + traceManager.activateThread(thread); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + assertProviderPopulated(); + } + + @Test + public void testAddStackThenActivateThreadPopulatesProvider() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + } + + @Test + public void testAppendStackUpdatesProvider() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + + try (UndoableTransaction tid = tb.startTransaction()) { + stack.setDepth(3, false); + } + waitForDomainObject(tb.trace); + + assertTableSize(3); + assertRow(0, tb.addr(0x00400100), "Hello", null); + assertRow(1, tb.addr(0x00400200), "World", null); + assertRow(2, null, null, null); + } + + @Test + public void testPushStackUpdatesProvider() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + + try (UndoableTransaction tid = tb.startTransaction()) { + stack.setDepth(3, true); + } + waitForDomainObject(tb.trace); + + assertTableSize(3); + assertRow(0, null, null, null); + assertRow(1, tb.addr(0x00400100), "Hello", null); + assertRow(2, tb.addr(0x00400200), "World", null); + } + + @Test + public void testTruncateStackUpdatesProvider() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + + try (UndoableTransaction tid = tb.startTransaction()) { + stack.setDepth(1, false); + } + waitForDomainObject(tb.trace); + + assertTableSize(1); + assertRow(0, tb.addr(0x00400100), "Hello", null); + } + + @Test + public void testPopStackUpdatesProvider() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + + try (UndoableTransaction tid = tb.startTransaction()) { + stack.setDepth(1, true); + } + waitForDomainObject(tb.trace); + + assertTableSize(1); + assertRow(0, tb.addr(0x00400200), "World", null); + } + + @Test + public void testDeleteStackUpdatesProvider() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + + try (UndoableTransaction tid = tb.startTransaction()) { + stack.delete(); + } + waitForDomainObject(tb.trace); + + assertProviderEmpty(); + } + + @Test + public void testActivateOtherThread() throws Exception { + createAndOpenTrace(); + + TraceThread thread1 = addThread("Processes[1].Threads[1]"); + TraceThread thread2 = addThread("Processes[1].Threads[2]"); + TraceStack stack = addStack(thread1); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread1); + waitForSwing(); + + assertProviderPopulated(); + + traceManager.activateThread(thread2); + waitForSwing(); + + assertProviderEmpty(); + } + + @Test + public void testActivateSnap() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + + addStack(thread, 1); + waitForSwing(); + + assertProviderPopulated(); + + traceManager.activateSnap(1); + waitForSwing(); + + assertProviderEmpty(); + } + + @Test + public void testCloseCurrentTraceEmpty() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + + traceManager.closeTrace(tb.trace); + waitForSwing(); + + assertProviderEmpty(); + } + + @Test + @Ignore("TODO") // Not sure why this fails under Gradle but not my IDE + public void testSelectRowActivatesFrame() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + + clickTableCellWithButton(stackProvider.legacyPanel.stackTable, 0, 0, MouseEvent.BUTTON1); + waitForSwing(); + + assertEquals(0, traceManager.getCurrentFrame()); + + clickTableCellWithButton(stackProvider.legacyPanel.stackTable, 1, 0, MouseEvent.BUTTON1); + waitForSwing(); + + assertEquals(1, traceManager.getCurrentFrame()); + } + + @Test + public void testActivateFrameSelectsRow() throws Exception { + createAndOpenTrace(); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + + traceManager.activateFrame(0); + waitForSwing(); + + assertEquals(0, stackProvider.legacyPanel.stackTable.getSelectedRow()); + + traceManager.activateFrame(1); + waitForSwing(); + + assertEquals(1, stackProvider.legacyPanel.stackTable.getSelectedRow()); + } + + @Test + public void testActivateThenAddMappingPopulatesFunctionColumn() throws Exception { + createTrace(); + createProgramFromTrace(); + + intoProject(tb.trace); + intoProject(program); + + traceManager.openTrace(tb.trace); + programManager.openProgram(program); + + TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceStack stack = addStack(thread); + addStackFrames(stack); + waitForDomainObject(tb.trace); + + traceManager.activateThread(thread); + waitForSwing(); + + assertProviderPopulated(); + + Function func; + try (UndoableTransaction tid = UndoableTransaction.start(program, "Add Function")) { + program.getMemory() + .createInitializedBlock(".text", addr(program, 0x00600000), 0x1000, (byte) 0, + TaskMonitor.DUMMY, false); + AddressSet body = new AddressSet(); + body.add(addr(program, 0x00600100), addr(program, 0x00600123)); + func = program.getFunctionManager() + .createFunction("func", body.getMinAddress(), body, SourceType.USER_DEFINED); + } + waitForDomainObject(program); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getMemoryManager() + .addRegion("Processes[1].Memory[bin:.text]", Lifespan.nowOn(0), + tb.drng(0x00400000, 0x00400fff), + TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); + + TraceLocation dloc = + new DefaultTraceLocation(tb.trace, null, Lifespan.nowOn(0), tb.addr(0x00400000)); + ProgramLocation sloc = new ProgramLocation(program, addr(program, 0x00600000)); + DebuggerStaticMappingUtils.addMapping(dloc, sloc, 0x1000, false); + } + waitForDomainObject(tb.trace); + + assertTableSize(2); + assertRow(0, tb.addr(0x00400100), "Hello", func); + assertRow(1, tb.addr(0x00400200), "World", null); + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderObjectTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderObjectTest.java deleted file mode 100644 index b067a16757..0000000000 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderObjectTest.java +++ /dev/null @@ -1,124 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.plugin.core.debug.gui.stack; - -import java.io.IOException; - -import ghidra.dbg.target.schema.SchemaContext; -import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; -import ghidra.dbg.target.schema.XmlSchemaContext; -import ghidra.trace.model.Trace; -import ghidra.trace.model.target.TraceObjectKeyPath; -import ghidra.trace.model.target.TraceObject.ConflictResolution; -import ghidra.trace.model.thread.TraceObjectThread; -import ghidra.trace.model.thread.TraceThread; -import ghidra.util.database.UndoableTransaction; -import ghidra.util.exception.DuplicateNameException; - -public class DebuggerStackProviderObjectTest extends DebuggerStackProviderTest { - - protected SchemaContext ctx; - - @Override - protected void createTrace(String langID) throws IOException { - super.createTrace(langID); - try { - activateObjectsMode(); - } - catch (Exception e) { - throw new AssertionError(e); - } - } - - @Override - protected void useTrace(Trace trace) { - super.useTrace(trace); - try { - activateObjectsMode(); - } - catch (Exception e) { - throw new AssertionError(e); - } - } - - public void activateObjectsMode() throws Exception { - // NOTE the use of index='1' allowing object-based managers to ID unique path - ctx = XmlSchemaContext.deserialize(""" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - """); - - try (UndoableTransaction tid = tb.startTransaction()) { - tb.trace.getObjectManager().createRootObject(ctx.getSchema(new SchemaName("Session"))); - } - } - - @Override - protected TraceThread addThread(String n) throws DuplicateNameException { - try (UndoableTransaction tid = tb.startTransaction()) { - TraceObjectThread thread = (TraceObjectThread) super.addThread(n); - TraceObjectKeyPath regsPath = thread.getObject().getCanonicalPath().extend("Registers"); - tb.trace.getObjectManager() - .createObject(regsPath) - .insert(thread.getLifespan(), ConflictResolution.DENY); - return thread; - } - } -} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderTest.java index 5a735ccb8e..27cc818af5 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/stack/DebuggerStackProviderTest.java @@ -15,36 +15,50 @@ */ package ghidra.app.plugin.core.debug.gui.stack; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; -import java.awt.event.MouseEvent; -import java.math.BigInteger; +import java.io.IOException; import java.util.List; +import java.util.Objects; import org.junit.*; -import generic.Unique; +import docking.widgets.table.DynamicTableColumn; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.app.plugin.core.debug.gui.model.columns.TraceValueObjectAttributeColumn; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingServicePlugin; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingUtils; import ghidra.app.services.DebuggerStaticMappingService; +import ghidra.dbg.target.TargetMemoryRegion; +import ghidra.dbg.target.TargetStackFrame; +import ghidra.dbg.target.schema.SchemaContext; +import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; +import ghidra.dbg.target.schema.XmlSchemaContext; +import ghidra.dbg.util.PathPattern; +import ghidra.dbg.util.PathUtils; +import ghidra.docking.settings.SettingsImpl; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSet; import ghidra.program.model.lang.Register; -import ghidra.program.model.lang.RegisterValue; import ghidra.program.model.listing.Function; import ghidra.program.model.symbol.SourceType; import ghidra.program.util.ProgramLocation; import ghidra.trace.model.*; -import ghidra.trace.model.memory.TraceMemoryFlag; -import ghidra.trace.model.memory.TraceMemorySpace; -import ghidra.trace.model.stack.TraceStack; -import ghidra.trace.model.stack.TraceStackFrame; -import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.memory.TraceObjectMemoryRegion; +import ghidra.trace.model.stack.TraceObjectStack; +import ghidra.trace.model.target.*; +import ghidra.trace.model.target.TraceObject.ConflictResolution; +import ghidra.trace.model.thread.TraceObjectThread; import ghidra.util.database.UndoableTransaction; -import ghidra.util.exception.DuplicateNameException; import ghidra.util.task.TaskMonitor; +/** + * NOTE: I no longer synthesize a stack frame when the stack is absent. It's a bit of a hack, and I + * don't know if it's really valuable. In fact, in might obscure the fact that the stack is absent. + */ public class DebuggerStackProviderTest extends AbstractGhidraHeadedDebuggerGUITest { protected DebuggerStackPlugin stackPlugin; protected DebuggerStackProvider stackProvider; @@ -52,6 +66,8 @@ public class DebuggerStackProviderTest extends AbstractGhidraHeadedDebuggerGUITe protected Register pc; + protected SchemaContext ctx; + @Before public void setUpStackProviderTest() throws Exception { stackPlugin = addPlugin(tool, DebuggerStackPlugin.class); @@ -62,80 +78,175 @@ public class DebuggerStackProviderTest extends AbstractGhidraHeadedDebuggerGUITe pc = getToyBE64Language().getProgramCounter(); } - protected TraceThread addThread(String n) throws DuplicateNameException { - try (UndoableTransaction tid = tb.startTransaction()) { - return tb.trace.getThreadManager().createThread(n, 0); + @After + public void tearDownStackProviderTest() throws Exception { + traceManager.activate(DebuggerCoordinates.NOWHERE); + waitForSwing(); + waitForTasks(); + runSwing(() -> traceManager.closeAllTraces()); + } + + @Override + protected void createTrace(String langID) throws IOException { + super.createTrace(langID); + try { + activateObjectsMode(); + } + catch (Exception e) { + throw new AssertionError(e); } } - protected void addRegVals(TraceThread thread) { - try (UndoableTransaction tid = tb.startTransaction()) { - TraceMemorySpace regs = - tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, true); - regs.setValue(0, new RegisterValue(pc, new BigInteger("00400123", 16))); + @Override + protected void useTrace(Trace trace) { + super.useTrace(trace); + try { + activateObjectsMode(); + } + catch (Exception e) { + throw new AssertionError(e); } } - protected TraceStack addStack(TraceThread thread, int snap) { + public void activateObjectsMode() throws Exception { + // NOTE the use of index='1' allowing object-based managers to ID unique path + ctx = XmlSchemaContext.deserialize(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """); + try (UndoableTransaction tid = tb.startTransaction()) { - return tb.trace.getStackManager().getStack(thread, snap, true); + tb.trace.getObjectManager().createRootObject(ctx.getSchema(new SchemaName("Session"))); } } - protected TraceStack addStack(TraceThread thread) { - return addStack(thread, 0); + protected TraceObjectThread addThread(int n) { + PathPattern threadPattern = new PathPattern(PathUtils.parse("Processes[1].Threads[]")); + TraceObjectKeyPath threadPath = + TraceObjectKeyPath.of(threadPattern.applyIntKeys(n).getSingletonPath()); + try (UndoableTransaction tid = tb.startTransaction()) { + return Objects.requireNonNull(tb.trace.getObjectManager() + .createObject(threadPath) + .insert(Lifespan.nowOn(0), ConflictResolution.TRUNCATE) + .getDestination(null) + .queryInterface(TraceObjectThread.class)); + } } - protected void addStackFrames(TraceStack stack) { + protected TraceObjectStack addStack(TraceObjectThread thread) { + TraceObjectKeyPath stackPath = thread.getObject().getCanonicalPath().extend("Stack"); try (UndoableTransaction tid = tb.startTransaction()) { - stack.setDepth(2, false); + return Objects.requireNonNull(tb.trace.getObjectManager() + .createObject(stackPath) + .insert(Lifespan.nowOn(0), ConflictResolution.TRUNCATE) + .getDestination(null) + .queryInterface(TraceObjectStack.class)); + } + } - TraceStackFrame frame = stack.getFrame(0, false); - frame.setProgramCounter(Lifespan.ALL, tb.addr(0x00400100)); - frame.setComment(stack.getSnap(), "Hello"); + protected void addStackFrames(TraceObjectStack stack) { + addStackFrames(stack, 2); + } - frame = stack.getFrame(1, false); - frame.setProgramCounter(Lifespan.ALL, tb.addr(0x00400200)); - frame.setComment(stack.getSnap(), "World"); + protected void addStackFrames(TraceObjectStack stack, int count) { + TraceObjectKeyPath stackPath = stack.getObject().getCanonicalPath(); + TraceObjectManager om = tb.trace.getObjectManager(); + try (UndoableTransaction tid = tb.startTransaction()) { + for (int i = 0; i < count; i++) { + TraceObject frame = om.createObject(stackPath.index(i)) + .insert(Lifespan.nowOn(0), ConflictResolution.TRUNCATE) + .getDestination(null); + frame.setAttribute(Lifespan.nowOn(0), TargetStackFrame.PC_ATTRIBUTE_NAME, + tb.addr(0x00400100 + 0x100 * i)); + } } } protected void assertProviderEmpty() { - List framesDisplayed = stackProvider.stackTableModel.getModelData(); - assertTrue(framesDisplayed.isEmpty()); - } - - protected void assertProviderPopulatedSynthetic() { - List framesDisplayed = stackProvider.stackTableModel.getModelData(); - StackFrameRow row = Unique.assertOne(framesDisplayed); - - assertNull(row.frame); - assertEquals(0x00400123, row.getProgramCounter().getOffset()); + assertTrue(stackProvider.panel.getAllItems().isEmpty()); } protected void assertTableSize(int size) { - assertEquals(size, stackProvider.stackTableModel.getModelData().size()); + assertEquals(size, stackProvider.panel.getAllItems().size()); } - protected void assertRow(int level, Address pcVal, String comment, Function func) { - StackFrameRow row = stackProvider.stackTableModel.getModelData().get(level); - assertEquals(level, row.getFrameLevel()); - assertNotNull(row.frame); - assertEquals(pcVal, row.getProgramCounter()); - assertEquals(comment, row.getComment()); - assertEquals(func, row.getFunction()); + protected Object rowColVal(ValueRow row, DynamicTableColumn col) { + if (col instanceof TraceValueObjectAttributeColumn attrCol) { + return attrCol.getAttributeValue(row); + } + Object value = col.getValue(row, SettingsImpl.NO_SETTINGS, tb.trace, tool); + return value; + } + + protected void assertRow(int level, Address pcVal, Function func) { + ValueRow row = stackProvider.panel.getAllItems().get(level); + + DynamicTableColumn levelCol = + stackProvider.panel.getColumnByNameAndType("Level", String.class); + DynamicTableColumn pcCol = + stackProvider.panel.getColumnByNameAndType("PC", ValueRow.class); + DynamicTableColumn funcCol = + stackProvider.panel.getColumnByNameAndType("Function", Function.class); + + assertEquals(PathUtils.makeKey(PathUtils.makeIndex(level)), rowColVal(row, levelCol)); + assertEquals(pcVal, rowColVal(row, pcCol)); + assertEquals(func, rowColVal(row, funcCol)); } protected void assertProviderPopulated() { assertTableSize(2); - assertRow(0, tb.addr(0x00400100), "Hello", null); - assertRow(1, tb.addr(0x00400200), "World", null); + assertRow(0, tb.addr(0x00400100), null); + assertRow(1, tb.addr(0x00400200), null); } @Test public void testEmpty() throws Exception { waitForSwing(); - assertProviderEmpty(); + waitForPass(() -> assertProviderEmpty()); } @Test @@ -143,318 +254,304 @@ public class DebuggerStackProviderTest extends AbstractGhidraHeadedDebuggerGUITe createAndOpenTrace(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertProviderEmpty(); + waitForPass(() -> assertProviderEmpty()); } @Test - public void testActivateThreadNoStackNoRegsEmpty() throws Exception { + public void testActivateThreadNoStackEmpty() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); + TraceObjectThread thread = addThread(1); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderEmpty(); + waitForPass(() -> assertProviderEmpty()); } @Test - public void testActivateThreadNoStackRegsSynthetic() throws Exception { + public void testActivateThreadThenAddEmptyStackEmpty() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - addRegVals(thread); - waitForDomainObject(tb.trace); - - traceManager.activateThread(thread); - waitForSwing(); - - assertProviderPopulatedSynthetic(); - } - - @Test - public void testActivateThreadRegsThenAddEmptyStackEmpty() throws Exception { - createAndOpenTrace(); - - TraceThread thread = addThread("Processes[1].Threads[1]"); - addRegVals(thread); + TraceObjectThread thread = addThread(1); addStack(thread); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderEmpty(); + waitForPass(() -> assertProviderEmpty()); } @Test public void testActivateThreadThenAddStackPopulatesProvider() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - traceManager.activateThread(thread); - TraceStack stack = addStack(thread); + TraceObjectThread thread = addThread(1); + traceManager.activateObject(thread.getObject()); + TraceObjectStack stack = addStack(thread); addStackFrames(stack); waitForDomainObject(tb.trace); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); } @Test public void testAddStackThenActivateThreadPopulatesProvider() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); + TraceObjectThread thread = addThread(1); + TraceObjectStack stack = addStack(thread); addStackFrames(stack); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); + } + + /** + * Because keys are strings, we need to ensure they get sorted numerically + * + * @throws Exception + */ + @Test + public void testTableSortedCorrectly() throws Exception { + createAndOpenTrace(); + TraceObjectThread thread = addThread(1); + TraceObjectStack stack = addStack(thread); + addStackFrames(stack, 15); + waitForDomainObject(tb.trace); + + traceManager.activateObject(thread.getObject()); + waitForTasks(); + + waitForPass(() -> { + assertTableSize(15); + List allItems = stackProvider.panel.getAllItems(); + for (int i = 0; i < 15; i++) { + assertEquals(PathUtils.makeKey(PathUtils.makeIndex(i)), allItems.get(i).getKey()); + } + }); } @Test public void testAppendStackUpdatesProvider() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); + TraceObjectThread thread = addThread(1); + TraceObjectStack stack = addStack(thread); addStackFrames(stack); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); try (UndoableTransaction tid = tb.startTransaction()) { - stack.setDepth(3, false); + TraceObject frame2 = tb.trace.getObjectManager() + .createObject(stack.getObject().getCanonicalPath().index(2)) + .insert(Lifespan.nowOn(0), ConflictResolution.TRUNCATE) + .getDestination(null); + frame2.setAttribute(Lifespan.nowOn(0), TargetStackFrame.PC_ATTRIBUTE_NAME, + tb.addr(0x00400300)); } waitForDomainObject(tb.trace); + waitForTasks(); - assertTableSize(3); - assertRow(0, tb.addr(0x00400100), "Hello", null); - assertRow(1, tb.addr(0x00400200), "World", null); - assertRow(2, null, null, null); + waitForPass(() -> { + assertTableSize(3); + assertRow(0, tb.addr(0x00400100), null); + assertRow(1, tb.addr(0x00400200), null); + assertRow(2, tb.addr(0x00400300), null); + }); } @Test - public void testPushStackUpdatesProvider() throws Exception { + public void testRemoveFrameUpdatesProvider() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); + TraceObjectThread thread = addThread(1); + TraceObjectStack stack = addStack(thread); addStackFrames(stack); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); try (UndoableTransaction tid = tb.startTransaction()) { - stack.setDepth(3, true); + TraceObject frame1 = stack.getObject().getElement(0, 1).getChild(); + frame1.removeTree(Lifespan.nowOn(0)); } waitForDomainObject(tb.trace); - - assertTableSize(3); - assertRow(0, null, null, null); - assertRow(1, tb.addr(0x00400100), "Hello", null); - assertRow(2, tb.addr(0x00400200), "World", null); + waitForTasks(); + waitForPass(() -> { + assertTableSize(1); + assertRow(0, tb.addr(0x00400100), null); + }); } @Test - public void testTruncateStackUpdatesProvider() throws Exception { + public void testRemoveStackUpdatesProvider() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); + TraceObjectThread thread = addThread(1); + TraceObjectStack stack = addStack(thread); addStackFrames(stack); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); try (UndoableTransaction tid = tb.startTransaction()) { - stack.setDepth(1, false); + stack.getObject().removeTree(Lifespan.nowOn(0)); } waitForDomainObject(tb.trace); + waitForTasks(); - assertTableSize(1); - assertRow(0, tb.addr(0x00400100), "Hello", null); + waitForPass(() -> assertProviderEmpty()); } @Test - public void testPopStackUpdatesProvider() throws Exception { + public void testActivateOtherThreadEmptiesProvider() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); - addStackFrames(stack); + TraceObjectThread thread1 = addThread(1); + TraceObjectThread thread2 = addThread(2); + TraceObjectStack stack1 = addStack(thread1); + addStackFrames(stack1); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread1.getObject()); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); - try (UndoableTransaction tid = tb.startTransaction()) { - stack.setDepth(1, true); - } - waitForDomainObject(tb.trace); + traceManager.activateObject(thread2.getObject()); + waitForTasks(); - assertTableSize(1); - assertRow(0, tb.addr(0x00400200), "World", null); - } - - @Test - public void testDeleteStackUpdatesProvider() throws Exception { - createAndOpenTrace(); - - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); - addStackFrames(stack); - waitForDomainObject(tb.trace); - - traceManager.activateThread(thread); - waitForSwing(); - - assertProviderPopulated(); - - try (UndoableTransaction tid = tb.startTransaction()) { - stack.delete(); - } - waitForDomainObject(tb.trace); - - assertProviderEmpty(); - } - - @Test - public void testActivateOtherThread() throws Exception { - createAndOpenTrace(); - - TraceThread thread1 = addThread("Processes[1].Threads[1]"); - TraceThread thread2 = addThread("Processes[1].Threads[2]"); - TraceStack stack = addStack(thread1); - addStackFrames(stack); - waitForDomainObject(tb.trace); - - traceManager.activateThread(thread1); - waitForSwing(); - - assertProviderPopulated(); - - traceManager.activateThread(thread2); - waitForSwing(); - - assertProviderEmpty(); + waitForPass(() -> assertProviderEmpty()); } @Test public void testActivateSnap() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); + TraceObjectThread thread = addThread(1); + TraceObjectStack stack = addStack(thread); addStackFrames(stack); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); - addStack(thread, 1); - waitForSwing(); + try (UndoableTransaction tid = tb.startTransaction()) { + stack.getObject().removeTree(Lifespan.nowOn(1)); + } + waitForDomainObject(tb.trace); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); traceManager.activateSnap(1); - waitForSwing(); + waitForTasks(); - assertProviderEmpty(); + waitForPass(() -> assertProviderEmpty()); + + traceManager.activateSnap(0); + waitForTasks(); + + waitForPass(() -> assertProviderPopulated()); } @Test public void testCloseCurrentTraceEmpty() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); + TraceObjectThread thread = addThread(1); + TraceObjectStack stack = addStack(thread); addStackFrames(stack); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); traceManager.closeTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertProviderEmpty(); + waitForPass(() -> assertProviderEmpty()); } @Test - @Ignore("TODO") // Not sure why this fails under Gradle but not my IDE - public void testSelectRowActivatesFrame() throws Exception { + public void testSelectRowActivateFrame() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); + TraceObjectThread thread = addThread(1); + TraceObjectStack stack = addStack(thread); addStackFrames(stack); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); - clickTableCellWithButton(stackProvider.stackTable, 0, 0, MouseEvent.BUTTON1); - waitForSwing(); + TraceObject frame0 = stack.getObject().getElement(0, 0).getChild(); + TraceObject frame1 = stack.getObject().getElement(0, 1).getChild(); + List allItems = stackProvider.panel.getAllItems(); - assertEquals(0, traceManager.getCurrentFrame()); + stackProvider.panel.setSelectedItem(allItems.get(1)); + waitForTasks(); + waitForPass(() -> assertEquals(frame1, traceManager.getCurrentObject())); - clickTableCellWithButton(stackProvider.stackTable, 1, 0, MouseEvent.BUTTON1); - waitForSwing(); - - assertEquals(1, traceManager.getCurrentFrame()); + stackProvider.panel.setSelectedItem(allItems.get(0)); + waitForTasks(); + waitForPass(() -> assertEquals(frame0, traceManager.getCurrentObject())); } @Test public void testActivateFrameSelectsRow() throws Exception { createAndOpenTrace(); - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); + TraceObjectThread thread = addThread(1); + TraceObjectStack stack = addStack(thread); addStackFrames(stack); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); - traceManager.activateFrame(0); - waitForSwing(); + TraceObject frame0 = stack.getObject().getElement(0, 0).getChild(); + TraceObject frame1 = stack.getObject().getElement(0, 1).getChild(); + List allItems = stackProvider.panel.getAllItems(); - assertEquals(0, stackProvider.stackTable.getSelectedRow()); + traceManager.activateObject(frame1); + waitForTasks(); + waitForPass(() -> assertEquals(allItems.get(1), stackProvider.panel.getSelectedItem())); - traceManager.activateFrame(1); - waitForSwing(); - - assertEquals(1, stackProvider.stackTable.getSelectedRow()); + traceManager.activateObject(frame0); + waitForTasks(); + waitForPass(() -> assertEquals(allItems.get(0), stackProvider.panel.getSelectedItem())); } @Test - public void testActivateThenAddMappingPopulatesFunctionColumn() throws Exception { + public void testActivateTheAddMappingPopulatesFunctionColumn() throws Exception { createTrace(); createProgramFromTrace(); @@ -464,15 +561,15 @@ public class DebuggerStackProviderTest extends AbstractGhidraHeadedDebuggerGUITe traceManager.openTrace(tb.trace); programManager.openProgram(program); - TraceThread thread = addThread("Processes[1].Threads[1]"); - TraceStack stack = addStack(thread); + TraceObjectThread thread = addThread(1); + TraceObjectStack stack = addStack(thread); addStackFrames(stack); waitForDomainObject(tb.trace); - traceManager.activateThread(thread); - waitForSwing(); + traceManager.activateObject(thread.getObject()); + waitForTasks(); - assertProviderPopulated(); + waitForPass(() -> assertProviderPopulated()); Function func; try (UndoableTransaction tid = UndoableTransaction.start(program, "Add Function")) { @@ -487,10 +584,14 @@ public class DebuggerStackProviderTest extends AbstractGhidraHeadedDebuggerGUITe waitForDomainObject(program); try (UndoableTransaction tid = tb.startTransaction()) { - tb.trace.getMemoryManager() - .addRegion("Processes[1].Memory[bin:.text]", Lifespan.nowOn(0), - tb.drng(0x00400000, 0x00400fff), - TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); + TraceObjectMemoryRegion region = Objects.requireNonNull(tb.trace.getObjectManager() + .createObject(TraceObjectKeyPath.parse("Processes[1].Memory[bin:.text]")) + .insert(Lifespan.nowOn(0), ConflictResolution.TRUNCATE) + .getDestination(null) + .queryInterface(TraceObjectMemoryRegion.class)); + region.getObject() + .setAttribute(Lifespan.nowOn(0), TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, + tb.drng(0x00400000, 0x00400fff)); TraceLocation dloc = new DefaultTraceLocation(tb.trace, null, Lifespan.nowOn(0), tb.addr(0x00400000)); @@ -498,9 +599,12 @@ public class DebuggerStackProviderTest extends AbstractGhidraHeadedDebuggerGUITe DebuggerStaticMappingUtils.addMapping(dloc, sloc, 0x1000, false); } waitForDomainObject(tb.trace); + waitForTasks(); - assertTableSize(2); - assertRow(0, tb.addr(0x00400100), "Hello", func); - assertRow(1, tb.addr(0x00400200), "World", null); + waitForPass(() -> { + assertTableSize(2); + assertRow(0, tb.addr(0x00400100), func); + assertRow(1, tb.addr(0x00400200), null); + }); } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java index 730f239130..0387113c79 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java @@ -696,6 +696,13 @@ public interface TargetObjectSchema { return null; } + /** + * Search for a suitable object with this schema at the given path + * + * @param type the type of object sought + * @param path the path of a seed object + * @return the expected path of the suitable object, or null + */ default List searchForSuitable(Class type, List path) { for (; path != null; path = PathUtils.parent(path)) { TargetObjectSchema schema = getSuccessorSchema(path); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java index 8f3066997e..971269c96f 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java @@ -260,7 +260,7 @@ public abstract class GDynamicColumnTableModel * @param column The field to add */ protected void addTableColumn(DynamicTableColumn column) { - addTableColumns(CollectionUtils.asSet(column)); + addTableColumns(CollectionUtils.asSet(column), true); } /** @@ -274,8 +274,24 @@ public abstract class GDynamicColumnTableModel * @param columns The columns to add */ protected void addTableColumns(Set> columns) { + addTableColumns(columns, true); + } + + /** + * Adds the given columns to the end of the list of columns. This method is intended for + * implementations to add custom column objects, rather than relying on generic, discovered + * DynamicTableColumn implementations. + * + *

+ * Note: this method assumes that the columns have already been sorted. + * + * @param columns The columns to add + * @param isDefault true if these are default columns + */ + protected void addTableColumns(Set> columns, + boolean isDefault) { for (DynamicTableColumn column : columns) { - doAddTableColumn(column, getDefaultTableColumns().size(), true); + doAddTableColumn(column, getDefaultTableColumns().size(), isDefault); } fireTableStructureChanged(); }