GP-2794: Refactor ThreadsProvider for new trace conventions

This commit is contained in:
Dan 2022-11-10 13:48:27 -05:00
parent c301dd2c89
commit 6c33d872fd
25 changed files with 1989 additions and 923 deletions

View file

@ -16,8 +16,7 @@
package ghidra.app.plugin.core.debug; package ghidra.app.plugin.core.debug;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.*;
import java.util.Objects;
import org.jdom.Element; import org.jdom.Element;
@ -163,6 +162,7 @@ public class DebuggerCoordinates {
return trace.getThreadManager() return trace.getThreadManager()
.getLiveThreads(snap) .getLiveThreads(snap)
.stream() .stream()
.sorted(Comparator.comparing(TraceThread::getKey))
.findFirst() .findFirst()
.orElse(null); .orElse(null);
} }
@ -171,7 +171,12 @@ public class DebuggerCoordinates {
return resolveThread(trace, TraceSchedule.ZERO); 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(); return trace.getObjectManager().getRootObject();
} }
@ -198,7 +203,7 @@ public class DebuggerCoordinates {
TraceProgramView newView = resolveView(newTrace); TraceProgramView newView = resolveView(newTrace);
TraceSchedule newTime = null; // Allow later resolution TraceSchedule newTime = null; // Allow later resolution
Integer newFrame = resolveFrame(newThread, newTime); 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, return new DebuggerCoordinates(newTrace, newPlatform, null, newThread, newView, newTime,
newFrame, newObject); newFrame, newObject);
} }
@ -242,9 +247,10 @@ public class DebuggerCoordinates {
.getObjectByCanonicalPath(TraceObjectKeyPath.of(object.getPath())); .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()) { if (recorder.getSnap() != time.getSnap() || !recorder.isSupportsFocus()) {
return resolveObject(recorder.getTrace()); return resolveObject(recorder.getTrace(), thread, frame, time);
} }
return resolveObject(recorder.getTrace(), recorder.getFocus()); return resolveObject(recorder.getTrace(), recorder.getFocus());
} }
@ -266,7 +272,7 @@ public class DebuggerCoordinates {
TraceProgramView newView = resolveView(newTrace); TraceProgramView newView = resolveView(newTrace);
TraceSchedule newTime = null; // Allow later resolution TraceSchedule newTime = null; // Allow later resolution
Integer newFrame = resolveFrame(newThread, newTime); 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, return new DebuggerCoordinates(newTrace, newPlatform, null, newThread, newView, newTime,
newFrame, newObject); newFrame, newObject);
} }
@ -294,7 +300,8 @@ public class DebuggerCoordinates {
TraceThread newThread = thread != null ? thread : resolveThread(newRecorder, newTime); TraceThread newThread = thread != null ? thread : resolveThread(newRecorder, newTime);
TraceProgramView newView = view != null ? view : resolveView(newTrace, newTime); TraceProgramView newView = view != null ? view : resolveView(newTrace, newTime);
Integer newFrame = frame != null ? frame : resolveFrame(newRecorder, newThread, 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, return new DebuggerCoordinates(newTrace, newPlatform, newRecorder, newThread, newView,
newTime, newFrame, newObject); newTime, newFrame, newObject);
} }
@ -332,13 +339,23 @@ public class DebuggerCoordinates {
* *
* @param ancestor the proposed ancestor * @param ancestor the proposed ancestor
* @param successor the proposed successor * @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 * @return true if ancestor is in fact an ancestor of successor at the given time
*/ */
private static boolean isAncestor(TraceObject ancestor, TraceObject successor, private static boolean isAncestor(TraceObject ancestor, TraceObject successor) {
TraceSchedule time) { return ancestor.getCanonicalPath().isAncestor(successor.getCanonicalPath());
return successor.getCanonicalParents(Lifespan.at(time.getSnap())) }
.anyMatch(p -> p == ancestor);
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) { public DebuggerCoordinates thread(TraceThread newThread) {
@ -358,9 +375,8 @@ public class DebuggerCoordinates {
// Yes, override frame with 0 on thread changes, unless target says otherwise // Yes, override frame with 0 on thread changes, unless target says otherwise
Integer newFrame = resolveFrame(recorder, newThread, newTime); Integer newFrame = resolveFrame(recorder, newThread, newTime);
// Yes, forced frame change may also force object change // Yes, forced frame change may also force object change
TraceObject ancestor = resolveObject(newThread, newFrame, newTime); TraceObject threadOrFrameObject = resolveObject(newThread, newFrame, newTime);
TraceObject newObject = TraceObject newObject = choose(object, threadOrFrameObject);
object != null && isAncestor(ancestor, object, newTime) ? object : ancestor;
return new DebuggerCoordinates(newTrace, newPlatform, recorder, newThread, newView, newTime, return new DebuggerCoordinates(newTrace, newPlatform, recorder, newThread, newView, newTime,
newFrame, newObject); newFrame, newObject);
} }
@ -384,9 +400,8 @@ public class DebuggerCoordinates {
: resolveThread(trace, recorder, newTime); : resolveThread(trace, recorder, newTime);
// This will cause the frame to reset to 0 on every snap change. That's fair.... // This will cause the frame to reset to 0 on every snap change. That's fair....
Integer newFrame = resolveFrame(newThread, newTime); Integer newFrame = resolveFrame(newThread, newTime);
TraceObject ancestor = resolveObject(newThread, newFrame, newTime); TraceObject threadOrFrameObject = resolveObject(newThread, newFrame, newTime);
TraceObject newObject = TraceObject newObject = choose(object, threadOrFrameObject);
object != null && isAncestor(ancestor, object, newTime) ? object : ancestor;
return new DebuggerCoordinates(trace, platform, recorder, newThread, view, newTime, return new DebuggerCoordinates(trace, platform, recorder, newThread, view, newTime,
newFrame, newObject); newFrame, newObject);
} }
@ -398,9 +413,8 @@ public class DebuggerCoordinates {
if (Objects.equals(frame, newFrame)) { if (Objects.equals(frame, newFrame)) {
return this; return this;
} }
TraceObject ancestor = resolveObject(thread, newFrame, getTime()); TraceObject threadOrFrameObject = resolveObject(thread, newFrame, getTime());
TraceObject newObject = TraceObject newObject = choose(object, threadOrFrameObject);
object != null && isAncestor(ancestor, object, getTime()) ? object : ancestor;
return new DebuggerCoordinates(trace, platform, recorder, thread, view, time, newFrame, return new DebuggerCoordinates(trace, platform, recorder, thread, view, time, newFrame,
newObject); newObject);
} }

