GP-4209: GhidraTime-MSTTD integration. Type hints for (most) Python agents.

This commit is contained in:
Dan 2025-03-24 18:28:07 +00:00
parent deb49d5322
commit 21a1602579
93 changed files with 6453 additions and 4118 deletions

View file

@ -513,8 +513,9 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
}
protected Lifespan computeFullRange() {
Long max = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap();
return Lifespan.span(0L, max == null ? 1 : max + 1);
Long maxBoxed = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap();
long max = maxBoxed == null ? 0 : maxBoxed;
return Lifespan.span(0L, max == Lifespan.DOMAIN.lmax() ? max : (max + 1));
}
protected void updateTimelineMax() {

View file

@ -122,8 +122,9 @@ public class PathTableModel extends AbstractQueryTableModel<PathRow> {
}
protected void updateTimelineMax() {
Long max = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap();
Lifespan fullRange = Lifespan.span(0L, max == null ? 1 : max + 1);
Long maxBoxed = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap();
long max = maxBoxed == null ? 0 : maxBoxed;
Lifespan fullRange = Lifespan.span(0L, max == Lifespan.DOMAIN.lmax() ? max : (max + 1));
lifespanPlotColumn.setFullRange(fullRange);
}

View file

@ -4,9 +4,9 @@
* 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.
@ -17,8 +17,7 @@ package ghidra.app.plugin.core.debug.gui.time;
import java.awt.BorderLayout;
import java.awt.Component;
import java.util.Collection;
import java.util.Objects;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -28,6 +27,7 @@ import javax.swing.table.*;
import docking.widgets.table.*;
import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.docking.settings.Settings;
import ghidra.framework.model.DomainObjectEvent;
import ghidra.framework.plugintool.PluginTool;
@ -112,9 +112,6 @@ public class DebuggerSnapshotTablePanel extends JPanel {
}
SnapshotRow row = new SnapshotRow(currentTrace, snapshot);
snapshotTableModel.add(row);
if (currentSnap == snapshot.getKey()) {
snapshotFilterPanel.setSelectedItem(row);
}
}
private void snapChanged(TraceSnapshot snapshot) {
@ -132,7 +129,7 @@ public class DebuggerSnapshotTablePanel extends JPanel {
}
}
final TableCellRenderer boldCurrentRenderer = new AbstractGColumnRenderer<Object>() {
final TableCellRenderer styleCurrentRenderer = new AbstractGColumnRenderer<Object>() {
@Override
public String getFilterString(Object t, Settings settings) {
return t == null ? "<null>" : t.toString();
@ -142,9 +139,16 @@ public class DebuggerSnapshotTablePanel extends JPanel {
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
super.getTableCellRendererComponent(data);
SnapshotRow row = (SnapshotRow) data.getRowObject();
if (row != null && currentSnap != null && currentSnap.longValue() == row.getSnap()) {
if (row == null || current == DebuggerCoordinates.NOWHERE) {
// When used in a dialog, only currentTrace is set
return this;
}
if (current.getViewSnap() == row.getSnap()) {
setBold();
}
else if (current.getSnap() == row.getSnap()) {
setItalic();
}
return this;
}
};
@ -155,7 +159,7 @@ public class DebuggerSnapshotTablePanel extends JPanel {
protected boolean hideScratch = true;
private Trace currentTrace;
private volatile Long currentSnap;
private volatile DebuggerCoordinates current = DebuggerCoordinates.NOWHERE;
protected final SnapshotListener listener = new SnapshotListener();
@ -173,19 +177,19 @@ public class DebuggerSnapshotTablePanel extends JPanel {
TableColumnModel columnModel = snapshotTable.getColumnModel();
TableColumn snapCol = columnModel.getColumn(SnapshotTableColumns.SNAP.ordinal());
snapCol.setPreferredWidth(40);
snapCol.setCellRenderer(boldCurrentRenderer);
snapCol.setCellRenderer(styleCurrentRenderer);
TableColumn timeCol = columnModel.getColumn(SnapshotTableColumns.TIMESTAMP.ordinal());
timeCol.setPreferredWidth(200);
timeCol.setCellRenderer(boldCurrentRenderer);
timeCol.setCellRenderer(styleCurrentRenderer);
TableColumn etCol = columnModel.getColumn(SnapshotTableColumns.EVENT_THREAD.ordinal());
etCol.setPreferredWidth(40);
etCol.setCellRenderer(boldCurrentRenderer);
etCol.setCellRenderer(styleCurrentRenderer);
TableColumn schdCol = columnModel.getColumn(SnapshotTableColumns.SCHEDULE.ordinal());
schdCol.setPreferredWidth(60);
schdCol.setCellRenderer(boldCurrentRenderer);
schdCol.setCellRenderer(styleCurrentRenderer);
TableColumn descCol = columnModel.getColumn(SnapshotTableColumns.DESCRIPTION.ordinal());
descCol.setPreferredWidth(200);
descCol.setCellRenderer(boldCurrentRenderer);
descCol.setCellRenderer(styleCurrentRenderer);
}
private void addNewListeners() {
@ -235,14 +239,18 @@ public class DebuggerSnapshotTablePanel extends JPanel {
return;
}
TraceTimeManager manager = currentTrace.getTimeManager();
Collection<? extends TraceSnapshot> snapshots =
hideScratch ? manager.getSnapshots(0, true, Long.MAX_VALUE, true)
: manager.getAllSnapshots();
// Use .collect instead of .toList to avoid size/sync issues
// Even though access is synchronized, size may change during iteration
snapshotTableModel.addAll(snapshots.stream()
.map(s -> new SnapshotRow(currentTrace, s))
.collect(Collectors.toList()));
List<SnapshotRow> toAdd = new ArrayList<>();
for (TraceSnapshot snapshot : hideScratch
? manager.getSnapshots(0, true, Long.MAX_VALUE, true)
: manager.getAllSnapshots()) {
SnapshotRow row = new SnapshotRow(currentTrace, snapshot);
toAdd.add(row);
if (current != DebuggerCoordinates.NOWHERE &&
snapshot.getKey() == current.getViewSnap()) {
}
}
snapshotTableModel.addAll(toAdd);
}
protected void deleteScratchSnapshots() {
@ -270,9 +278,12 @@ public class DebuggerSnapshotTablePanel extends JPanel {
return row == null ? null : row.getSnap();
}
public void setCurrentSnapshot(Long snap) {
currentSnap = snap;
snapshotTableModel.fireTableDataChanged();
public void setCurrent(DebuggerCoordinates coords) {
boolean fire = coords.getViewSnap() != current.getViewSnap();
current = coords;
if (fire) {
snapshotTableModel.fireTableDataChanged();
}
}
public void setSelectedSnapshot(Long snap) {

View file

@ -4,9 +4,9 @@
* 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.
@ -19,7 +19,6 @@ import static ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import java.awt.event.*;
import java.lang.invoke.MethodHandles;
import java.util.Objects;
import javax.swing.Icon;
import javax.swing.JComponent;
@ -37,6 +36,8 @@ import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.AutoService.Wiring;
import ghidra.framework.plugintool.annotation.AutoConfigStateField;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.trace.model.Trace;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.util.HelpLocation;
@ -62,16 +63,6 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter {
}
}
protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) {
if (!Objects.equals(a.getTrace(), b.getTrace())) {
return false;
}
if (!Objects.equals(a.getTime(), b.getTime())) {
return false;
}
return true;
}
protected final DebuggerTimePlugin plugin;
DebuggerCoordinates current = DebuggerCoordinates.NOWHERE;
@ -154,7 +145,7 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) {
activateSelectedSnapshot();
activateSelectedSnapshot(e);
}
}
});
@ -162,18 +153,44 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
activateSelectedSnapshot();
activateSelectedSnapshot(e);
e.consume(); // lest it select the next row down
}
}
});
}
private void activateSelectedSnapshot() {
Long snap = mainPanel.getSelectedSnapshot();
if (snap != null && traceManager != null) {
traceManager.activateSnap(snap);
private TraceSchedule computeSelectedSchedule(InputEvent e, long snap) {
if ((e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0) {
return TraceSchedule.snap(snap);
}
if (snap >= 0) {
return TraceSchedule.snap(snap);
}
Trace trace = current.getTrace();
if (trace == null) {
return TraceSchedule.snap(snap);
}
TraceSnapshot snapshot = trace.getTimeManager().getSnapshot(snap, false);
if (snapshot == null) { // Really shouldn't happen, but okay
return TraceSchedule.snap(snap);
}
TraceSchedule schedule = snapshot.getSchedule();
if (schedule == null) {
return TraceSchedule.snap(snap);
}
return schedule;
}
private void activateSelectedSnapshot(InputEvent e) {
if (traceManager == null) {
return;
}
Long snap = mainPanel.getSelectedSnapshot();
if (snap == null) {
return;
}
traceManager.activateTime(computeSelectedSchedule(e, snap));
}
protected void createActions() {
@ -202,14 +219,9 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter {
}
public void coordinatesActivated(DebuggerCoordinates coordinates) {
if (sameCoordinates(current, coordinates)) {
current = coordinates;
return;
}
current = coordinates;
mainPanel.setTrace(current.getTrace());
mainPanel.setCurrentSnapshot(current.getSnap());
mainPanel.setCurrent(current);
}
public void writeConfigState(SaveState saveState) {

View file

@ -634,27 +634,6 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm
return task.future;
}
protected TraceSnapshot findScratch(Trace trace, TraceSchedule time) {
Collection<? extends TraceSnapshot> exist =
trace.getTimeManager().getSnapshotsWithSchedule(time);
if (!exist.isEmpty()) {
return exist.iterator().next();
}
/**
* TODO: This could be more sophisticated.... Does it need to be, though? Ideally, we'd only
* keep state around that has annotations, e.g., bookmarks and code units. That needs a new
* query (latestStartSince) on those managers, though. It must find the latest start tick
* since a given snap. We consider only start snaps because placed code units go "from now
* on out".
*/
TraceSnapshot last = trace.getTimeManager().getMostRecentSnapshot(-1);
long snap = last == null ? Long.MIN_VALUE : last.getKey() + 1;
TraceSnapshot snapshot = trace.getTimeManager().getSnapshot(snap, true);
snapshot.setDescription("Emulated");
snapshot.setSchedule(time);
return snapshot;
}
protected void installBreakpoints(Trace trace, long snap, DebuggerPcodeMachine<?> emu) {
Lifespan span = Lifespan.at(snap);
TraceBreakpointManager bm = trace.getBreakpointManager();
@ -753,7 +732,8 @@ public class DebuggerEmulationServicePlugin extends Plugin implements DebuggerEm
protected TraceSnapshot writeToScratch(CacheKey key, CachedEmulator ce) {
TraceSnapshot destSnap;
try (Transaction tx = key.trace.openTransaction("Emulate")) {
destSnap = findScratch(key.trace, key.time);
destSnap = key.trace.getTimeManager().findScratchSnapshot(key.time);
destSnap.setDescription("Emulated");
try {
ce.emulator().writeDown(key.platform, destSnap.getKey(), key.time.getSnap());
}

View file

@ -726,10 +726,42 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
@Override
public CompletableFuture<Long> materialize(DebuggerCoordinates coordinates) {
return materialize(DebuggerCoordinates.NOWHERE, coordinates, ActivationCause.USER);
}
protected CompletableFuture<Long> materialize(DebuggerCoordinates previous,
DebuggerCoordinates coordinates, ActivationCause cause) {
/**
* NOTE: If we're requested the snapshot, we don't care if we can find the snapshot already
* materialized. We're going to let the back end actually materialize and activate. When it
* activates (check the cause), we'll look for the materialized snapshot.
*
* If we go to a found snapshot on our request, the back-end may intermittently revert to
* the another snapshot, because it may not realize what we've done at the front end, or it
* may re-validate the request and go elsewhere, resulting in abrasive navigation. While we
* could attempt some bookkeeping on the back-end, we don't control how the native debugger
* issues events, so it's easier to just give it our request and then let it drive.
*/
ControlMode mode = getEffectiveControlMode(coordinates.getTrace());
Target target = coordinates.getTarget();
// NOTE: We've already validated at this point
if (mode.isTarget() && cause == ActivationCause.USER && target != null) {
// Yes, use getSnap for the materialized (view) snapshot
return target.activateAsync(previous, coordinates).thenApply(__ -> target.getSnap());
}
Long found = findSnapshot(coordinates);
if (found != null) {
return CompletableFuture.completedFuture(found);
}
/**
* NOTE: We can actually reach this point in RO_TARGET mode, though ordinarily, it should
* only reach here in RW_EMULATOR mode. The reason is because during many automated tests,
* the "default" mode of RO_TARGET is taken as the effective mode, and the tests still
* expect emulation behavior. So do it.
*/
if (emulationService == null) {
Msg.warn(this, "Cannot navigate to coordinates with execution schedules, " +
"because the emulation service is not available.");
@ -738,16 +770,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
return emulationService.backgroundEmulate(coordinates.getPlatform(), coordinates.getTime());
}
protected CompletableFuture<Void> prepareViewAndFireEvent(DebuggerCoordinates coordinates,
ActivationCause cause) {
protected CompletableFuture<Void> prepareViewAndFireEvent(DebuggerCoordinates previous,
DebuggerCoordinates coordinates, ActivationCause cause) {
TraceVariableSnapProgramView varView = (TraceVariableSnapProgramView) coordinates.getView();
if (varView == null) { // Should only happen with NOWHERE
fireLocationEvent(coordinates, cause);
return AsyncUtils.nil();
}
return materialize(coordinates).thenAcceptAsync(snap -> {
return materialize(previous, coordinates, cause).thenAcceptAsync(snap -> {
if (snap == null) {
return; // The tool is probably closing
/**
* Either the tool is closing, or we're going to let the target materialize and
* activate the actual snap.
*/
return;
}
if (!coordinates.equals(current)) {
return; // We navigated elsewhere before emulation completed
@ -1150,21 +1186,12 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
return AsyncUtils.nil();
}
CompletableFuture<Void> future =
prepareViewAndFireEvent(resolved, cause).exceptionally(ex -> {
prepareViewAndFireEvent(prev, resolved, cause).exceptionally(ex -> {
// Emulation service will already display error
doSetCurrent(prev);
return null;
});
if (cause != ActivationCause.USER) {
return future;
}
Target target = resolved.getTarget();
if (target == null) {
return future;
}
return future.thenCompose(__ -> target.activateAsync(prev, resolved));
return future;
}
@Override

View file

@ -24,6 +24,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
@ -69,8 +70,7 @@ import ghidra.trace.database.ToyDBTraceBuilder;
import ghidra.trace.model.Trace;
import ghidra.trace.model.target.schema.SchemaContext;
import ghidra.trace.model.target.schema.XmlSchemaContext;
import ghidra.util.InvalidNameException;
import ghidra.util.NumericUtilities;
import ghidra.util.*;
import ghidra.util.datastruct.TestDataStructureErrorHandlerInstaller;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.ConsoleTaskMonitor;
@ -310,6 +310,25 @@ public abstract class AbstractGhidraHeadedDebuggerTest
}, () -> lastError.get().getMessage());
}
public static void waitForPass(Object originator, Runnable runnable, long duration,
TimeUnit unit) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < unit.toMillis(duration)) {
try {
waitForPass(runnable);
break;
}
catch (Throwable e) {
Msg.warn(originator, "Long wait: " + e);
try {
Thread.sleep(500);
}
catch (InterruptedException e1) {
}
}
}
}
public static <T> T waitForPass(Supplier<T> supplier) {
var locals = new Object() {
AssertionError lastError;

View file

@ -33,8 +33,10 @@ import ghidra.trace.model.breakpoint.TraceBreakpoint;
import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.trace.model.guest.TracePlatform;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.path.KeyPath;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.schedule.TraceSchedule.ScheduleForm;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -70,6 +72,11 @@ public class MockTarget implements Target {
return snap;
}
@Override
public ScheduleForm getSupportedTimeForm(TraceObject obj, long snap) {
return null;
}
@Override
public Map<String, ActionEntry> collectActions(ActionName name, ActionContext context,
ObjectArgumentPolicy policy) {