diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java index c8c6cf0a91..e556bd5631 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java @@ -16,8 +16,7 @@ package ghidra.app.plugin.core.debug; import java.io.IOException; -import java.util.Collection; -import java.util.Objects; +import java.util.*; import org.jdom.Element; @@ -163,6 +162,7 @@ public class DebuggerCoordinates { return trace.getThreadManager() .getLiveThreads(snap) .stream() + .sorted(Comparator.comparing(TraceThread::getKey)) .findFirst() .orElse(null); } @@ -171,7 +171,12 @@ public class DebuggerCoordinates { return resolveThread(trace, TraceSchedule.ZERO); } - private static TraceObject resolveObject(Trace trace) { + private static TraceObject resolveObject(Trace trace, TraceThread thread, Integer frame, + TraceSchedule time) { + TraceObject object = resolveObject(thread, frame, time); + if (object != null) { + return object; + } return trace.getObjectManager().getRootObject(); } @@ -198,7 +203,7 @@ public class DebuggerCoordinates { TraceProgramView newView = resolveView(newTrace); TraceSchedule newTime = null; // Allow later resolution Integer newFrame = resolveFrame(newThread, newTime); - TraceObject newObject = resolveObject(newTrace); + TraceObject newObject = resolveObject(newTrace, newThread, newFrame, newTime); return new DebuggerCoordinates(newTrace, newPlatform, null, newThread, newView, newTime, newFrame, newObject); } @@ -242,9 +247,10 @@ public class DebuggerCoordinates { .getObjectByCanonicalPath(TraceObjectKeyPath.of(object.getPath())); } - private static TraceObject resolveObject(TraceRecorder recorder, TraceSchedule time) { + private static TraceObject resolveObject(TraceRecorder recorder, TraceThread thread, + Integer frame, TraceSchedule time) { if (recorder.getSnap() != time.getSnap() || !recorder.isSupportsFocus()) { - return resolveObject(recorder.getTrace()); + return resolveObject(recorder.getTrace(), thread, frame, time); } return resolveObject(recorder.getTrace(), recorder.getFocus()); } @@ -266,7 +272,7 @@ public class DebuggerCoordinates { TraceProgramView newView = resolveView(newTrace); TraceSchedule newTime = null; // Allow later resolution Integer newFrame = resolveFrame(newThread, newTime); - TraceObject newObject = resolveObject(newTrace); + TraceObject newObject = resolveObject(newTrace, newThread, newFrame, newTime); return new DebuggerCoordinates(newTrace, newPlatform, null, newThread, newView, newTime, newFrame, newObject); } @@ -294,7 +300,8 @@ public class DebuggerCoordinates { TraceThread newThread = thread != null ? thread : resolveThread(newRecorder, newTime); TraceProgramView newView = view != null ? view : resolveView(newTrace, newTime); Integer newFrame = frame != null ? frame : resolveFrame(newRecorder, newThread, newTime); - TraceObject newObject = object != null ? object : resolveObject(newRecorder, newTime); + TraceObject threadOrFrameObject = resolveObject(newRecorder, newThread, newFrame, newTime); + TraceObject newObject = choose(object, threadOrFrameObject); return new DebuggerCoordinates(newTrace, newPlatform, newRecorder, newThread, newView, newTime, newFrame, newObject); } @@ -332,13 +339,23 @@ public class DebuggerCoordinates { * * @param ancestor the proposed ancestor * @param successor the proposed successor - * @param time the time to consider (only the snap matters) * @return true if ancestor is in fact an ancestor of successor at the given time */ - private static boolean isAncestor(TraceObject ancestor, TraceObject successor, - TraceSchedule time) { - return successor.getCanonicalParents(Lifespan.at(time.getSnap())) - .anyMatch(p -> p == ancestor); + private static boolean isAncestor(TraceObject ancestor, TraceObject successor) { + return ancestor.getCanonicalPath().isAncestor(successor.getCanonicalPath()); + } + + private static TraceObject choose(TraceObject curObj, TraceObject newObj) { + if (curObj == null) { + return newObj; + } + if (newObj == null) { + return curObj; + } + if (isAncestor(newObj, curObj)) { + return curObj; + } + return newObj; } public DebuggerCoordinates thread(TraceThread newThread) { @@ -358,9 +375,8 @@ public class DebuggerCoordinates { // Yes, override frame with 0 on thread changes, unless target says otherwise Integer newFrame = resolveFrame(recorder, newThread, newTime); // Yes, forced frame change may also force object change - TraceObject ancestor = resolveObject(newThread, newFrame, newTime); - TraceObject newObject = - object != null && isAncestor(ancestor, object, newTime) ? object : ancestor; + TraceObject threadOrFrameObject = resolveObject(newThread, newFrame, newTime); + TraceObject newObject = choose(object, threadOrFrameObject); return new DebuggerCoordinates(newTrace, newPlatform, recorder, newThread, newView, newTime, newFrame, newObject); } @@ -384,9 +400,8 @@ public class DebuggerCoordinates { : resolveThread(trace, recorder, newTime); // This will cause the frame to reset to 0 on every snap change. That's fair.... Integer newFrame = resolveFrame(newThread, newTime); - TraceObject ancestor = resolveObject(newThread, newFrame, newTime); - TraceObject newObject = - object != null && isAncestor(ancestor, object, newTime) ? object : ancestor; + TraceObject threadOrFrameObject = resolveObject(newThread, newFrame, newTime); + TraceObject newObject = choose(object, threadOrFrameObject); return new DebuggerCoordinates(trace, platform, recorder, newThread, view, newTime, newFrame, newObject); } @@ -398,9 +413,8 @@ public class DebuggerCoordinates { if (Objects.equals(frame, newFrame)) { return this; } - TraceObject ancestor = resolveObject(thread, newFrame, getTime()); - TraceObject newObject = - object != null && isAncestor(ancestor, object, getTime()) ? object : ancestor; + TraceObject threadOrFrameObject = resolveObject(thread, newFrame, getTime()); + TraceObject newObject = choose(object, threadOrFrameObject); return new DebuggerCoordinates(trace, platform, recorder, thread, view, time, newFrame, newObject); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java index 1e7aafea83..1de5371052 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java @@ -28,7 +28,9 @@ import ghidra.app.plugin.core.debug.gui.model.columns.*; import ghidra.dbg.target.schema.SchemaContext; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; +import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.Plugin; +import ghidra.framework.plugintool.ServiceProvider; import ghidra.program.model.address.Address; import ghidra.trace.model.Lifespan; import ghidra.trace.model.Lifespan.*; @@ -75,6 +77,29 @@ public class ObjectTableModel extends AbstractQueryTableModel { } } + public static class ValueFixedProperty implements ValueProperty { + private T value; + + public ValueFixedProperty(T value) { + this.value = value; + } + + @Override + public Class getType() { + throw new UnsupportedOperationException(); + } + + @Override + public ValueRow getRow() { + throw new UnsupportedOperationException(); + } + + @Override + public T getValue() { + return value; + } + } + public static abstract class ValueDerivedProperty implements ValueProperty { protected final ValueRow row; protected final Class type; @@ -353,7 +378,8 @@ public class ObjectTableModel extends AbstractQueryTableModel { protected static class ColKey { public static ColKey fromSchema(SchemaContext ctx, AttributeSchema attributeSchema) { String name = attributeSchema.getName(); - Class type = TraceValueObjectAttributeColumn.computeColumnType(ctx, attributeSchema); + Class type = + TraceValueObjectAttributeColumn.computeAttributeType(ctx, attributeSchema); return new ColKey(name, type); } @@ -395,7 +421,7 @@ public class ObjectTableModel extends AbstractQueryTableModel { public static TraceValueObjectAttributeColumn fromSchema(SchemaContext ctx, AttributeSchema attributeSchema) { String name = attributeSchema.getName(); - Class type = computeColumnType(ctx, attributeSchema); + Class type = computeAttributeType(ctx, attributeSchema); return new AutoAttributeColumn<>(name, type); } @@ -628,4 +654,74 @@ public class ObjectTableModel extends AbstractQueryTableModel { } } } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + initializeSorting(); + List modelData = getModelData(); + + if (rowIndex < 0 || rowIndex >= modelData.size()) { + return false; + } + + ValueRow t = modelData.get(rowIndex); + return isColumnEditableForRow(t, columnIndex); + } + + public final boolean isColumnEditableForRow(ValueRow t, int columnIndex) { + if (columnIndex < 0 || columnIndex >= tableColumns.size()) { + return false; + } + + Trace dataSource = getDataSource(); + + @SuppressWarnings("unchecked") + DynamicTableColumn column = + (DynamicTableColumn) tableColumns.get(columnIndex); + if (!(column instanceof EditableColumn editable)) { + return false; + } + return editable.isEditable(t, columnSettings.get(column), dataSource, serviceProvider); + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + initializeSorting(); + List modelData = getModelData(); + + if (rowIndex < 0 || rowIndex >= modelData.size()) { + return; + } + + ValueRow t = modelData.get(rowIndex); + setColumnValueForRow(t, aValue, columnIndex); + } + + public void setColumnValueForRow(ValueRow t, Object aValue, int columnIndex) { + if (columnIndex < 0 || columnIndex >= tableColumns.size()) { + return; + } + + Trace dataSource = getDataSource(); + + @SuppressWarnings("unchecked") + DynamicTableColumn column = + (DynamicTableColumn) tableColumns.get(columnIndex); + if (!(column instanceof EditableColumn editable)) { + return; + } + Settings settings = columnSettings.get(column); + if (!editable.isEditable(t, settings, dataSource, serviceProvider)) { + return; + } + doSetValue(editable, t, aValue, settings, dataSource, serviceProvider); + } + + @SuppressWarnings("unchecked") + private static void doSetValue( + EditableColumn editable, ROW_TYPE t, + Object aValue, Settings settings, DATA_SOURCE dataSource, + ServiceProvider serviceProvider) { + editable.setValue(t, (COLUMN_TYPE) aValue, settings, dataSource, serviceProvider); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java index 49bba8b77f..8e691e40b8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java @@ -15,13 +15,49 @@ */ package ghidra.app.plugin.core.debug.gui.model; -import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import java.awt.Component; + +import javax.swing.JTable; +import javax.swing.JTextField; + +import docking.widgets.table.GTableTextCellEditor; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.*; import ghidra.framework.plugintool.Plugin; import ghidra.trace.model.target.TraceObject; public class ObjectsTablePanel extends AbstractQueryTablePanel { + + private static class PropertyEditor extends GTableTextCellEditor { + private final JTextField textField; + + public PropertyEditor() { + super(new JTextField()); + textField = (JTextField) getComponent(); + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, + int row, int column) { + super.getTableCellEditorComponent(table, value, isSelected, row, column); + if (value instanceof ValueProperty property) { + textField.setText(property.getDisplay()); + } + else { + textField.setText(value.toString()); + } + return textField; + } + + @Override + public Object getCellEditorValue() { + Object value = super.getCellEditorValue(); + return new ValueFixedProperty<>(value); + } + } + public ObjectsTablePanel(Plugin plugin) { super(plugin); + table.setDefaultEditor(ValueProperty.class, new PropertyEditor()); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/EditableColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/EditableColumn.java new file mode 100644 index 0000000000..149251308e --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/EditableColumn.java @@ -0,0 +1,31 @@ +/* ### + * 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.model.columns; + +import docking.widgets.table.DynamicTableColumn; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; + +public interface EditableColumn + extends DynamicTableColumn { + boolean isEditable(ROW_TYPE row, + Settings settings, DATA_SOURCE dataSource, ServiceProvider serviceProvider); + + // TODO: getCellEditor? + + void setValue(ROW_TYPE row, COLUMN_TYPE value, Settings settings, DATA_SOURCE dataSource, + ServiceProvider serviceProvider); +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java index cf2ef975d3..07bc79ffb5 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java @@ -31,7 +31,7 @@ public class TracePathLastLifespanPlotColumn private final SpanTableCellRenderer cellRenderer = new SpanTableCellRenderer<>(); private final RangeCursorTableHeaderRenderer headerRenderer = - new RangeCursorTableHeaderRenderer<>(); + new RangeCursorTableHeaderRenderer<>(0L); @Override public String getColumnName() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java index 710d46c496..1a5b9c9151 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java @@ -31,7 +31,7 @@ public class TraceValueLifePlotColumn private final SpanSetTableCellRenderer cellRenderer = new SpanSetTableCellRenderer<>(); private final RangeCursorTableHeaderRenderer headerRenderer = - new RangeCursorTableHeaderRenderer<>(); + new RangeCursorTableHeaderRenderer<>(0L); @Override public String getColumnName() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java index 43560c858f..8f6fc1792b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java @@ -26,12 +26,28 @@ import ghidra.dbg.target.TargetSteppable.TargetStepKindSet; import ghidra.dbg.target.schema.SchemaContext; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; import ghidra.trace.model.target.TraceObject; -public class TraceValueObjectAttributeColumn - extends TraceValueObjectPropertyColumn { +/** + * A column which displays the object's value for a given attribute + * + * @param the type of the attribute + */ +public class TraceValueObjectAttributeColumn extends TraceValueObjectPropertyColumn { - public static Class computeColumnType(SchemaContext ctx, AttributeSchema attributeSchema) { + /** + * Get the type of a given attribute for the model schema + * + * @param ctx the schema context + * @param attributeSchema the attribute entry from the schema + * @return the type, as a Java class + */ + public static Class computeAttributeType(SchemaContext ctx, + AttributeSchema attributeSchema) { TargetObjectSchema schema = ctx.getSchema(attributeSchema.getSchema()); Class type = schema.getType(); if (type == TargetObject.class) { @@ -57,6 +73,13 @@ public class TraceValueObjectAttributeColumn protected final String attributeName; + /** + * Construct an attribute-value column + * + * @param attributeName the name of the attribute + * @param attributeType the type of the attribute (see + * {@link #computeAttributeType(SchemaContext, AttributeSchema)}) + */ public TraceValueObjectAttributeColumn(String attributeName, Class attributeType) { super(attributeType); this.attributeName = attributeName; @@ -64,12 +87,6 @@ public class TraceValueObjectAttributeColumn @Override public String getColumnName() { - /** - * TODO: These are going to have "_"-prefixed things.... Sure, they're "hidden", but if we - * remove them, we're going to hide important info. I'd like a way in the schema to specify - * which "interface attribute" an attribute satisfies. That way, the name can be - * human-friendly, but the interface can still find what it needs. - */ return attributeName; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectEditableAttributeColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectEditableAttributeColumn.java new file mode 100644 index 0000000000..e11e184ad5 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectEditableAttributeColumn.java @@ -0,0 +1,48 @@ +/* ### + * 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.model.columns; + +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueProperty; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObject; +import ghidra.util.database.UndoableTransaction; + +public class TraceValueObjectEditableAttributeColumn extends TraceValueObjectAttributeColumn + implements EditableColumn, Trace> { + public TraceValueObjectEditableAttributeColumn(String attributeName, Class attributeType) { + super(attributeName, attributeType); + } + + @Override + public boolean isEditable(ValueRow row, Settings settings, Trace dataSource, + ServiceProvider serviceProvider) { + return row != null; + } + + @Override + public void setValue(ValueRow row, ValueProperty value, Settings settings, Trace dataSource, + ServiceProvider serviceProvider) { + TraceObject object = row.getValue().getChild(); + try (UndoableTransaction tid = + UndoableTransaction.start(object.getTrace(), "Edit column " + getColumnName())) { + object.setAttribute(Lifespan.nowOn(row.currentSnap()), attributeName, value.getValue()); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java index c3c0ad88fb..c192ac94d1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java @@ -29,6 +29,7 @@ import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.ServiceProvider; import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceClosedException; import ghidra.util.table.column.AbstractGColumnRenderer; import ghidra.util.table.column.GColumnRenderer; @@ -48,9 +49,18 @@ public class TraceValueValColumn extends AbstractDynamicTableColumn(stackTable, stackTableModel); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerLegacyThreadsPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerLegacyThreadsPanel.java new file mode 100644 index 0000000000..47c2066af1 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerLegacyThreadsPanel.java @@ -0,0 +1,343 @@ +/* ### + * 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.thread; + +import java.awt.BorderLayout; +import java.awt.event.*; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import docking.ActionContext; +import docking.widgets.table.*; +import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; +import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.gui.DebuggerSnapActionContext; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.framework.model.DomainObject; +import ghidra.framework.model.DomainObjectChangeRecord; +import ghidra.framework.plugintool.AutoService; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.trace.model.*; +import ghidra.trace.model.Trace.TraceSnapshotChangeType; +import ghidra.trace.model.Trace.TraceThreadChangeType; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.util.database.ObjectKey; +import ghidra.util.table.GhidraTable; +import ghidra.util.table.GhidraTableFilterPanel; +import utilities.util.SuppressableCallback; +import utilities.util.SuppressableCallback.Suppression; + +public class DebuggerLegacyThreadsPanel extends JPanel { + + protected static long orZero(Long l) { + return l == null ? 0 : l; + } + + protected enum ThreadTableColumns + implements EnumeratedTableColumn { + NAME("Name", String.class, ThreadRow::getName, ThreadRow::setName, true), + CREATED("Created", Long.class, ThreadRow::getCreationSnap, true), + DESTROYED("Destroyed", String.class, ThreadRow::getDestructionSnap, true), + STATE("State", ThreadState.class, ThreadRow::getState, true), + COMMENT("Comment", String.class, ThreadRow::getComment, ThreadRow::setComment, true), + PLOT("Plot", Lifespan.class, ThreadRow::getLifespan, false); + + private final String header; + private final Function getter; + private final BiConsumer setter; + private final boolean sortable; + private final Class cls; + + ThreadTableColumns(String header, Class cls, Function getter, + boolean sortable) { + this(header, cls, getter, null, sortable); + } + + @SuppressWarnings("unchecked") + ThreadTableColumns(String header, Class cls, Function getter, + BiConsumer setter, boolean sortable) { + this.header = header; + this.cls = cls; + this.getter = getter; + this.setter = (BiConsumer) setter; + this.sortable = sortable; + } + + @Override + public String getHeader() { + return header; + } + + @Override + public Class getValueClass() { + return cls; + } + + @Override + public Object getValueOf(ThreadRow row) { + return getter.apply(row); + } + + @Override + public boolean isEditable(ThreadRow row) { + return setter != null; + } + + @Override + public boolean isSortable() { + return sortable; + } + + @Override + public void setValueOf(ThreadRow row, Object value) { + setter.accept(row, value); + } + } + + protected static class ThreadTableModel extends RowWrappedEnumeratedColumnTableModel< // + ThreadTableColumns, ObjectKey, ThreadRow, TraceThread> { + + public ThreadTableModel(DebuggerThreadsProvider provider) { + super(provider.getTool(), "Threads", ThreadTableColumns.class, + TraceThread::getObjectKey, t -> new ThreadRow(provider.modelService, t)); + } + } + + private class ForThreadsListener extends TraceDomainObjectListener { + public ForThreadsListener() { + listenForUntyped(DomainObject.DO_OBJECT_RESTORED, this::objectRestored); + + listenFor(TraceThreadChangeType.ADDED, this::threadAdded); + listenFor(TraceThreadChangeType.CHANGED, this::threadChanged); + listenFor(TraceThreadChangeType.LIFESPAN_CHANGED, this::threadChanged); + listenFor(TraceThreadChangeType.DELETED, this::threadDeleted); + + listenFor(TraceSnapshotChangeType.ADDED, this::snapAdded); + listenFor(TraceSnapshotChangeType.DELETED, this::snapDeleted); + } + + private void objectRestored(DomainObjectChangeRecord rec) { + loadThreads(); + } + + private void threadAdded(TraceThread thread) { + threadTableModel.addItem(thread); + } + + private void threadChanged(TraceThread thread) { + threadTableModel.updateItem(thread); + } + + private void threadDeleted(TraceThread thread) { + threadTableModel.deleteItem(thread); + } + + private void snapAdded(TraceSnapshot snapshot) { + updateTimelineMax(); + } + + private void snapDeleted() { + updateTimelineMax(); + } + } + + private final DebuggerThreadsProvider provider; + + DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; + private Trace currentTrace; // Copy for transition + + @AutoServiceConsumed + private DebuggerTraceManagerService traceManager; + @SuppressWarnings("unused") + private final AutoService.Wiring autoServiceWiring; + + private final ForThreadsListener forThreadsListener = new ForThreadsListener(); + + private final SuppressableCallback cbCoordinateActivation = new SuppressableCallback<>(); + + /* package access for testing */ + final SpanTableCellRenderer spanRenderer = new SpanTableCellRenderer<>(); + final RangeCursorTableHeaderRenderer headerRenderer = + new RangeCursorTableHeaderRenderer<>(0L); + + final ThreadTableModel threadTableModel; + final GTable threadTable; + final GhidraTableFilterPanel threadFilterPanel; + + private ActionContext myActionContext; + + // strong ref + SeekListener seekListener; + + public DebuggerLegacyThreadsPanel(DebuggerThreadsPlugin plugin, + DebuggerThreadsProvider provider) { + super(new BorderLayout()); + this.provider = provider; + + this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); + + threadTableModel = new ThreadTableModel(provider); + threadTable = new GhidraTable(threadTableModel); + threadTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + add(new JScrollPane(threadTable)); + threadFilterPanel = new GhidraTableFilterPanel<>(threadTable, threadTableModel); + add(threadFilterPanel, BorderLayout.SOUTH); + + myActionContext = new DebuggerSnapActionContext(current.getTrace(), current.getViewSnap()); + + threadTable.getSelectionModel().addListSelectionListener(this::threadRowSelected); + threadTable.addFocusListener(new FocusAdapter() { + @Override + public void focusGained(FocusEvent e) { + setThreadRowActionContext(); + } + }); + threadTable.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + setThreadRowActionContext(); + } + }); + + TableColumnModel columnModel = threadTable.getColumnModel(); + TableColumn colName = columnModel.getColumn(ThreadTableColumns.NAME.ordinal()); + colName.setPreferredWidth(100); + TableColumn colCreated = columnModel.getColumn(ThreadTableColumns.CREATED.ordinal()); + colCreated.setPreferredWidth(10); + TableColumn colDestroyed = columnModel.getColumn(ThreadTableColumns.DESTROYED.ordinal()); + colDestroyed.setPreferredWidth(10); + TableColumn colState = columnModel.getColumn(ThreadTableColumns.STATE.ordinal()); + colState.setPreferredWidth(20); + TableColumn colComment = columnModel.getColumn(ThreadTableColumns.COMMENT.ordinal()); + colComment.setPreferredWidth(100); + TableColumn colPlot = columnModel.getColumn(ThreadTableColumns.PLOT.ordinal()); + colPlot.setPreferredWidth(200); + colPlot.setCellRenderer(spanRenderer); + colPlot.setHeaderRenderer(headerRenderer); + + headerRenderer.addSeekListener(seekListener = pos -> { + long snap = Math.round(pos); + if (current.getTrace() == null || snap < 0) { + snap = 0; + } + traceManager.activateSnap(snap); + myActionContext = new DebuggerSnapActionContext(current.getTrace(), snap); + provider.legacyThreadsPanelContextChanged(); + }); + } + + private void removeOldListeners() { + if (currentTrace == null) { + return; + } + currentTrace.removeListener(forThreadsListener); + } + + private void addNewListeners() { + if (currentTrace == null) { + return; + } + currentTrace.addListener(forThreadsListener); + } + + private void doSetTrace(Trace trace) { + if (currentTrace == trace) { + return; + } + removeOldListeners(); + currentTrace = trace; + addNewListeners(); + + loadThreads(); + } + + protected void coordinatesActivated(DebuggerCoordinates coordinates) { + current = coordinates; + doSetTrace(coordinates.getTrace()); + doSetThread(coordinates.getThread()); + doSetSnap(coordinates.getSnap()); + } + + private void doSetThread(TraceThread thread) { + ThreadRow row = threadFilterPanel.getSelectedItem(); + TraceThread curThread = row == null ? null : row.getThread(); + if (curThread == thread) { + return; + } + try (Suppression supp = cbCoordinateActivation.suppress(null)) { + if (thread != null) { + threadFilterPanel.setSelectedItem(threadTableModel.getRow(thread)); + } + else { + threadTable.clearSelection(); + } + } + } + + private void doSetSnap(long snap) { + headerRenderer.setCursorPosition(snap); + threadTable.getTableHeader().repaint(); + } + + protected void loadThreads() { + threadTableModel.clear(); + Trace curTrace = current.getTrace(); + if (curTrace == null) { + return; + } + TraceThreadManager manager = curTrace.getThreadManager(); + threadTableModel.addAllItems(manager.getAllThreads()); + updateTimelineMax(); + } + + protected void updateTimelineMax() { + long max = orZero(current.getTrace().getTimeManager().getMaxSnap()); + Lifespan fullRange = Lifespan.span(0, max + 1); + spanRenderer.setFullRange(fullRange); + headerRenderer.setFullRange(fullRange); + threadTable.getTableHeader().repaint(); + } + + private void threadRowSelected(ListSelectionEvent e) { + if (e.getValueIsAdjusting()) { + return; + } + ThreadRow row = setThreadRowActionContext(); + if (row != null && traceManager != null) { + cbCoordinateActivation.invoke(() -> traceManager.activateThread(row.getThread())); + } + } + + public ActionContext getActionContext() { + return myActionContext; + } + + private ThreadRow setThreadRowActionContext() { + ThreadRow row = threadFilterPanel.getSelectedItem(); + myActionContext = new DebuggerThreadActionContext(current.getTrace(), + row == null ? null : row.getThread()); + provider.legacyThreadsPanelContextChanged(); + return row; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPanel.java new file mode 100644 index 0000000000..4afb66fff4 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPanel.java @@ -0,0 +1,245 @@ +/* ### + * 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.thread; + +import java.util.List; + +import javax.swing.event.ListSelectionEvent; + +import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener; +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.*; +import ghidra.app.plugin.core.debug.gui.model.columns.*; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.dbg.target.*; +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.Plugin; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.trace.model.thread.TraceObjectThread; +import utilities.util.SuppressableCallback; +import utilities.util.SuppressableCallback.Suppression; + +public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel { + + protected static ModelQuery successorThreads(TargetObjectSchema rootSchema, List path) { + TargetObjectSchema schema = rootSchema.getSuccessorSchema(path); + return new ModelQuery(schema.searchFor(TargetThread.class, path, true)); + } + + private static class ThreadPathColumn extends TraceValueKeyColumn { + @Override + public String getColumnName() { + return "Path"; + } + + @Override + public String getValue(ValueRow rowObject, Settings settings, Trace data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getValue().getCanonicalPath().toString(); + } + } + + private static class ThreadNameColumn extends TraceValueValColumn { + @Override + public String getColumnName() { + return "Name"; + } + } + + private abstract static class AbstractThreadLifeBoundColumn + extends TraceValueObjectPropertyColumn { + public AbstractThreadLifeBoundColumn() { + super(Long.class); + } + + abstract Long fromLifespan(Lifespan lifespan); + + @Override + public ValueProperty getProperty(ValueRow row) { + return new ValueDerivedProperty<>(row, Long.class) { + @Override + public Long getValue() { + // De-duplication may not select parent value at current snap + TraceObjectValue curVal = + row.getValue().getChild().getCanonicalParent(row.currentSnap()); + if (curVal == null) { + // Thread is not actually alive a current snap + return null; + } + return fromLifespan(curVal.getLifespan()); + } + }; + } + } + + private static class ThreadCreatedColumn extends AbstractThreadLifeBoundColumn { + @Override + public String getColumnName() { + return "Created"; + } + + @Override + Long fromLifespan(Lifespan lifespan) { + return lifespan.minIsFinite() ? lifespan.lmin() : null; + } + } + + private static class ThreadDestroyedColumn extends AbstractThreadLifeBoundColumn { + @Override + public String getColumnName() { + return "Destroyed"; + } + + @Override + Long fromLifespan(Lifespan lifespan) { + return lifespan.maxIsFinite() ? lifespan.lmax() : null; + } + } + + private static class ThreadStateColumn extends TraceValueObjectAttributeColumn { + public ThreadStateColumn() { + // NB. The recorder converts enums to strings + super(TargetExecutionStateful.STATE_ATTRIBUTE_NAME, String.class); + } + + @Override + public String getColumnName() { + return "State"; + } + } + + private static class ThreadCommentColumn + extends TraceValueObjectEditableAttributeColumn { + public ThreadCommentColumn() { + super(TraceObjectThread.KEY_COMMENT, String.class); + } + + @Override + public String getColumnName() { + return "Comment"; + } + } + + private static class ThreadPlotColumn extends TraceValueLifePlotColumn { + } + + private static class ThreadTableModel extends ObjectTableModel { + protected ThreadTableModel(Plugin plugin) { + super(plugin); + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + descriptor.addHiddenColumn(new ThreadPathColumn()); + descriptor.addVisibleColumn(new ThreadNameColumn(), 1, true); + descriptor.addVisibleColumn(new ThreadCreatedColumn()); + descriptor.addVisibleColumn(new ThreadDestroyedColumn()); + descriptor.addVisibleColumn(new ThreadStateColumn()); + descriptor.addVisibleColumn(new ThreadCommentColumn()); + descriptor.addVisibleColumn(new ThreadPlotColumn()); + return descriptor; + } + } + + @AutoServiceConsumed + protected DebuggerTraceManagerService traceManager; + + private final SuppressableCallback cbThreadSelected = new SuppressableCallback<>(); + + private final SeekListener seekListener = pos -> { + long snap = Math.round(pos); + if (current.getTrace() == null || snap < 0) { + snap = 0; + } + traceManager.activateSnap(snap); + }; + + public DebuggerThreadsPanel(DebuggerThreadsProvider provider) { + super(provider.plugin, provider, TraceObjectThread.class); + setLimitToSnap(false); // TODO: Toggle for this? + + tableModel.addTableModelListener(e -> { + // This seems a bit heavy handed + trySelectCurrentThread(); + }); + addSeekListener(seekListener); + } + + @Override + protected ObjectTableModel createModel(Plugin plugin) { + return new ThreadTableModel(plugin); + } + + @Override + protected ModelQuery computeQuery(TraceObject object) { + TargetObjectSchema rootSchema = object.getRoot().getTargetSchema(); + List seedPath = object.getCanonicalPath().getKeyList(); + List processPath = rootSchema.searchForAncestor(TargetProcess.class, seedPath); + if (processPath != null) { + return successorThreads(rootSchema, processPath); + } + List containerPath = + rootSchema.searchForSuitableContainer(TargetThread.class, seedPath); + + if (containerPath != null) { + return successorThreads(rootSchema, containerPath); + } + return successorThreads(rootSchema, List.of()); + } + + private void trySelectCurrentThread() { + TraceObject object = current.getObject(); + if (object != null) { + try (Suppression supp = cbThreadSelected.suppress(null)) { + trySelectAncestor(object); + } + } + } + + @Override + public void coordinatesActivated(DebuggerCoordinates coordinates) { + super.coordinatesActivated(coordinates); + trySelectCurrentThread(); + } + + @Override + public void valueChanged(ListSelectionEvent e) { + super.valueChanged(e); + if (e.getValueIsAdjusting()) { + return; + } + ValueRow item = getSelectedItem(); + if (item != null) { + cbThreadSelected.invoke(() -> { + if (current.getTrace() != item.getValue().getTrace()) { + // Prevent timing issues during navigation from causing trace changes + // Thread table should never cause trace change anyway + return; + } + traceManager.activateObject(item.getValue().getChild()); + }); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java index ee16aecd66..11e97c7c5e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java @@ -23,20 +23,18 @@ import ghidra.app.services.DebuggerTraceManagerService; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; -@PluginInfo( // - shortDescription = "Debugger registers manager", // - description = "GUI to view and modify register values", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { TraceOpenedPluginEvent.class, // - TraceClosedPluginEvent.class, // - TraceActivatedPluginEvent.class, // - }, // - servicesRequired = { // - DebuggerTraceManagerService.class, // - } // -) +@PluginInfo( + shortDescription = "Debugger registers manager", + description = "GUI to view and modify register values", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { TraceOpenedPluginEvent.class, + TraceActivatedPluginEvent.class, + }, + servicesRequired = { + DebuggerTraceManagerService.class, + }) public class DebuggerThreadsPlugin extends AbstractDebuggerPlugin { protected DebuggerThreadsProvider provider; @@ -59,17 +57,9 @@ public class DebuggerThreadsPlugin extends AbstractDebuggerPlugin { @Override public void processEvent(PluginEvent event) { super.processEvent(event); - if (event instanceof TraceOpenedPluginEvent) { - TraceOpenedPluginEvent ev = (TraceOpenedPluginEvent) event; - provider.traceOpened(ev.getTrace()); - } if (event instanceof TraceActivatedPluginEvent) { TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; provider.coordinatesActivated(ev.getActiveCoordinates()); } - if (event instanceof TraceClosedPluginEvent) { - TraceClosedPluginEvent ev = (TraceClosedPluginEvent) event; - provider.traceClosed(ev.getTrace()); - } } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java index 986ad22ef2..2aa581c0fc 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java @@ -16,59 +16,37 @@ package ghidra.app.plugin.core.debug.gui.thread; import java.awt.BorderLayout; -import java.awt.Rectangle; -import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Objects; import javax.swing.*; -import javax.swing.event.ListSelectionEvent; -import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; + +import org.apache.commons.lang3.ArrayUtils; import docking.ActionContext; import docking.WindowPosition; import docking.action.*; -import docking.widgets.HorizontalTabPanel; -import docking.widgets.HorizontalTabPanel.TabListCellRenderer; import docking.widgets.dialogs.InputDialog; -import docking.widgets.table.*; -import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener; import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; -import ghidra.app.plugin.core.debug.gui.DebuggerSnapActionContext; import ghidra.app.services.*; import ghidra.app.services.DebuggerTraceManagerService.BooleanChangeAdapter; -import ghidra.dbg.DebugModelConventions; -import ghidra.dbg.target.TargetThread; import ghidra.framework.model.DomainObject; +import ghidra.framework.model.DomainObjectChangeRecord; import ghidra.framework.plugintool.AutoService; import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; -import ghidra.trace.model.*; +import ghidra.trace.model.Trace; import ghidra.trace.model.Trace.TraceSnapshotChangeType; -import ghidra.trace.model.Trace.TraceThreadChangeType; -import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.trace.model.TraceDomainObjectListener; import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.util.Msg; -import ghidra.util.Swing; -import ghidra.util.database.ObjectKey; -import ghidra.util.datastruct.CollectionChangeListener; -import ghidra.util.table.GhidraTable; -import ghidra.util.table.GhidraTableFilterPanel; -import utilities.util.SuppressableCallback; -import utilities.util.SuppressableCallback.Suppression; public class DebuggerThreadsProvider extends ComponentProviderAdapter { - protected static long orZero(Long l) { - return l == null ? 0 : l; - } - protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) { if (!Objects.equals(a.getTrace(), b.getTrace())) { return false; @@ -85,134 +63,80 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { return true; } - protected static class ThreadTableModel - extends RowWrappedEnumeratedColumnTableModel< // - ThreadTableColumns, ObjectKey, ThreadRow, TraceThread> { + private class ForSnapsListener extends TraceDomainObjectListener { + private Trace currentTrace; - public ThreadTableModel(DebuggerThreadsProvider provider) { - super(provider.getTool(), "Threads", ThreadTableColumns.class, - TraceThread::getObjectKey, t -> new ThreadRow(provider.modelService, t)); - } - } - - private class ThreadsListener extends TraceDomainObjectListener { - public ThreadsListener() { - listenForUntyped(DomainObject.DO_OBJECT_RESTORED, e -> objectRestored()); - - listenFor(TraceThreadChangeType.ADDED, this::threadAdded); - listenFor(TraceThreadChangeType.CHANGED, this::threadChanged); - listenFor(TraceThreadChangeType.LIFESPAN_CHANGED, this::threadChanged); - listenFor(TraceThreadChangeType.DELETED, this::threadDeleted); + public ForSnapsListener() { + listenForUntyped(DomainObject.DO_OBJECT_RESTORED, this::objectRestored); listenFor(TraceSnapshotChangeType.ADDED, this::snapAdded); listenFor(TraceSnapshotChangeType.DELETED, this::snapDeleted); } - private void objectRestored() { - loadThreads(); + private void setTrace(Trace trace) { + if (currentTrace != null) { + currentTrace.removeListener(this); + } + currentTrace = trace; + if (currentTrace != null) { + currentTrace.addListener(this); + } } - private void threadAdded(TraceThread thread) { - threadTableModel.addItem(thread); - } - - private void threadChanged(TraceThread thread) { - threadTableModel.updateItem(thread); - } - - private void threadDeleted(TraceThread thread) { - threadTableModel.deleteItem(thread); + private void objectRestored(DomainObjectChangeRecord rec) { + contextChanged(); } private void snapAdded(TraceSnapshot snapshot) { - updateTimelineMax(); contextChanged(); } private void snapDeleted() { - updateTimelineMax(); + contextChanged(); } } - private class RecordersChangeListener implements CollectionChangeListener { - @Override - public void elementAdded(TraceRecorder element) { - Swing.runIfSwingOrRunLater(() -> traceTabs.repaint()); - } - - @Override - public void elementModified(TraceRecorder element) { - Swing.runIfSwingOrRunLater(() -> traceTabs.repaint()); - } - - @Override - public void elementRemoved(TraceRecorder element) { - Swing.runIfSwingOrRunLater(() -> traceTabs.repaint()); - } - } - - private final DebuggerThreadsPlugin plugin; - - // @AutoServiceConsumed by method - private DebuggerModelService modelService; - // @AutoServiceConsumed by method - private DebuggerTraceManagerService traceManager; - @AutoServiceConsumed // NB, also by method - private DebuggerEmulationService emulationService; - @SuppressWarnings("unused") - private final AutoService.Wiring autoWiring; + final DebuggerThreadsPlugin plugin; DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; - private Trace currentTrace; // Copy for transition - private final SuppressableCallback cbCoordinateActivation = new SuppressableCallback<>(); + Trace currentTrace; // Copy for transition + + // @AutoServiceConsumed by method + DebuggerModelService modelService; + // @AutoServiceConsumed by method + private DebuggerTraceManagerService traceManager; + @SuppressWarnings("unused") + private final AutoService.Wiring autoServiceWiring; - private final ThreadsListener threadsListener = new ThreadsListener(); - private final CollectionChangeListener recordersListener = - new RecordersChangeListener(); private final BooleanChangeAdapter activatePresentChangeListener = this::changedAutoActivatePresent; private final BooleanChangeAdapter synchronizeFocusChangeListener = this::changedSynchronizeFocus; - /* package access for testing */ - final SpanTableCellRenderer spanRenderer = new SpanTableCellRenderer<>(); - final RangeCursorTableHeaderRenderer headerRenderer = - new RangeCursorTableHeaderRenderer<>(); - protected final ThreadTableModel threadTableModel = new ThreadTableModel(this); + private final ForSnapsListener forSnapsListener = new ForSnapsListener(); private JPanel mainPanel; - HorizontalTabPanel traceTabs; - GTable threadTable; - GhidraTableFilterPanel threadFilterPanel; + DebuggerTraceTabPanel traceTabs; JPopupMenu traceTabPopupMenu; - - private ActionContext myActionContext; + DebuggerThreadsPanel panel; + DebuggerLegacyThreadsPanel legacyPanel; DockingAction actionSaveTrace; - DockingAction actionStepSnapBackward; - DockingAction actionEmulateTickBackward; - DockingAction actionEmulateTickForward; - DockingAction actionEmulateTickSkipForward; - DockingAction actionStepSnapForward; ToggleDockingAction actionSeekTracePresent; ToggleDockingAction actionSyncFocus; DockingAction actionGoToTime; - DockingAction actionCloseTrace; - DockingAction actionCloseOtherTraces; - DockingAction actionCloseDeadTraces; - DockingAction actionCloseAllTraces; + ActionContext myActionContext; - // strong refs + // strong ref ToToggleSelectionListener toToggleSelectionListener; - SeekListener seekListener; public DebuggerThreadsProvider(final DebuggerThreadsPlugin plugin) { super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_THREADS, plugin.getName()); this.plugin = plugin; - this.autoWiring = AutoService.wireServicesConsumed(plugin, this); + this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); setIcon(DebuggerResources.ICON_PROVIDER_THREADS); setHelpLocation(DebuggerResources.HELP_PROVIDER_THREADS); @@ -222,24 +146,12 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { setDefaultWindowPosition(WindowPosition.BOTTOM); - myActionContext = new DebuggerSnapActionContext(current.getTrace(), current.getViewSnap()); createActions(); contextChanged(); setVisible(true); } - @AutoServiceConsumed - public void setModelService(DebuggerModelService modelService) { - if (this.modelService != null) { - this.modelService.removeTraceRecordersChangedListener(recordersListener); - } - this.modelService = modelService; - if (this.modelService != null) { - this.modelService.addTraceRecordersChangedListener(recordersListener); - } - } - @AutoServiceConsumed public void setTraceManager(DebuggerTraceManagerService traceManager) { if (this.traceManager != null) { @@ -266,62 +178,8 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { contextChanged(); } - private void removeOldListeners() { - if (currentTrace == null) { - return; - } - currentTrace.removeListener(threadsListener); - } - - private void addNewListeners() { - if (currentTrace == null) { - return; - } - currentTrace.addListener(threadsListener); - } - - private void doSetTrace(Trace trace) { - if (currentTrace == trace) { - return; - } - removeOldListeners(); - currentTrace = trace; - addNewListeners(); - - try (Suppression supp = cbCoordinateActivation.suppress(null)) { - traceTabs.setSelectedItem(trace); - } - loadThreads(); - } - - private void doSetThread(TraceThread thread) { - ThreadRow row = threadFilterPanel.getSelectedItem(); - TraceThread curThread = row == null ? null : row.getThread(); - if (curThread == thread) { - return; - } - try (Suppression supp = cbCoordinateActivation.suppress(null)) { - if (thread != null) { - threadFilterPanel.setSelectedItem(threadTableModel.getRow(thread)); - } - else { - threadTable.clearSelection(); - } - } - } - - private void doSetSnap(long snap) { - headerRenderer.setCursorPosition(snap); - threadTable.getTableHeader().repaint(); - } - - public void traceOpened(Trace trace) { - traceTabs.addItem(trace); - } - - public void traceClosed(Trace trace) { - traceTabs.removeItem(trace); - // manager will issue activate-null event if current trace is closed + private boolean isLegacy(Trace trace) { + return trace != null && trace.getObjectManager().getRootSchema() == null; } public void coordinatesActivated(DebuggerCoordinates coordinates) { @@ -332,32 +190,30 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { current = coordinates; - doSetTrace(current.getTrace()); - doSetThread(current.getThread()); - doSetSnap(current.getSnap()); - - setSubTitle(current.getTime().toString()); - - contextChanged(); - } - - protected void loadThreads() { - threadTableModel.clear(); - Trace curTrace = current.getTrace(); - if (curTrace == null) { - return; + traceTabs.coordinatesActivated(coordinates); + if (isLegacy(coordinates.getTrace())) { + panel.coordinatesActivated(DebuggerCoordinates.NOWHERE); + legacyPanel.coordinatesActivated(coordinates); + if (ArrayUtils.indexOf(mainPanel.getComponents(), legacyPanel) == -1) { + mainPanel.remove(panel); + mainPanel.add(legacyPanel); + mainPanel.validate(); + } + } + else { + legacyPanel.coordinatesActivated(DebuggerCoordinates.NOWHERE); + panel.coordinatesActivated(coordinates); + if (ArrayUtils.indexOf(mainPanel.getComponents(), panel) == -1) { + mainPanel.remove(legacyPanel); + mainPanel.add(panel); + mainPanel.validate(); + } } - TraceThreadManager manager = curTrace.getThreadManager(); - threadTableModel.addAllItems(manager.getAllThreads()); - updateTimelineMax(); - } - protected void updateTimelineMax() { - long max = orZero(current.getTrace().getTimeManager().getMaxSnap()); - Lifespan fullRange = Lifespan.span(0, max + 1); - spanRenderer.setFullRange(fullRange); - headerRenderer.setFullRange(fullRange); - threadTable.getTableHeader().repaint(); + forSnapsListener.setTrace(coordinates.getTrace()); + + setSubTitle(coordinates.getTime().toString()); + contextChanged(); } @Override @@ -365,6 +221,14 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { super.addLocalAction(action); } + void legacyThreadsPanelContextChanged() { + myActionContext = legacyPanel.getActionContext(); + } + + void traceTabsContextChanged() { + myActionContext = traceTabs.getActionContext(); + } + @Override public ActionContext getActionContext(MouseEvent event) { if (myActionContext == null) { @@ -373,131 +237,21 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { return myActionContext; } - private void rowActivated(ThreadRow row) { - if (row == null) { - return; - } - TraceThread thread = row.getThread(); - Trace trace = thread.getTrace(); - TraceRecorder recorder = modelService.getRecorder(trace); - if (recorder == null) { - return; - } - TargetThread targetThread = recorder.getTargetThread(thread); - if (targetThread == null || !targetThread.isValid()) { - return; - } - DebugModelConventions.requestActivation(targetThread); - } - protected void buildMainPanel() { traceTabPopupMenu = new JPopupMenu("Trace"); mainPanel = new JPanel(new BorderLayout()); - threadTable = new GhidraTable(threadTableModel); - threadTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - mainPanel.add(new JScrollPane(threadTable)); + panel = new DebuggerThreadsPanel(this); + legacyPanel = new DebuggerLegacyThreadsPanel(plugin, this); + mainPanel.add(panel); - threadFilterPanel = new GhidraTableFilterPanel<>(threadTable, threadTableModel); - mainPanel.add(threadFilterPanel, BorderLayout.SOUTH); + traceTabs = new DebuggerTraceTabPanel(this); - threadTable.getSelectionModel().addListSelectionListener(this::threadRowSelected); - threadTable.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - setThreadRowActionContext(); - } - - @Override - public void mouseReleased(MouseEvent e) { - int selectedRow = threadTable.getSelectedRow(); - ThreadRow row = threadTableModel.getRowObject(selectedRow); - rowActivated(row); - } - }); - - traceTabs = new HorizontalTabPanel<>(); - traceTabs.getList().setCellRenderer(new TabListCellRenderer<>() { - protected String getText(Trace value) { - return value.getName(); - } - - protected Icon getIcon(Trace value) { - if (modelService == null) { - return super.getIcon(value); - } - TraceRecorder recorder = modelService.getRecorder(value); - if (recorder == null || !recorder.isRecording()) { - return super.getIcon(value); - } - return DebuggerResources.ICON_RECORD; - } - }); - JList list = traceTabs.getList(); - list.getSelectionModel().addListSelectionListener(this::traceTabSelected); - list.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - setTraceTabActionContext(e); - } - - @Override - public void mouseReleased(MouseEvent e) { - } - }); mainPanel.add(traceTabs, BorderLayout.NORTH); - - TableColumnModel columnModel = threadTable.getColumnModel(); - TableColumn colName = columnModel.getColumn(ThreadTableColumns.NAME.ordinal()); - colName.setPreferredWidth(100); - TableColumn colCreated = columnModel.getColumn(ThreadTableColumns.CREATED.ordinal()); - colCreated.setPreferredWidth(10); - TableColumn colDestroyed = columnModel.getColumn(ThreadTableColumns.DESTROYED.ordinal()); - colDestroyed.setPreferredWidth(10); - TableColumn colState = columnModel.getColumn(ThreadTableColumns.STATE.ordinal()); - colState.setPreferredWidth(20); - TableColumn colComment = columnModel.getColumn(ThreadTableColumns.COMMENT.ordinal()); - colComment.setPreferredWidth(100); - TableColumn colPlot = columnModel.getColumn(ThreadTableColumns.PLOT.ordinal()); - colPlot.setPreferredWidth(200); - colPlot.setCellRenderer(spanRenderer); - colPlot.setHeaderRenderer(headerRenderer); - - headerRenderer.addSeekListener(seekListener = pos -> { - long snap = Math.round(pos); - if (current.getTrace() == null || snap < 0) { - snap = 0; - } - traceManager.activateSnap(snap); - myActionContext = new DebuggerSnapActionContext(current.getTrace(), snap); - contextChanged(); - }); } protected void createActions() { - actionStepSnapBackward = StepSnapBackwardAction.builder(plugin) - .enabledWhen(this::isStepSnapBackwardEnabled) - .enabled(false) - .onAction(this::activatedStepSnapBackward) - .buildAndInstallLocal(this); - actionEmulateTickBackward = EmulateTickBackwardAction.builder(plugin) - .enabledWhen(this::isEmulateTickBackwardEnabled) - .onAction(this::activatedEmulateTickBackward) - .buildAndInstallLocal(this); - actionEmulateTickForward = EmulateTickForwardAction.builder(plugin) - .enabledWhen(this::isEmulateTickForwardEnabled) - .onAction(this::activatedEmulateTickForward) - .buildAndInstallLocal(this); - actionEmulateTickSkipForward = EmulateSkipTickForwardAction.builder(plugin) - .enabledWhen(this::isEmulateSkipTickForwardEnabled) - .onAction(this::activatedEmulateSkipTickForward) - .buildAndInstallLocal(this); - actionStepSnapForward = StepSnapForwardAction.builder(plugin) - .enabledWhen(this::isStepSnapForwardEnabled) - .enabled(false) - .onAction(this::activatedStepSnapForward) - .buildAndInstallLocal(this); actionSeekTracePresent = SeekTracePresentAction.builder(plugin) .enabledWhen(this::isSeekTracePresentEnabled) .onAction(this::toggledSeekTracePresent) @@ -515,125 +269,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { .buildAndInstallLocal(this); traceManager.addSynchronizeFocusChangeListener(toToggleSelectionListener = new ToToggleSelectionListener(actionSyncFocus)); - - actionCloseTrace = CloseTraceAction.builderPopup(plugin) - .withContext(DebuggerTraceFileActionContext.class) - .popupWhen(c -> c.getTrace() != null) - .onAction(c -> traceManager.closeTrace(c.getTrace())) - .buildAndInstallLocal(this); - actionCloseAllTraces = CloseAllTracesAction.builderPopup(plugin) - .withContext(DebuggerTraceFileActionContext.class) - .popupWhen(c -> !traceManager.getOpenTraces().isEmpty()) - .onAction(c -> traceManager.closeAllTraces()) - .buildAndInstallLocal(this); - actionCloseOtherTraces = CloseOtherTracesAction.builderPopup(plugin) - .withContext(DebuggerTraceFileActionContext.class) - .popupWhen(c -> traceManager.getOpenTraces().size() > 1 && c.getTrace() != null) - .onAction(c -> traceManager.closeOtherTraces(c.getTrace())) - .buildAndInstallLocal(this); - actionCloseDeadTraces = CloseDeadTracesAction.builderPopup(plugin) - .withContext(DebuggerTraceFileActionContext.class) - .popupWhen(c -> !traceManager.getOpenTraces().isEmpty() && modelService != null) - .onAction(c -> traceManager.closeDeadTraces()) - .buildAndInstallLocal(this); - } - - private boolean isStepSnapBackwardEnabled(ActionContext context) { - if (current.getTrace() == null) { - return false; - } - if (!current.getTime().isSnapOnly()) { - return true; - } - if (current.getSnap() <= 0) { - return false; - } - return true; - } - - private void activatedStepSnapBackward(ActionContext context) { - if (current.getTime().isSnapOnly()) { - traceManager.activateSnap(current.getSnap() - 1); - } - else { - traceManager.activateSnap(current.getSnap()); - } - } - - private boolean isEmulateTickBackwardEnabled(ActionContext context) { - if (emulationService == null) { - return false; - } - if (current.getTrace() == null) { - return false; - } - if (current.getTime().steppedBackward(current.getTrace(), 1) == null) { - return false; - } - return true; - } - - private void activatedEmulateTickBackward(ActionContext context) { - if (current.getTrace() == null) { - return; - } - TraceSchedule time = current.getTime().steppedBackward(current.getTrace(), 1); - if (time == null) { - return; - } - traceManager.activateTime(time); - } - - private boolean isEmulateTickForwardEnabled(ActionContext context) { - if (emulationService == null) { - return false; - } - if (current.getThread() == null) { - return false; - } - return true; - } - - private void activatedEmulateTickForward(ActionContext context) { - if (current.getThread() == null) { - return; - } - TraceSchedule time = current.getTime().steppedForward(current.getThread(), 1); - traceManager.activateTime(time); - } - - private boolean isEmulateSkipTickForwardEnabled(ActionContext context) { - if (emulationService == null) { - return false; - } - if (current.getThread() == null) { - return false; - } - return true; - } - - private void activatedEmulateSkipTickForward(ActionContext context) { - if (current.getThread() == null) { - return; - } - TraceSchedule time = current.getTime().skippedForward(current.getThread(), 1); - traceManager.activateTime(time); - } - - private boolean isStepSnapForwardEnabled(ActionContext context) { - Trace curTrace = current.getTrace(); - if (curTrace == null) { - return false; - } - Long maxSnap = curTrace.getTimeManager().getMaxSnap(); - if (maxSnap == null || current.getSnap() >= maxSnap) { - return false; - } - return true; - } - - private void activatedStepSnapForward(ActionContext contetxt) { - traceManager.activateSnap(current.getSnap() + 1); } private boolean isSeekTracePresentEnabled(ActionContext context) { @@ -684,55 +319,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { } } - private Trace computeClickedTraceTab(MouseEvent e) { - JList list = traceTabs.getList(); - int i = list.locationToIndex(e.getPoint()); - if (i < 0) { - return null; - } - Rectangle cell = list.getCellBounds(i, i); - if (!cell.contains(e.getPoint())) { - return null; - } - return traceTabs.getItem(i); - } - - private Trace setTraceTabActionContext(MouseEvent e) { - Trace newTrace = e == null ? traceTabs.getSelectedItem() : computeClickedTraceTab(e); - actionCloseTrace.getPopupMenuData() - .setMenuItemName( - CloseTraceAction.NAME_PREFIX + (newTrace == null ? "..." : newTrace.getName())); - myActionContext = new DebuggerTraceFileActionContext(newTrace); - contextChanged(); - return newTrace; - } - - private void traceTabSelected(ListSelectionEvent e) { - if (e.getValueIsAdjusting()) { - return; - } - Trace newTrace = setTraceTabActionContext(null); - cbCoordinateActivation.invoke(() -> traceManager.activateTrace(newTrace)); - } - - private ThreadRow setThreadRowActionContext() { - ThreadRow row = threadFilterPanel.getSelectedItem(); - myActionContext = new DebuggerThreadActionContext(current.getTrace(), - row == null ? null : row.getThread()); - contextChanged(); - return row; - } - - private void threadRowSelected(ListSelectionEvent e) { - if (e.getValueIsAdjusting()) { - return; - } - ThreadRow row = setThreadRowActionContext(); - if (row != null && traceManager != null) { - cbCoordinateActivation.invoke(() -> traceManager.activateThread(row.getThread())); - } - } - @Override public JComponent getComponent() { return mainPanel; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerTraceTabPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerTraceTabPanel.java new file mode 100644 index 0000000000..78dc211c87 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerTraceTabPanel.java @@ -0,0 +1,211 @@ +/* ### + * 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.thread; + +import java.awt.Rectangle; +import java.awt.event.*; +import java.util.Objects; + +import javax.swing.Icon; +import javax.swing.JList; +import javax.swing.event.ListSelectionEvent; + +import docking.action.DockingAction; +import docking.widgets.HorizontalTabPanel; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; +import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; +import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; +import ghidra.app.services.*; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.framework.plugintool.util.PluginEventListener; +import ghidra.trace.model.Trace; +import ghidra.util.Swing; +import ghidra.util.datastruct.CollectionChangeListener; +import utilities.util.SuppressableCallback; +import utilities.util.SuppressableCallback.Suppression; + +public class DebuggerTraceTabPanel extends HorizontalTabPanel + implements PluginEventListener { + + private class RecordersChangeListener implements CollectionChangeListener { + @Override + public void elementAdded(TraceRecorder element) { + Swing.runIfSwingOrRunLater(() -> repaint()); + } + + @Override + public void elementModified(TraceRecorder element) { + Swing.runIfSwingOrRunLater(() -> repaint()); + } + + @Override + public void elementRemoved(TraceRecorder element) { + Swing.runIfSwingOrRunLater(() -> repaint()); + } + } + + private final DebuggerThreadsPlugin plugin; + private final DebuggerThreadsProvider provider; + + // @AutoServiceConsumed by method + DebuggerModelService modelService; + @AutoServiceConsumed + private DebuggerTraceManagerService traceManager; + @SuppressWarnings("unused") + private final AutoService.Wiring autoServiceWiring; + + private final CollectionChangeListener recordersListener = + new RecordersChangeListener(); + + DockingAction actionCloseTrace; + DockingAction actionCloseOtherTraces; + DockingAction actionCloseDeadTraces; + DockingAction actionCloseAllTraces; + + private final SuppressableCallback cbCoordinateActivation = new SuppressableCallback<>(); + + private DebuggerTraceFileActionContext myActionContext; + + public DebuggerTraceTabPanel(DebuggerThreadsProvider provider) { + this.plugin = provider.plugin; + this.provider = provider; + + this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); + + PluginTool tool = plugin.getTool(); + tool.addEventListener(TraceOpenedPluginEvent.class, this); + tool.addEventListener(TraceClosedPluginEvent.class, this); + + list.setCellRenderer(new TabListCellRenderer<>() { + protected String getText(Trace value) { + return value.getName(); + } + + protected Icon getIcon(Trace value) { + if (modelService == null) { + return super.getIcon(value); + } + TraceRecorder recorder = modelService.getRecorder(value); + if (recorder == null || !recorder.isRecording()) { + return super.getIcon(value); + } + return DebuggerResources.ICON_RECORD; + } + }); + list.getSelectionModel().addListSelectionListener(this::traceTabSelected); + list.addFocusListener(new FocusAdapter() { + @Override + public void focusGained(FocusEvent e) { + setTraceTabActionContext(null); + } + }); + list.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + setTraceTabActionContext(e); + } + }); + + actionCloseTrace = CloseTraceAction.builderPopup(plugin) + .withContext(DebuggerTraceFileActionContext.class) + .popupWhen(c -> c.getTrace() != null) + .onAction(c -> traceManager.closeTrace(c.getTrace())) + .buildAndInstallLocal(provider); + actionCloseAllTraces = CloseAllTracesAction.builderPopup(plugin) + .withContext(DebuggerTraceFileActionContext.class) + .popupWhen(c -> !traceManager.getOpenTraces().isEmpty()) + .onAction(c -> traceManager.closeAllTraces()) + .buildAndInstallLocal(provider); + actionCloseOtherTraces = CloseOtherTracesAction.builderPopup(plugin) + .withContext(DebuggerTraceFileActionContext.class) + .popupWhen(c -> traceManager.getOpenTraces().size() > 1 && c.getTrace() != null) + .onAction(c -> traceManager.closeOtherTraces(c.getTrace())) + .buildAndInstallLocal(provider); + actionCloseDeadTraces = CloseDeadTracesAction.builderPopup(plugin) + .withContext(DebuggerTraceFileActionContext.class) + .popupWhen(c -> !traceManager.getOpenTraces().isEmpty() && modelService != null) + .onAction(c -> traceManager.closeDeadTraces()) + .buildAndInstallLocal(provider); + } + + private Trace computeClickedTraceTab(MouseEvent e) { + JList list = getList(); + int i = list.locationToIndex(e.getPoint()); + if (i < 0) { + return null; + } + Rectangle cell = list.getCellBounds(i, i); + if (!cell.contains(e.getPoint())) { + return null; + } + return getItem(i); + } + + private Trace setTraceTabActionContext(MouseEvent e) { + Trace newTrace = e == null ? getSelectedItem() : computeClickedTraceTab(e); + actionCloseTrace.getPopupMenuData() + .setMenuItemName( + CloseTraceAction.NAME_PREFIX + (newTrace == null ? "..." : newTrace.getName())); + myActionContext = new DebuggerTraceFileActionContext(newTrace); + provider.traceTabsContextChanged(); + return newTrace; + } + + public DebuggerTraceFileActionContext getActionContext() { + return myActionContext; + } + + public void coordinatesActivated(DebuggerCoordinates coordinates) { + try (Suppression supp = cbCoordinateActivation.suppress(null)) { + setSelectedItem(coordinates.getTrace()); + } + } + + @AutoServiceConsumed + public void setModelService(DebuggerModelService modelService) { + if (this.modelService != null) { + this.modelService.removeTraceRecordersChangedListener(recordersListener); + } + this.modelService = modelService; + if (this.modelService != null) { + this.modelService.addTraceRecordersChangedListener(recordersListener); + } + } + + @Override + public void eventSent(PluginEvent event) { + if (Objects.equals(event.getSourceName(), plugin.getName())) { + return; + } + if (event instanceof TraceOpenedPluginEvent evt) { + addItem(evt.getTrace()); + } + else if (event instanceof TraceClosedPluginEvent evt) { + removeItem(evt.getTrace()); + } + } + + private void traceTabSelected(ListSelectionEvent e) { + if (e.getValueIsAdjusting()) { + return; + } + Trace newTrace = setTraceTabActionContext(null); + cbCoordinateActivation.invoke(() -> traceManager.activateTrace(newTrace)); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadState.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadState.java index 21e9a395cf..49d49fc569 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadState.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadState.java @@ -19,6 +19,7 @@ public enum ThreadState { /** * The last recorded state is alive, but the recorder is not tracking the live thread * + *

* This state is generally erroneous. If it is seen, the recorder has fallen out of sync with * the live session and/or the trace. */ @@ -26,7 +27,6 @@ public enum ThreadState { /** * The last recorded state is alive, but there is no live session to know STOPPED or RUNNING */ - // TODO: Should the thread state transitions be recorded? ALIVE, /** * The thread is alive, but suspended diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadTableColumns.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadTableColumns.java deleted file mode 100644 index 476d9edcc3..0000000000 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadTableColumns.java +++ /dev/null @@ -1,82 +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.thread; - -import java.util.function.BiConsumer; -import java.util.function.Function; - -import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; -import ghidra.trace.model.Lifespan; - -public enum ThreadTableColumns implements EnumeratedTableColumn { - NAME("Name", String.class, ThreadRow::getName, ThreadRow::setName, true), - CREATED("Created", Long.class, ThreadRow::getCreationSnap, true), - DESTROYED("Destroyed", String.class, ThreadRow::getDestructionSnap, true), - STATE("State", ThreadState.class, ThreadRow::getState, true), - COMMENT("Comment", String.class, ThreadRow::getComment, ThreadRow::setComment, true), - PLOT("Plot", Lifespan.class, ThreadRow::getLifespan, false); - - private final String header; - private final Function getter; - private final BiConsumer setter; - private final boolean sortable; - private final Class cls; - - ThreadTableColumns(String header, Class cls, Function getter, - boolean sortable) { - this(header, cls, getter, null, sortable); - } - - @SuppressWarnings("unchecked") - ThreadTableColumns(String header, Class cls, Function getter, - BiConsumer setter, boolean sortable) { - this.header = header; - this.cls = cls; - this.getter = getter; - this.setter = (BiConsumer) setter; - this.sortable = sortable; - } - - @Override - public String getHeader() { - return header; - } - - @Override - public Class getValueClass() { - return cls; - } - - @Override - public Object getValueOf(ThreadRow row) { - return getter.apply(row); - } - - @Override - public boolean isEditable(ThreadRow row) { - return setter != null; - } - - @Override - public boolean isSortable() { - return sortable; - } - - @Override - public void setValueOf(ThreadRow row, Object value) { - setter.accept(row, value); - } -} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/QueryPanelTestHelper.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/QueryPanelTestHelper.java index a9516e8347..6634b30945 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/QueryPanelTestHelper.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/QueryPanelTestHelper.java @@ -15,10 +15,18 @@ */ package ghidra.app.plugin.core.debug.gui.model; +import docking.widgets.table.*; +import ghidra.app.plugin.core.debug.gui.thread.DebuggerThreadsPanel; import ghidra.util.table.GhidraTable; import ghidra.util.table.GhidraTableFilterPanel; +import ghidra.util.table.column.GColumnRenderer; public class QueryPanelTestHelper { + + public static ObjectTableModel getTableModel(DebuggerThreadsPanel panel) { + return panel.tableModel; + } + public static GhidraTable getTable(AbstractQueryTablePanel panel) { return panel.table; } @@ -27,4 +35,32 @@ public class QueryPanelTestHelper { AbstractQueryTablePanel panel) { return panel.filterPanel; } + + @SuppressWarnings("unchecked") + public static SpannedRenderer getSpannedCellRenderer( + AbstractQueryTablePanel panel) { + int count = panel.tableModel.getColumnCount(); + for (int i = 0; i < count; i++) { + DynamicTableColumn column = panel.tableModel.getColumn(i); + GColumnRenderer renderer = column.getColumnRenderer(); + if (renderer instanceof SpannedRenderer spanned) { + return (SpannedRenderer) spanned; + } + } + return null; + } + + @SuppressWarnings("unchecked") + public static RangeCursorTableHeaderRenderer getCursorHeaderRenderer( + AbstractQueryTablePanel panel) { + int count = panel.tableModel.getColumnCount(); + for (int i = 0; i < count; i++) { + DynamicTableColumn column = panel.tableModel.getColumn(i); + GTableHeaderRenderer renderer = column.getHeaderRenderer(); + if (renderer instanceof RangeCursorTableHeaderRenderer spanned) { + return (RangeCursorTableHeaderRenderer) spanned; + } + } + return null; + } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java new file mode 100644 index 0000000000..7d8f1256c5 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java @@ -0,0 +1,452 @@ +/* ### + * 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.thread; + +import static org.junit.Assert.*; + +import java.awt.event.MouseEvent; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import generic.test.category.NightlyCategory; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.thread.DebuggerLegacyThreadsPanel.ThreadTableColumns; +import ghidra.app.services.TraceRecorder; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.trace.model.time.TraceTimeManager; +import ghidra.util.database.UndoableTransaction; + +@Category(NightlyCategory.class) // this may actually be an @PortSensitive test +public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebuggerGUITest { + + protected DebuggerThreadsPlugin threadsPlugin; + protected DebuggerThreadsProvider threadsProvider; + + protected TraceThread thread1; + protected TraceThread thread2; + + @Before + public void setUpThreadsProviderTest() throws Exception { + threadsPlugin = addPlugin(tool, DebuggerThreadsPlugin.class); + threadsProvider = waitForComponentProvider(DebuggerThreadsProvider.class); + } + + protected void addThreads() throws Exception { + TraceThreadManager manager = tb.trace.getThreadManager(); + try (UndoableTransaction tid = tb.startTransaction()) { + thread1 = manager.addThread("Processes[1].Threads[1]", Lifespan.nowOn(0)); + thread1.setComment("A comment"); + thread2 = manager.addThread("Processes[1].Threads[2]", Lifespan.span(5, 10)); + thread2.setComment("Another comment"); + } + } + + /** + * Check that there exist no tabs, and that the tab row is invisible + */ + protected void assertZeroTabs() { + assertEquals(0, threadsProvider.traceTabs.getList().getModel().getSize()); + assertEquals("Tab row should not be visible", 0, + threadsProvider.traceTabs.getVisibleRect().height); + } + + /** + * Check that exactly one tab exists, and that the tab row is visible + */ + protected void assertOneTabPopulated() { + assertEquals(1, threadsProvider.traceTabs.getList().getModel().getSize()); + assertNotEquals("Tab row should be visible", 0, + threadsProvider.traceTabs.getVisibleRect().height); + } + + protected void assertNoTabSelected() { + assertTabSelected(null); + } + + protected void assertTabSelected(Trace trace) { + assertEquals(trace, threadsProvider.traceTabs.getSelectedItem()); + } + + protected void assertThreadsEmpty() { + List threadsDisplayed = + threadsProvider.legacyPanel.threadTableModel.getModelData(); + assertTrue(threadsDisplayed.isEmpty()); + } + + protected void assertThreadsPopulated() { + List threadsDisplayed = + threadsProvider.legacyPanel.threadTableModel.getModelData(); + assertEquals(2, threadsDisplayed.size()); + + ThreadRow thread1Record = threadsDisplayed.get(0); + assertEquals(thread1, thread1Record.getThread()); + assertEquals("Processes[1].Threads[1]", thread1Record.getName()); + assertEquals(Lifespan.nowOn(0), thread1Record.getLifespan()); + assertEquals(0, thread1Record.getCreationSnap()); + assertEquals("", thread1Record.getDestructionSnap()); + assertEquals(tb.trace, thread1Record.getTrace()); + assertEquals(ThreadState.ALIVE, thread1Record.getState()); + assertEquals("A comment", thread1Record.getComment()); + + ThreadRow thread2Record = threadsDisplayed.get(1); + assertEquals(thread2, thread2Record.getThread()); + } + + protected void assertNoThreadSelected() { + assertNull(threadsProvider.legacyPanel.threadFilterPanel.getSelectedItem()); + } + + protected void assertThreadSelected(TraceThread thread) { + ThreadRow row = threadsProvider.legacyPanel.threadFilterPanel.getSelectedItem(); + assertNotNull(row); + assertEquals(thread, row.getThread()); + } + + protected void assertProviderEmpty() { + assertZeroTabs(); + assertThreadsEmpty(); + } + + @Test + public void testEmpty() { + waitForSwing(); + assertProviderEmpty(); + } + + @Test + public void testOpenTracePopupatesTab() throws Exception { + createAndOpenTrace(); + waitForSwing(); + + assertOneTabPopulated(); + assertNoTabSelected(); + assertThreadsEmpty(); + } + + @Test + public void testActivateTraceSelectsTab() throws Exception { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertOneTabPopulated(); + assertTabSelected(tb.trace); + + traceManager.activateTrace(null); + waitForSwing(); + + assertOneTabPopulated(); + assertNoTabSelected(); + } + + @Test + public void testSelectTabActivatesTrace() throws Exception { + createAndOpenTrace(); + waitForSwing(); + threadsProvider.traceTabs.setSelectedItem(tb.trace); + waitForSwing(); + + assertEquals(tb.trace, traceManager.getCurrentTrace()); + assertEquals(tb.trace, threadsProvider.current.getTrace()); + } + + @Test + public void testActivateNoTraceEmptiesProvider() throws Exception { + createAndOpenTrace(); + addThreads(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertThreadsPopulated(); // Sanity + + traceManager.activateTrace(null); + waitForSwing(); + + assertThreadsEmpty(); + } + + @Test + public void testCurrentTraceClosedUpdatesTabs() throws Exception { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertOneTabPopulated(); + assertTabSelected(tb.trace); + + traceManager.closeTrace(tb.trace); + waitForSwing(); + + assertZeroTabs(); + assertNoTabSelected(); + } + + @Test + public void testCurrentTraceClosedEmptiesProvider() throws Exception { + createAndOpenTrace(); + addThreads(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertThreadsPopulated(); + + traceManager.closeTrace(tb.trace); + waitForSwing(); + + assertThreadsEmpty(); + } + + @Test + public void testCloseTraceTabPopupMenuItem() throws Exception { + createAndOpenTrace(); + waitForSwing(); + + assertOneTabPopulated(); // pre-check + clickListItem(threadsProvider.traceTabs.getList(), 0, MouseEvent.BUTTON3); + waitForSwing(); + Set expected = Set.of("Close " + tb.trace.getName()); + assertMenu(expected, expected); + + clickSubMenuItemByText("Close " + tb.trace.getName()); + waitForSwing(); + + waitForPass(() -> { + assertEquals(Set.of(), traceManager.getOpenTraces()); + }); + } + + @Test + public void testActivateThenAddThreadsPopulatesProvider() throws Exception { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + addThreads(); + waitForSwing(); + + assertThreadsPopulated(); + } + + @Test + public void testAddThreadsThenActivatePopulatesProvider() throws Exception { + createAndOpenTrace(); + addThreads(); + waitForSwing(); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertThreadsPopulated(); + } + + @Test + public void testAddSnapUpdatesTimelineMax() throws Exception { + createAndOpenTrace(); + TraceTimeManager manager = tb.trace.getTimeManager(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertEquals(1, threadsProvider.legacyPanel.spanRenderer.getFullRange().max().longValue()); + + try (UndoableTransaction tid = tb.startTransaction()) { + manager.getSnapshot(10, true); + } + waitForSwing(); + + assertEquals(11, threadsProvider.legacyPanel.spanRenderer.getFullRange().max().longValue()); + } + + // NOTE: Do not test delete updates timeline max, as maxSnap does not reflect deletion + + @Test + public void testChangeThreadUpdatesProvider() throws Exception { + createAndOpenTrace(); + addThreads(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + try (UndoableTransaction tid = tb.startTransaction()) { + thread1.setDestructionSnap(15); + } + waitForSwing(); + + assertEquals("15", threadsProvider.legacyPanel.threadTableModel.getModelData() + .get(0) + .getDestructionSnap()); + // NOTE: Plot max is based on time table, never thread destruction + } + + @Test + public void testDeleteThreadUpdatesProvider() throws Exception { + createAndOpenTrace(); + addThreads(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertEquals(2, threadsProvider.legacyPanel.threadTableModel.getModelData().size()); + + try (UndoableTransaction tid = tb.startTransaction()) { + thread2.delete(); + } + waitForSwing(); + + assertEquals(1, threadsProvider.legacyPanel.threadTableModel.getModelData().size()); + // NOTE: Plot max is based on time table, never thread destruction + } + + @Test + public void testEditThreadFields() throws Exception { + createAndOpenTrace(); + addThreads(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + runSwing(() -> { + threadsProvider.legacyPanel.threadTableModel.setValueAt("My Thread", 0, + ThreadTableColumns.NAME.ordinal()); + threadsProvider.legacyPanel.threadTableModel.setValueAt("A different comment", 0, + ThreadTableColumns.COMMENT.ordinal()); + }); + + assertEquals("My Thread", thread1.getName()); + assertEquals("A different comment", thread1.getComment()); + } + + @Test + public void testUndoRedoCausesUpdateInProvider() throws Exception { + createAndOpenTrace(); + addThreads(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertThreadsPopulated(); + + undo(tb.trace); + assertThreadsEmpty(); + + redo(tb.trace); + assertThreadsPopulated(); + } + + @Test + public void testActivateThreadSelectsThread() throws Exception { + createAndOpenTrace(); + addThreads(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertThreadsPopulated(); + assertThreadSelected(thread1); + + traceManager.activateThread(thread2); + waitForSwing(); + + assertThreadSelected(thread2); + } + + @Test + public void testSelectThreadInTableActivatesThread() throws Exception { + createAndOpenTrace(); + addThreads(); + traceManager.activateTrace(tb.trace); + waitForDomainObject(tb.trace); + + assertThreadsPopulated(); + assertThreadSelected(thread1); // Manager selects default if not live + + clickTableCellWithButton(threadsProvider.legacyPanel.threadTable, 1, 0, MouseEvent.BUTTON1); + + waitForPass(() -> { + assertThreadSelected(thread2); + assertEquals(thread2, traceManager.getCurrentThread()); + }); + } + + @Test + public void testActivateSnapUpdatesTimelineCursor() throws Exception { + createAndOpenTrace(); + addThreads(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertThreadsPopulated(); + assertEquals(0, traceManager.getCurrentSnap()); + assertEquals(0, threadsProvider.legacyPanel.headerRenderer.getCursorPosition().longValue()); + + traceManager.activateSnap(6); + waitForSwing(); + + assertEquals(6, threadsProvider.legacyPanel.headerRenderer.getCursorPosition().longValue()); + } + + @Test + public void testActionSeekTracePresent() throws Exception { + assertTrue(threadsProvider.actionSeekTracePresent.isSelected()); + + createAndOpenTrace(); + addThreads(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertEquals(0, traceManager.getCurrentSnap()); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getTimeManager().createSnapshot("Next snapshot"); + } + waitForDomainObject(tb.trace); + + // Not live, so no seek + assertEquals(0, traceManager.getCurrentSnap()); + + tb.close(); + + createTestModel(); + mb.createTestProcessesAndThreads(); + // Threads needs registers to be recognized by the recorder + mb.createTestThreadRegisterBanks(); + + TraceRecorder recorder = modelService.recordTargetAndActivateTrace(mb.testProcess1, + createTargetTraceMapper(mb.testProcess1)); + Trace trace = recorder.getTrace(); + + // Wait till two threads are observed in the database + waitForPass(() -> assertEquals(2, trace.getThreadManager().getAllThreads().size())); + waitForSwing(); + + TraceSnapshot snapshot = recorder.forceSnapshot(); + waitForDomainObject(trace); + + assertEquals(snapshot.getKey(), traceManager.getCurrentSnap()); + + performAction(threadsProvider.actionSeekTracePresent); + waitForSwing(); + + assertFalse(threadsProvider.actionSeekTracePresent.isSelected()); + + recorder.forceSnapshot(); + waitForSwing(); + + assertEquals(snapshot.getKey(), traceManager.getCurrentSnap()); + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderObjectTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderObjectTest.java deleted file mode 100644 index 9fd75693d0..0000000000 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderObjectTest.java +++ /dev/null @@ -1,83 +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.thread; - -import java.io.IOException; - -import org.junit.experimental.categories.Category; - -import generic.test.category.NightlyCategory; -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.util.database.UndoableTransaction; - -@Category(NightlyCategory.class) -public class DebuggerThreadsProviderObjectTest extends DebuggerThreadsProviderTest { - - 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("" + // - "" + // - " " + // - " " + // - " " + // - " " + // - " " + // <---- NOTE HERE - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - " " + // - ""); - - try (UndoableTransaction tid = tb.startTransaction()) { - tb.trace.getObjectManager().createRootObject(ctx.getSchema(new SchemaName("Session"))); - } - } -} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java index 3c9e5a8f29..8e08331c40 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java @@ -18,46 +18,146 @@ package ghidra.app.plugin.core.debug.gui.thread; import static org.junit.Assert.*; import java.awt.event.MouseEvent; -import java.util.List; +import java.io.IOException; +import java.util.Objects; import java.util.Set; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; import org.junit.experimental.categories.Category; +import docking.widgets.table.*; import generic.test.category.NightlyCategory; +import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel; +import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.*; +import ghidra.app.plugin.core.debug.mapping.DebuggerTargetTraceMapper; +import ghidra.app.plugin.core.debug.mapping.ObjectBasedDebuggerTargetTraceMapper; +import ghidra.app.plugin.core.debug.gui.model.QueryPanelTestHelper; import ghidra.app.services.TraceRecorder; +import ghidra.dbg.target.TargetExecutionStateful; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.schema.SchemaContext; +import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; +import ghidra.dbg.target.schema.XmlSchemaContext; +import ghidra.dbg.util.PathPattern; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.lang.CompilerSpecID; +import ghidra.program.model.lang.LanguageID; import ghidra.trace.model.Lifespan; import ghidra.trace.model.Trace; -import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.trace.model.target.TraceObject.ConflictResolution; +import ghidra.trace.model.target.TraceObjectKeyPath; +import ghidra.trace.model.target.TraceObjectManager; +import ghidra.trace.model.thread.TraceObjectThread; import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.model.time.TraceTimeManager; import ghidra.util.database.UndoableTransaction; +import ghidra.util.table.GhidraTable; -@Category(NightlyCategory.class) // this may actually be an @PortSensitive test +@Category(NightlyCategory.class) public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUITest { - protected DebuggerThreadsPlugin threadsPlugin; - protected DebuggerThreadsProvider threadsProvider; + DebuggerThreadsProvider provider; - protected TraceThread thread1; - protected TraceThread thread2; + protected TraceObjectThread thread1; + protected TraceObjectThread thread2; - @Before - public void setUpThreadsProviderTest() throws Exception { - threadsPlugin = addPlugin(tool, DebuggerThreadsPlugin.class); - threadsProvider = waitForComponentProvider(DebuggerThreadsProvider.class); + protected SchemaContext ctx; + + @Override + protected DebuggerTargetTraceMapper createTargetTraceMapper(TargetObject target) + throws Exception { + return new ObjectBasedDebuggerTargetTraceMapper(target, + new LanguageID("DATA:BE:64:default"), new CompilerSpecID("pointer64"), Set.of()); + } + + @Override + protected TraceRecorder recordAndWaitSync() throws Throwable { + TraceRecorder recorder = super.recordAndWaitSync(); + useTrace(recorder.getTrace()); + return recorder; + } + + @Override + protected TargetObject chooseTarget() { + return mb.testModel.session; + } + + @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); + if (trace.getObjectManager().getRootObject() != null) { + // If live, recorder will have created it + return; + } + 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("" + // + "" + // + " " + // + " " + // + " " + // + " " + // + " " + // <---- NOTE HERE + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + " " + // + ""); + + try (UndoableTransaction tid = tb.startTransaction()) { + tb.trace.getObjectManager().createRootObject(ctx.getSchema(new SchemaName("Session"))); + } + } + + protected TraceObjectThread addThread(int index, Lifespan lifespan, String comment) { + TraceObjectManager om = tb.trace.getObjectManager(); + PathPattern threadPattern = new PathPattern(PathUtils.parse("Processes[1].Threads[]")); + TraceObjectThread thread = Objects.requireNonNull(om.createObject( + TraceObjectKeyPath.of(threadPattern.applyIntKeys(index).getSingletonPath())) + .insert(lifespan, ConflictResolution.TRUNCATE) + .getDestination(null) + .queryInterface(TraceObjectThread.class)); + thread.getObject() + .setAttribute(lifespan, TargetExecutionStateful.STATE_ATTRIBUTE_NAME, + TargetExecutionState.STOPPED.name()); + thread.getObject().setAttribute(lifespan, TraceObjectThread.KEY_COMMENT, comment); + return thread; } protected void addThreads() throws Exception { - TraceThreadManager manager = tb.trace.getThreadManager(); try (UndoableTransaction tid = tb.startTransaction()) { - thread1 = manager.addThread("Processes[1].Threads[1]", Lifespan.nowOn(0)); - thread1.setComment("A comment"); - thread2 = manager.addThread("Processes[1].Threads[2]", Lifespan.span(5, 10)); - thread2.setComment("Another comment"); + thread1 = addThread(1, Lifespan.nowOn(0), "A comment"); + thread2 = addThread(2, Lifespan.span(0, 10), "Another comment"); } } @@ -65,18 +165,18 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI * Check that there exist no tabs, and that the tab row is invisible */ protected void assertZeroTabs() { - assertEquals(0, threadsProvider.traceTabs.getList().getModel().getSize()); + assertEquals(0, provider.traceTabs.getList().getModel().getSize()); assertEquals("Tab row should not be visible", 0, - threadsProvider.traceTabs.getVisibleRect().height); + provider.traceTabs.getVisibleRect().height); } /** * Check that exactly one tab exists, and that the tab row is visible */ protected void assertOneTabPopulated() { - assertEquals(1, threadsProvider.traceTabs.getList().getModel().getSize()); + assertEquals(1, provider.traceTabs.getList().getModel().getSize()); assertNotEquals("Tab row should be visible", 0, - threadsProvider.traceTabs.getVisibleRect().height); + provider.traceTabs.getVisibleRect().height); } protected void assertNoTabSelected() { @@ -84,40 +184,57 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI } protected void assertTabSelected(Trace trace) { - assertEquals(trace, threadsProvider.traceTabs.getSelectedItem()); + assertEquals(trace, provider.traceTabs.getSelectedItem()); + } + + protected void assertThreadsTableSize(int size) { + assertEquals(size, provider.panel.getAllItems().size()); } protected void assertThreadsEmpty() { - List threadsDisplayed = threadsProvider.threadTableModel.getModelData(); - assertTrue(threadsDisplayed.isEmpty()); + assertThreadsTableSize(0); + } + + protected void assertThreadRow(int position, Object object, String name, Long created, + Long destroyed, TargetExecutionState state, String comment) { + // NB. Not testing plot, since that's unmodified from generic ObjectTable + ValueRow row = provider.panel.getAllItems().get(position); + DynamicTableColumn nameCol = + provider.panel.getColumnByNameAndType("Name", ValueRow.class).getValue(); + DynamicTableColumn createdCol = + provider.panel.getColumnByNameAndType("Created", ValueProperty.class).getValue(); + DynamicTableColumn destroyedCol = + provider.panel.getColumnByNameAndType("Destroyed", ValueProperty.class).getValue(); + DynamicTableColumn stateCol = + provider.panel.getColumnByNameAndType("State", ValueProperty.class).getValue(); + DynamicTableColumn commentCol = + provider.panel.getColumnByNameAndType("Comment", ValueProperty.class).getValue(); + + assertSame(object, row.getValue().getValue()); + assertEquals(name, rowColDisplay(row, nameCol)); + assertEquals(created, rowColVal(row, createdCol)); + assertEquals(destroyed, rowColVal(row, destroyedCol)); + assertEquals(state.name(), rowColVal(row, stateCol)); + assertEquals(comment, rowColVal(row, commentCol)); } protected void assertThreadsPopulated() { - List threadsDisplayed = threadsProvider.threadTableModel.getModelData(); - assertEquals(2, threadsDisplayed.size()); + assertThreadsTableSize(2); - ThreadRow thread1Record = threadsDisplayed.get(0); - assertEquals(thread1, thread1Record.getThread()); - assertEquals("Processes[1].Threads[1]", thread1Record.getName()); - assertEquals(Lifespan.nowOn(0), thread1Record.getLifespan()); - assertEquals(0, thread1Record.getCreationSnap()); - assertEquals("", thread1Record.getDestructionSnap()); - assertEquals(tb.trace, thread1Record.getTrace()); - assertEquals(ThreadState.ALIVE, thread1Record.getState()); - assertEquals("A comment", thread1Record.getComment()); - - ThreadRow thread2Record = threadsDisplayed.get(1); - assertEquals(thread2, thread2Record.getThread()); + assertThreadRow(0, thread1.getObject(), "Processes[1].Threads[1]", 0L, null, + TargetExecutionState.STOPPED, "A comment"); + assertThreadRow(1, thread2.getObject(), "Processes[1].Threads[2]", 0L, 10L, + TargetExecutionState.STOPPED, "Another comment"); } protected void assertNoThreadSelected() { - assertNull(threadsProvider.threadFilterPanel.getSelectedItem()); + assertNull(provider.panel.getSelectedItem()); } - protected void assertThreadSelected(TraceThread thread) { - ThreadRow row = threadsProvider.threadFilterPanel.getSelectedItem(); + protected void assertThreadSelected(TraceObjectThread thread) { + ValueRow row = provider.panel.getSelectedItem(); assertNotNull(row); - assertEquals(thread, row.getThread()); + assertEquals(thread.getObject(), row.getValue().getChild()); } protected void assertProviderEmpty() { @@ -125,47 +242,68 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI assertThreadsEmpty(); } + @Before + public void setUpThreadsProviderTest() throws Exception { + addPlugin(tool, DebuggerThreadsPlugin.class); + provider = waitForComponentProvider(DebuggerThreadsProvider.class); + } + + @After + public void tearDownThreadsProviderTest() throws Exception { + traceManager.activate(DebuggerCoordinates.NOWHERE); + waitForTasks(); + runSwing(() -> traceManager.closeAllTraces()); + } + @Test public void testEmpty() { - waitForSwing(); - assertProviderEmpty(); + waitForTasks(); + waitForPass(() -> assertProviderEmpty()); } @Test public void testOpenTracePopupatesTab() throws Exception { createAndOpenTrace(); - waitForSwing(); + waitForTasks(); - assertOneTabPopulated(); - assertNoTabSelected(); - assertThreadsEmpty(); + waitForPass(() -> { + assertOneTabPopulated(); + assertNoTabSelected(); + assertThreadsEmpty(); + }); } @Test public void testActivateTraceSelectsTab() throws Exception { createAndOpenTrace(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertOneTabPopulated(); - assertTabSelected(tb.trace); + waitForPass(() -> { + assertOneTabPopulated(); + assertTabSelected(tb.trace); + }); traceManager.activateTrace(null); - waitForSwing(); + waitForTasks(); - assertOneTabPopulated(); - assertNoTabSelected(); + waitForPass(() -> { + assertOneTabPopulated(); + assertNoTabSelected(); + }); } @Test public void testSelectTabActivatesTrace() throws Exception { createAndOpenTrace(); - waitForSwing(); - threadsProvider.traceTabs.setSelectedItem(tb.trace); - waitForSwing(); + waitForTasks(); + provider.traceTabs.setSelectedItem(tb.trace); + waitForTasks(); - assertEquals(tb.trace, traceManager.getCurrentTrace()); - assertEquals(tb.trace, threadsProvider.current.getTrace()); + waitForPass(() -> { + assertEquals(tb.trace, traceManager.getCurrentTrace()); + assertEquals(tb.trace, provider.current.getTrace()); + }); } @Test @@ -173,30 +311,34 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertThreadsPopulated(); // Sanity + waitForPass(() -> assertThreadsPopulated()); traceManager.activateTrace(null); - waitForSwing(); + waitForTasks(); - assertThreadsEmpty(); + waitForPass(() -> assertThreadsEmpty()); } @Test public void testCurrentTraceClosedUpdatesTabs() throws Exception { createAndOpenTrace(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertOneTabPopulated(); - assertTabSelected(tb.trace); + waitForPass(() -> { + assertOneTabPopulated(); + assertTabSelected(tb.trace); + }); traceManager.closeTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertZeroTabs(); - assertNoTabSelected(); + waitForPass(() -> { + assertZeroTabs(); + assertNoTabSelected(); + }); } @Test @@ -204,29 +346,29 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertThreadsPopulated(); + waitForPass(() -> assertThreadsPopulated()); traceManager.closeTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertThreadsEmpty(); + waitForPass(() -> assertThreadsEmpty()); } @Test public void testCloseTraceTabPopupMenuItem() throws Exception { createAndOpenTrace(); - waitForSwing(); + waitForTasks(); - assertOneTabPopulated(); // pre-check - clickListItem(threadsProvider.traceTabs.getList(), 0, MouseEvent.BUTTON3); - waitForSwing(); + waitForPass(() -> assertOneTabPopulated()); + clickListItem(provider.traceTabs.getList(), 0, MouseEvent.BUTTON3); + waitForTasks(); Set expected = Set.of("Close " + tb.trace.getName()); assertMenu(expected, expected); clickSubMenuItemByText("Close " + tb.trace.getName()); - waitForSwing(); + waitForTasks(); waitForPass(() -> { assertEquals(Set.of(), traceManager.getOpenTraces()); @@ -237,24 +379,24 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI public void testActivateThenAddThreadsPopulatesProvider() throws Exception { createAndOpenTrace(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); addThreads(); - waitForSwing(); + waitForTasks(); - assertThreadsPopulated(); + waitForPass(() -> assertThreadsPopulated()); } @Test public void testAddThreadsThenActivatePopulatesProvider() throws Exception { createAndOpenTrace(); addThreads(); - waitForSwing(); + waitForTasks(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertThreadsPopulated(); + waitForPass(() -> assertThreadsPopulated()); } @Test @@ -262,16 +404,24 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI createAndOpenTrace(); TraceTimeManager manager = tb.trace.getTimeManager(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertEquals(1, threadsProvider.spanRenderer.getFullRange().max().longValue()); + waitForPass(() -> { + SpannedRenderer renderer = + QueryPanelTestHelper.getSpannedCellRenderer(provider.panel); + assertEquals(1, renderer.getFullRange().max().longValue()); + }); try (UndoableTransaction tid = tb.startTransaction()) { manager.getSnapshot(10, true); } waitForSwing(); - assertEquals(11, threadsProvider.spanRenderer.getFullRange().max().longValue()); + waitForPass(() -> { + SpannedRenderer renderer = + QueryPanelTestHelper.getSpannedCellRenderer(provider.panel); + assertEquals(11, renderer.getFullRange().max().longValue()); + }); } // NOTE: Do not test delete updates timeline max, as maxSnap does not reflect deletion @@ -281,16 +431,18 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); try (UndoableTransaction tid = tb.startTransaction()) { - thread1.setDestructionSnap(15); + thread1.getObject().removeTree(Lifespan.nowOn(16)); } - waitForSwing(); + waitForTasks(); - assertEquals("15", - threadsProvider.threadTableModel.getModelData().get(0).getDestructionSnap()); - // NOTE: Plot max is based on time table, never thread destruction + waitForPass(() -> { + assertThreadRow(0, thread1.getObject(), "Processes[1].Threads[1]", 0L, 15L, + TargetExecutionState.STOPPED, "A comment"); + }); + // NOTE: Destruction will not be visible in plot unless snapshot 15 is created } @Test @@ -298,51 +450,57 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertEquals(2, threadsProvider.threadTableModel.getModelData().size()); + waitForPass(() -> assertThreadsTableSize(2)); try (UndoableTransaction tid = tb.startTransaction()) { - thread2.delete(); + thread2.getObject().removeTree(Lifespan.ALL); } - waitForSwing(); + waitForTasks(); - assertEquals(1, threadsProvider.threadTableModel.getModelData().size()); - // NOTE: Plot max is based on time table, never thread destruction + waitForPass(() -> assertThreadsTableSize(1)); } @Test - public void testEditThreadFields() throws Exception { + public void testEditThreadComment() throws Exception { createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); + + int commentViewIdx = + provider.panel.getColumnByNameAndType("Comment", ValueProperty.class).getKey(); + ObjectTableModel tableModel = QueryPanelTestHelper.getTableModel(provider.panel); + GhidraTable table = QueryPanelTestHelper.getTable(provider.panel); + int commentModelIdx = table.convertColumnIndexToModel(commentViewIdx); runSwing(() -> { - threadsProvider.threadTableModel.setValueAt("My Thread", 0, - ThreadTableColumns.NAME.ordinal()); - threadsProvider.threadTableModel.setValueAt("A different comment", 0, - ThreadTableColumns.COMMENT.ordinal()); + tableModel.setValueAt(new ValueFixedProperty<>("A different comment"), 0, + commentModelIdx); }); + waitForTasks(); - assertEquals("My Thread", thread1.getName()); - assertEquals("A different comment", thread1.getComment()); + waitForPass(() -> assertEquals("A different comment", + thread1.getObject().getAttribute(0, TraceObjectThread.KEY_COMMENT).getValue())); } @Test public void testUndoRedoCausesUpdateInProvider() throws Exception { createAndOpenTrace(); addThreads(); - traceManager.activateTrace(tb.trace); - waitForSwing(); - assertThreadsPopulated(); + traceManager.activateTrace(tb.trace); + waitForTasks(); + waitForPass(() -> assertThreadsPopulated()); undo(tb.trace); - assertThreadsEmpty(); + waitForTasks(); + waitForPass(() -> assertThreadsEmpty()); redo(tb.trace); - assertThreadsPopulated(); + waitForTasks(); + waitForPass(() -> assertThreadsPopulated()); } @Test @@ -350,15 +508,17 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertThreadsPopulated(); - assertThreadSelected(thread1); + waitForPass(() -> { + assertThreadsPopulated(); + assertThreadSelected(thread1); + }); traceManager.activateThread(thread2); - waitForSwing(); + waitForTasks(); - assertThreadSelected(thread2); + waitForPass(() -> assertThreadSelected(thread2)); } @Test @@ -367,11 +527,15 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI addThreads(); traceManager.activateTrace(tb.trace); waitForDomainObject(tb.trace); + waitForTasks(); - assertThreadsPopulated(); - assertThreadSelected(thread1); // Manager selects default if not live + waitForPass(() -> { + assertThreadsPopulated(); + assertThreadSelected(thread1); // Manager selects default if not live + }); - clickTableCellWithButton(threadsProvider.threadTable, 1, 0, MouseEvent.BUTTON1); + GhidraTable table = QueryPanelTestHelper.getTable(provider.panel); + clickTableCellWithButton(table, 1, 0, MouseEvent.BUTTON1); waitForPass(() -> { assertThreadSelected(thread2); @@ -384,85 +548,31 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); - assertThreadsPopulated(); - assertEquals(0, traceManager.getCurrentSnap()); - assertEquals(0, threadsProvider.headerRenderer.getCursorPosition().longValue()); + RangeCursorTableHeaderRenderer renderer = + QueryPanelTestHelper.getCursorHeaderRenderer(provider.panel); + + waitForPass(() -> { + assertThreadsPopulated(); + assertEquals(0, traceManager.getCurrentSnap()); + assertEquals(Long.valueOf(0), renderer.getCursorPosition()); + }); traceManager.activateSnap(6); - waitForSwing(); + waitForTasks(); - assertEquals(6, threadsProvider.headerRenderer.getCursorPosition().longValue()); + waitForPass(() -> assertEquals(Long.valueOf(6), renderer.getCursorPosition())); } @Test - public void testActionStepTraceBackward() throws Exception { - assertFalse(threadsProvider.actionStepSnapBackward.isEnabled()); + public void testActionSeekTracePresent() throws Throwable { + assertTrue(provider.actionSeekTracePresent.isSelected()); createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); - waitForSwing(); - - assertFalse(threadsProvider.actionStepSnapBackward.isEnabled()); - - try (UndoableTransaction tid = tb.startTransaction()) { - tb.trace.getTimeManager().getSnapshot(10, true); - } - waitForDomainObject(tb.trace); - - assertFalse(threadsProvider.actionStepSnapBackward.isEnabled()); - - traceManager.activateSnap(2); - waitForSwing(); - - assertTrue(threadsProvider.actionStepSnapBackward.isEnabled()); - - performAction(threadsProvider.actionStepSnapBackward); - waitForSwing(); - - assertEquals(1, traceManager.getCurrentSnap()); - } - - @Test - public void testActionStepTraceForward() throws Exception { - assertFalse(threadsProvider.actionStepSnapForward.isEnabled()); - - createAndOpenTrace(); - addThreads(); - traceManager.activateTrace(tb.trace); - waitForSwing(); - - assertFalse(threadsProvider.actionStepSnapForward.isEnabled()); - - try (UndoableTransaction tid = tb.startTransaction()) { - tb.trace.getTimeManager().getSnapshot(10, true); - } - waitForDomainObject(tb.trace); - - assertTrue(threadsProvider.actionStepSnapForward.isEnabled()); - - performAction(threadsProvider.actionStepSnapForward); - waitForSwing(); - - assertEquals(1, traceManager.getCurrentSnap()); - assertTrue(threadsProvider.actionStepSnapForward.isEnabled()); - - traceManager.activateSnap(10); - waitForSwing(); - - assertFalse(threadsProvider.actionStepSnapForward.isEnabled()); - } - - @Test - public void testActionSeekTracePresent() throws Exception { - assertTrue(threadsProvider.actionSeekTracePresent.isSelected()); - - createAndOpenTrace(); - addThreads(); - traceManager.activateTrace(tb.trace); - waitForSwing(); + waitForTasks(); assertEquals(0, traceManager.getCurrentSnap()); @@ -470,9 +580,10 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI tb.trace.getTimeManager().createSnapshot("Next snapshot"); } waitForDomainObject(tb.trace); + waitForTasks(); // Not live, so no seek - assertEquals(0, traceManager.getCurrentSnap()); + waitForPass(() -> assertEquals(0, traceManager.getCurrentSnap())); tb.close(); @@ -481,27 +592,24 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI // Threads needs registers to be recognized by the recorder mb.createTestThreadRegisterBanks(); - TraceRecorder recorder = modelService.recordTargetAndActivateTrace(mb.testProcess1, - createTargetTraceMapper(mb.testProcess1)); - Trace trace = recorder.getTrace(); - - // Wait till two threads are observed in the database - waitForPass(() -> assertEquals(2, trace.getThreadManager().getAllThreads().size())); - waitForSwing(); + TraceRecorder recorder = recordAndWaitSync(); + traceManager.openTrace(tb.trace); + traceManager.activateTrace(tb.trace); TraceSnapshot snapshot = recorder.forceSnapshot(); - waitForDomainObject(trace); + waitForDomainObject(tb.trace); + waitForTasks(); - assertEquals(snapshot.getKey(), traceManager.getCurrentSnap()); + waitForPass(() -> assertEquals(snapshot.getKey(), traceManager.getCurrentSnap())); - performAction(threadsProvider.actionSeekTracePresent); - waitForSwing(); + performEnabledAction(provider, provider.actionSeekTracePresent, true); + waitForTasks(); - assertFalse(threadsProvider.actionSeekTracePresent.isSelected()); + assertFalse(provider.actionSeekTracePresent.isSelected()); recorder.forceSnapshot(); - waitForSwing(); + waitForTasks(); - assertEquals(snapshot.getKey(), traceManager.getCurrentSnap()); + waitForPass(() -> assertEquals(snapshot.getKey(), traceManager.getCurrentSnap())); } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java index 0ac8991f4b..2155534397 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java @@ -787,7 +787,12 @@ public interface TargetObjectSchema { List schemas = getSuccessorSchemas(path); for (; path != null; path = PathUtils.parent(path)) { TargetObjectSchema schema = schemas.get(path.size()); - if (schema.getInterfaces().contains(type)) { + if (!schema.isCanonicalContainer()) { + continue; + } + TargetObjectSchema deSchema = + schema.getContext().getSchema(schema.getDefaultElementSchema()); + if (deSchema.getInterfaces().contains(type)) { return path; } List inAgg = Private.searchForSuitableContainerInAggregate(schema, type); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/HorizontalTabPanel.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/HorizontalTabPanel.java index a0d40cac32..d7a8ac8254 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/HorizontalTabPanel.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/HorizontalTabPanel.java @@ -68,7 +68,7 @@ public class HorizontalTabPanel extends JPanel { } } - private final JList list = new JList<>(); + protected final JList list = new JList<>(); private final JScrollPane scroll = new JScrollPane(list); private final JViewport viewport = scroll.getViewport(); private final DefaultListModel model = new DefaultListModel<>(); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java index 6d75911596..1648b0b228 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java @@ -107,6 +107,10 @@ public class RangeCursorTableHeaderRenderer> private final ForSeekMouseListener forSeekMouseListener = new ForSeekMouseListener(); private final ListenerSet listeners = new ListenerSet<>(SeekListener.class); + public RangeCursorTableHeaderRenderer(N pos) { + this.pos = pos; + } + @Override public void setFullRange(Span fullRange) { this.fullRangeDouble = SpannedRenderer.validateViewRange(fullRange); diff --git a/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/table/DemoSpanCellRendererTest.java b/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/table/DemoSpanCellRendererTest.java index 79f754a5d9..fed523a4cb 100644 --- a/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/table/DemoSpanCellRendererTest.java +++ b/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/table/DemoSpanCellRendererTest.java @@ -215,7 +215,7 @@ public class DemoSpanCellRendererTest extends AbstractGhidraHeadedIntegrationTes TableColumn column = table.getColumnModel().getColumn(MyColumns.LIFESPAN.ordinal()); SpanTableCellRenderer rangeRenderer = new SpanTableCellRenderer<>(); RangeCursorTableHeaderRenderer headerRenderer = - new RangeCursorTableHeaderRenderer<>(); + new RangeCursorTableHeaderRenderer<>(0); column.setCellRenderer(rangeRenderer); column.setHeaderRenderer(headerRenderer);