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));
}
@TargetAttributeType(name = FUNC_ATTRIBUTE_NAME)
@TargetAttributeType(name = FUNC_ATTRIBUTE_NAME, hidden = true)
public String getFunction() {
return func;
}
@@ -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);
@@ -45,14 +45,14 @@ public abstract class AbstractQueryTableModel<T> extends ThreadedTableModel<T, T
}
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?
}
}
protected void valueDeleted(TraceObjectValue value) {
if (query != null && query.includes(span, value)) {
reload();
if (query != null && query.involves(span, value)) {
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 inNew = span.intersects(newSpan);
boolean queryIncludes = query.includes(Lifespan.ALL, value);
boolean queryIncludes = query.involves(Lifespan.ALL, value);
if (queryIncludes) {
if (inOld != inNew) {
reload();
@@ -25,17 +25,20 @@ import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.event.ListSelectionListener;
import docking.widgets.table.DynamicTableColumn;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.framework.plugintool.Plugin;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.target.TraceObject;
import ghidra.util.table.GhidraTable;
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 GhidraTableFilterPanel<T> filterPanel;
@@ -53,7 +56,7 @@ public abstract class AbstractQueryTablePanel<T> extends JPanel {
add(filterPanel, BorderLayout.SOUTH);
}
protected abstract AbstractQueryTableModel<T> 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<T> extends JPanel {
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) {
tableModel.setDiffColor(diffColor);
}
@@ -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<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
*
@@ -122,15 +140,31 @@ public class ModelQuery {
* @return the list of attributes
*/
public Stream<AttributeSchema> 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<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
*
@@ -144,21 +178,57 @@ public class ModelQuery {
* @return true if the value would be accepted
*/
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())) {
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<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 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;
}
}
@@ -37,9 +37,6 @@ import ghidra.trace.model.target.TraceObjectValue;
import ghidra.util.HTMLUtilities;
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(
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
private Map<ColKey, TraceValueObjectAttributeColumn> columnCache = new HashMap<>();
@@ -307,7 +317,13 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
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<ValueRow, ?, ?> column = getColumn(i);
if (column instanceof TraceValueLifePlotColumn plotCol) {
plotCol.setFullRange(fullRange);
}
}
}
protected List<AttributeSchema> computeAttributeSchemas() {
@@ -337,7 +353,6 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
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<ValueRow> {
protected Set<DynamicTableColumn<ValueRow, ?, ?>> computeAttributeColumns(
Collection<AttributeSchema> 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<ValueRow> {
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<AttributeSchema> attributes) {
Set<DynamicTableColumn<ValueRow, ?, ?>> columns =
new HashSet<>(computeAttributeColumns(attributes));
Map<Boolean, List<AttributeSchema>> byVisible = attributes == null ? Map.of()
: 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<>();
for (int i = 0; i < getColumnCount(); i++) {
DynamicTableColumn<ValueRow, ?, ?> 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<ValueRow> {
protected TableColumnDescriptor<ValueRow> createTableColumnDescriptor() {
TableColumnDescriptor<ValueRow> 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<ValueRow> {
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<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()) {
column.setDiffColor(diffColor);
}
@@ -415,7 +468,16 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
@Override
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()) {
column.setDiffColorSel(diffColorSel);
}
@@ -424,11 +486,24 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
@Override
protected void 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
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 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 "<html><em>No trace is active</em>";
}
@@ -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>" + html;
@@ -380,7 +399,7 @@ public class ObjectTreeModel implements DisplaysModified {
}
@Override
public String getName() {
public String getDisplayText() {
return "<html>" + HTMLUtilities.escapeHTML(value.getEntryKey()) + ": <em>" +
HTMLUtilities.escapeHTML(display.getObjectLinkDisplay(value)) + "</em>";
}
@@ -422,7 +441,7 @@ public class ObjectTreeModel implements DisplaysModified {
}
@Override
public String getName() {
public String getDisplayText() {
return "<html>" + HTMLUtilities.escapeHTML(display.getObjectDisplay(value));
}
@@ -606,6 +625,7 @@ public class ObjectTreeModel implements DisplaysModified {
List<GTreeNode> result = ObjectTableModel
.distinctCanonical(object.getValues().stream().filter(this::isValueVisible))
.map(v -> nodeCache.getOrCreateNode(v))
.sorted()
.collect(Collectors.toList());
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.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) {
super(plugin);
}
@Override
protected AbstractQueryTableModel<ValueRow> 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;
}
}
@@ -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<PathRow> {
public class PathsTablePanel extends AbstractQueryTablePanel<PathRow, PathTableModel> {
public PathsTablePanel(Plugin plugin) {
super(plugin);
}
@Override
protected AbstractQueryTableModel<PathRow> createModel(Plugin plugin) {
protected PathTableModel createModel(Plugin plugin) {
return new PathTableModel(plugin);
}
}
@@ -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<ValueRow, St
ServiceProvider serviceProvider) throws IllegalArgumentException {
return rowObject.getKey();
}
@Override
public Comparator<String> getComparator() {
return TargetObjectKeyComparator.CHILD;
}
}
@@ -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();
}
@@ -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.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());
}
}
}
@@ -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() {
@@ -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));
}
}
}
@@ -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;
}
/**
* 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) {
for (; path != null; path = PathUtils.parent(path)) {
TargetObjectSchema schema = getSuccessorSchema(path);
@@ -260,7 +260,7 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
* @param column The field to add
*/
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
*/
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) {
doAddTableColumn(column, getDefaultTableColumns().size(), true);
doAddTableColumn(column, getDefaultTableColumns().size(), isDefault);
}
fireTableStructureChanged();
}