View file

@ -28,7 +28,9 @@ import ghidra.app.plugin.core.debug.gui.model.columns.*;
import ghidra.dbg.target.schema.SchemaContext; import ghidra.dbg.target.schema.SchemaContext;
import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.address.Address; import ghidra.program.model.address.Address;
import ghidra.trace.model.Lifespan; import ghidra.trace.model.Lifespan;
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> { public static abstract class ValueDerivedProperty<T> implements ValueProperty<T> {
protected final ValueRow row; protected final ValueRow row;
protected final Class<T> type; protected final Class<T> type;
@ -353,7 +378,8 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
protected static class ColKey { protected static class ColKey {
public static ColKey fromSchema(SchemaContext ctx, AttributeSchema attributeSchema) { public static ColKey fromSchema(SchemaContext ctx, AttributeSchema attributeSchema) {
String name = attributeSchema.getName(); String name = attributeSchema.getName();
Class<?> type = TraceValueObjectAttributeColumn.computeColumnType(ctx, attributeSchema); Class<?> type =
TraceValueObjectAttributeColumn.computeAttributeType(ctx, attributeSchema);
return new ColKey(name, type); return new ColKey(name, type);
} }
@ -395,7 +421,7 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
public static TraceValueObjectAttributeColumn<?> fromSchema(SchemaContext ctx, public static TraceValueObjectAttributeColumn<?> fromSchema(SchemaContext ctx,
AttributeSchema attributeSchema) { AttributeSchema attributeSchema) {
String name = attributeSchema.getName(); String name = attributeSchema.getName();
Class<?> type = computeColumnType(ctx, attributeSchema); Class<?> type = computeAttributeType(ctx, attributeSchema);
return new AutoAttributeColumn<>(name, type); 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);
}
} }

View file

@ -15,13 +15,49 @@
*/ */
package ghidra.app.plugin.core.debug.gui.model; 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.framework.plugintool.Plugin;
import ghidra.trace.model.target.TraceObject; import ghidra.trace.model.target.TraceObject;
public class ObjectsTablePanel extends AbstractQueryTablePanel<ValueRow, ObjectTableModel> { 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) { public ObjectsTablePanel(Plugin plugin) {
super(plugin); super(plugin);
table.setDefaultEditor(ValueProperty.class, new PropertyEditor());
} }
@Override @Override

View file

@ -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);
}

View file

@ -31,7 +31,7 @@ public class TracePathLastLifespanPlotColumn
private final SpanTableCellRenderer<Long> cellRenderer = new SpanTableCellRenderer<>(); private final SpanTableCellRenderer<Long> cellRenderer = new SpanTableCellRenderer<>();
private final RangeCursorTableHeaderRenderer<Long> headerRenderer = private final RangeCursorTableHeaderRenderer<Long> headerRenderer =
new RangeCursorTableHeaderRenderer<>(); new RangeCursorTableHeaderRenderer<>(0L);
@Override @Override
public String getColumnName() { public String getColumnName() {

View file

@ -31,7 +31,7 @@ public class TraceValueLifePlotColumn
private final SpanSetTableCellRenderer<Long> cellRenderer = new SpanSetTableCellRenderer<>(); private final SpanSetTableCellRenderer<Long> cellRenderer = new SpanSetTableCellRenderer<>();
private final RangeCursorTableHeaderRenderer<Long> headerRenderer = private final RangeCursorTableHeaderRenderer<Long> headerRenderer =
new RangeCursorTableHeaderRenderer<>(); new RangeCursorTableHeaderRenderer<>(0L);
@Override @Override
public String getColumnName() { public String getColumnName() {

View file

@ -26,12 +26,28 @@ import ghidra.dbg.target.TargetSteppable.TargetStepKindSet;
import ghidra.dbg.target.schema.SchemaContext; import ghidra.dbg.target.schema.SchemaContext;
import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema;
import ghidra.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.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()); TargetObjectSchema schema = ctx.getSchema(attributeSchema.getSchema());
Class<?> type = schema.getType(); Class<?> type = schema.getType();
if (type == TargetObject.class) { if (type == TargetObject.class) {
@ -57,6 +73,13 @@ public class TraceValueObjectAttributeColumn<T>
protected final String attributeName; 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) { public TraceValueObjectAttributeColumn(String attributeName, Class<T> attributeType) {
super(attributeType); super(attributeType);
this.attributeName = attributeName; this.attributeName = attributeName;
@ -64,12 +87,6 @@ public class TraceValueObjectAttributeColumn<T>
@Override @Override
public String getColumnName() { 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; return attributeName;
} }

View file

@ -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());
}
}
}

View file

@ -29,6 +29,7 @@ import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
import ghidra.docking.settings.Settings; import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider; import ghidra.framework.plugintool.ServiceProvider;
import ghidra.trace.model.Trace; import ghidra.trace.model.Trace;
import ghidra.trace.model.TraceClosedException;
import ghidra.util.table.column.AbstractGColumnRenderer; import ghidra.util.table.column.AbstractGColumnRenderer;
import ghidra.util.table.column.GColumnRenderer; import ghidra.util.table.column.GColumnRenderer;
@ -48,9 +49,18 @@ public class TraceValueValColumn extends AbstractDynamicTableColumn<ValueRow, Va
public Component getTableCellRendererComponent(GTableCellRenderingData data) { public Component getTableCellRendererComponent(GTableCellRenderingData data) {
super.getTableCellRendererComponent(data); super.getTableCellRendererComponent(data);
ValueRow row = (ValueRow) data.getValue(); ValueRow row = (ValueRow) data.getValue();
setText(row.getHtmlDisplay()); try {
setToolTipText(row.getToolTip()); setText(row.getHtmlDisplay());
setForeground(getForegroundFor(data.getTable(), row.isModified(), data.isSelected())); 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; return this;
} }

View file

@ -233,10 +233,9 @@ public class DebuggerLegacyStackPanel extends JPanel {
public DebuggerLegacyStackPanel(DebuggerStackPlugin plugin, DebuggerStackProvider provider) { public DebuggerLegacyStackPanel(DebuggerStackPlugin plugin, DebuggerStackProvider provider) {
super(new BorderLayout()); super(new BorderLayout());
this.provider = provider; this.provider = provider;
stackTableModel = new StackTableModel(provider.getTool());
this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
stackTableModel = new StackTableModel(provider.getTool());
stackTable = new GhidraTable(stackTableModel); stackTable = new GhidraTable(stackTableModel);
add(new JScrollPane(stackTable)); add(new JScrollPane(stackTable));
stackFilterPanel = new GhidraTableFilterPanel<>(stackTable, stackTableModel); stackFilterPanel = new GhidraTableFilterPanel<>(stackTable, stackTableModel);

View file

@ -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;
}
}

View file

@ -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());
});
}
}
}

