mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 10:19:23 +02:00
GP-2794: Refactor ThreadsProvider for new trace conventions
This commit is contained in:
parent
c301dd2c89
commit
6c33d872fd
25 changed files with 1989 additions and 923 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<ValueRow> {
|
|||
}
|
||||
}
|
||||
|
||||
public static class ValueFixedProperty<T> implements ValueProperty<T> {
|
||||
private T value;
|
||||
|
||||
public ValueFixedProperty(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> getType() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValueRow getRow() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class ValueDerivedProperty<T> implements ValueProperty<T> {
|
||||
protected final ValueRow row;
|
||||
protected final Class<T> type;
|
||||
|
@ -353,7 +378,8 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
|||
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<ValueRow> {
|
|||
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<ValueRow> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable(int rowIndex, int columnIndex) {
|
||||
initializeSorting();
|
||||
List<ValueRow> 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<ValueRow, ?, Trace> column =
|
||||
(DynamicTableColumn<ValueRow, ?, Trace>) tableColumns.get(columnIndex);
|
||||
if (!(column instanceof EditableColumn<ValueRow, ?, Trace> editable)) {
|
||||
return false;
|
||||
}
|
||||
return editable.isEditable(t, columnSettings.get(column), dataSource, serviceProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
|
||||
initializeSorting();
|
||||
List<ValueRow> 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<ValueRow, ?, Trace> column =
|
||||
(DynamicTableColumn<ValueRow, ?, Trace>) tableColumns.get(columnIndex);
|
||||
if (!(column instanceof EditableColumn<ValueRow, ?, Trace> 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 <ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> void doSetValue(
|
||||
EditableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> editable, ROW_TYPE t,
|
||||
Object aValue, Settings settings, DATA_SOURCE dataSource,
|
||||
ServiceProvider serviceProvider) {
|
||||
editable.setValue(t, (COLUMN_TYPE) aValue, settings, dataSource, serviceProvider);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ValueRow, ObjectTableModel> {
|
||||
|
||||
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
|
||||
|
|
|
@ -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<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE>
|
||||
extends DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
|
||||
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);
|
||||
}
|
|
@ -31,7 +31,7 @@ public class TracePathLastLifespanPlotColumn
|
|||
|
||||
private final SpanTableCellRenderer<Long> cellRenderer = new SpanTableCellRenderer<>();
|
||||
private final RangeCursorTableHeaderRenderer<Long> headerRenderer =
|
||||
new RangeCursorTableHeaderRenderer<>();
|
||||
new RangeCursorTableHeaderRenderer<>(0L);
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
|
|
|
@ -31,7 +31,7 @@ public class TraceValueLifePlotColumn
|
|||
|
||||
private final SpanSetTableCellRenderer<Long> cellRenderer = new SpanSetTableCellRenderer<>();
|
||||
private final RangeCursorTableHeaderRenderer<Long> headerRenderer =
|
||||
new RangeCursorTableHeaderRenderer<>();
|
||||
new RangeCursorTableHeaderRenderer<>(0L);
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
|
|
|
@ -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<T>
|
||||
extends TraceValueObjectPropertyColumn<T> {
|
||||
/**
|
||||
* A column which displays the object's value for a given attribute
|
||||
*
|
||||
* @param <T> the type of the attribute
|
||||
*/
|
||||
public class TraceValueObjectAttributeColumn<T> extends TraceValueObjectPropertyColumn<T> {
|
||||
|
||||
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<T>
|
|||
|
||||
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<T> attributeType) {
|
||||
super(attributeType);
|
||||
this.attributeName = attributeName;
|
||||
|
@ -64,12 +87,6 @@ public class TraceValueObjectAttributeColumn<T>
|
|||
|
||||
@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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<T> extends TraceValueObjectAttributeColumn<T>
|
||||
implements EditableColumn<ValueRow, ValueProperty<T>, Trace> {
|
||||
public TraceValueObjectEditableAttributeColumn(String attributeName, Class<T> 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<T> 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ValueRow, Va
|
|||
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
|
||||
super.getTableCellRendererComponent(data);
|
||||
ValueRow row = (ValueRow) data.getValue();
|
||||
setText(row.getHtmlDisplay());
|
||||
setToolTipText(row.getToolTip());
|
||||
setForeground(getForegroundFor(data.getTable(), row.isModified(), data.isSelected()));
|
||||
try {
|
||||
setText(row.getHtmlDisplay());
|
||||
setToolTipText(row.getToolTip());
|
||||
setForeground(
|
||||
getForegroundFor(data.getTable(), row.isModified(), data.isSelected()));
|
||||
}
|
||||
catch (TraceClosedException e) {
|
||||
setText("ERROR: Trace Closed");
|
||||
setToolTipText(
|
||||
"This row is stale, since it refers to a trace that has since been closed");
|
||||
setForeground(getForegroundFor(data.getTable(), false, data.isSelected()));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -233,10 +233,9 @@ public class DebuggerLegacyStackPanel extends JPanel {
|
|||
public DebuggerLegacyStackPanel(DebuggerStackPlugin plugin, DebuggerStackProvider provider) {
|
||||
super(new BorderLayout());
|
||||
this.provider = provider;
|
||||
stackTableModel = new StackTableModel(provider.getTool());
|
||||
|
||||
this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
|
||||
|
||||
stackTableModel = new StackTableModel(provider.getTool());
|
||||
stackTable = new GhidraTable(stackTableModel);
|
||||
add(new JScrollPane(stackTable));
|
||||
stackFilterPanel = new GhidraTableFilterPanel<>(stackTable, stackTableModel);
|
||||
|
|
|
@ -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<ThreadTableColumns, ThreadRow> {
|
||||
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<ThreadRow, ?> getter;
|
||||
private final BiConsumer<ThreadRow, Object> setter;
|
||||
private final boolean sortable;
|
||||
private final Class<?> cls;
|
||||
|
||||
<T> ThreadTableColumns(String header, Class<T> cls, Function<ThreadRow, T> getter,
|
||||
boolean sortable) {
|
||||
this(header, cls, getter, null, sortable);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> ThreadTableColumns(String header, Class<T> cls, Function<ThreadRow, T> getter,
|
||||
BiConsumer<ThreadRow, T> setter, boolean sortable) {
|
||||
this.header = header;
|
||||
this.cls = cls;
|
||||
this.getter = getter;
|
||||
this.setter = (BiConsumer<ThreadRow, Object>) 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<Void> cbCoordinateActivation = new SuppressableCallback<>();
|
||||
|
||||
/* package access for testing */
|
||||
final SpanTableCellRenderer<Long> spanRenderer = new SpanTableCellRenderer<>();
|
||||
final RangeCursorTableHeaderRenderer<Long> headerRenderer =
|
||||
new RangeCursorTableHeaderRenderer<>(0L);
|
||||
|
||||
final ThreadTableModel threadTableModel;
|
||||
final GTable threadTable;
|
||||
final GhidraTableFilterPanel<ThreadRow> 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;
|
||||
}
|
||||
}
|
|
@ -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<TraceObjectThread> {
|
||||
|
||||
protected static ModelQuery successorThreads(TargetObjectSchema rootSchema, List<String> 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<Long> {
|
||||
public AbstractThreadLifeBoundColumn() {
|
||||
super(Long.class);
|
||||
}
|
||||
|
||||
abstract Long fromLifespan(Lifespan lifespan);
|
||||
|
||||
@Override
|
||||
public ValueProperty<Long> 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<String> {
|
||||
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<String> {
|
||||
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<ValueRow> createTableColumnDescriptor() {
|
||||
TableColumnDescriptor<ValueRow> 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<Void> 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<String> seedPath = object.getCanonicalPath().getKeyList();
|
||||
List<String> processPath = rootSchema.searchForAncestor(TargetProcess.class, seedPath);
|
||||
if (processPath != null) {
|
||||
return successorThreads(rootSchema, processPath);
|
||||
}
|
||||
List<String> 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());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<TraceRecorder> {
|
||||
@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<Void> 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<TraceRecorder> recordersListener =
|
||||
new RecordersChangeListener();
|
||||
private final BooleanChangeAdapter activatePresentChangeListener =
|
||||
this::changedAutoActivatePresent;
|
||||
private final BooleanChangeAdapter synchronizeFocusChangeListener =
|
||||
this::changedSynchronizeFocus;
|
||||
/* package access for testing */
|
||||
final SpanTableCellRenderer<Long> spanRenderer = new SpanTableCellRenderer<>();
|
||||
final RangeCursorTableHeaderRenderer<Long> headerRenderer =
|
||||
new RangeCursorTableHeaderRenderer<>();
|
||||
|
||||
protected final ThreadTableModel threadTableModel = new ThreadTableModel(this);
|
||||
private final ForSnapsListener forSnapsListener = new ForSnapsListener();
|
||||
|
||||
private JPanel mainPanel;
|
||||
|
||||
HorizontalTabPanel<Trace> traceTabs;
|
||||
GTable threadTable;
|
||||
GhidraTableFilterPanel<ThreadRow> 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<Trace> 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<Trace> 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;
|
||||
|
|
|
@ -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<Trace>
|
||||
implements PluginEventListener {
|
||||
|
||||
private class RecordersChangeListener implements CollectionChangeListener<TraceRecorder> {
|
||||
@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<TraceRecorder> recordersListener =
|
||||
new RecordersChangeListener();
|
||||
|
||||
DockingAction actionCloseTrace;
|
||||
DockingAction actionCloseOtherTraces;
|
||||
DockingAction actionCloseDeadTraces;
|
||||
DockingAction actionCloseAllTraces;
|
||||
|
||||
private final SuppressableCallback<Void> 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<Trace> 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));
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ public enum ThreadState {
|
|||
/**
|
||||
* The last recorded state is alive, but the recorder is not tracking the live thread
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
|
|
|
@ -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<ThreadTableColumns, ThreadRow> {
|
||||
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<ThreadRow, ?> getter;
|
||||
private final BiConsumer<ThreadRow, Object> setter;
|
||||
private final boolean sortable;
|
||||
private final Class<?> cls;
|
||||
|
||||
<T> ThreadTableColumns(String header, Class<T> cls, Function<ThreadRow, T> getter,
|
||||
boolean sortable) {
|
||||
this(header, cls, getter, null, sortable);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> ThreadTableColumns(String header, Class<T> cls, Function<ThreadRow, T> getter,
|
||||
BiConsumer<ThreadRow, T> setter, boolean sortable) {
|
||||
this.header = header;
|
||||
this.cls = cls;
|
||||
this.getter = getter;
|
||||
this.setter = (BiConsumer<ThreadRow, Object>) 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);
|
||||
}
|
||||
}
|
|
@ -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<T, ?> panel) {
|
||||
return panel.filterPanel;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static SpannedRenderer<Long> 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<Long>) spanned;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static RangeCursorTableHeaderRenderer<Long> 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<Long>) spanned;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ThreadRow> threadsDisplayed =
|
||||
threadsProvider.legacyPanel.threadTableModel.getModelData();
|
||||
assertTrue(threadsDisplayed.isEmpty());
|
||||
}
|
||||
|
||||
protected void assertThreadsPopulated() {
|
||||
List<ThreadRow> 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<String> 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());
|
||||
}
|
||||
}
|
|
@ -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("" + //
|
||||
"<context>" + //
|
||||
" <schema name='Session' elementResync='NEVER' attributeResync='ONCE'>" + //
|
||||
" <attribute name='Processes' schema='ProcessContainer' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='ProcessContainer' canonical='yes' elementResync='NEVER' " + //
|
||||
" attributeResync='ONCE'>" + //
|
||||
" <element index='1' schema='Process' />" + // <---- NOTE HERE
|
||||
" </schema>" + //
|
||||
" <schema name='Process' elementResync='NEVER' attributeResync='ONCE'>" + //
|
||||
" <attribute name='Threads' schema='ThreadContainer' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='ThreadContainer' canonical='yes' elementResync='NEVER' " + //
|
||||
" attributeResync='ONCE'>" + //
|
||||
" <element schema='Thread' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='Thread' elementResync='NEVER' attributeResync='NEVER'>" + //
|
||||
" <interface name='Thread' />" + //
|
||||
" </schema>" + //
|
||||
"</context>");
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
tb.trace.getObjectManager().createRootObject(ctx.getSchema(new SchemaName("Session")));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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("" + //
|
||||
"<context>" + //
|
||||
" <schema name='Session' elementResync='NEVER' attributeResync='ONCE'>" + //
|
||||
" <attribute name='Processes' schema='ProcessContainer' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='ProcessContainer' canonical='yes' elementResync='NEVER' " + //
|
||||
" attributeResync='ONCE'>" + //
|
||||
" <element index='1' schema='Process' />" + // <---- NOTE HERE
|
||||
" </schema>" + //
|
||||
" <schema name='Process' elementResync='NEVER' attributeResync='ONCE'>" + //
|
||||
" <attribute name='Threads' schema='ThreadContainer' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='ThreadContainer' canonical='yes' elementResync='NEVER' " + //
|
||||
" attributeResync='ONCE'>" + //
|
||||
" <element schema='Thread' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='Thread' elementResync='NEVER' attributeResync='NEVER'>" + //
|
||||
" <interface name='Thread' />" + //
|
||||
" </schema>" + //
|
||||
"</context>");
|
||||
|
||||
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<ThreadRow> 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<ValueRow, ?, Trace> nameCol =
|
||||
provider.panel.getColumnByNameAndType("Name", ValueRow.class).getValue();
|
||||
DynamicTableColumn<ValueRow, ?, Trace> createdCol =
|
||||
provider.panel.getColumnByNameAndType("Created", ValueProperty.class).getValue();
|
||||
DynamicTableColumn<ValueRow, ?, Trace> destroyedCol =
|
||||
provider.panel.getColumnByNameAndType("Destroyed", ValueProperty.class).getValue();
|
||||
DynamicTableColumn<ValueRow, ?, Trace> stateCol =
|
||||
provider.panel.getColumnByNameAndType("State", ValueProperty.class).getValue();
|
||||
DynamicTableColumn<ValueRow, ?, Trace> 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<ThreadRow> 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<String> 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<Long> 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<Long> 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<Long> 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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -787,7 +787,12 @@ public interface TargetObjectSchema {
|
|||
List<TargetObjectSchema> 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<String> inAgg = Private.searchForSuitableContainerInAggregate(schema, type);
|
||||
|
|
|
@ -68,7 +68,7 @@ public class HorizontalTabPanel<T> extends JPanel {
|
|||
}
|
||||
}
|
||||
|
||||
private final JList<T> list = new JList<>();
|
||||
protected final JList<T> list = new JList<>();
|
||||
private final JScrollPane scroll = new JScrollPane(list);
|
||||
private final JViewport viewport = scroll.getViewport();
|
||||
private final DefaultListModel<T> model = new DefaultListModel<>();
|
||||
|
|
|
@ -107,6 +107,10 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
|
|||
private final ForSeekMouseListener forSeekMouseListener = new ForSeekMouseListener();
|
||||
private final ListenerSet<SeekListener> listeners = new ListenerSet<>(SeekListener.class);
|
||||
|
||||
public RangeCursorTableHeaderRenderer(N pos) {
|
||||
this.pos = pos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFullRange(Span<N, ?> fullRange) {
|
||||
this.fullRangeDouble = SpannedRenderer.validateViewRange(fullRange);
|
||||
|
|
|
@ -215,7 +215,7 @@ public class DemoSpanCellRendererTest extends AbstractGhidraHeadedIntegrationTes
|
|||
TableColumn column = table.getColumnModel().getColumn(MyColumns.LIFESPAN.ordinal());
|
||||
SpanTableCellRenderer<Integer> rangeRenderer = new SpanTableCellRenderer<>();
|
||||
RangeCursorTableHeaderRenderer<Integer> headerRenderer =
|
||||
new RangeCursorTableHeaderRenderer<>();
|
||||
new RangeCursorTableHeaderRenderer<>(0);
|
||||
column.setCellRenderer(rangeRenderer);
|
||||
column.setHeaderRenderer(headerRenderer);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue