GP-2555: Refactor StackProvider for object-mode trace.

This commit is contained in:
Dan
2022-11-02 10:31:19 -04:00
parent 284fd2dc03
commit 0d7cb0dd50
22 changed files with 1911 additions and 882 deletions
@@ -120,7 +120,7 @@ public class GdbModelTargetStackFrame
return impl.gateFuture(frame.setActive(false)); return impl.gateFuture(frame.setActive(false));
} }
@TargetAttributeType(name = FUNC_ATTRIBUTE_NAME) @TargetAttributeType(name = FUNC_ATTRIBUTE_NAME, hidden = true)
public String getFunction() { public String getFunction() {
return func; return func;
} }
@@ -107,7 +107,11 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
if (object != null) { if (object != null) {
return iface.cast(recorder.getTargetObject(object)); 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); abstract boolean isEnabledForObject(T t);
@@ -45,14 +45,14 @@ public abstract class AbstractQueryTableModel<T> extends ThreadedTableModel<T, T
} }
protected void valueCreated(TraceObjectValue value) { protected void valueCreated(TraceObjectValue value) {
if (query != null && query.includes(span, value)) { if (query != null && query.involves(span, value)) {
reload(); // Can I be more surgical? reload(); // Can I be more surgical?
} }
} }
protected void valueDeleted(TraceObjectValue value) { protected void valueDeleted(TraceObjectValue value) {
if (query != null && query.includes(span, value)) { if (query != null && query.involves(span, value)) {
reload(); reload(); // Can I be more surgical?
} }
} }
@@ -63,7 +63,7 @@ public abstract class AbstractQueryTableModel<T> extends ThreadedTableModel<T, T
} }
boolean inOld = span.intersects(oldSpan); boolean inOld = span.intersects(oldSpan);
boolean inNew = span.intersects(newSpan); boolean inNew = span.intersects(newSpan);
boolean queryIncludes = query.includes(Lifespan.ALL, value); boolean queryIncludes = query.involves(Lifespan.ALL, value);
if (queryIncludes) { if (queryIncludes) {
if (inOld != inNew) { if (inOld != inNew) {
reload(); reload();
@@ -25,17 +25,20 @@ import javax.swing.JPanel;
import javax.swing.JScrollPane; import javax.swing.JScrollPane;
import javax.swing.event.ListSelectionListener; import javax.swing.event.ListSelectionListener;
import docking.widgets.table.DynamicTableColumn;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener; import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.Plugin;
import ghidra.trace.model.Lifespan; import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.target.TraceObject; import ghidra.trace.model.target.TraceObject;
import ghidra.util.table.GhidraTable; import ghidra.util.table.GhidraTable;
import ghidra.util.table.GhidraTableFilterPanel; import ghidra.util.table.GhidraTableFilterPanel;
public abstract class AbstractQueryTablePanel<T> extends JPanel { public abstract class AbstractQueryTablePanel<T, M extends AbstractQueryTableModel<T>>
extends JPanel {
protected final AbstractQueryTableModel<T> tableModel; protected final M tableModel;
protected final GhidraTable table; protected final GhidraTable table;
protected final GhidraTableFilterPanel<T> filterPanel; protected final GhidraTableFilterPanel<T> filterPanel;
@@ -53,7 +56,7 @@ public abstract class AbstractQueryTablePanel<T> extends JPanel {
add(filterPanel, BorderLayout.SOUTH); add(filterPanel, BorderLayout.SOUTH);
} }
protected abstract AbstractQueryTableModel<T> createModel(Plugin plugin); protected abstract M createModel(Plugin plugin);
public void goToCoordinates(DebuggerCoordinates coords) { public void goToCoordinates(DebuggerCoordinates coords) {
if (DebuggerCoordinates.equalsIgnoreRecorderAndView(current, coords)) { if (DebuggerCoordinates.equalsIgnoreRecorderAndView(current, coords)) {
@@ -184,6 +187,26 @@ public abstract class AbstractQueryTablePanel<T> extends JPanel {
return filterPanel.getSelectedItem(); return filterPanel.getSelectedItem();
} }
public List<T> getAllItems() {
return List.copyOf(tableModel.getModelData());
}
@SuppressWarnings("unchecked")
public <V> DynamicTableColumn<T, V, Trace> getColumnByNameAndType(String name, Class<V> type) {
int count = tableModel.getColumnCount();
for (int i = 0; i < count; i++) {
DynamicTableColumn<T, ?, ?> column = tableModel.getColumn(i);
if (!name.equals(column.getColumnName())) {
continue;
}
if (column.getColumnClass() != type) {
continue;
}
return (DynamicTableColumn<T, V, Trace>) column;
}
return null;
}
public void setDiffColor(Color diffColor) { public void setDiffColor(Color diffColor) {
tableModel.setDiffColor(diffColor); tableModel.setDiffColor(diffColor);
} }
@@ -15,19 +15,20 @@
*/ */
package ghidra.app.plugin.core.debug.gui.model; package ghidra.app.plugin.core.debug.gui.model;
import java.util.List; import java.util.*;
import java.util.Objects; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema;
import ghidra.dbg.util.*; import ghidra.dbg.util.*;
import ghidra.trace.database.DBTraceUtils;
import ghidra.trace.model.Lifespan; import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace; import ghidra.trace.model.Trace;
import ghidra.trace.model.target.*; import ghidra.trace.model.target.*;
public class ModelQuery { public class ModelQuery {
public static final ModelQuery EMPTY = new ModelQuery(PathPredicates.EMPTY);
// TODO: A more capable query language, e.g., with WHERE clauses. // TODO: A more capable query language, e.g., with WHERE clauses.
// Could also want math expressions for the conditionals... Hmm. // Could also want math expressions for the conditionals... Hmm.
// They need to be user enterable, so just a Java API won't suffice. // 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); return trace.getObjectManager().getValuePaths(span, predicates).map(p -> p);
} }
public List<TargetObjectSchema> 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<TargetObjectSchema> 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 * Compute the named attributes for resulting objects, according to the schema
* *
@@ -122,15 +140,31 @@ public class ModelQuery {
* @return the list of attributes * @return the list of attributes
*/ */
public Stream<AttributeSchema> computeAttributes(Trace trace) { public Stream<AttributeSchema> computeAttributes(Trace trace) {
TraceObjectManager objects = trace.getObjectManager(); TargetObjectSchema schema = computeSingleSchema(trace);
TargetObjectSchema schema =
objects.getRootSchema().getSuccessorSchema(predicates.getSingletonPattern().asPath());
return schema.getAttributeSchemas() return schema.getAttributeSchemas()
.values() .values()
.stream() .stream()
.filter(as -> !"".equals(as.getName())); .filter(as -> !"".equals(as.getName()));
} }
protected static boolean includes(Lifespan span, PathPattern pattern, TraceObjectValue value) {
List<String> 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 * 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 * @return true if the value would be accepted
*/ */
public boolean includes(Lifespan span, TraceObjectValue value) { public boolean includes(Lifespan span, TraceObjectValue value) {
List<String> 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())) { if (!span.intersects(value.getLifespan())) {
return false; 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(); TraceObject parent = value.getParent();
// Every query involves the root
if (parent == null) { if (parent == null) {
return true;
}
// Check if any of the value's paths could be an ancestor of a result
List<String> 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 false;
} }
return parent.getAncestors(span, predicates.removeRight(1)) for (PathPattern pattern : predicates.getPatterns()) {
.anyMatch(v -> v.getSource(parent).isRoot()); if (involves(span, pattern, value)) {
return true;
}
}
return false;
} }
} }
@@ -37,9 +37,6 @@ import ghidra.trace.model.target.TraceObjectValue;
import ghidra.util.HTMLUtilities; import ghidra.util.HTMLUtilities;
public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> { public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
/** Initialized in {@link #createTableColumnDescriptor()}, which precedes this. */
private TraceValueValColumn valueColumn;
private TraceValueLifePlotColumn lifePlotColumn;
protected static Stream<? extends TraceObjectValue> distinctCanonical( protected static Stream<? extends TraceObjectValue> distinctCanonical(
Stream<? extends TraceObjectValue> stream) { Stream<? extends TraceObjectValue> stream) {
@@ -272,6 +269,19 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
} }
} }
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 // TODO: Save and restore these between sessions, esp., their settings
private Map<ColKey, TraceValueObjectAttributeColumn> columnCache = new HashMap<>(); private Map<ColKey, TraceValueObjectAttributeColumn> columnCache = new HashMap<>();
@@ -307,7 +317,13 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
protected void updateTimelineMax() { protected void updateTimelineMax() {
Long max = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap(); Long max = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap();
Lifespan fullRange = Lifespan.span(0L, max == null ? 1 : max + 1); Lifespan fullRange = Lifespan.span(0L, max == null ? 1 : max + 1);
lifePlotColumn.setFullRange(fullRange); int count = getColumnCount();
for (int i = 0; i < count; i++) {
DynamicTableColumn<ValueRow, ?, ?> column = getColumn(i);
if (column instanceof TraceValueLifePlotColumn plotCol) {
plotCol.setFullRange(fullRange);
}
}
} }
protected List<AttributeSchema> computeAttributeSchemas() { protected List<AttributeSchema> computeAttributeSchemas() {
@@ -337,7 +353,6 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
else { else {
SchemaContext ctx = trace.getObjectManager().getRootSchema().getContext(); SchemaContext ctx = trace.getObjectManager().getRootSchema().getContext();
attributes = query.computeAttributes(trace) attributes = query.computeAttributes(trace)
.filter(a -> isShowHidden() || !a.isHidden())
.filter(a -> !ctx.getSchema(a.getSchema()).isCanonicalContainer()) .filter(a -> !ctx.getSchema(a.getSchema()).isCanonicalContainer())
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@@ -346,6 +361,9 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
protected Set<DynamicTableColumn<ValueRow, ?, ?>> computeAttributeColumns( protected Set<DynamicTableColumn<ValueRow, ?, ?>> computeAttributeColumns(
Collection<AttributeSchema> attributes) { Collection<AttributeSchema> attributes) {
if (attributes == null) {
return Set.of();
}
Trace trace = getTrace(); Trace trace = getTrace();
if (trace == null) { if (trace == null) {
return Set.of(); return Set.of();
@@ -357,25 +375,31 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
SchemaContext ctx = rootSchema.getContext(); SchemaContext ctx = rootSchema.getContext();
return attributes.stream() return attributes.stream()
.map(as -> columnCache.computeIfAbsent(ColKey.fromSchema(ctx, as), .map(as -> columnCache.computeIfAbsent(ColKey.fromSchema(ctx, as),
ck -> TraceValueObjectAttributeColumn.fromSchema(ctx, as))) ck -> AutoAttributeColumn.fromSchema(ctx, as)))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
protected void resyncAttributeColumns(Collection<AttributeSchema> attributes) { protected void resyncAttributeColumns(Collection<AttributeSchema> attributes) {
Set<DynamicTableColumn<ValueRow, ?, ?>> columns = Map<Boolean, List<AttributeSchema>> byVisible = attributes == null ? Map.of()
new HashSet<>(computeAttributeColumns(attributes)); : attributes.stream()
.collect(Collectors.groupingBy(a -> !a.isHidden() || isShowHidden()));
Set<DynamicTableColumn<ValueRow, ?, ?>> visibleColumns =
new HashSet<>(computeAttributeColumns(byVisible.get(true)));
Set<DynamicTableColumn<ValueRow, ?, ?>> hiddenColumns =
new HashSet<>(computeAttributeColumns(byVisible.get(false)));
Set<DynamicTableColumn<ValueRow, ?, ?>> toRemove = new HashSet<>(); Set<DynamicTableColumn<ValueRow, ?, ?>> toRemove = new HashSet<>();
for (int i = 0; i < getColumnCount(); i++) { for (int i = 0; i < getColumnCount(); i++) {
DynamicTableColumn<ValueRow, ?, ?> exists = getColumn(i); DynamicTableColumn<ValueRow, ?, ?> exists = getColumn(i);
if (!(exists instanceof TraceValueObjectAttributeColumn)) { if (!(exists instanceof AutoAttributeColumn)) {
continue; continue;
} }
if (!columns.remove(exists)) { if (!visibleColumns.remove(exists) && !hiddenColumns.remove(exists)) {
toRemove.add(exists); toRemove.add(exists);
} }
} }
removeTableColumns(toRemove); removeTableColumns(toRemove);
addTableColumns(columns); addTableColumns(visibleColumns, true);
addTableColumns(hiddenColumns, false);
} }
@Override @Override
@@ -389,9 +413,9 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
protected TableColumnDescriptor<ValueRow> createTableColumnDescriptor() { protected TableColumnDescriptor<ValueRow> createTableColumnDescriptor() {
TableColumnDescriptor<ValueRow> descriptor = new TableColumnDescriptor<>(); TableColumnDescriptor<ValueRow> descriptor = new TableColumnDescriptor<>();
descriptor.addVisibleColumn(new TraceValueKeyColumn()); descriptor.addVisibleColumn(new TraceValueKeyColumn());
descriptor.addVisibleColumn(valueColumn = new TraceValueValColumn()); descriptor.addVisibleColumn(new TraceValueValColumn());
descriptor.addVisibleColumn(new TraceValueLifeColumn()); descriptor.addVisibleColumn(new TraceValueLifeColumn());
descriptor.addHiddenColumn(lifePlotColumn = new TraceValueLifePlotColumn()); descriptor.addHiddenColumn(new TraceValueLifePlotColumn());
return descriptor; return descriptor;
} }
@@ -405,9 +429,38 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
return null; 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 @Override
public void setDiffColor(Color diffColor) { public void setDiffColor(Color diffColor) {
valueColumn.setDiffColor(diffColor); int count = getColumnCount();
for (int i = 0; i < count; i++) {
DynamicTableColumn<ValueRow, ?, ?> 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()) { for (TraceValueObjectAttributeColumn column : columnCache.values()) {
column.setDiffColor(diffColor); column.setDiffColor(diffColor);
} }
@@ -415,7 +468,16 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
@Override @Override
public void setDiffColorSel(Color diffColorSel) { public void setDiffColorSel(Color diffColorSel) {
valueColumn.setDiffColorSel(diffColorSel); int count = getColumnCount();
for (int i = 0; i < count; i++) {
DynamicTableColumn<ValueRow, ?, ?> 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()) { for (TraceValueObjectAttributeColumn column : columnCache.values()) {
column.setDiffColorSel(diffColorSel); column.setDiffColorSel(diffColorSel);
} }
@@ -424,11 +486,24 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
@Override @Override
protected void snapChanged() { protected void snapChanged() {
super.snapChanged(); super.snapChanged();
lifePlotColumn.setSnap(getSnap()); long snap = getSnap();
int count = getColumnCount();
for (int i = 0; i < count; i++) {
DynamicTableColumn<ValueRow, ?, ?> column = getColumn(i);
if (column instanceof TraceValueLifePlotColumn plotCol) {
plotCol.setSnap(snap);
}
}
} }
@Override @Override
public void addSeekListener(SeekListener listener) { public void addSeekListener(SeekListener listener) {
lifePlotColumn.addSeekListener(listener); int count = getColumnCount();
for (int i = 0; i < count; i++) {
DynamicTableColumn<ValueRow, ?, ?> column = getColumn(i);
if (column instanceof TraceValueLifePlotColumn plotCol) {
plotCol.addSeekListener(listener);
}
}
} }
} }
@@ -24,6 +24,7 @@ import docking.widgets.tree.GTreeLazyNode;
import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeNode;
import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.dbg.target.*; import ghidra.dbg.target.*;
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
import ghidra.framework.model.DomainObjectClosedListener; import ghidra.framework.model.DomainObjectClosedListener;
import ghidra.trace.model.*; import ghidra.trace.model.*;
import ghidra.trace.model.Trace.TraceObjectChangeType; import ghidra.trace.model.Trace.TraceObjectChangeType;
@@ -189,6 +190,19 @@ public class ObjectTreeModel implements DisplaysModified {
public abstract class AbstractNode extends GTreeLazyNode { public abstract class AbstractNode extends GTreeLazyNode {
public abstract TraceObjectValue getValue(); 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) { protected void childCreated(TraceObjectValue value) {
if (getParent() == null || !isLoaded()) { if (getParent() == null || !isLoaded()) {
return; return;
@@ -249,6 +263,11 @@ public class ObjectTreeModel implements DisplaysModified {
@Override @Override
public String getName() { public String getName() {
return "Root";
}
@Override
public String getDisplayText() {
if (trace == null) { if (trace == null) {
return "<html><em>No trace is active</em>"; return "<html><em>No trace is active</em>";
} }
@@ -332,7 +351,7 @@ public class ObjectTreeModel implements DisplaysModified {
} }
@Override @Override
public String getName() { public String getDisplayText() {
String html = HTMLUtilities.escapeHTML( String html = HTMLUtilities.escapeHTML(
value.getEntryKey() + ": " + display.getPrimitiveValueDisplay(value.getValue())); value.getEntryKey() + ": " + display.getPrimitiveValueDisplay(value.getValue()));
return "<html>" + html; return "<html>" + html;
@@ -380,7 +399,7 @@ public class ObjectTreeModel implements DisplaysModified {
} }
@Override @Override
public String getName() { public String getDisplayText() {
return "<html>" + HTMLUtilities.escapeHTML(value.getEntryKey()) + ": <em>" + return "<html>" + HTMLUtilities.escapeHTML(value.getEntryKey()) + ": <em>" +
HTMLUtilities.escapeHTML(display.getObjectLinkDisplay(value)) + "</em>"; HTMLUtilities.escapeHTML(display.getObjectLinkDisplay(value)) + "</em>";
} }
@@ -422,7 +441,7 @@ public class ObjectTreeModel implements DisplaysModified {
} }
@Override @Override
public String getName() { public String getDisplayText() {
return "<html>" + HTMLUtilities.escapeHTML(display.getObjectDisplay(value)); return "<html>" + HTMLUtilities.escapeHTML(display.getObjectDisplay(value));
} }
@@ -606,6 +625,7 @@ public class ObjectTreeModel implements DisplaysModified {
List<GTreeNode> result = ObjectTableModel List<GTreeNode> result = ObjectTableModel
.distinctCanonical(object.getValues().stream().filter(this::isValueVisible)) .distinctCanonical(object.getValues().stream().filter(this::isValueVisible))
.map(v -> nodeCache.getOrCreateNode(v)) .map(v -> nodeCache.getOrCreateNode(v))
.sorted()
.collect(Collectors.toList()); .collect(Collectors.toList());
return result; return result;
} }
@@ -17,14 +17,24 @@ package 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.ObjectTableModel.ValueRow;
import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.Plugin;
import ghidra.trace.model.target.TraceObject;
public class ObjectsTablePanel extends AbstractQueryTablePanel<ValueRow> { public class ObjectsTablePanel extends AbstractQueryTablePanel<ValueRow, ObjectTableModel> {
public ObjectsTablePanel(Plugin plugin) { public ObjectsTablePanel(Plugin plugin) {
super(plugin); super(plugin);
} }
@Override @Override
protected AbstractQueryTableModel<ValueRow> createModel(Plugin plugin) { protected ObjectTableModel createModel(Plugin plugin) {
return new ObjectTableModel(plugin); return new ObjectTableModel(plugin);
} }
public boolean trySelectAncestor(TraceObject successor) {
ValueRow row = tableModel.findTraceObjectAncestor(successor);
if (row == null) {
return false;
}
setSelectedItem(row);
return true;
}
} }
@@ -18,13 +18,13 @@ package ghidra.app.plugin.core.debug.gui.model;
import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.Plugin;
public class PathsTablePanel extends AbstractQueryTablePanel<PathRow> { public class PathsTablePanel extends AbstractQueryTablePanel<PathRow, PathTableModel> {
public PathsTablePanel(Plugin plugin) { public PathsTablePanel(Plugin plugin) {
super(plugin); super(plugin);
} }
@Override @Override
protected AbstractQueryTableModel<PathRow> createModel(Plugin plugin) { protected PathTableModel createModel(Plugin plugin) {
return new PathTableModel(plugin); return new PathTableModel(plugin);
} }
} }
@@ -15,8 +15,11 @@
*/ */
package ghidra.app.plugin.core.debug.gui.model.columns; package ghidra.app.plugin.core.debug.gui.model.columns;
import java.util.Comparator;
import docking.widgets.table.AbstractDynamicTableColumn; import docking.widgets.table.AbstractDynamicTableColumn;
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
import ghidra.docking.settings.Settings; import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider; import ghidra.framework.plugintool.ServiceProvider;
import ghidra.trace.model.Trace; import ghidra.trace.model.Trace;
@@ -32,4 +35,9 @@ public class TraceValueKeyColumn extends AbstractDynamicTableColumn<ValueRow, St
ServiceProvider serviceProvider) throws IllegalArgumentException { ServiceProvider serviceProvider) throws IllegalArgumentException {
return rowObject.getKey(); return rowObject.getKey();
} }
@Override
public Comparator<String> getComparator() {
return TargetObjectKeyComparator.CHILD;
}
} }
@@ -105,13 +105,6 @@ public class TraceValueObjectAttributeColumn
return type; 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 String attributeName;
private final Class<?> attributeType; private final Class<?> attributeType;
private final AttributeRenderer renderer = new AttributeRenderer(); private final AttributeRenderer renderer = new AttributeRenderer();
@@ -155,7 +148,7 @@ public class TraceValueObjectAttributeColumn
new ColumnRenderedValueBackupComparator<>(model, columnIndex)); new ColumnRenderedValueBackupComparator<>(model, columnIndex));
} }
protected Object getAttributeValue(ValueRow row) { public Object getAttributeValue(ValueRow row) {
TraceObjectValue edge = row.getAttribute(attributeName); TraceObjectValue edge = row.getAttribute(attributeName);
return edge == null ? null : edge.getValue(); return edge == null ? null : edge.getValue();
} }
@@ -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<StackTableColumns, StackFrameRow> {
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<StackFrameRow, ?> getter;
private final BiConsumer<StackFrameRow, Object> setter;
private final Predicate<StackFrameRow> editable;
private final Class<?> cls;
@SuppressWarnings("unchecked")
<T> StackTableColumns(String header, Class<T> cls, Function<StackFrameRow, T> getter,
BiConsumer<StackFrameRow, T> setter, Predicate<StackFrameRow> editable) {
this.header = header;
this.cls = cls;
this.getter = getter;
this.setter = (BiConsumer<StackFrameRow, Object>) setter;
this.editable = editable;
}
<T> StackTableColumns(String header, Class<T> cls, Function<StackFrameRow, T> 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<StackTableColumns, StackFrameRow> {
public StackTableModel(PluginTool tool) {
super(tool, "Stack", StackTableColumns.class);
}
@Override
public List<StackTableColumns> 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<StackFrameRow> 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<Trace> affectedTraces, Set<Program> 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<Void> cbFrameSelected = new SuppressableCallback<>();
protected final StackTableModel stackTableModel;
protected GhidraTable stackTable;
protected GhidraTableFilterPanel<StackFrameRow> 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<TraceStackFrame> toAdd = new LinkedHashSet<>(currentStack.getFrames(current.getSnap()));
for (Iterator<StackFrameRow> 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);
}
}
}
@@ -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<ValueRow, Function, Trace> {
@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<ValueRow> createTableColumnDescriptor() {
TableColumnDescriptor<ValueRow> 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<Void> 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<String> 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<ValueRow> 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()));
}
}
}
@@ -22,20 +22,24 @@ import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus; 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( // @PluginInfo(
shortDescription = "Debugger stack view", // shortDescription = "Debugger stack view",
description = "GUI to navigate the stack", // description = "GUI to navigate the stack",
category = PluginCategoryNames.DEBUGGER, // category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME, // packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.RELEASED, // status = PluginStatus.RELEASED,
eventsConsumed = { eventsConsumed = {
TraceActivatedPluginEvent.class, // TraceActivatedPluginEvent.class,
TraceClosedPluginEvent.class, // TraceClosedPluginEvent.class,
}, // },
servicesRequired = { // servicesRequired = {
} // })
)
public class DebuggerStackPlugin extends AbstractDebuggerPlugin { public class DebuggerStackPlugin extends AbstractDebuggerPlugin {
protected DebuggerStackProvider provider; protected DebuggerStackProvider provider;
@@ -58,9 +62,11 @@ public class DebuggerStackPlugin extends AbstractDebuggerPlugin {
@Override @Override
public void processEvent(PluginEvent event) { public void processEvent(PluginEvent event) {
super.processEvent(event); super.processEvent(event);
if (event instanceof TraceActivatedPluginEvent) { if (event instanceof TraceActivatedPluginEvent ev) {
TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event;
provider.coordinatesActivated(ev.getActiveCoordinates()); provider.coordinatesActivated(ev.getActiveCoordinates());
} }
else if (event instanceof TraceClosedPluginEvent ev) {
provider.traceClosed(ev.getTrace());
}
} }
} }
@@ -17,18 +17,15 @@ package ghidra.app.plugin.core.debug.gui.stack;
import ghidra.program.model.address.Address; import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function; 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.stack.TraceStackFrame;
import ghidra.trace.model.thread.TraceThread;
import ghidra.util.database.UndoableTransaction; import ghidra.util.database.UndoableTransaction;
public class StackFrameRow { public class StackFrameRow {
public static class Synthetic extends StackFrameRow { public static class Synthetic extends StackFrameRow {
private Address pc; private Address pc;
public Synthetic(DebuggerStackProvider provider, Address pc) { public Synthetic(DebuggerLegacyStackPanel panel, Address pc) {
super(provider); super(panel);
this.pc = pc; this.pc = pc;
} }
@@ -42,19 +39,19 @@ public class StackFrameRow {
} }
} }
private final DebuggerStackProvider provider; private final DebuggerLegacyStackPanel panel;
final TraceStackFrame frame; final TraceStackFrame frame;
private int level; private int level;
public StackFrameRow(DebuggerStackProvider provider, TraceStackFrame frame) { public StackFrameRow(DebuggerLegacyStackPanel panel, TraceStackFrame frame) {
this.provider = provider; this.panel = panel;
this.frame = frame; this.frame = frame;
this.level = frame.getLevel(); this.level = frame.getLevel();
} }
private StackFrameRow(DebuggerStackProvider provider) { private StackFrameRow(DebuggerLegacyStackPanel panel) {
this.provider = provider; this.panel = panel;
this.frame = null; this.frame = null;
this.level = 0; this.level = 0;
} }
@@ -64,7 +61,7 @@ public class StackFrameRow {
} }
public long getSnap() { public long getSnap() {
return provider.current.getSnap(); return panel.current.getSnap();
} }
public Address getProgramCounter() { public Address getProgramCounter() {
@@ -87,24 +84,7 @@ public class StackFrameRow {
} }
public Function getFunction() { public Function getFunction() {
if (provider.mappingService == null) { return panel.provider.getFunction(getProgramCounter());
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());
} }
protected void update() { protected void update() {
@@ -57,4 +57,31 @@ public class ModelQueryTest extends AbstractGhidraHeadedDebuggerGUITest {
assertFalse(threadQuery.includes(Lifespan.before(0), thread0Val)); 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));
}
}
} }
@@ -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("""
<context>
<schema name='Session' elementResync='NEVER' attributeResync='ONCE'>
<attribute name='Processes' schema='ProcessContainer' />
</schema>
<schema name='ProcessContainer' canonical='yes' elementResync='NEVER'
attributeResync='ONCE'>
<element index='1' schema='Process' />
</schema>
<schema name='Process' elementResync='NEVER' attributeResync='ONCE'>
<attribute name='Threads' schema='ThreadContainer' />
<attribute name='Memory' schema='RegionContainer' />
</schema>
<schema name='ThreadContainer' canonical='yes' elementResync='NEVER'
attributeResync='ONCE'>
<element schema='Thread' />
</schema>
<schema name='Thread' elementResync='NEVER' attributeResync='NEVER'>
<interface name='Thread' />
<interface name='Aggregate' />
<attribute name='Stack' schema='Stack' />
<attribute name='Registers' schema='RegisterContainer' />
</schema>
<schema name='Stack' canonical='yes' elementResync='NEVER'
attributeResync='ONCE'>
<interface name='Stack' />
<element schema='Frame' />
</schema>
<schema name='Frame' elementResync='NEVER' attributeResync='NEVER'>
<interface name='StackFrame' />
</schema>
<schema name='RegisterContainer' canonical='yes' elementResync='NEVER'
attributeResync='NEVER'>
<interface name='RegisterContainer' />
<element schema='Register' />
</schema>
<schema name='Register' elementResync='NEVER' attributeResync='NEVER'>
<interface name='Register' />
</schema>
<schema name='RegionContainer' canonical='yes' elementResync='NEVER'
attributeResync='ONCE'>
<element schema='Region' />
</schema>
<schema name='Region' elementResync='NEVER' attributeResync='NEVER'>
<interface name='MemoryRegion' />
</schema>
</context>
""");
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;
}
}
}
@@ -696,6 +696,13 @@ public interface TargetObjectSchema {
return null; 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<String> searchForSuitable(Class<? extends TargetObject> type, List<String> path) { default List<String> searchForSuitable(Class<? extends TargetObject> type, List<String> path) {
for (; path != null; path = PathUtils.parent(path)) { for (; path != null; path = PathUtils.parent(path)) {
TargetObjectSchema schema = getSuccessorSchema(path); TargetObjectSchema schema = getSuccessorSchema(path);
@@ -260,7 +260,7 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
* @param column The field to add * @param column The field to add
*/ */
protected void addTableColumn(DynamicTableColumn<ROW_TYPE, ?, ?> column) { protected void addTableColumn(DynamicTableColumn<ROW_TYPE, ?, ?> column) {
addTableColumns(CollectionUtils.asSet(column)); addTableColumns(CollectionUtils.asSet(column), true);
} }
/** /**
@@ -274,8 +274,24 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
* @param columns The columns to add * @param columns The columns to add
*/ */
protected void addTableColumns(Set<DynamicTableColumn<ROW_TYPE, ?, ?>> columns) { protected void addTableColumns(Set<DynamicTableColumn<ROW_TYPE, ?, ?>> 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.
*
* <p>
* <b>Note: this method assumes that the columns have already been sorted.</b>
*
* @param columns The columns to add
* @param isDefault true if these are default columns
*/
protected void addTableColumns(Set<DynamicTableColumn<ROW_TYPE, ?, ?>> columns,
boolean isDefault) {
for (DynamicTableColumn<ROW_TYPE, ?, ?> column : columns) { for (DynamicTableColumn<ROW_TYPE, ?, ?> column : columns) {
doAddTableColumn(column, getDefaultTableColumns().size(), true); doAddTableColumn(column, getDefaultTableColumns().size(), isDefault);
} }
fireTableStructureChanged(); fireTableStructureChanged();
} }