View file

@ -23,20 +23,18 @@ import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus; import ghidra.framework.plugintool.util.PluginStatus;
@PluginInfo( // @PluginInfo(
shortDescription = "Debugger registers manager", // shortDescription = "Debugger registers manager",
description = "GUI to view and modify register values", // description = "GUI to view and modify register values",
category = PluginCategoryNames.DEBUGGER, // category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME, // packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.RELEASED, // status = PluginStatus.RELEASED,
eventsConsumed = { TraceOpenedPluginEvent.class, // eventsConsumed = { TraceOpenedPluginEvent.class,
TraceClosedPluginEvent.class, // TraceActivatedPluginEvent.class,
TraceActivatedPluginEvent.class, // },
}, // servicesRequired = {
servicesRequired = { // DebuggerTraceManagerService.class,
DebuggerTraceManagerService.class, // })
} //
)
public class DebuggerThreadsPlugin extends AbstractDebuggerPlugin { public class DebuggerThreadsPlugin extends AbstractDebuggerPlugin {
protected DebuggerThreadsProvider provider; protected DebuggerThreadsProvider provider;
@ -59,17 +57,9 @@ public class DebuggerThreadsPlugin extends AbstractDebuggerPlugin {
@Override @Override
public void processEvent(PluginEvent event) { public void processEvent(PluginEvent event) {
super.processEvent(event); super.processEvent(event);
if (event instanceof TraceOpenedPluginEvent) {
TraceOpenedPluginEvent ev = (TraceOpenedPluginEvent) event;
provider.traceOpened(ev.getTrace());
}
if (event instanceof TraceActivatedPluginEvent) { if (event instanceof TraceActivatedPluginEvent) {
TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event; TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event;
provider.coordinatesActivated(ev.getActiveCoordinates()); provider.coordinatesActivated(ev.getActiveCoordinates());
} }
if (event instanceof TraceClosedPluginEvent) {
TraceClosedPluginEvent ev = (TraceClosedPluginEvent) event;
provider.traceClosed(ev.getTrace());
}
} }
} }

View file

@ -16,59 +16,37 @@
package ghidra.app.plugin.core.debug.gui.thread; package ghidra.app.plugin.core.debug.gui.thread;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.util.Objects; import java.util.Objects;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.table.TableColumn; import org.apache.commons.lang3.ArrayUtils;
import javax.swing.table.TableColumnModel;
import docking.ActionContext; import docking.ActionContext;
import docking.WindowPosition; import docking.WindowPosition;
import docking.action.*; import docking.action.*;
import docking.widgets.HorizontalTabPanel;
import docking.widgets.HorizontalTabPanel.TabListCellRenderer;
import docking.widgets.dialogs.InputDialog; 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.DebuggerCoordinates;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage; 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.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.*;
import ghidra.app.services.DebuggerTraceManagerService.BooleanChangeAdapter; import ghidra.app.services.DebuggerTraceManagerService.BooleanChangeAdapter;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.target.TargetThread;
import ghidra.framework.model.DomainObject; import ghidra.framework.model.DomainObject;
import ghidra.framework.model.DomainObjectChangeRecord;
import ghidra.framework.plugintool.AutoService; import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed; 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.TraceSnapshotChangeType;
import ghidra.trace.model.Trace.TraceThreadChangeType; import ghidra.trace.model.TraceDomainObjectListener;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.thread.TraceThreadManager;
import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.util.Msg; 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 { public class DebuggerThreadsProvider extends ComponentProviderAdapter {
protected static long orZero(Long l) {
return l == null ? 0 : l;
}
protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) { protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) {
if (!Objects.equals(a.getTrace(), b.getTrace())) { if (!Objects.equals(a.getTrace(), b.getTrace())) {
return false; return false;
@ -85,134 +63,80 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
return true; return true;
} }
protected static class ThreadTableModel private class ForSnapsListener extends TraceDomainObjectListener {
extends RowWrappedEnumeratedColumnTableModel< // private Trace currentTrace;
ThreadTableColumns, ObjectKey, ThreadRow, TraceThread> {
public ThreadTableModel(DebuggerThreadsProvider provider) { public ForSnapsListener() {
super(provider.getTool(), "Threads", ThreadTableColumns.class, listenForUntyped(DomainObject.DO_OBJECT_RESTORED, this::objectRestored);
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);
listenFor(TraceSnapshotChangeType.ADDED, this::snapAdded); listenFor(TraceSnapshotChangeType.ADDED, this::snapAdded);
listenFor(TraceSnapshotChangeType.DELETED, this::snapDeleted); listenFor(TraceSnapshotChangeType.DELETED, this::snapDeleted);
} }
private void objectRestored() { private void setTrace(Trace trace) {
loadThreads(); if (currentTrace != null) {
currentTrace.removeListener(this);
}
currentTrace = trace;
if (currentTrace != null) {
currentTrace.addListener(this);
}
} }
private void threadAdded(TraceThread thread) { private void objectRestored(DomainObjectChangeRecord rec) {
threadTableModel.addItem(thread); contextChanged();
}
private void threadChanged(TraceThread thread) {
threadTableModel.updateItem(thread);
}
private void threadDeleted(TraceThread thread) {
threadTableModel.deleteItem(thread);
} }
private void snapAdded(TraceSnapshot snapshot) { private void snapAdded(TraceSnapshot snapshot) {
updateTimelineMax();
contextChanged(); contextChanged();
} }
private void snapDeleted() { private void snapDeleted() {
updateTimelineMax(); contextChanged();
} }
} }
private class RecordersChangeListener implements CollectionChangeListener<TraceRecorder> { final DebuggerThreadsPlugin plugin;
@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;
DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; DebuggerCoordinates current = DebuggerCoordinates.NOWHERE;
private Trace currentTrace; // Copy for transition Trace currentTrace; // Copy for transition
private final SuppressableCallback<Void> cbCoordinateActivation = new SuppressableCallback<>();
// @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 = private final BooleanChangeAdapter activatePresentChangeListener =
this::changedAutoActivatePresent; this::changedAutoActivatePresent;
private final BooleanChangeAdapter synchronizeFocusChangeListener = private final BooleanChangeAdapter synchronizeFocusChangeListener =
this::changedSynchronizeFocus; 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; private JPanel mainPanel;
HorizontalTabPanel<Trace> traceTabs; DebuggerTraceTabPanel traceTabs;
GTable threadTable;
GhidraTableFilterPanel<ThreadRow> threadFilterPanel;
JPopupMenu traceTabPopupMenu; JPopupMenu traceTabPopupMenu;
DebuggerThreadsPanel panel;
private ActionContext myActionContext; DebuggerLegacyThreadsPanel legacyPanel;
DockingAction actionSaveTrace; DockingAction actionSaveTrace;
DockingAction actionStepSnapBackward;
DockingAction actionEmulateTickBackward;
DockingAction actionEmulateTickForward;
DockingAction actionEmulateTickSkipForward;
DockingAction actionStepSnapForward;
ToggleDockingAction actionSeekTracePresent; ToggleDockingAction actionSeekTracePresent;
ToggleDockingAction actionSyncFocus; ToggleDockingAction actionSyncFocus;
DockingAction actionGoToTime; DockingAction actionGoToTime;
DockingAction actionCloseTrace; ActionContext myActionContext;
DockingAction actionCloseOtherTraces;
DockingAction actionCloseDeadTraces;
DockingAction actionCloseAllTraces;
// strong refs // strong ref
ToToggleSelectionListener toToggleSelectionListener; ToToggleSelectionListener toToggleSelectionListener;
SeekListener seekListener;
public DebuggerThreadsProvider(final DebuggerThreadsPlugin plugin) { public DebuggerThreadsProvider(final DebuggerThreadsPlugin plugin) {
super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_THREADS, plugin.getName()); super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_THREADS, plugin.getName());
this.plugin = plugin; this.plugin = plugin;
this.autoWiring = AutoService.wireServicesConsumed(plugin, this); this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
setIcon(DebuggerResources.ICON_PROVIDER_THREADS); setIcon(DebuggerResources.ICON_PROVIDER_THREADS);
setHelpLocation(DebuggerResources.HELP_PROVIDER_THREADS); setHelpLocation(DebuggerResources.HELP_PROVIDER_THREADS);
@ -222,24 +146,12 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
setDefaultWindowPosition(WindowPosition.BOTTOM); setDefaultWindowPosition(WindowPosition.BOTTOM);
myActionContext = new DebuggerSnapActionContext(current.getTrace(), current.getViewSnap());
createActions(); createActions();
contextChanged(); contextChanged();
setVisible(true); 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 @AutoServiceConsumed
public void setTraceManager(DebuggerTraceManagerService traceManager) { public void setTraceManager(DebuggerTraceManagerService traceManager) {
if (this.traceManager != null) { if (this.traceManager != null) {
@ -266,62 +178,8 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
contextChanged(); contextChanged();
} }
private void removeOldListeners() { private boolean isLegacy(Trace trace) {
if (currentTrace == null) { return trace != null && trace.getObjectManager().getRootSchema() == 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
} }
public void coordinatesActivated(DebuggerCoordinates coordinates) { public void coordinatesActivated(DebuggerCoordinates coordinates) {
@ -332,32 +190,30 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
current = coordinates; current = coordinates;
doSetTrace(current.getTrace()); traceTabs.coordinatesActivated(coordinates);
doSetThread(current.getThread()); if (isLegacy(coordinates.getTrace())) {
doSetSnap(current.getSnap()); panel.coordinatesActivated(DebuggerCoordinates.NOWHERE);
legacyPanel.coordinatesActivated(coordinates);
setSubTitle(current.getTime().toString()); if (ArrayUtils.indexOf(mainPanel.getComponents(), legacyPanel) == -1) {
mainPanel.remove(panel);
contextChanged(); mainPanel.add(legacyPanel);
} mainPanel.validate();
}
protected void loadThreads() { }
threadTableModel.clear(); else {
Trace curTrace = current.getTrace(); legacyPanel.coordinatesActivated(DebuggerCoordinates.NOWHERE);
if (curTrace == null) { panel.coordinatesActivated(coordinates);
return; 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() { forSnapsListener.setTrace(coordinates.getTrace());
long max = orZero(current.getTrace().getTimeManager().getMaxSnap());
Lifespan fullRange = Lifespan.span(0, max + 1); setSubTitle(coordinates.getTime().toString());
spanRenderer.setFullRange(fullRange); contextChanged();
headerRenderer.setFullRange(fullRange);
threadTable.getTableHeader().repaint();
} }
@Override @Override
@ -365,6 +221,14 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
super.addLocalAction(action); super.addLocalAction(action);
} }
void legacyThreadsPanelContextChanged() {
myActionContext = legacyPanel.getActionContext();
}
void traceTabsContextChanged() {
myActionContext = traceTabs.getActionContext();
}
@Override @Override
public ActionContext getActionContext(MouseEvent event) { public ActionContext getActionContext(MouseEvent event) {
if (myActionContext == null) { if (myActionContext == null) {
@ -373,131 +237,21 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
return myActionContext; 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() { protected void buildMainPanel() {
traceTabPopupMenu = new JPopupMenu("Trace"); traceTabPopupMenu = new JPopupMenu("Trace");
mainPanel = new JPanel(new BorderLayout()); mainPanel = new JPanel(new BorderLayout());
threadTable = new GhidraTable(threadTableModel); panel = new DebuggerThreadsPanel(this);
threadTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); legacyPanel = new DebuggerLegacyThreadsPanel(plugin, this);
mainPanel.add(new JScrollPane(threadTable)); mainPanel.add(panel);
threadFilterPanel = new GhidraTableFilterPanel<>(threadTable, threadTableModel); traceTabs = new DebuggerTraceTabPanel(this);
mainPanel.add(threadFilterPanel, BorderLayout.SOUTH);
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); 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() { 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) actionSeekTracePresent = SeekTracePresentAction.builder(plugin)
.enabledWhen(this::isSeekTracePresentEnabled) .enabledWhen(this::isSeekTracePresentEnabled)
.onAction(this::toggledSeekTracePresent) .onAction(this::toggledSeekTracePresent)
@ -515,125 +269,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
.buildAndInstallLocal(this); .buildAndInstallLocal(this);
traceManager.addSynchronizeFocusChangeListener(toToggleSelectionListener = traceManager.addSynchronizeFocusChangeListener(toToggleSelectionListener =
new ToToggleSelectionListener(actionSyncFocus)); 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) { 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 @Override
public JComponent getComponent() { public JComponent getComponent() {
return mainPanel; return mainPanel;

View file

@ -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));
}
}

View file

@ -19,6 +19,7 @@ public enum ThreadState {
/** /**
* The last recorded state is alive, but the recorder is not tracking the live thread * 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 * This state is generally erroneous. If it is seen, the recorder has fallen out of sync with
* the live session and/or the trace. * 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 * 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, ALIVE,
/** /**
* The thread is alive, but suspended * The thread is alive, but suspended

View file

@ -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);
}
}

View file

@ -15,10 +15,18 @@
*/ */
package ghidra.app.plugin.core.debug.gui.model; 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.GhidraTable;
import ghidra.util.table.GhidraTableFilterPanel; import ghidra.util.table.GhidraTableFilterPanel;
import ghidra.util.table.column.GColumnRenderer;
public class QueryPanelTestHelper { public class QueryPanelTestHelper {
public static ObjectTableModel getTableModel(DebuggerThreadsPanel panel) {
return panel.tableModel;
}
public static GhidraTable getTable(AbstractQueryTablePanel<?, ?> panel) { public static GhidraTable getTable(AbstractQueryTablePanel<?, ?> panel) {
return panel.table; return panel.table;
} }
@ -27,4 +35,32 @@ public class QueryPanelTestHelper {
AbstractQueryTablePanel<T, ?> panel) { AbstractQueryTablePanel<T, ?> panel) {
return panel.filterPanel; 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;
}
} }

View file

@ -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());
}
}

View file

@ -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")));
}
}
}

View file

@ -18,46 +18,146 @@ package ghidra.app.plugin.core.debug.gui.thread;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.util.List; import java.io.IOException;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import org.junit.Before; import org.junit.*;
import org.junit.Test;
import org.junit.experimental.categories.Category; import org.junit.experimental.categories.Category;
import docking.widgets.table.*;
import generic.test.category.NightlyCategory; 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.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.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.Lifespan;
import ghidra.trace.model.Trace; import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.target.TraceObject.ConflictResolution;
import ghidra.trace.model.thread.TraceThreadManager; 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.TraceSnapshot;
import ghidra.trace.model.time.TraceTimeManager; import ghidra.trace.model.time.TraceTimeManager;
import ghidra.util.database.UndoableTransaction; 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 { public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
protected DebuggerThreadsPlugin threadsPlugin; DebuggerThreadsProvider provider;
protected DebuggerThreadsProvider threadsProvider;
protected TraceThread thread1; protected TraceObjectThread thread1;
protected TraceThread thread2; protected TraceObjectThread thread2;
@Before protected SchemaContext ctx;
public void setUpThreadsProviderTest() throws Exception {
threadsPlugin = addPlugin(tool, DebuggerThreadsPlugin.class); @Override
threadsProvider = waitForComponentProvider(DebuggerThreadsProvider.class); 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 { protected void addThreads() throws Exception {
TraceThreadManager manager = tb.trace.getThreadManager();
try (UndoableTransaction tid = tb.startTransaction()) { try (UndoableTransaction tid = tb.startTransaction()) {
thread1 = manager.addThread("Processes[1].Threads[1]", Lifespan.nowOn(0)); thread1 = addThread(1, Lifespan.nowOn(0), "A comment");
thread1.setComment("A comment"); thread2 = addThread(2, Lifespan.span(0, 10), "Another comment");
thread2 = manager.addThread("Processes[1].Threads[2]", Lifespan.span(5, 10));
thread2.setComment("Another comment");
} }
} }
@ -65,18 +165,18 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
* Check that there exist no tabs, and that the tab row is invisible * Check that there exist no tabs, and that the tab row is invisible
*/ */
protected void assertZeroTabs() { 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, 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 * Check that exactly one tab exists, and that the tab row is visible
*/ */
protected void assertOneTabPopulated() { protected void assertOneTabPopulated() {
assertEquals(1, threadsProvider.traceTabs.getList().getModel().getSize()); assertEquals(1, provider.traceTabs.getList().getModel().getSize());
assertNotEquals("Tab row should be visible", 0, assertNotEquals("Tab row should be visible", 0,
threadsProvider.traceTabs.getVisibleRect().height); provider.traceTabs.getVisibleRect().height);
} }
protected void assertNoTabSelected() { protected void assertNoTabSelected() {
@ -84,40 +184,57 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
} }
protected void assertTabSelected(Trace trace) { 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() { protected void assertThreadsEmpty() {
List<ThreadRow> threadsDisplayed = threadsProvider.threadTableModel.getModelData(); assertThreadsTableSize(0);
assertTrue(threadsDisplayed.isEmpty()); }
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() { protected void assertThreadsPopulated() {
List<ThreadRow> threadsDisplayed = threadsProvider.threadTableModel.getModelData(); assertThreadsTableSize(2);
assertEquals(2, threadsDisplayed.size());
ThreadRow thread1Record = threadsDisplayed.get(0); assertThreadRow(0, thread1.getObject(), "Processes[1].Threads[1]", 0L, null,
assertEquals(thread1, thread1Record.getThread()); TargetExecutionState.STOPPED, "A comment");
assertEquals("Processes[1].Threads[1]", thread1Record.getName()); assertThreadRow(1, thread2.getObject(), "Processes[1].Threads[2]", 0L, 10L,
assertEquals(Lifespan.nowOn(0), thread1Record.getLifespan()); TargetExecutionState.STOPPED, "Another comment");
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() { protected void assertNoThreadSelected() {
assertNull(threadsProvider.threadFilterPanel.getSelectedItem()); assertNull(provider.panel.getSelectedItem());
} }
protected void assertThreadSelected(TraceThread thread) { protected void assertThreadSelected(TraceObjectThread thread) {
ThreadRow row = threadsProvider.threadFilterPanel.getSelectedItem(); ValueRow row = provider.panel.getSelectedItem();
assertNotNull(row); assertNotNull(row);
assertEquals(thread, row.getThread()); assertEquals(thread.getObject(), row.getValue().getChild());
} }
protected void assertProviderEmpty() { protected void assertProviderEmpty() {
@ -125,47 +242,68 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
assertThreadsEmpty(); 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 @Test
public void testEmpty() { public void testEmpty() {
waitForSwing(); waitForTasks();
assertProviderEmpty(); waitForPass(() -> assertProviderEmpty());
} }
@Test @Test
public void testOpenTracePopupatesTab() throws Exception { public void testOpenTracePopupatesTab() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
waitForSwing(); waitForTasks();
assertOneTabPopulated(); waitForPass(() -> {
assertNoTabSelected(); assertOneTabPopulated();
assertThreadsEmpty(); assertNoTabSelected();
assertThreadsEmpty();
});
} }
@Test @Test
public void testActivateTraceSelectsTab() throws Exception { public void testActivateTraceSelectsTab() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
assertOneTabPopulated(); waitForPass(() -> {
assertTabSelected(tb.trace); assertOneTabPopulated();
assertTabSelected(tb.trace);
});
traceManager.activateTrace(null); traceManager.activateTrace(null);
waitForSwing(); waitForTasks();
assertOneTabPopulated(); waitForPass(() -> {
assertNoTabSelected(); assertOneTabPopulated();
assertNoTabSelected();
});
} }
@Test @Test
public void testSelectTabActivatesTrace() throws Exception { public void testSelectTabActivatesTrace() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
waitForSwing(); waitForTasks();
threadsProvider.traceTabs.setSelectedItem(tb.trace); provider.traceTabs.setSelectedItem(tb.trace);
waitForSwing(); waitForTasks();
assertEquals(tb.trace, traceManager.getCurrentTrace()); waitForPass(() -> {
assertEquals(tb.trace, threadsProvider.current.getTrace()); assertEquals(tb.trace, traceManager.getCurrentTrace());
assertEquals(tb.trace, provider.current.getTrace());
});
} }
@Test @Test
@ -173,30 +311,34 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
assertThreadsPopulated(); // Sanity waitForPass(() -> assertThreadsPopulated());
traceManager.activateTrace(null); traceManager.activateTrace(null);
waitForSwing(); waitForTasks();
assertThreadsEmpty(); waitForPass(() -> assertThreadsEmpty());
} }
@Test @Test
public void testCurrentTraceClosedUpdatesTabs() throws Exception { public void testCurrentTraceClosedUpdatesTabs() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
assertOneTabPopulated(); waitForPass(() -> {
assertTabSelected(tb.trace); assertOneTabPopulated();
assertTabSelected(tb.trace);
});
traceManager.closeTrace(tb.trace); traceManager.closeTrace(tb.trace);
waitForSwing(); waitForTasks();
assertZeroTabs(); waitForPass(() -> {
assertNoTabSelected(); assertZeroTabs();
assertNoTabSelected();
});
} }
@Test @Test
@ -204,29 +346,29 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
assertThreadsPopulated(); waitForPass(() -> assertThreadsPopulated());
traceManager.closeTrace(tb.trace); traceManager.closeTrace(tb.trace);
waitForSwing(); waitForTasks();
assertThreadsEmpty(); waitForPass(() -> assertThreadsEmpty());
} }
@Test @Test
public void testCloseTraceTabPopupMenuItem() throws Exception { public void testCloseTraceTabPopupMenuItem() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
waitForSwing(); waitForTasks();
assertOneTabPopulated(); // pre-check waitForPass(() -> assertOneTabPopulated());
clickListItem(threadsProvider.traceTabs.getList(), 0, MouseEvent.BUTTON3); clickListItem(provider.traceTabs.getList(), 0, MouseEvent.BUTTON3);
waitForSwing(); waitForTasks();
Set<String> expected = Set.of("Close " + tb.trace.getName()); Set<String> expected = Set.of("Close " + tb.trace.getName());
assertMenu(expected, expected); assertMenu(expected, expected);
clickSubMenuItemByText("Close " + tb.trace.getName()); clickSubMenuItemByText("Close " + tb.trace.getName());
waitForSwing(); waitForTasks();
waitForPass(() -> { waitForPass(() -> {
assertEquals(Set.of(), traceManager.getOpenTraces()); assertEquals(Set.of(), traceManager.getOpenTraces());
@ -237,24 +379,24 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
public void testActivateThenAddThreadsPopulatesProvider() throws Exception { public void testActivateThenAddThreadsPopulatesProvider() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
addThreads(); addThreads();
waitForSwing(); waitForTasks();
assertThreadsPopulated(); waitForPass(() -> assertThreadsPopulated());
} }
@Test @Test
public void testAddThreadsThenActivatePopulatesProvider() throws Exception { public void testAddThreadsThenActivatePopulatesProvider() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
waitForSwing(); waitForTasks();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
assertThreadsPopulated(); waitForPass(() -> assertThreadsPopulated());
} }
@Test @Test
@ -262,16 +404,24 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
createAndOpenTrace(); createAndOpenTrace();
TraceTimeManager manager = tb.trace.getTimeManager(); TraceTimeManager manager = tb.trace.getTimeManager();
traceManager.activateTrace(tb.trace); 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()) { try (UndoableTransaction tid = tb.startTransaction()) {
manager.getSnapshot(10, true); manager.getSnapshot(10, true);
} }
waitForSwing(); 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 // NOTE: Do not test delete updates timeline max, as maxSnap does not reflect deletion
@ -281,16 +431,18 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
try (UndoableTransaction tid = tb.startTransaction()) { try (UndoableTransaction tid = tb.startTransaction()) {
thread1.setDestructionSnap(15); thread1.getObject().removeTree(Lifespan.nowOn(16));
} }
waitForSwing(); waitForTasks();
assertEquals("15", waitForPass(() -> {
threadsProvider.threadTableModel.getModelData().get(0).getDestructionSnap()); assertThreadRow(0, thread1.getObject(), "Processes[1].Threads[1]", 0L, 15L,
// NOTE: Plot max is based on time table, never thread destruction TargetExecutionState.STOPPED, "A comment");
});
// NOTE: Destruction will not be visible in plot unless snapshot 15 is created
} }
@Test @Test
@ -298,51 +450,57 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
assertEquals(2, threadsProvider.threadTableModel.getModelData().size()); waitForPass(() -> assertThreadsTableSize(2));
try (UndoableTransaction tid = tb.startTransaction()) { try (UndoableTransaction tid = tb.startTransaction()) {
thread2.delete(); thread2.getObject().removeTree(Lifespan.ALL);
} }
waitForSwing(); waitForTasks();
assertEquals(1, threadsProvider.threadTableModel.getModelData().size()); waitForPass(() -> assertThreadsTableSize(1));
// NOTE: Plot max is based on time table, never thread destruction
} }
@Test @Test
public void testEditThreadFields() throws Exception { public void testEditThreadComment() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); 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(() -> { runSwing(() -> {
threadsProvider.threadTableModel.setValueAt("My Thread", 0, tableModel.setValueAt(new ValueFixedProperty<>("A different comment"), 0,
ThreadTableColumns.NAME.ordinal()); commentModelIdx);
threadsProvider.threadTableModel.setValueAt("A different comment", 0,
ThreadTableColumns.COMMENT.ordinal());
}); });
waitForTasks();
assertEquals("My Thread", thread1.getName()); waitForPass(() -> assertEquals("A different comment",
assertEquals("A different comment", thread1.getComment()); thread1.getObject().getAttribute(0, TraceObjectThread.KEY_COMMENT).getValue()));
} }
@Test @Test
public void testUndoRedoCausesUpdateInProvider() throws Exception { public void testUndoRedoCausesUpdateInProvider() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace);
waitForSwing();
assertThreadsPopulated(); traceManager.activateTrace(tb.trace);
waitForTasks();
waitForPass(() -> assertThreadsPopulated());
undo(tb.trace); undo(tb.trace);
assertThreadsEmpty(); waitForTasks();
waitForPass(() -> assertThreadsEmpty());
redo(tb.trace); redo(tb.trace);
assertThreadsPopulated(); waitForTasks();
waitForPass(() -> assertThreadsPopulated());
} }
@Test @Test
@ -350,15 +508,17 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
assertThreadsPopulated(); waitForPass(() -> {
assertThreadSelected(thread1); assertThreadsPopulated();
assertThreadSelected(thread1);
});
traceManager.activateThread(thread2); traceManager.activateThread(thread2);
waitForSwing(); waitForTasks();
assertThreadSelected(thread2); waitForPass(() -> assertThreadSelected(thread2));
} }
@Test @Test
@ -367,11 +527,15 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForDomainObject(tb.trace); waitForDomainObject(tb.trace);
waitForTasks();
assertThreadsPopulated(); waitForPass(() -> {
assertThreadSelected(thread1); // Manager selects default if not live 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(() -> { waitForPass(() -> {
assertThreadSelected(thread2); assertThreadSelected(thread2);
@ -384,85 +548,31 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
assertThreadsPopulated(); RangeCursorTableHeaderRenderer<Long> renderer =
assertEquals(0, traceManager.getCurrentSnap()); QueryPanelTestHelper.getCursorHeaderRenderer(provider.panel);
assertEquals(0, threadsProvider.headerRenderer.getCursorPosition().longValue());
waitForPass(() -> {
assertThreadsPopulated();
assertEquals(0, traceManager.getCurrentSnap());
assertEquals(Long.valueOf(0), renderer.getCursorPosition());
});
traceManager.activateSnap(6); traceManager.activateSnap(6);
waitForSwing(); waitForTasks();
assertEquals(6, threadsProvider.headerRenderer.getCursorPosition().longValue()); waitForPass(() -> assertEquals(Long.valueOf(6), renderer.getCursorPosition()));
} }
@Test @Test
public void testActionStepTraceBackward() throws Exception { public void testActionSeekTracePresent() throws Throwable {
assertFalse(threadsProvider.actionStepSnapBackward.isEnabled()); assertTrue(provider.actionSeekTracePresent.isSelected());
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForTasks();
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();
assertEquals(0, traceManager.getCurrentSnap()); assertEquals(0, traceManager.getCurrentSnap());
@ -470,9 +580,10 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
tb.trace.getTimeManager().createSnapshot("Next snapshot"); tb.trace.getTimeManager().createSnapshot("Next snapshot");
} }
waitForDomainObject(tb.trace); waitForDomainObject(tb.trace);
waitForTasks();
// Not live, so no seek // Not live, so no seek
assertEquals(0, traceManager.getCurrentSnap()); waitForPass(() -> assertEquals(0, traceManager.getCurrentSnap()));
tb.close(); tb.close();
@ -481,27 +592,24 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
// Threads needs registers to be recognized by the recorder // Threads needs registers to be recognized by the recorder
mb.createTestThreadRegisterBanks(); mb.createTestThreadRegisterBanks();
TraceRecorder recorder = modelService.recordTargetAndActivateTrace(mb.testProcess1, TraceRecorder recorder = recordAndWaitSync();
createTargetTraceMapper(mb.testProcess1)); traceManager.openTrace(tb.trace);
Trace trace = recorder.getTrace(); traceManager.activateTrace(tb.trace);
// Wait till two threads are observed in the database
waitForPass(() -> assertEquals(2, trace.getThreadManager().getAllThreads().size()));
waitForSwing();
TraceSnapshot snapshot = recorder.forceSnapshot(); TraceSnapshot snapshot = recorder.forceSnapshot();
waitForDomainObject(trace); waitForDomainObject(tb.trace);
waitForTasks();
assertEquals(snapshot.getKey(), traceManager.getCurrentSnap()); waitForPass(() -> assertEquals(snapshot.getKey(), traceManager.getCurrentSnap()));
performAction(threadsProvider.actionSeekTracePresent); performEnabledAction(provider, provider.actionSeekTracePresent, true);
waitForSwing(); waitForTasks();
assertFalse(threadsProvider.actionSeekTracePresent.isSelected()); assertFalse(provider.actionSeekTracePresent.isSelected());
recorder.forceSnapshot(); recorder.forceSnapshot();
waitForSwing(); waitForTasks();
assertEquals(snapshot.getKey(), traceManager.getCurrentSnap()); waitForPass(() -> assertEquals(snapshot.getKey(), traceManager.getCurrentSnap()));
} }
} }

View file

@ -787,7 +787,12 @@ public interface TargetObjectSchema {
List<TargetObjectSchema> schemas = getSuccessorSchemas(path); List<TargetObjectSchema> schemas = getSuccessorSchemas(path);
for (; path != null; path = PathUtils.parent(path)) { for (; path != null; path = PathUtils.parent(path)) {
TargetObjectSchema schema = schemas.get(path.size()); 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; return path;
} }
List<String> inAgg = Private.searchForSuitableContainerInAggregate(schema, type); List<String> inAgg = Private.searchForSuitableContainerInAggregate(schema, type);

View file

@ -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 JScrollPane scroll = new JScrollPane(list);
private final JViewport viewport = scroll.getViewport(); private final JViewport viewport = scroll.getViewport();
private final DefaultListModel<T> model = new DefaultListModel<>(); private final DefaultListModel<T> model = new DefaultListModel<>();

View file

@ -107,6 +107,10 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
private final ForSeekMouseListener forSeekMouseListener = new ForSeekMouseListener(); private final ForSeekMouseListener forSeekMouseListener = new ForSeekMouseListener();
private final ListenerSet<SeekListener> listeners = new ListenerSet<>(SeekListener.class); private final ListenerSet<SeekListener> listeners = new ListenerSet<>(SeekListener.class);
public RangeCursorTableHeaderRenderer(N pos) {
this.pos = pos;
}
@Override @Override
public void setFullRange(Span<N, ?> fullRange) { public void setFullRange(Span<N, ?> fullRange) {
this.fullRangeDouble = SpannedRenderer.validateViewRange(fullRange); this.fullRangeDouble = SpannedRenderer.validateViewRange(fullRange);

View file

@ -215,7 +215,7 @@ public class DemoSpanCellRendererTest extends AbstractGhidraHeadedIntegrationTes
TableColumn column = table.getColumnModel().getColumn(MyColumns.LIFESPAN.ordinal()); TableColumn column = table.getColumnModel().getColumn(MyColumns.LIFESPAN.ordinal());
SpanTableCellRenderer<Integer> rangeRenderer = new SpanTableCellRenderer<>(); SpanTableCellRenderer<Integer> rangeRenderer = new SpanTableCellRenderer<>();
RangeCursorTableHeaderRenderer<Integer> headerRenderer = RangeCursorTableHeaderRenderer<Integer> headerRenderer =
new RangeCursorTableHeaderRenderer<>(); new RangeCursorTableHeaderRenderer<>(0);
column.setCellRenderer(rangeRenderer); column.setCellRenderer(rangeRenderer);
column.setHeaderRenderer(headerRenderer); column.setHeaderRenderer(headerRenderer);