diff --git a/Ghidra/Debug/Debugger/build.gradle b/Ghidra/Debug/Debugger/build.gradle index 29c6dd693b..7030ea7f14 100644 --- a/Ghidra/Debug/Debugger/build.gradle +++ b/Ghidra/Debug/Debugger/build.gradle @@ -31,6 +31,7 @@ dependencies { api project(':ProposedUtils') helpPath project(path: ':Base', configuration: 'helpPath') + helpPath project(path: ':ProgramDiff', configuration: 'helpPath') testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts') testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts') diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index c0fbb2759e..ce88827e9d 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -119,6 +119,9 @@ src/main/help/help/topics/DebuggerThreadsPlugin/images/stepinto.png||GHIDRA||||E src/main/help/help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerTimePlugin/images/DebuggerTimePlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerTraceManagerServicePlugin/DebuggerTraceManagerServicePlugin.html||GHIDRA||||END| +src/main/help/help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html||GHIDRA||||END| +src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTimeSelectionDialog.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTraceViewDiffPlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerWatchesPlugin/images/DebuggerWatchesPlugin.png||GHIDRA||||END| src/main/resources/defaultTools/Debugger.tool||GHIDRA||||END| @@ -184,6 +187,7 @@ src/main/resources/images/stop.png||GHIDRA||||END| src/main/resources/images/sync_enabled.png||GHIDRA||||END| src/main/resources/images/system-switch-user.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/table.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| +src/main/resources/images/table_relationship.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/text-xml.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/thread.png||GHIDRA||||END| src/main/resources/images/time.png||FAMFAMFAM Icons - CC 2.5||||END| diff --git a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml index 9e57f22ea4..f45b46b223 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml @@ -157,6 +157,10 @@ sortgroup="p" target="help/topics/DebuggerPcodeStepperPlugin/DebuggerPcodeStepperPlugin.html" /> + + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html index 559d91baf8..6f49e8d8ee 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html @@ -56,15 +56,20 @@

Actions

-

The time window provides the following action:

+

Rename Snapshot

+ +

This action is available in the Debugger menu whenever the focused + window has an associated snapshot. It will prompt for a new description for the current + snapshot. This is a shortcut to modifying the description in the time table, but can be + accessed outside of the time window.

Hide Scratch

-

This toggle action is always available. It is enabled by default. The emulation service, - which enables trace extrapolation and interpolation, writes emulated state into the trace's - "scratch space," which comprises all negative snaps. When this toggle is enabled, those - snapshots are hidden. They can be displayed by disabling this toggle. Note that navigating into - scratch space may cause temporary undefined behavior in some windows, and may prevent - interaction with the target.

+

This toggle action is always available in the drop-down actions of the Time window. It is + enabled by default. The emulation service, which enables trace extrapolation and interpolation, + writes emulated state into the trace's "scratch space," which comprises all negative snaps. + When this toggle is enabled, those snapshots are hidden. They can be displayed by disabling + this toggle. Note that navigating into scratch space may cause temporary undefined behavior in + some windows, and may prevent interaction with the target.

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html new file mode 100644 index 0000000000..0221bc8c13 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html @@ -0,0 +1,158 @@ + + + + + + + Debugger: Comparing Times + + + + + +

Debugger: Comparing Times

+ + + + + + + +
+ +

A common strategy in dynamic analysis is to compare machine + state between two points in time. To this end, to support comparison of bytes in memory, the + "trace diff" plugin extends the Dynamic Listing to provide + side-by-side comparison of two different points in time. When active, listings for both points + in time are displayed and the byte value differences between them are highlighted. NOTE: + This does not compare annotations. It only compares raw byte values. Additionally, all stale + values are ignored, i.e., to show as a difference, the memory must be observed at both + points in time, and the values must differ.

+ +

NOTE: This plugin only facilitates the comparison of memory displayed in listings. To + compare registers or SLEIGH expressions, use the respective windows: Registers and Watches. By navigating back + and forth between two points in time, using the Time Window, the differences are + displayed in red.

+ +

Actions

+ +

The plugin adds actions to the main Dynamic Listing. When active, additional actions are + present.

+ +

Compare

+ +

This action is available whenever a trace is active in the main listing. It prompts for an + alternative point in time:

+ + + + + + + +
+ +

The snapshot table is exactly the same as that in the Time Window. In most cases, simply + selecting a snapshot suffices.

+ +

Perhaps the most common use of this action is to identify where a given variable is stored + in memory. The trace saves a record of observed memory from the debugging session. Comparing + snapshots thus identifies changes over time; however, there is no guarantee that the desired + variable was ever observed. Assuming the general vicinity of the variable is known, e.g., + "somewhere in the .data section," the Read Selected + Memory action can ensure its value is recorded. Of course, it can also read "all memory," + but that operation and the follow-on comparison could take time. In general, the procedure to + locate a variable is to capture a baseline, execute the target until the variable has changed, + capture again, then compare:

+ +
    +
  1. Execute the target up to a baseline, and take note of the variable's value, as displayed + by the target program.
  2. + +
  3. Consider naming the current snapshot for later reference, using the Rename Current + Snapshot action. Ideally, the name should indicate the variable's value.
  4. + +
  5. Select the range of memory believed to contain the variable. Consider using the Modules or Regions window to form the + selection.
  6. + +
  7. Use the Read Selected + Memory action to ensure the variable's value is stored in the trace.
  8. + +
  9. Allow the target to execute until the variable has changed. Ideally, execute as little as + necessary, so that few or no other variables change.
  10. + +
  11. Execution will cause the trace to advance some number of snapshots. Once suspended, it's + a good idea to rename the current snapshot, again indicating the variable's new value and/or + the cause of its change.
  12. + +
  13. Repeat the selection and capture steps to ensure the variable's new value is stored in + the trace.
  14. + +
  15. Use this Compare action and select the baseline snapshot. It's easy to locate in + the table if named appropriately.
  16. +
+ +

Assuming the variable is actually contained in the captured memory ranges, then it should be + among the differences shown. If too many differences appear, repeat the experiment. Consider + executing less code, establishing a new baseline, taking the intersection of the results, etc. + Remember, the variable's storage should encode its value.

+ +

Optionally, the specified time may also include emulation. See the Go To Time action + for the syntax of the Time Schedule expression. For simple schedules, the step buttons + provide convenient forward and backward changes to the emulation schedule. Perhaps the most + common use of this is to see what changes from executing an isolated block of code. Ideally, + the baseline is a relatively complete capture or represents the present in a live session, so + that the emulator does not depend on un-recorded state:

+ +
    +
  1. Execute the target up to a baseline, probably using a breakpoint at the start of the + interesting block of code.
  2. + +
  3. Keeping the target alive, use the Emulate + Forward and/or Go To Time + actions to reach the end of the interesting block.
  4. + +
  5. Use this Compare action and select the baseline snapshot.
  6. +
+ +

Alternatively, if the number of steps to reach the end of the block is already known, just + use the emulation expression in the Compare action's dialog. NOTE: When used this + way, the baseline snapshot will be in the left pane, and the emulated snapshot in the right, + which is opposite the result from the steps above.

+ +

In either case, this will highlight any memory that was modified by the emulated code. Of + course, this could also be accomplished by setting a second breakpoint and allowing the target + to execute; however, emulation does not necessarily require large memory captures. It only + observes what it needs, and its internal state contains everything that changed. Furthermore, + if establishing the baseline is difficult, emulation allows the target to remain at that + baseline. Assuming sufficient state is captured, emulation can also be performed offline, + without a live target.

+ +

Previous / Next Difference

+ +

These actions are only present when the comparison listing is visible. Each is available + when there exists a previous or next range from the main listing's cursor. Clicking the action + navigates to the nearest address in that range.

+ +

Tool Options: Colors

+ +

The difference highlight color is replicated from the Program Differences plugin.

+ + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTimeSelectionDialog.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTimeSelectionDialog.png new file mode 100644 index 0000000000..e4a617d110 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTimeSelectionDialog.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTraceViewDiffPlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTraceViewDiffPlugin.png new file mode 100644 index 0000000000..7056ec724d Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTraceViewDiffPlugin.png differ diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java index 8d0c47fba0..ddaa613ae9 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java @@ -241,6 +241,10 @@ public class DebuggerCoordinates { return all(trace, recorder, thread, view, newTime, frame); } + public DebuggerCoordinates withView(TraceProgramView newView) { + return all(trace, recorder, thread, newView, time, frame); + } + public TraceSchedule getTime() { return time; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java index c2bacf96e7..b99fd8d9b0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java @@ -147,6 +147,8 @@ public interface DebuggerResources { ImageIcon ICON_READ_MEMORY = ICON_REGIONS; //ResourceManager.loadImage("images/read-memory.png"); + ImageIcon ICON_RENAME_SNAPSHOT = ICON_TIME; + // TODO: Draw an icon ImageIcon ICON_MAP_IDENTICALLY = ResourceManager.loadImage("images/doubleArrow.png"); ImageIcon ICON_MAP_MODULES = ResourceManager.loadImage("images/modules.png"); @@ -175,6 +177,10 @@ public interface DebuggerResources { ImageIcon ICON_CONFIG = ResourceManager.loadImage("images/conf.png"); ImageIcon ICON_TOGGLE = ResourceManager.loadImage("images/system-switch-user.png"); + ImageIcon ICON_DIFF = ResourceManager.loadImage("images/table_relationship.png"); + ImageIcon ICON_DIFF_PREV = ResourceManager.loadImage("images/up.png"); + ImageIcon ICON_DIFF_NEXT = ResourceManager.loadImage("images/down.png"); + HelpLocation HELP_PACKAGE = new HelpLocation("Debugger", "package"); String HELP_ANCHOR_PLUGIN = "plugin"; @@ -367,6 +373,7 @@ public interface DebuggerResources { String GROUP_TRACE_CLOSE = "Dbg7.b. Trace Close"; String GROUP_MAINTENANCE = "Dbg8. Maintenance"; String GROUP_MAPPING = "Dbg9. Map Modules/Sections"; + String GROUP_DIFF_NAV = "DiffNavigate"; static void tableRowActivationAction(GTable table, Runnable runnable) { table.addMouseListener(new MouseAdapter() { @@ -1587,6 +1594,26 @@ public interface DebuggerResources { } } + // TODO: Perhaps to reduce overloading of "snapshot" we should use "event" instead? + interface RenameSnapshotAction { + String NAME = "Rename Current Snapshot"; + String DESCRIPTION = + "Modify the description of the snapshot (event) in the current view"; + String GROUP = GROUP_TRACE; + Icon ICON = ICON_RENAME_SNAPSHOT; + String HELP_ANCHOR = "rename_snapshot"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .menuPath(DebuggerPluginPackage.NAME, NAME) + .menuGroup(GROUP, "zzz") + .keyBinding("CTRL SHIFT N") + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + interface SynchronizeFocusAction { String NAME = "Synchronize Focus"; String DESCRIPTION = "Synchronize trace activation with debugger focus/select"; @@ -1824,6 +1851,57 @@ public interface DebuggerResources { } } + interface CompareTimesAction { + String NAME = "Compare"; + String DESCRIPTION = "Compare this point in time to another"; + String GROUP = "zzz"; // Same as for "Diff" action + Icon ICON = ICON_DIFF; + String HELP_ANCHOR = "compare"; + + static ToggleActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ToggleActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarGroup(GROUP) + .toolBarIcon(ICON) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface PrevDifferenceAction { + String NAME = "Previous Difference"; + String DESCRIPTION = "Go to the previous highlighted difference"; + String GROUP = GROUP_DIFF_NAV; + Icon ICON = ICON_DIFF_PREV; + String HELP_ANCHOR = "prev_diff"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarGroup(GROUP) + .toolBarIcon(ICON) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface NextDifferenceAction { + String NAME = "Next Difference"; + String DESCRIPTION = "Go to the next highlighted difference"; + String GROUP = GROUP_DIFF_NAV; + Icon ICON = ICON_DIFF_NEXT; + String HELP_ANCHOR = "next_diff"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarGroup(GROUP) + .toolBarIcon(ICON) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + public abstract class AbstractDebuggerConnectionsNode extends GTreeNode { @Override public String getName() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerSnapActionContext.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerSnapActionContext.java index b092e2ed4c..bb1072a60a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerSnapActionContext.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerSnapActionContext.java @@ -16,16 +16,22 @@ package ghidra.app.plugin.core.debug.gui; import docking.ActionContext; +import ghidra.trace.model.Trace; public class DebuggerSnapActionContext extends ActionContext { - private final long tick; + private final Trace trace; + private final long snap; - public DebuggerSnapActionContext(long tick) { - // TODO: Also require track object? - this.tick = tick; + public DebuggerSnapActionContext(Trace trace, long snap) { + this.trace = trace; + this.snap = snap; } - public long getTick() { - return tick; + public Trace getTrace() { + return trace; + } + + public long getSnap() { + return snap; } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java index 6f343444ad..7a064623ab 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerTrackLocationTrait.java @@ -227,7 +227,7 @@ public class DebuggerTrackLocationTrait { protected void doSetSpec(LocationTrackingSpec spec) { if (this.spec != spec) { this.spec = spec; - specChanged(); + specChanged(spec); } doTrack(); } @@ -299,7 +299,7 @@ public class DebuggerTrackLocationTrait { // Listener method } - protected void specChanged() { + protected void specChanged(LocationTrackingSpec spec) { // Listener method } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPlugin.java new file mode 100644 index 0000000000..bba465234e --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPlugin.java @@ -0,0 +1,630 @@ +/* ### + * 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.diff; + +import java.awt.Color; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiPredicate; +import java.util.function.Function; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import docking.ActionContext; +import docking.action.DockingAction; +import docking.action.ToggleDockingAction; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.codebrowser.MarkerServiceBackgroundColorModel; +import ghidra.app.plugin.core.debug.*; +import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; +import ghidra.app.plugin.core.debug.gui.action.DebuggerTrackLocationTrait; +import ghidra.app.plugin.core.debug.gui.action.LocationTrackingSpec; +import ghidra.app.plugin.core.debug.gui.listing.MultiBlendedListingBackgroundColorModel; +import ghidra.app.plugin.core.debug.gui.time.DebuggerTimeSelectionDialog; +import ghidra.app.plugin.core.debug.utils.BackgroundUtils.PluginToolExecutorService; +import ghidra.app.services.*; +import ghidra.app.services.DebuggerListingService.LocationTrackingSpecChangeListener; +import ghidra.app.util.viewer.listingpanel.ListingPanel; +import ghidra.async.AsyncUtils; +import ghidra.framework.options.AutoOptions; +import ghidra.framework.options.annotation.AutoOptionConsumed; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.annotation.AutoServiceConsumed; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemoryManager; +import ghidra.trace.model.memory.TraceMemoryState; +import ghidra.trace.model.program.TraceProgramView; +import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.util.Msg; + +@PluginInfo( + shortDescription = "Compare memory state between times in a trace", + description = "Provides a side-by-side diff view between snapshots (points in time) in a " + + "trace. The comparison is limited to raw bytes.", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + eventsConsumed = { + TraceClosedPluginEvent.class, + }, + eventsProduced = {}, + servicesRequired = { + DebuggerListingService.class, + }, + servicesProvided = {}) +public class DebuggerTraceViewDiffPlugin extends AbstractDebuggerPlugin { + protected static final String MARKER_NAME = "Trace Diff"; + protected static final String MARKER_DESCRIPTION = "Difference between snapshots in this trace"; + + public static final String DIFF_COLOR_CATEGORY = "Listing Fields"; + public static final String DIFF_COLOR_NAME = "Selection Colors.Difference Color"; + public static final Color DEFAULT_DIFF_COLOR = new Color(255, 230, 180); // light orange + + protected class ListingCoordinationListener implements CoordinatedListingPanelListener { + @Override + public boolean listingClosed() { + return endComparison(); + } + + @Override + public void activeProgramChanged(Program activeProgram) { + endComparison(); + } + } + + protected class ForAltListingTrackingTrait extends DebuggerTrackLocationTrait { + public ForAltListingTrackingTrait() { + super(DebuggerTraceViewDiffPlugin.this.getTool(), DebuggerTraceViewDiffPlugin.this, + null); + } + + @Override + protected void locationTracked() { + if (altListingPanel == null) { + return; + } + // NB. Don't goTo here. The left listing controls navigation + altListingPanel.getFieldPanel().repaint(); + } + } + + protected class SyncAltListingTrackingSpecChangeListener + implements LocationTrackingSpecChangeListener { + @Override + public void locationTrackingSpecChanged(LocationTrackingSpec spec) { + trackingTrait.setSpec(spec); + } + } + + protected class MarkerSetChangeListener implements ChangeListener { + @Override + public void stateChanged(ChangeEvent e) { + if (altListingPanel == null) { + return; + } + altListingPanel.getFieldPanel().repaint(); + } + } + + // @AutoServiceConsumed via method + private DebuggerListingService listingService; + @AutoServiceConsumed + private DebuggerTraceManagerService traceManager; + //@AutoServiceConsumed via method + private MarkerService markerService; + + @AutoOptionConsumed(category = DIFF_COLOR_CATEGORY, name = DIFF_COLOR_NAME) + private Color diffColor = DEFAULT_DIFF_COLOR; + @SuppressWarnings("unused") + private final AutoOptions.Wiring autoOptions; + + protected final DebuggerTimeSelectionDialog timeDialog; + + protected ToggleDockingAction actionCompare; + protected DockingAction actionPrevDiff; + protected DockingAction actionNextDiff; + + protected ListingPanel altListingPanel; + protected final ForAltListingTrackingTrait trackingTrait; + protected boolean sessionActive; + + protected final ListingCoordinationListener coordinationListener = + new ListingCoordinationListener(); + protected final SyncAltListingTrackingSpecChangeListener syncTrackingSpecListener = + new SyncAltListingTrackingSpecChangeListener(); + + protected MultiBlendedListingBackgroundColorModel colorModel; + protected final MarkerSetChangeListener markerChangeListener = new MarkerSetChangeListener(); + protected MarkerServiceBackgroundColorModel markerServiceColorModel; + + protected MarkerSet diffMarkersL; + protected MarkerSet diffMarkersR; + + public DebuggerTraceViewDiffPlugin(PluginTool tool) { + super(tool); + autoOptions = AutoOptions.wireOptions(this); + + timeDialog = new DebuggerTimeSelectionDialog(tool); + trackingTrait = new ForAltListingTrackingTrait(); + createActions(); + } + + protected void createActions() { + actionCompare = CompareTimesAction.builder(this) + .enabled(false) + .enabledWhen(ctx -> traceManager != null && traceManager.getCurrentTrace() != null) + .onAction(this::activatedCompare) + .build(); + actionPrevDiff = PrevDifferenceAction.builder(this) + .enabled(false) + .enabledWhen(ctx -> hasPrevDiff()) + .onAction(ctx -> gotoPrevDiff()) + .build(); + actionNextDiff = NextDifferenceAction.builder(this) + .enabled(false) + .enabledWhen(ctx -> hasNextDiff()) + .onAction(ctx -> gotoNextDiff()) + .build(); + } + + protected void activatedCompare(ActionContext ctx) { + if (!actionCompare.isSelected()) { + endComparison(); + return; + } + if (sessionActive) { + return; + } + + DebuggerCoordinates current = traceManager.getCurrent(); + TraceSchedule time = timeDialog.promptTime(current.getTrace(), current.getTime()); + if (time == null) { + // Cancelled + return; + } + if (traceManager == null) { + // Can happen if tool is closed while dialog was up + return; + } + if (traceManager.getCurrentTrace() != current.getTrace()) { + Msg.warn(this, "Trace changed during time prompt. Aborting"); + return; + } + // NB. startComparison will handle failure + startComparison(time); + } + + /** + * Begin a snapshot/time comparison session + * + *

+ * NOTE: This method handles asynchronous errors by popping an error dialog. Callers need not + * handle exceptional completion. + * + * @param time the alternative time + * @return a future which completes when the alternative listing and difference is presented + */ + public CompletableFuture startComparison(TraceSchedule time) { + sessionActive = true; // prevents the action from performing anything + actionCompare.setSelected(true); + + DebuggerCoordinates current = traceManager.getCurrent(); + DebuggerCoordinates alternate = + traceManager.resolveCoordinates(DebuggerCoordinates.time(time)); + PluginToolExecutorService toolExecutorService = + new PluginToolExecutorService(tool, "Computing diff", true, true, false, 500); + return traceManager.materialize(alternate).thenApplyAsync(snap -> { + clearMarkers(); + TraceProgramView altView = alternate.getTrace().getFixedProgramView(snap); + altListingPanel.setProgram(altView); + trackingTrait.goToCoordinates(alternate.withView(altView)); + listingService.setListingPanel(altListingPanel); + return altView; + }, AsyncUtils.SWING_EXECUTOR).thenApplyAsync(altView -> { + return computeDiff(current.getView(), altView); + }, toolExecutorService).thenAcceptAsync(diffSet -> { + addMarkers(diffSet); + listingService.addLocalAction(actionNextDiff); + listingService.addLocalAction(actionPrevDiff); + updateActions(); + }, AsyncUtils.SWING_EXECUTOR).exceptionally(ex -> { + Msg.showError(this, null, "Compare", "Could not compare trace snapshots/times", ex); + return null; + }); + } + + protected void updateActions() { + // May not be necessary often, since contextChanged in ListingProvider should do it + actionNextDiff.setEnabled(actionNextDiff.isEnabledForContext(null)); + actionPrevDiff.setEnabled(actionPrevDiff.isEnabledForContext(null)); + } + + public boolean endComparison() { + sessionActive = false; + actionCompare.setSelected(false); + clearMarkers(); + if (altListingPanel.getProgram() != null) { + listingService.removeListingPanel(altListingPanel); + altListingPanel.setProgram(null); + + listingService.removeLocalAction(actionNextDiff); + listingService.removeLocalAction(actionPrevDiff); + + return true; + } + return false; + } + + protected Address getCurrentAddress() { + if (listingService == null) { + return null; + } + ProgramLocation loc = listingService.getCurrentLocation(); + if (loc == null) { + return null; + } + return loc.getAddress(); + } + + public AddressSetView getDiffs() { + if (diffMarkersL == null) { + return null; + } + return diffMarkersL.getAddressSet(); + } + + protected boolean hasSeqDiff(Function getExtremeRange, + BiPredicate checkRange) { + Address cur = getCurrentAddress(); + if (cur == null) { + return false; + } + AddressSetView set = getDiffs(); + if (set == null) { + return false; + } + AddressRange extreme = getExtremeRange.apply(set); + if (extreme == null) { + return false; + } + return checkRange.test(extreme, cur); + } + + public boolean hasPrevDiff() { + return hasSeqDiff(AddressSetView::getFirstRange, + (first, cur) -> first.getMaxAddress().compareTo(cur) < 0); + } + + public boolean hasNextDiff() { + return hasSeqDiff(AddressSetView::getLastRange, + (last, cur) -> cur.compareTo(last.getMinAddress()) < 0); + } + + protected Address getSeqDiff(boolean forward, + Function getFarthestAddress, + Function getStepped) { + Address cur = getCurrentAddress(); + if (cur == null) { + return null; + } + AddressSetView set = getDiffs(); + if (set == null) { + return null; + } + AddressRange range = set.getRangeContaining(cur); + if (range != null) { + cur = getFarthestAddress.apply(range); + } + cur = getStepped.apply(cur); + if (cur == null) { + return null; + } + AddressIterator it = set.getAddresses(cur, forward); + if (!it.hasNext()) { + return null; + } + return it.next(); + } + + public Address getPrevDiff() { + return getSeqDiff(false, AddressRange::getMinAddress, Address::previous); + } + + public Address getNextDiff() { + return getSeqDiff(true, AddressRange::getMaxAddress, Address::next); + } + + public boolean gotoPrevDiff() { + Address prevDiff = getPrevDiff(); + if (prevDiff == null) { + return false; + } + return listingService.goTo(prevDiff, true) && altListingPanel.goTo(prevDiff); + } + + public boolean gotoNextDiff() { + Address nextDiff = getNextDiff(); + if (nextDiff == null) { + return false; + } + return listingService.goTo(nextDiff, true) && altListingPanel.goTo(nextDiff); + } + + protected void injectOnListingService() { + if (listingService != null) { + listingService.addLocalAction(actionCompare); + altListingPanel = new ListingPanel(listingService.getFormatManager()); + listingService.setCoordinatedListingPanelListener(coordinationListener); + listingService.addTrackingSpecChangeListener(syncTrackingSpecListener); + + colorModel = listingService.createListingBackgroundColorModel(altListingPanel); + colorModel.addModel(trackingTrait.createListingBackgroundColorModel(altListingPanel)); + altListingPanel.setBackgroundColorModel(colorModel); + updateMarkerServiceColorModel(); + } + } + + protected void ejectFromListingService() { + if (altListingPanel != null) { + altListingPanel.dispose(); + altListingPanel = null; + } + colorModel = null; + if (listingService != null) { + listingService.removeLocalAction(actionCompare); + listingService.setCoordinatedListingPanelListener(null); + listingService.removeTrackingSpecChangeListener(syncTrackingSpecListener); + } + } + + @AutoServiceConsumed + private void setListingService(DebuggerListingService listingService) { + ejectFromListingService(); + this.listingService = listingService; + injectOnListingService(); + } + + protected void updateMarkerServiceColorModel() { + if (colorModel == null) { + return; + } + colorModel.removeModel(markerServiceColorModel); + if (markerService != null && altListingPanel != null) { + colorModel.addModel(markerServiceColorModel = new MarkerServiceBackgroundColorModel( + markerService, altListingPanel.getProgram(), altListingPanel.getAddressIndexMap())); + } + } + + protected void createMarkers() { + if (diffMarkersL != null) { + return; + } + if (markerService == null) { + diffMarkersL = null; + diffMarkersR = null; + return; + } + if (altListingPanel == null) { + diffMarkersL = null; + diffMarkersR = null; + return; + } + Program viewR = altListingPanel.getProgram(); + if (viewR == null) { + diffMarkersR = null; + diffMarkersL = null; + return; + } + Color diffColor = this.diffColor == null ? DEFAULT_DIFF_COLOR : this.diffColor; + TraceProgramView viewL = traceManager.getCurrentView(); + diffMarkersL = markerService.createAreaMarker(MARKER_NAME, MARKER_DESCRIPTION, viewL, 0, + true, true, true, diffColor, true); + diffMarkersR = markerService.createAreaMarker(MARKER_NAME, MARKER_DESCRIPTION, viewR, 0, + true, true, true, diffColor, true); + return; + } + + protected void addMarkers(AddressSetView diffSet) { + createMarkers(); + if (diffMarkersL != null) { + diffMarkersL.add(diffSet); + } + if (diffMarkersR != null) { + diffMarkersR.add(diffSet); + } + } + + protected void clearMarkers() { + if (diffMarkersL != null) { + diffMarkersL.clearAll(); + } + if (diffMarkersR != null) { + diffMarkersR.clearAll(); + } + } + + protected void deleteMarkers() { + if (diffMarkersL == null) { + return; + } + if (markerService == null) { + return; + } + if (altListingPanel == null) { + return; + } + Program altView = altListingPanel.getProgram(); + if (altView == null) { + return; + } + markerService.removeMarker(diffMarkersL, altView); + markerService.removeMarker(diffMarkersR, altView); + } + + @AutoServiceConsumed + private void setMarkerService(MarkerService markerService) { + if (this.markerService != null) { + this.markerService.removeChangeListener(markerChangeListener); + deleteMarkers(); + } + this.markerService = markerService; + updateMarkerServiceColorModel(); + + if (this.markerService != null) { + this.markerService.addChangeListener(markerChangeListener); + } + } + + @AutoOptionConsumed(category = DIFF_COLOR_CATEGORY, name = DIFF_COLOR_NAME) + private void setDiffColor(Color diffColor) { + if (diffMarkersL != null) { + diffMarkersL.setMarkerColor(diffColor); + } + if (diffMarkersR != null) { + diffMarkersR.setMarkerColor(diffColor); + } + } + + @Override + public void processEvent(PluginEvent event) { + super.processEvent(event); + if (event instanceof TraceClosedPluginEvent) { + TraceClosedPluginEvent evt = (TraceClosedPluginEvent) event; + if (timeDialog.getTrace() == evt.getTrace()) { + timeDialog.close(); + } + } + } + + public static int lenRemainsBlock(int blockSize, long off) { + return blockSize - (int) (off % blockSize); + } + + public static long minOfBlock(int blockSize, long off) { + return off / blockSize * blockSize; + } + + public static long maxOfBlock(int blockSize, long off) { + return (off + blockSize - 1) / blockSize * blockSize - 1; + } + + public static Address maxOfBlock(int blockSize, Address address) { + long off = address.getOffset(); + long max = maxOfBlock(blockSize, off); + AddressSpace space = address.getAddressSpace(); + return space.getAddress(max); + } + + public static AddressRange blockFor(int blockSize, Address address) { + long off = address.getOffset(); + // TODO: Require powers of 2? + long min = minOfBlock(blockSize, off); + long max = maxOfBlock(blockSize, off); + AddressSpace space = address.getAddressSpace(); + return new AddressRangeImpl(space.getAddress(min), space.getAddress(max)); + } + + protected AddressSetView computeDiff(TraceProgramView view1, TraceProgramView view2) { + Trace trace = view1.getTrace(); + assert trace == view2.getTrace(); + long snap1 = view1.getSnap(); + long snap2 = view2.getSnap(); + + if (snap1 == snap2) { + // Punt on the degenerate case + return new AddressSet(); + } + + TraceMemoryManager mm = trace.getMemoryManager(); + + AddressSetView known1 = mm.getAddressesWithState(snap1, s -> s == TraceMemoryState.KNOWN); + AddressSetView known2 = mm.getAddressesWithState(snap2, s -> s == TraceMemoryState.KNOWN); + + //AddressSet knownEither = known1.union(known2); + AddressSet knownBoth = known1.intersect(known2); // Will need byte-by-byte examination + + // Symmetric difference in state counts as difference? + // TODO: Should that be togglable? + + AddressSet diff = new AddressSet(); //knownEither; + //knownEither = null; // Don't need knownEither anymore. Avoid accidental use + //diff.delete(knownBoth); + + int blockSize = mm.getBlockSize(); + if (blockSize == 0) { + throw new UnsupportedOperationException("TODO: Unoptimized byte diff"); + } + ByteBuffer buf1 = ByteBuffer.allocate(blockSize); + ByteBuffer buf2 = ByteBuffer.allocate(blockSize); + + while (!knownBoth.isEmpty()) { + Address next = knownBoth.getMinAddress(); + Long mrs1 = mm.getSnapOfMostRecentChangeToBlock(snap1, next); + Long mrs2 = mm.getSnapOfMostRecentChangeToBlock(snap2, next); + if (Objects.equals(mrs1, mrs2)) { + knownBoth.delete(blockFor(blockSize, next)); + continue; + } + + int len = lenRemainsBlock(blockSize, next.getOffset()); + buf1.clear(); + buf1.limit(len); + if (len != mm.getBytes(snap1, next, buf1)) { + throw new AssertionError("Read failed"); + } + buf2.clear(); + buf2.limit(len); + if (len != mm.getBytes(snap2, next, buf2)) { + throw new AssertionError("Read failed"); + } + + compareBytes(diff, next, buf1, buf2); + knownBoth.delete(blockFor(blockSize, next)); + } + + return diff; + } + + protected void compareBytes(AddressSet diff, Address addr, ByteBuffer buf1, ByteBuffer buf2) { + int len = buf1.limit(); + byte[] arr1 = buf1.array(); + byte[] arr2 = buf2.array(); + Address rngStart = null; + for (int i = 0; i < len; i++) { + if (arr1[i] != arr2[i]) { + if (rngStart == null) { + rngStart = addr.add(i); + } + } + else { + if (rngStart != null) { + diff.add(rngStart, addr.add(i - 1)); + rngStart = null; + } + } + } + if (rngStart != null) { + diff.add(rngStart, addr.add(len - 1)); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/CursorBackgroundColorModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/CursorBackgroundColorModel.java index e5b00f5971..32ac0957b0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/CursorBackgroundColorModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/CursorBackgroundColorModel.java @@ -26,6 +26,7 @@ import ghidra.app.util.viewer.util.AddressIndexMap; import ghidra.framework.options.AutoOptions; import ghidra.framework.options.AutoOptions.Wiring; import ghidra.framework.options.annotation.AutoOptionConsumed; +import ghidra.framework.plugintool.Plugin; import ghidra.program.model.address.Address; import ghidra.program.util.ProgramLocation; @@ -41,7 +42,7 @@ class CursorBackgroundColorModel implements ListingBackgroundColorModel { @SuppressWarnings("unused") private final Wiring autoOptionsWiring; - public CursorBackgroundColorModel(DebuggerListingPlugin plugin, ListingPanel listingPanel) { + public CursorBackgroundColorModel(Plugin plugin, ListingPanel listingPanel) { autoOptionsWiring = AutoOptions.wireOptions(plugin, this); modelDataChanged(listingPanel); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java index b00daed407..4fc878e3e8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPlugin.java @@ -38,6 +38,7 @@ import ghidra.app.plugin.core.debug.gui.action.LocationTrackingSpec; import ghidra.app.plugin.core.debug.gui.action.NoneLocationTrackingSpec; import ghidra.app.services.*; import ghidra.app.util.viewer.format.FormatManager; +import ghidra.app.util.viewer.listingpanel.ListingPanel; import ghidra.framework.options.AutoOptions; import ghidra.framework.options.SaveState; import ghidra.framework.options.annotation.AutoOptionDefined; @@ -162,6 +163,16 @@ public class DebuggerListingPlugin extends AbstractCodeBrowserPlugin trackingSpecChangeListeners = + new ListenerSet<>(LocationTrackingSpecChangeListener.class); protected final DebuggerLocationLabel locationLabel = new DebuggerLocationLabel(); @@ -275,10 +285,8 @@ public class DebuggerListingProvider extends CodeViewerProvider { readsMemTrait = new ForListingReadsMemoryTrait(); ListingPanel listingPanel = getListingPanel(); - colorModel = new MultiBlendedListingBackgroundColorModel(); + colorModel = plugin.createListingBackgroundColorModel(listingPanel); colorModel.addModel(trackingTrait.createListingBackgroundColorModel(listingPanel)); - colorModel.addModel(new MemoryStateListingBackgroundColorModel(plugin, listingPanel)); - colorModel.addModel(new CursorBackgroundColorModel(plugin, listingPanel)); listingPanel.setBackgroundColorModel(colorModel); autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); @@ -493,12 +501,10 @@ public class DebuggerListingProvider extends CodeViewerProvider { if (this.markerService != null) { this.markerService.removeChangeListener(markerChangeListener); } - this.markerService = markerService; - updateMarkerServiceColorModel(); - removeOldStaticTrackingMarker(); this.markerService = markerService; createNewStaticTrackingMarker(); + updateMarkerServiceColorModel(); if (this.markerService != null && !isMainListing()) { // NOTE: Connected provider marker listener is taken care of by CodeBrowserPlugin @@ -594,6 +600,38 @@ public class DebuggerListingProvider extends CodeViewerProvider { setSubTitle(computeSubTitle()); } + @Override + protected String computePanelTitle(Program panelProgram) { + if (!(panelProgram instanceof TraceProgramView)) { + // really shouldn't happen anyway... + return super.computePanelTitle(panelProgram); + } + TraceProgramView view = (TraceProgramView) panelProgram; + TraceSnapshot snapshot = + view.getTrace().getTimeManager().getSnapshot(view.getSnap(), false); + if (snapshot == null) { + return Long.toString(view.getSnap()); + } + String description = snapshot.getDescription(); + String schedule = snapshot.getScheduleString(); + if (description == null) { + description = ""; + } + if (schedule == null) { + schedule = ""; + } + if (!description.isBlank() && !schedule.isBlank()) { + return description + " (" + schedule + ")"; + } + if (!description.isBlank()) { + return description; + } + if (!schedule.isBlank()) { + return schedule; + } + return DateUtils.formatDateTimestamp(new Date(snapshot.getRealTime())); + } + protected void createActions() { if (isMainListing()) { actionSyncToStaticListing = new SyncToStaticListingAction(); @@ -842,6 +880,14 @@ public class DebuggerListingProvider extends CodeViewerProvider { return trackingTrait.getSpec(); } + public void addTrackingSpecChangeListener(LocationTrackingSpecChangeListener listener) { + trackingSpecChangeListeners.add(listener); + } + + public void removeTrackingSpecChangeListener(LocationTrackingSpecChangeListener listener) { + trackingSpecChangeListeners.remove(listener); + } + public void setSyncToStaticListing(boolean sync) { if (!isMainListing()) { throw new IllegalStateException( diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/MemoryStateListingBackgroundColorModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/MemoryStateListingBackgroundColorModel.java index 01773dcf50..a8fbe4b64a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/MemoryStateListingBackgroundColorModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/MemoryStateListingBackgroundColorModel.java @@ -25,6 +25,7 @@ import ghidra.app.util.viewer.listingpanel.ListingPanel; import ghidra.app.util.viewer.util.AddressIndexMap; import ghidra.framework.options.AutoOptions; import ghidra.framework.options.annotation.AutoOptionConsumed; +import ghidra.framework.plugintool.Plugin; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; import ghidra.trace.model.TraceAddressSnapRange; @@ -47,7 +48,7 @@ public class MemoryStateListingBackgroundColorModel implements ListingBackground @SuppressWarnings("unused") private final AutoOptions.Wiring autoOptionsWiring; - public MemoryStateListingBackgroundColorModel(DebuggerListingPlugin plugin, + public MemoryStateListingBackgroundColorModel(Plugin plugin, ListingPanel listingPanel) { autoOptionsWiring = AutoOptions.wireOptions(plugin, this); modelDataChanged(listingPanel); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java index 8d8c16953d..177e2caa2b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java @@ -379,7 +379,7 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { // TODO: Should I receive clicks on that renderer to seek to a given snap? setDefaultWindowPosition(WindowPosition.BOTTOM); - myActionContext = new DebuggerSnapActionContext(0); + myActionContext = new DebuggerSnapActionContext(current.getTrace(), current.getViewSnap()); createActions(); contextChanged(); @@ -617,7 +617,7 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { snap = 0; } traceManager.activateSnap(snap); - myActionContext = new DebuggerSnapActionContext(snap); + myActionContext = new DebuggerSnapActionContext(current.getTrace(), snap); contextChanged(); }); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerSnapshotTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerSnapshotTablePanel.java new file mode 100644 index 0000000000..38d4346201 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerSnapshotTablePanel.java @@ -0,0 +1,262 @@ +/* ### + * 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.time; + +import java.awt.BorderLayout; +import java.util.Collection; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import javax.swing.*; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import com.google.common.collect.Collections2; + +import docking.widgets.table.*; +import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; +import ghidra.framework.model.DomainObject; +import ghidra.trace.model.Trace; +import ghidra.trace.model.Trace.TraceSnapshotChangeType; +import ghidra.trace.model.TraceDomainObjectListener; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.trace.model.time.TraceTimeManager; +import ghidra.util.table.GhidraTableFilterPanel; + +public class DebuggerSnapshotTablePanel extends JPanel { + + protected enum SnapshotTableColumns + implements EnumeratedTableColumn { + SNAP("Snap", Long.class, SnapshotRow::getSnap), + TIMESTAMP("Timestamp", String.class, SnapshotRow::getTimeStamp), // TODO: Use Date type here + EVENT_THREAD("Event Thread", String.class, SnapshotRow::getEventThreadName), + SCHEDULE("Schedule", String.class, SnapshotRow::getSchedule), + DESCRIPTION("Description", String.class, SnapshotRow::getDescription, SnapshotRow::setDescription); + + private final String header; + private final Function getter; + private final BiConsumer setter; + private final Class cls; + + SnapshotTableColumns(String header, Class cls, Function getter) { + this(header, cls, getter, null); + } + + @SuppressWarnings("unchecked") + SnapshotTableColumns(String header, Class cls, Function getter, + BiConsumer setter) { + this.header = header; + this.cls = cls; + this.getter = getter; + this.setter = (BiConsumer) setter; + } + + @Override + public Class getValueClass() { + return cls; + } + + @Override + public Object getValueOf(SnapshotRow row) { + return getter.apply(row); + } + + @Override + public String getHeader() { + return header; + } + + @Override + public boolean isEditable(SnapshotRow row) { + return setter != null; + } + + @Override + public void setValueOf(SnapshotRow row, Object value) { + setter.accept(row, value); + } + } + + private class SnapshotListener extends TraceDomainObjectListener { + public SnapshotListener() { + listenForUntyped(DomainObject.DO_OBJECT_RESTORED, e -> objectRestored()); + + listenFor(TraceSnapshotChangeType.ADDED, this::snapAdded); + listenFor(TraceSnapshotChangeType.CHANGED, this::snapChanged); + listenFor(TraceSnapshotChangeType.DELETED, this::snapDeleted); + } + + private void objectRestored() { + loadSnapshots(); + } + + private void snapAdded(TraceSnapshot snapshot) { + if (snapshot.getKey() < 0 && hideScratch) { + return; + } + SnapshotRow row = new SnapshotRow(currentTrace, snapshot); + snapshotTableModel.add(row); + if (currentSnap == snapshot.getKey()) { + snapshotFilterPanel.setSelectedItem(row); + } + } + + private void snapChanged(TraceSnapshot snapshot) { + if (snapshot.getKey() < 0 && hideScratch) { + return; + } + snapshotTableModel.notifyUpdatedWith(row -> row.getSnapshot() == snapshot); + } + + private void snapDeleted(TraceSnapshot snapshot) { + if (snapshot.getKey() < 0 && hideScratch) { + return; + } + snapshotTableModel.deleteWith(row -> row.getSnapshot() == snapshot); + } + } + + protected final EnumeratedColumnTableModel snapshotTableModel = + new DefaultEnumeratedColumnTableModel<>("Snapshots", SnapshotTableColumns.class); + protected final GTable snapshotTable; + protected final GhidraTableFilterPanel snapshotFilterPanel; + protected boolean hideScratch = true; + + private Trace currentTrace; + private Long currentSnap; + + protected final SnapshotListener listener = new SnapshotListener(); + + public DebuggerSnapshotTablePanel() { + super(new BorderLayout()); + snapshotTable = new GTable(snapshotTableModel); + snapshotTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + add(new JScrollPane(snapshotTable)); + + snapshotFilterPanel = new GhidraTableFilterPanel<>(snapshotTable, snapshotTableModel); + add(snapshotFilterPanel, BorderLayout.SOUTH); + + TableColumnModel columnModel = snapshotTable.getColumnModel(); + TableColumn snapCol = columnModel.getColumn(SnapshotTableColumns.SNAP.ordinal()); + snapCol.setPreferredWidth(40); + TableColumn timeCol = columnModel.getColumn(SnapshotTableColumns.TIMESTAMP.ordinal()); + timeCol.setPreferredWidth(200); + TableColumn etCol = columnModel.getColumn(SnapshotTableColumns.EVENT_THREAD.ordinal()); + etCol.setPreferredWidth(40); + TableColumn schdCol = columnModel.getColumn(SnapshotTableColumns.SCHEDULE.ordinal()); + schdCol.setPreferredWidth(60); + TableColumn descCol = columnModel.getColumn(SnapshotTableColumns.DESCRIPTION.ordinal()); + descCol.setPreferredWidth(200); + } + + private void addNewListeners() { + if (currentTrace == null) { + return; + } + currentTrace.addListener(listener); + } + + private void removeOldListeners() { + if (currentTrace == null) { + return; + } + currentTrace.removeListener(listener); + } + + public void setTrace(Trace trace) { + if (currentTrace == trace) { + return; + } + removeOldListeners(); + currentTrace = trace; + addNewListeners(); + loadSnapshots(); + } + + public Trace getTrace() { + return currentTrace; + } + + public void setHideScratchSnapshots(boolean hideScratch) { + if (this.hideScratch == hideScratch) { + return; + } + this.hideScratch = hideScratch; + if (hideScratch) { + deleteScratchSnapshots(); + } + else { + loadScratchSnapshots(); + } + } + + protected void loadSnapshots() { + snapshotTableModel.clear(); + if (currentTrace == null) { + return; + } + TraceTimeManager manager = currentTrace.getTimeManager(); + Collection snapshots = hideScratch + ? manager.getSnapshots(0, true, Long.MAX_VALUE, true) + : manager.getAllSnapshots(); + snapshotTableModel.addAll(Collections2.transform(snapshots, + s -> new SnapshotRow(currentTrace, s))); + } + + protected void deleteScratchSnapshots() { + snapshotTableModel.deleteWith(s -> s.getSnap() < 0); + } + + protected void loadScratchSnapshots() { + if (currentTrace == null) { + return; + } + TraceTimeManager manager = currentTrace.getTimeManager(); + snapshotTableModel.addAll(Collections2.transform( + manager.getSnapshots(Long.MIN_VALUE, true, 0, false), + s -> new SnapshotRow(currentTrace, s))); + } + + public ListSelectionModel getSelectionModel() { + return snapshotTable.getSelectionModel(); + } + + public Long getSelectedSnapshot() { + SnapshotRow row = snapshotFilterPanel.getSelectedItem(); + return row == null ? null : row.getSnap(); + } + + public void setSelectedSnapshot(Long snap) { + currentSnap = snap; + if (snap == null) { + snapshotTable.clearSelection(); + return; + } + + SnapshotRow sel = snapshotFilterPanel.getSelectedItem(); + Long curSnap = sel == null ? null : sel.getSnap(); + if (Objects.equals(curSnap, snap)) { + return; + } + SnapshotRow row = snapshotTableModel.findFirst(r -> r.getSnap() == snap); + if (row == null) { + snapshotTable.clearSelection(); + return; + } + snapshotFilterPanel.setSelectedItem(row); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java index 3cb19e1c49..8d42bcf254 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimePlugin.java @@ -15,14 +15,29 @@ */ package ghidra.app.plugin.core.debug.gui.time; +import java.util.Map; +import java.util.Map.Entry; + +import docking.ActionContext; +import docking.action.DockingAction; +import docking.widgets.dialogs.InputDialog; +import ghidra.app.context.ProgramLocationActionContext; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.RenameSnapshotAction; +import ghidra.app.plugin.core.debug.gui.DebuggerSnapActionContext; import ghidra.app.services.DebuggerTraceManagerService; import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.model.listing.Program; +import ghidra.trace.model.Trace; +import ghidra.trace.model.program.TraceProgramView; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.trace.model.time.TraceTimeManager; +import ghidra.util.database.UndoableTransaction; @PluginInfo( shortDescription = "Lists recorded snapshots in a trace", @@ -39,8 +54,12 @@ import ghidra.framework.plugintool.util.PluginStatus; public class DebuggerTimePlugin extends AbstractDebuggerPlugin { protected DebuggerTimeProvider provider; + protected DockingAction actionRenameSnapshot; + public DebuggerTimePlugin(PluginTool tool) { super(tool); + + createActions(); } @Override @@ -49,6 +68,58 @@ public class DebuggerTimePlugin extends AbstractDebuggerPlugin { super.init(); } + protected void createActions() { + actionRenameSnapshot = RenameSnapshotAction.builder(this) + .enabled(false) + .enabledWhen(ctx -> contextGetTraceSnap(ctx) != null) + .onAction(this::activatedRenameSnapshot) + .buildAndInstall(tool); + } + + protected Entry contextGetTraceSnap(ActionContext context) { + if (context instanceof ProgramLocationActionContext) { + ProgramLocationActionContext ctx = (ProgramLocationActionContext) context; + Program program = ctx.getProgram(); + if (program instanceof TraceProgramView) { + TraceProgramView view = (TraceProgramView) program; + return Map.entry(view.getTrace(), view.getSnap()); + } + return null; + } + if (context instanceof DebuggerSnapActionContext) { + DebuggerSnapActionContext ctx = (DebuggerSnapActionContext) context; + if (ctx.getTrace() != null) { + return Map.entry(ctx.getTrace(), ctx.getSnap()); + } + return null; + } + return null; + } + + protected void activatedRenameSnapshot(ActionContext context) { + Entry traceSnap = contextGetTraceSnap(context); + if (traceSnap == null) { + return; + } + Trace trace = traceSnap.getKey(); + long snap = traceSnap.getValue(); + TraceTimeManager manager = trace.getTimeManager(); + TraceSnapshot snapshot = manager.getSnapshot(snap, false); + + InputDialog dialog = new InputDialog("Rename Snapshot", "Description", + snapshot == null ? "" : snapshot.getDescription()); + tool.showDialog(dialog); + if (dialog.isCanceled()) { + return; + } + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Rename Snapshot", true)) { + if (snapshot == null) { + snapshot = manager.getSnapshot(snap, true); + } + snapshot.setDescription(dialog.getValue()); + } + } + @Override protected void dispose() { tool.removeComponentProvider(provider); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java index a7a10ff5cb..97d173bd64 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProvider.java @@ -17,138 +17,30 @@ package ghidra.app.plugin.core.debug.gui.time; import static ghidra.app.plugin.core.debug.gui.DebuggerResources.*; -import java.awt.BorderLayout; import java.awt.event.MouseEvent; import java.lang.invoke.MethodHandles; -import java.util.Collection; import java.util.Objects; -import java.util.function.BiConsumer; -import java.util.function.Function; -import javax.swing.*; -import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; - -import com.google.common.collect.Collections2; +import javax.swing.JComponent; import docking.ActionContext; import docking.action.DockingActionIf; import docking.action.ToggleDockingAction; -import docking.widgets.table.*; -import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerSnapActionContext; import ghidra.app.services.DebuggerTraceManagerService; -import ghidra.framework.model.DomainObject; import ghidra.framework.options.SaveState; 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.Trace.TraceSnapshotChangeType; -import ghidra.trace.model.TraceDomainObjectListener; -import ghidra.trace.model.time.TraceSnapshot; -import ghidra.trace.model.time.TraceTimeManager; -import ghidra.util.table.GhidraTableFilterPanel; public class DebuggerTimeProvider extends ComponentProviderAdapter { private static final AutoConfigState.ClassHandler CONFIG_STATE_HANDLER = AutoConfigState.wireHandler(DebuggerTimeProvider.class, MethodHandles.lookup()); - protected enum SnapshotTableColumns - implements EnumeratedTableColumn { - SNAP("Snap", Long.class, SnapshotRow::getSnap), - TIMESTAMP("Timestamp", String.class, SnapshotRow::getTimeStamp), // TODO: Use Date type here - EVENT_THREAD("Event Thread", String.class, SnapshotRow::getEventThreadName), - SCHEDULE("Schedule", String.class, SnapshotRow::getSchedule), - DESCRIPTION("Description", String.class, SnapshotRow::getDescription, SnapshotRow::setDescription); - - private final String header; - private final Function getter; - private final BiConsumer setter; - private final Class cls; - - SnapshotTableColumns(String header, Class cls, Function getter) { - this(header, cls, getter, null); - } - - @SuppressWarnings("unchecked") - SnapshotTableColumns(String header, Class cls, Function getter, - BiConsumer setter) { - this.header = header; - this.cls = cls; - this.getter = getter; - this.setter = (BiConsumer) setter; - } - - @Override - public Class getValueClass() { - return cls; - } - - @Override - public Object getValueOf(SnapshotRow row) { - return getter.apply(row); - } - - @Override - public String getHeader() { - return header; - } - - @Override - public boolean isEditable(SnapshotRow row) { - return setter != null; - } - - @Override - public void setValueOf(SnapshotRow row, Object value) { - setter.accept(row, value); - } - } - - private class SnapshotListener extends TraceDomainObjectListener { - public SnapshotListener() { - listenForUntyped(DomainObject.DO_OBJECT_RESTORED, e -> objectRestored()); - - listenFor(TraceSnapshotChangeType.ADDED, this::snapAdded); - listenFor(TraceSnapshotChangeType.CHANGED, this::snapChanged); - listenFor(TraceSnapshotChangeType.DELETED, this::snapDeleted); - } - - private void objectRestored() { - loadSnapshots(); - } - - private void snapAdded(TraceSnapshot snapshot) { - if (snapshot.getKey() < 0 && hideScratch) { - return; - } - SnapshotRow row = new SnapshotRow(current.getTrace(), snapshot); - snapshotTableModel.add(row); - if (current.getSnap() == snapshot.getKey()) { - snapshotFilterPanel.setSelectedItem(row); - } - } - - private void snapChanged(TraceSnapshot snapshot) { - if (snapshot.getKey() < 0 && hideScratch) { - return; - } - snapshotTableModel.notifyUpdatedWith(row -> row.getSnapshot() == snapshot); - } - - private void snapDeleted(TraceSnapshot snapshot) { - if (snapshot.getKey() < 0 && hideScratch) { - return; - } - snapshotTableModel.deleteWith(row -> row.getSnapshot() == snapshot); - } - } - protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) { if (!Objects.equals(a.getTrace(), b.getTrace())) { return false; @@ -162,28 +54,20 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { protected final DebuggerTimePlugin plugin; DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; - private Trace currentTrace; // copy for transition - - protected final SnapshotListener listener = new SnapshotListener(); @AutoServiceConsumed protected DebuggerTraceManagerService viewManager; @SuppressWarnings("unused") private final Wiring autoServiceWiring; - private final JPanel mainPanel = new JPanel(new BorderLayout()); - - /* testing */ final EnumeratedColumnTableModel snapshotTableModel = - new DefaultEnumeratedColumnTableModel<>("Snapshots", SnapshotTableColumns.class); - /* testing */ GTable snapshotTable; - /* testing */ GhidraTableFilterPanel snapshotFilterPanel; + /*testing*/ final DebuggerSnapshotTablePanel mainPanel = new DebuggerSnapshotTablePanel(); private DebuggerSnapActionContext myActionContext; ToggleDockingAction actionHideScratch; @AutoConfigStateField - /* testing */ boolean hideScratch = true; + /*testing*/ boolean hideScratch = true; public DebuggerTimeProvider(DebuggerTimePlugin plugin) { super(plugin.getTool(), TITLE_PROVIDER_TIME, plugin.getName()); @@ -198,7 +82,7 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { buildMainPanel(); - myActionContext = new DebuggerSnapActionContext(0); + myActionContext = new DebuggerSnapActionContext(current.getTrace(), current.getSnap()); createActions(); contextChanged(); @@ -224,42 +108,22 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { } protected void buildMainPanel() { - snapshotTable = new GTable(snapshotTableModel); - snapshotTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - mainPanel.add(new JScrollPane(snapshotTable)); - - snapshotFilterPanel = new GhidraTableFilterPanel<>(snapshotTable, snapshotTableModel); - mainPanel.add(snapshotFilterPanel, BorderLayout.SOUTH); - - snapshotTable.getSelectionModel().addListSelectionListener(evt -> { + mainPanel.getSelectionModel().addListSelectionListener(evt -> { if (evt.getValueIsAdjusting()) { return; } - SnapshotRow row = snapshotFilterPanel.getSelectedItem(); - if (row == null) { + Long snap = mainPanel.getSelectedSnapshot(); + if (snap == null) { myActionContext = null; return; } - long snap = row.getSnap(); - if (snap == current.getSnap().longValue()) { + if (snap.longValue() == current.getSnap().longValue()) { return; } - myActionContext = new DebuggerSnapActionContext(snap); + myActionContext = new DebuggerSnapActionContext(current.getTrace(), snap); viewManager.activateSnap(snap); contextChanged(); }); - - TableColumnModel columnModel = snapshotTable.getColumnModel(); - TableColumn snapCol = columnModel.getColumn(SnapshotTableColumns.SNAP.ordinal()); - snapCol.setPreferredWidth(40); - TableColumn timeCol = columnModel.getColumn(SnapshotTableColumns.TIMESTAMP.ordinal()); - timeCol.setPreferredWidth(200); - TableColumn etCol = columnModel.getColumn(SnapshotTableColumns.EVENT_THREAD.ordinal()); - etCol.setPreferredWidth(40); - TableColumn schdCol = columnModel.getColumn(SnapshotTableColumns.SCHEDULE.ordinal()); - schdCol.setPreferredWidth(60); - TableColumn descCol = columnModel.getColumn(SnapshotTableColumns.DESCRIPTION.ordinal()); - descCol.setPreferredWidth(200); } protected void createActions() { @@ -271,51 +135,7 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { private void activatedHideScratch(ActionContext ctx) { hideScratch = !hideScratch; - if (hideScratch) { - deleteScratchSnapshots(); - } - else { - loadScratchSnapshots(); - } - } - - private void addNewListeners() { - if (currentTrace == null) { - return; - } - currentTrace.addListener(listener); - } - - private void removeOldListeners() { - if (currentTrace == null) { - return; - } - currentTrace.removeListener(listener); - } - - protected void doSetTrace(Trace trace) { - if (currentTrace == trace) { - return; - } - removeOldListeners(); - currentTrace = trace; - addNewListeners(); - loadSnapshots(); - } - - protected void doSetSnap(long snap) { - SnapshotRow sel = snapshotFilterPanel.getSelectedItem(); - Long curSnap = sel == null ? null : sel.getSnap(); - if (curSnap != null && curSnap.longValue() == snap) { - return; - } - SnapshotRow row = snapshotTableModel.findFirst(r -> r.getSnap() == snap); - if (row == null) { - snapshotTable.clearSelection(); - } - else { - snapshotFilterPanel.setSelectedItem(row); - } + mainPanel.setHideScratchSnapshots(hideScratch); } public void coordinatesActivated(DebuggerCoordinates coordinates) { @@ -325,37 +145,8 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { } current = coordinates; - doSetTrace(current.getTrace()); - doSetSnap(current.getSnap()); - } - - protected void loadSnapshots() { - snapshotTableModel.clear(); - Trace curTrace = current.getTrace(); - if (curTrace == null) { - return; - } - TraceTimeManager manager = curTrace.getTimeManager(); - Collection snapshots = hideScratch - ? manager.getSnapshots(0, true, Long.MAX_VALUE, true) - : manager.getAllSnapshots(); - snapshotTableModel.addAll(Collections2.transform(snapshots, - s -> new SnapshotRow(curTrace, s))); - } - - protected void deleteScratchSnapshots() { - snapshotTableModel.deleteWith(s -> s.getSnap() < 0); - } - - protected void loadScratchSnapshots() { - Trace curTrace = current.getTrace(); - if (curTrace == null) { - return; - } - TraceTimeManager manager = curTrace.getTimeManager(); - snapshotTableModel.addAll(Collections2.transform( - manager.getSnapshots(Long.MIN_VALUE, true, 0, false), - s -> new SnapshotRow(curTrace, s))); + mainPanel.setTrace(current.getTrace()); + mainPanel.setSelectedSnapshot(current.getSnap()); } public void writeConfigState(SaveState saveState) { @@ -366,5 +157,6 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter { CONFIG_STATE_HANDLER.readConfigState(this, saveState); actionHideScratch.setSelected(hideScratch); + mainPanel.setHideScratchSnapshots(hideScratch); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeSelectionDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeSelectionDialog.java new file mode 100644 index 0000000000..aa82540213 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeSelectionDialog.java @@ -0,0 +1,193 @@ +/* ### + * 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.time; + +import java.awt.BorderLayout; +import java.util.function.Function; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import docking.DialogComponentProvider; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.framework.plugintool.PluginTool; +import ghidra.trace.model.Trace; +import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.util.MessageType; +import ghidra.util.Msg; + +public class DebuggerTimeSelectionDialog extends DialogComponentProvider { + + private final PluginTool tool; + + DebuggerSnapshotTablePanel snapshotPanel; + JTextField scheduleText; + TraceSchedule schedule; + + JButton tickStep; + JButton tickBack; + JButton opStep; + JButton opBack; + + public DebuggerTimeSelectionDialog(PluginTool tool) { + super("Select Time", true, true, true, false); + this.tool = tool; + populateComponents(); + } + + protected void doStep(Function stepper) { + try { + TraceSchedule stepped = stepper.apply(schedule); + if (stepped == null) { + return; + } + setScheduleText(stepped.toString()); + } + catch (Throwable e) { + Msg.warn(this, e.getMessage()); + } + } + + protected void populateComponents() { + JPanel workPanel = new JPanel(new BorderLayout()); + + { + Box hbox = Box.createHorizontalBox(); + hbox.setBorder(BorderFactory.createTitledBorder("Schedule")); + hbox.add(new JLabel("Expression: ")); + scheduleText = new JTextField(); + hbox.add(scheduleText); + hbox.add(new JLabel("Ticks: ")); + hbox.add(tickBack = new JButton(DebuggerResources.ICON_STEP_BACK)); + hbox.add(tickStep = new JButton(DebuggerResources.ICON_STEP_INTO)); + hbox.add(new JLabel("Ops: ")); + hbox.add(opBack = new JButton(DebuggerResources.ICON_STEP_BACK)); + hbox.add(opStep = new JButton(DebuggerResources.ICON_STEP_INTO)); + workPanel.add(hbox, BorderLayout.NORTH); + } + + tickBack.addActionListener(evt -> doStep(s -> s.steppedBackward(getTrace(), 1))); + tickStep.addActionListener(evt -> doStep(s -> s.steppedForward(null, 1))); + opBack.addActionListener(evt -> doStep(s -> s.steppedPcodeBackward(1))); + opStep.addActionListener(evt -> doStep(s -> s.steppedPcodeForward(null, 1))); + + { + snapshotPanel = new DebuggerSnapshotTablePanel(); + workPanel.add(snapshotPanel, BorderLayout.CENTER); + } + + snapshotPanel.getSelectionModel().addListSelectionListener(evt -> { + Long snap = snapshotPanel.getSelectedSnapshot(); + if (snap == null) { + return; + } + if (schedule.getSnap() == snap.longValue()) { + return; + } + scheduleText.setText(snap.toString()); + }); + + scheduleText.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + scheduleTextChanged(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + scheduleTextChanged(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + scheduleTextChanged(); + } + }); + + addWorkPanel(workPanel); + addOKButton(); + addCancelButton(); + + setMinimumSize(600, 600); + } + + protected void scheduleTextChanged() { + schedule = null; + try { + schedule = TraceSchedule.parse(scheduleText.getText()); + snapshotPanel.setSelectedSnapshot(schedule.getSnap()); + schedule.validate(getTrace()); + setStatusText(""); + setOkEnabled(true); + } + catch (Exception e) { + setStatusText(e.getMessage(), MessageType.ERROR); + setOkEnabled(false); + } + enableStepButtons(schedule != null); + } + + protected void enableStepButtons(boolean enabled) { + tickBack.setEnabled(enabled); + tickStep.setEnabled(enabled); + opBack.setEnabled(enabled); + opStep.setEnabled(enabled); + } + + @Override // Public for test access + public void okCallback() { + assert schedule != null; + super.okCallback(); + close(); + } + + @Override // Public for test access + public void cancelCallback() { + this.schedule = null; + super.cancelCallback(); + } + + @Override + public void close() { + super.close(); + snapshotPanel.setTrace(null); + snapshotPanel.setSelectedSnapshot(null); + } + + /** + * Prompts the user to select a snapshot and optionally specify a full schedule + * + * @param trace the trace from whose snapshots to select + * @param defaultTime, optionally the time to select initially + * @return the schedule, likely specifying just the snapshot selection + */ + public TraceSchedule promptTime(Trace trace, TraceSchedule defaultTime) { + snapshotPanel.setTrace(trace); + schedule = defaultTime; + scheduleText.setText(defaultTime.toString()); + tool.showDialog(this); + return schedule; + } + + public Trace getTrace() { + return snapshotPanel.getTrace(); + } + + public void setScheduleText(String text) { + scheduleText.setText(text); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java index daa02b2ea5..498e9f7a78 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java @@ -661,44 +661,39 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return current.getFrame(); } + @Override + public CompletableFuture materialize(DebuggerCoordinates coordinates) { + if (coordinates.getTime().isSnapOnly()) { + return CompletableFuture.completedFuture(coordinates.getSnap()); + } + Collection suitable = coordinates.getTrace() + .getTimeManager() + .getSnapshotsWithSchedule(coordinates.getTime()); + if (!suitable.isEmpty()) { + TraceSnapshot found = suitable.iterator().next(); + return CompletableFuture.completedFuture(found.getKey()); + } + if (emulationService == null) { + throw new IllegalStateException( + "Cannot navigate to coordinates with execution schedules, " + + "because the emulation service is not available."); + } + return emulationService.backgroundEmulate(coordinates.getTrace(), coordinates.getTime()); + } + protected void prepareViewAndFireEvent(DebuggerCoordinates coordinates) { TraceVariableSnapProgramView varView = (TraceVariableSnapProgramView) coordinates.getView(); if (varView == null) { // Should only happen with NOWHERE fireLocationEvent(coordinates); + return; } - else if (coordinates.getTime().isSnapOnly()) { - varView.setSnap(coordinates.getSnap()); + materialize(coordinates).thenAcceptAsync(snap -> { + if (!coordinates.equals(current)) { + return; // We navigated elsewhere before emulation completed + } + varView.setSnap(snap); fireLocationEvent(coordinates); - } - else { - Collection suitable = coordinates.getTrace() - .getTimeManager() - .getSnapshotsWithSchedule(coordinates.getTime()); - if (!suitable.isEmpty()) { - TraceSnapshot found = suitable.iterator().next(); - varView.setSnap(found.getKey()); - fireLocationEvent(coordinates); - return; - } - if (emulationService == null) { - throw new IllegalStateException( - "Cannot navigate to coordinates with execution schedules, " + - "because the emulation service is not available."); - } - CompletableFuture bg = - emulationService.backgroundEmulate(coordinates.getTrace(), coordinates.getTime()); - bg.thenAccept(emuSnap -> Swing.runLater(() -> { - if (!coordinates.equals(current)) { - return; // We navigated elsewhere before emulation completed - } - varView.setSnap(emuSnap); - fireLocationEvent(coordinates); - })).exceptionally(ex -> { - Msg.showError(this, null, "Emulate", "Could not navigate to emulated coordinates", - ex); - return null; - }); - } + }, AsyncUtils.SWING_EXECUTOR); } protected void fireLocationEvent(DebuggerCoordinates coordinates) { @@ -892,6 +887,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin future.completeExceptionally(e); } } + }); } return future; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java index ce0d24eb1c..a35f337a0c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java @@ -15,6 +15,7 @@ */ package ghidra.app.plugin.core.debug.utils; +import java.util.List; import java.util.concurrent.*; import java.util.function.BiFunction; @@ -24,8 +25,8 @@ import ghidra.framework.cmd.BackgroundCommand; import ghidra.framework.model.DomainObject; import ghidra.framework.model.UndoableDomainObject; import ghidra.framework.plugintool.PluginTool; -import ghidra.util.task.CancelledListener; -import ghidra.util.task.TaskMonitor; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.*; public enum BackgroundUtils { ; @@ -82,4 +83,59 @@ public enum BackgroundUtils { tool.executeBackgroundCommand(cmd, obj); return cmd; } + + public static class PluginToolExecutorService extends AbstractExecutorService { + private final PluginTool tool; + private String name; + private boolean canCancel; + private boolean hasProgress; + private boolean isModal; + private final int delay; + + public PluginToolExecutorService(PluginTool tool, String name, boolean canCancel, + boolean hasProgress, boolean isModal, int delay) { + this.tool = tool; + this.name = name; + this.canCancel = canCancel; + this.hasProgress = hasProgress; + this.isModal = isModal; + this.delay = delay; + } + + @Override + public void shutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public List shutdownNow() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public void execute(Runnable command) { + Task task = new Task(name, canCancel, hasProgress, isModal) { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + command.run(); + } + }; + tool.execute(task, delay); + } + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerListingService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerListingService.java index 0dca9e813d..031c65922b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerListingService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerListingService.java @@ -17,19 +17,89 @@ package ghidra.app.services; import ghidra.app.plugin.core.debug.gui.action.LocationTrackingSpec; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; +import ghidra.app.plugin.core.debug.gui.listing.MultiBlendedListingBackgroundColorModel; +import ghidra.app.util.viewer.listingpanel.ListingPanel; import ghidra.framework.plugintool.ServiceInfo; import ghidra.program.model.address.Address; import ghidra.program.util.ProgramSelection; +/** + * A service providing access to the main listing panel + */ @ServiceInfo( // defaultProvider = DebuggerListingPlugin.class, // description = "Replacement CodeViewerService for Debugger" // ) public interface DebuggerListingService extends CodeViewerService { + /** + * A listener for changes in location tracking specification + */ + interface LocationTrackingSpecChangeListener { + /** + * The specification has changed + * + * @param spec the new specification + */ + void locationTrackingSpecChanged(LocationTrackingSpec spec); + } + + /** + * Set the tracking specification of the listing. Navigates immediately. + * + * @param spec the desired specification + */ void setTrackingSpec(LocationTrackingSpec spec); + /** + * Get the tracking specification of the listing. + * + * @return the current specification + */ + LocationTrackingSpec getTrackingSpec(); + + /** + * Add a listener for changes to the tracking specification. + * + * @param listener the listener to receive change notifications + */ + void addTrackingSpecChangeListener(LocationTrackingSpecChangeListener listener); + + /** + * Remove a listener for changes to the tracking specification. + * + * @param listener the listener receiving change notifications + */ + void removeTrackingSpecChangeListener(LocationTrackingSpecChangeListener listener); + + /** + * Set the selection of addresses in this listing. + * + * @param selection the desired selection + */ void setCurrentSelection(ProgramSelection selection); + /** + * Navigate to the given address + * + * @param address the desired address + * @param centerOnScreen true to center the cursor in the listing + * @return true if the request was effective + */ boolean goTo(Address address, boolean centerOnScreen); + + /** + * Obtain a coloring background model suitable for the given listing + * + *

+ * This may be used, e.g., to style an alternative view in the same manner as listings managed + * by this service. Namely, this provides coloring for memory state and the user's cursor. + * Coloring for tracked locations and the marker service in general must still be added + * separately, since they incorporate additional dependencies. + * + * @param listingPanel the panel to be colored + * @return a blended background color model implementing the common debugger listing style + */ + MultiBlendedListingBackgroundColorModel createListingBackgroundColorModel( + ListingPanel listingPanel); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java index 4f60525cd3..6f7b3f73f4 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java @@ -20,39 +20,117 @@ import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin; +import ghidra.async.AsyncReference; import ghidra.framework.model.DomainFile; import ghidra.framework.plugintool.ServiceInfo; +import ghidra.program.model.listing.Program; import ghidra.trace.model.Trace; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.util.TriConsumer; +/** + * The interface for managing open traces and navigating among them and their contents + */ @ServiceInfo(defaultProvider = DebuggerTraceManagerServicePlugin.class) public interface DebuggerTraceManagerService { + + /** + * An adapter that works nicely with an {@link AsyncReference} + * + *

+ * TODO: Seems this is still leaking an implementation detail + */ public interface BooleanChangeAdapter extends TriConsumer { @Override default void accept(Boolean oldVal, Boolean newVal, Void cause) { changed(newVal); } + /** + * The value has changed + * + * @param value the new value + */ void changed(Boolean value); } + /** + * Get all the open traces + * + * @return all open traces + */ Collection getOpenTraces(); + /** + * Get the current coordinates + * + *

+ * This entails everything except the current address + * + * @return the current coordinates + */ DebuggerCoordinates getCurrent(); + /** + * Get the active trace + * + * @return the active trace, or null + */ Trace getCurrentTrace(); + /** + * Get the active view + * + *

+ * Every trace has an associated variable-snap view. When the manager navigates to a new point + * in time, it is accomplished by changing the snap of this view. This view is suitable for use + * in most places where a {@link Program} is ordinarily required. + * + * @return the active view, or null + */ TraceProgramView getCurrentView(); + /** + * Get the active thread + * + *

+ * It is possible to have an active trace, but no active thread. + * + * @return the active thread, or null + */ TraceThread getCurrentThread(); + /** + * Get the active thread for a given trace + * + *

+ * The manager remembers the last active thread for every open trace. If the trace has never + * been active, then the last active thread is null. If trace is the active trace, then this + * will return the currently active thread. + * + * @param trace the trace + * @return the thread, or null + */ TraceThread getCurrentThreadFor(Trace trace); + /** + * Get the active snap + * + *

+ * Note that if emulation was used to materialize the current coordinates, then the current snap + * will differ from the view's snap. + * + * @return the active snap, or 0 + */ long getCurrentSnap(); + /** + * Get the active frame + * + * @return the active frame, or 0 + */ int getCurrentFrame(); /** @@ -105,10 +183,23 @@ public interface DebuggerTraceManagerService { */ CompletableFuture saveTrace(Trace trace); + /** + * Close the given trace + * + * @param trace the trace to close + */ void closeTrace(Trace trace); + /** + * Close all traces + */ void closeAllTraces(); + /** + * Close all traces except the given one + * + * @param keep the trace to keep open + */ void closeOtherTraces(Trace keep); /** @@ -120,24 +211,84 @@ public interface DebuggerTraceManagerService { */ void closeDeadTraces(); + /** + * Activate the given coordinates + * + *

+ * This operation may be completed asynchronously, esp., if emulation is required to materialize + * the coordinates. The coordinates are "resolved" as a means of filling in missing parts. For + * example, if the thread is not specified, the manager may activate the last-active thread for + * the desired trace. + * + * @param coordinates the desired coordinates + */ void activate(DebuggerCoordinates coordinates); + /** + * Activate the given trace + * + * @param trace the desired trace + */ void activateTrace(Trace trace); + /** + * Activate the given thread + * + * @param thread the desired thread + */ void activateThread(TraceThread thread); + /** + * Activate the given snapshot key + * + * @param snap the desired snapshot key + */ void activateSnap(long snap); + /** + * Activate the given point in time, possibly invoking emulation + * + * @param time the desired schedule + */ void activateTime(TraceSchedule time); + /** + * Activate the given stack frame + * + * @param frameLevel the level of the desired frame, 0 being innermost + */ void activateFrame(int frameLevel); + /** + * Control whether the trace manager automatically activates the "present snapshot" + * + *

+ * Auto activation only applies when the current trace advances. It never changes to another + * trace. + * + * @param enabled true to enable auto activation + */ void setAutoActivatePresent(boolean enabled); + /** + * Check if the trace manager automatically activate the "present snapshot" + * + * @return true if auto activation is enabled + */ boolean isAutoActivatePresent(); + /** + * Add a listener for changes to auto activation enablement + * + * @param listener the listener to receive change notifications + */ void addAutoActivatePresentChangeListener(BooleanChangeAdapter listener); + /** + * Remove a listener for changes to auto activation enablement + * + * @param listener the listener receiving change notifications + */ void removeAutoActivatePresentChangeListener(BooleanChangeAdapter listener); /** @@ -154,8 +305,18 @@ public interface DebuggerTraceManagerService { */ boolean isSynchronizeFocus(); + /** + * Add a listener for changes to focus synchronization enablement + * + * @param listener the listener to receive change notifications + */ void addSynchronizeFocusChangeListener(BooleanChangeAdapter listener); + /** + * Remove a listener for changes to focus synchronization enablement + * + * @param listener the listener receiving change notifications + */ void removeSynchronizeFocusChangeListener(BooleanChangeAdapter listener); /** @@ -172,8 +333,18 @@ public interface DebuggerTraceManagerService { */ boolean isSaveTracesByDefault(); + /** + * Add a listener for changes to save-by-default enablement + * + * @param listener the listener to receive change notifications + */ void addSaveTracesByDefaultChangeListener(BooleanChangeAdapter listener); + /** + * Remove a listener for changes to save-by-default enablement + * + * @param listener the listener receiving change notifications + */ void removeSaveTracesByDefaultChangeListener(BooleanChangeAdapter listener); /** @@ -190,15 +361,40 @@ public interface DebuggerTraceManagerService { */ boolean isAutoCloseOnTerminate(); + /** + * Add a listener for changes to close-on-terminate enablement + * + * @param listener the listener to receive change notifications + */ void addAutoCloseOnTerminateChangeListener(BooleanChangeAdapter listener); + /** + * Remove a listener for changes to close-on-terminate enablement + * + * @param listener the listener receiving change notifications + */ void removeAutoCloseOnTerminateChangeListener(BooleanChangeAdapter listener); /** - * Fill in an incomplete coordinate specification, using the manager's "best judgement" + * Fill in an incomplete coordinate specification, using the manager's "best judgment" * * @param coords the possibly-incomplete coordinates * @return the complete resolved coordinates */ - DebuggerCoordinates resolveCoordinates(DebuggerCoordinates coords); + DebuggerCoordinates resolveCoordinates(DebuggerCoordinates coordinates); + + /** + * Materialize the given coordinates to a snapshot in the same trace + * + *

+ * If the given coordinates do not require emulation, then this must complete immediately with + * the snapshot key given by the coordinates. If the given schedule is already materialized in + * the trace, then this may complete immediately with the previously-materialized snapshot key. + * Otherwise, this must invoke emulation, store the result into a chosen snapshot, and complete + * with its key. + * + * @param coordinates the coordinates to materialize + * @return a future that completes with the snapshot key of the materialized coordinates + */ + CompletableFuture materialize(DebuggerCoordinates coordinates); } diff --git a/Ghidra/Debug/Debugger/src/main/resources/images/table_relationship.png b/Ghidra/Debug/Debugger/src/main/resources/images/table_relationship.png new file mode 100644 index 0000000000..28b8505c0e Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/resources/images/table_relationship.png differ diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPluginScreenShots.java new file mode 100644 index 0000000000..0ccc5377a1 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPluginScreenShots.java @@ -0,0 +1,128 @@ +/* ### + * 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.diff; + +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.junit.*; + +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingProvider; +import ghidra.app.plugin.core.debug.gui.time.DebuggerTimeSelectionDialog; +import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.async.AsyncTestUtils; +import ghidra.test.ToyProgramBuilder; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.database.memory.DBTraceMemoryManager; +import ghidra.trace.database.thread.DBTraceThread; +import ghidra.trace.database.time.DBTraceTimeManager; +import ghidra.trace.model.memory.TraceMemoryFlag; +import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.util.Swing; +import ghidra.util.database.UndoableTransaction; +import help.screenshot.GhidraScreenShotGenerator; + +public class DebuggerTraceViewDiffPluginScreenShots extends GhidraScreenShotGenerator + implements AsyncTestUtils { + + DebuggerTraceManagerService traceManager; + DebuggerTraceViewDiffPlugin diffPlugin; + DebuggerListingPlugin listingPlugin; + DebuggerListingProvider listingProvider; + ToyDBTraceBuilder tb; + + @Before + public void setUpMine() throws Throwable { + traceManager = addPlugin(tool, DebuggerTraceManagerServicePlugin.class); + diffPlugin = addPlugin(tool, DebuggerTraceViewDiffPlugin.class); + listingPlugin = addPlugin(tool, DebuggerListingPlugin.class); + listingProvider = waitForComponentProvider(DebuggerListingProvider.class); + + tb = new ToyDBTraceBuilder("tictactoe", ToyProgramBuilder._X64); + } + + @After + public void tearDownMine() { + tb.close(); + } + + @Test + public void testCaptureDebuggerTraceViewDiffPlugin() throws Throwable { + long snap1, snap2; + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceTimeManager tm = tb.trace.getTimeManager(); + snap1 = tm.createSnapshot("Baseline").getKey(); + snap2 = tm.createSnapshot("X's first move").getKey(); + DBTraceMemoryManager mm = tb.trace.getMemoryManager(); + mm.createRegion(".data", snap1, tb.range(0x00600000, 0x0060ffff), + TraceMemoryFlag.READ, TraceMemoryFlag.WRITE); + + ByteBuffer buf = ByteBuffer.allocate(0x1000).order(ByteOrder.LITTLE_ENDIAN); + buf.put((byte) 'X'); + buf.putInt(3); + buf.putInt(3); + for (int i = 0; i < 9; i++) { + buf.put((byte) ' '); + } + buf.flip(); + buf.limit(0x1000); + mm.putBytes(snap1, tb.addr(0x00600000), buf); + + buf.put(0, (byte) 'O'); + buf.put(13, (byte) 'X'); + buf.position(0); + buf.limit(0x1000); + mm.putBytes(snap2, tb.addr(0x00600000), buf); + } + + traceManager.openTrace(tb.trace); + traceManager.activateTrace(tb.trace); + traceManager.activateSnap(snap1); + waitForSwing(); + + waitOn(diffPlugin.startComparison(TraceSchedule.snap(snap2))); + assertTrue(diffPlugin.gotoNextDiff()); + + captureIsolatedProvider(DebuggerListingProvider.class, 900, 600); + } + + @Test + public void testCaptureDebuggerTimeSelectionDialog() throws Throwable { + DBTraceThread thread; + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceTimeManager tm = tb.trace.getTimeManager(); + thread = tb.getOrAddThread("main", 0); + tm.createSnapshot("Break on main").setEventThread(thread); + tm.createSnapshot("Game started").setEventThread(thread); + tm.createSnapshot("X's moved").setEventThread(thread); + tm.createSnapshot("O's moved").setEventThread(thread); + } + traceManager.openTrace(tb.trace); + traceManager.activateThread(thread); + waitForSwing(); + + performAction(diffPlugin.actionCompare, false); + DebuggerTimeSelectionDialog dialog = + waitForDialogComponent(DebuggerTimeSelectionDialog.class); + Swing.runNow(() -> dialog.setScheduleText("2")); + + captureDialog(dialog); + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java index a8906486d3..96020bad89 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.*; import java.awt.*; import java.awt.event.*; +import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.math.BigInteger; @@ -40,11 +41,13 @@ import org.junit.runner.Description; import docking.widgets.tree.GTree; import docking.widgets.tree.GTreeNode; import generic.Unique; +import ghidra.app.plugin.core.debug.gui.action.*; import ghidra.app.plugin.core.debug.mapping.*; import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceInternal; import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceProxyPlugin; import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin; import ghidra.app.services.*; +import ghidra.app.util.viewer.listingpanel.ListingPanel; import ghidra.dbg.model.AbstractTestTargetRegisterBank; import ghidra.dbg.model.TestDebuggerModelBuilder; import ghidra.dbg.target.*; @@ -57,6 +60,7 @@ import ghidra.program.model.data.DataType; import ghidra.program.model.lang.*; import ghidra.program.model.listing.Program; import ghidra.program.util.DefaultLanguageService; +import ghidra.program.util.ProgramLocation; import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.TestEnv; import ghidra.trace.database.ToyDBTraceBuilder; @@ -409,6 +413,64 @@ public abstract class AbstractGhidraHeadedDebuggerGUITest clickMouse(button, m); } + protected static void assertListingBackgroundAt(Color expected, ListingPanel panel, + Address addr, int yAdjust) throws AWTException, InterruptedException { + ProgramLocation oneBack = new ProgramLocation(panel.getProgram(), addr.previous()); + runSwing(() -> panel.goTo(addr)); + runSwing(() -> panel.goTo(oneBack, false)); + waitForPass(() -> { + Rectangle r = panel.getBounds(); + // Capture off screen, so that focus/stacking doesn't matter + BufferedImage image = new BufferedImage(r.width, r.height, BufferedImage.TYPE_INT_ARGB); + Graphics g = image.getGraphics(); + try { + runSwing(() -> panel.paint(g)); + } + finally { + g.dispose(); + } + Point locP = panel.getLocationOnScreen(); + Point locFP = panel.getLocationOnScreen(); + locFP.translate(-locP.x, -locP.y); + Rectangle cursor = panel.getCursorBounds(); + assertNotNull("Cannot get cursor bounds", cursor); + Color actual = new Color(image.getRGB(locFP.x + cursor.x - 1, + locFP.y + cursor.y + cursor.height * 3 / 2 + yAdjust)); + assertEquals(expected, actual); + }); + } + + protected static void goTo(ListingPanel listingPanel, ProgramLocation location) { + waitForPass(() -> { + runSwing(() -> listingPanel.goTo(location)); + ProgramLocation confirm = listingPanel.getCursorLocation(); + assertNotNull(confirm); + assertEquals(location.getAddress(), confirm.getAddress()); + }); + } + + protected static LocationTrackingSpec getLocationTrackingSpec(String name) { + return LocationTrackingSpec.fromConfigName(name); + } + + protected static AutoReadMemorySpec getAutoReadMemorySpec(String name) { + return AutoReadMemorySpec.fromConfigName(name); + } + + protected final LocationTrackingSpec trackNone = + getLocationTrackingSpec(NoneLocationTrackingSpec.CONFIG_NAME); + protected final LocationTrackingSpec trackPc = + getLocationTrackingSpec(PCLocationTrackingSpec.CONFIG_NAME); + protected final LocationTrackingSpec trackSp = + getLocationTrackingSpec(SPLocationTrackingSpec.CONFIG_NAME); + + protected final AutoReadMemorySpec readNone = + getAutoReadMemorySpec(NoneAutoReadMemorySpec.CONFIG_NAME); + protected final AutoReadMemorySpec readVisible = + getAutoReadMemorySpec(VisibleAutoReadMemorySpec.CONFIG_NAME); + protected final AutoReadMemorySpec readVisROOnce = + getAutoReadMemorySpec(VisibleROOnceAutoReadMemorySpec.CONFIG_NAME); + protected TestEnv env; protected PluginTool tool; diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPluginTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPluginTest.java new file mode 100644 index 0000000000..c7c9445079 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPluginTest.java @@ -0,0 +1,233 @@ +/* ### + * 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.diff; + +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingProvider; +import ghidra.app.plugin.core.debug.gui.time.DebuggerTimeSelectionDialog; +import ghidra.async.AsyncTestUtils; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.util.ProgramLocation; +import ghidra.trace.database.memory.DBTraceMemoryManager; +import ghidra.trace.model.memory.TraceMemoryFlag; +import ghidra.trace.model.time.schedule.TraceSchedule; +import ghidra.util.Swing; +import ghidra.util.database.UndoableTransaction; + +public class DebuggerTraceViewDiffPluginTest extends AbstractGhidraHeadedDebuggerGUITest + implements AsyncTestUtils { + + protected DebuggerTraceViewDiffPlugin traceDiffPlugin; + protected DebuggerListingPlugin listingPlugin; + + protected DebuggerListingProvider listingProvider; + + @Before + public void setUpTraceViewDiffPluginTest() throws Exception { + traceDiffPlugin = addPlugin(tool, DebuggerTraceViewDiffPlugin.class); + listingPlugin = addPlugin(tool, DebuggerListingPlugin.class); + + listingProvider = waitForComponentProvider(DebuggerListingProvider.class); + } + + @Test + public void testActionCompareConfirm() throws Exception { + assertFalse(traceDiffPlugin.actionCompare.isEnabled()); + assertNull(listingPlugin.getProvider().getOtherPanel()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertTrue(traceDiffPlugin.actionCompare.isEnabled()); + performAction(traceDiffPlugin.actionCompare, false); + + DebuggerTimeSelectionDialog dialog = + waitForDialogComponent(DebuggerTimeSelectionDialog.class); + Swing.runNow(() -> { + dialog.setScheduleText("0"); + dialog.okCallback(); + }); + waitForSwing(); + + assertNotNull(listingPlugin.getProvider().getOtherPanel()); + } + + @Test + public void testActionCompareCancel() throws Exception { + assertFalse(traceDiffPlugin.actionCompare.isEnabled()); + assertNull(listingPlugin.getProvider().getOtherPanel()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertTrue(traceDiffPlugin.actionCompare.isEnabled()); + performAction(traceDiffPlugin.actionCompare, false); + + DebuggerTimeSelectionDialog dialog = + waitForDialogComponent(DebuggerTimeSelectionDialog.class); + Swing.runNow(() -> { + dialog.setScheduleText("0"); + dialog.cancelCallback(); + }); + waitForSwing(); + + assertNull(listingPlugin.getProvider().getOtherPanel()); + } + + // TODO: Test schedule input validation? + // TODO: Test stepping buttons? + + @Test + public void testActionCompareClosesWhenAlreadyActive() throws Exception { + assertFalse(traceDiffPlugin.actionCompare.isEnabled()); + assertNull(listingPlugin.getProvider().getOtherPanel()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertTrue(traceDiffPlugin.actionCompare.isEnabled()); + performAction(traceDiffPlugin.actionCompare, false); + + DebuggerTimeSelectionDialog dialog = + waitForDialogComponent(DebuggerTimeSelectionDialog.class); + Swing.runNow(() -> { + dialog.setScheduleText("0"); + dialog.okCallback(); + }); + waitForSwing(); + + assertNotNull(listingPlugin.getProvider().getOtherPanel()); + + assertTrue(traceDiffPlugin.actionCompare.isEnabled()); + performAction(traceDiffPlugin.actionCompare, false); + assertNull(listingPlugin.getProvider().getOtherPanel()); + } + + @Test + public void testColorsDiffBytes() throws Throwable { + createAndOpenTrace(); + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceMemoryManager mm = tb.trace.getMemoryManager(); + mm.createRegion(".text", 0, tb.range(0x00400000, 0x0040ffff), + TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); + + ByteBuffer buf = ByteBuffer.allocate(0x1000); // Yes, smaller than .text + buf.limit(0x1000); + mm.putBytes(0, tb.addr(0x00400000), buf); + buf.position(0); + buf.putLong(0x0123, 0x1122334455667788L); + mm.putBytes(1, tb.addr(0x00400000), buf); + } + traceManager.activateTrace(tb.trace); + waitForSwing(); + + waitOn(traceDiffPlugin.startComparison(TraceSchedule.snap(1))); + + assertListingBackgroundAt(DebuggerTraceViewDiffPlugin.DEFAULT_DIFF_COLOR, + traceDiffPlugin.altListingPanel, tb.addr(0x00400123), 0); + assertListingBackgroundAt(DebuggerTraceViewDiffPlugin.DEFAULT_DIFF_COLOR, + listingProvider.getListingPanel(), tb.addr(0x00400123), 0); + + AddressSetView expected = tb.set(tb.range(0x00400123, 0x0040012a)); + assertEquals(expected, Swing.runNow(() -> traceDiffPlugin.diffMarkersL.getAddressSet())); + assertEquals(expected, Swing.runNow(() -> traceDiffPlugin.diffMarkersR.getAddressSet())); + + Swing.runNow(() -> traceDiffPlugin.endComparison()); + + assertTrue(Swing.runNow(() -> traceDiffPlugin.diffMarkersL.getAddressSet()).isEmpty()); + assertTrue(Swing.runNow(() -> traceDiffPlugin.diffMarkersR.getAddressSet()).isEmpty()); + } + + @Test + public void testActionPrevDiff() throws Throwable { + createAndOpenTrace(); + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceMemoryManager mm = tb.trace.getMemoryManager(); + mm.createRegion(".text", 0, tb.range(0x00400000, 0x0040ffff), + TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); + + ByteBuffer buf = ByteBuffer.allocate(0x1000); // Yes, smaller than .text + buf.limit(0x1000); + mm.putBytes(0, tb.addr(0x00400000), buf); + buf.position(0); + buf.putLong(0x0123, 0x1122334455667788L); + buf.putLong(0x0321, 0x1122334455667788L); + mm.putBytes(1, tb.addr(0x00400000), buf); + } + traceManager.activateTrace(tb.trace); + waitForSwing(); + + waitOn(traceDiffPlugin.startComparison(TraceSchedule.snap(1))); + + assertFalse(traceDiffPlugin.actionPrevDiff.isEnabled()); + goTo(listingProvider.getListingPanel(), + new ProgramLocation(tb.trace.getProgramView(), tb.addr(0x00401000))); + waitForSwing(); + + assertTrue(traceDiffPlugin.actionPrevDiff.isEnabled()); + performAction(traceDiffPlugin.actionPrevDiff); + assertEquals(tb.addr(0x00400328), traceDiffPlugin.getCurrentAddress()); + + assertTrue(traceDiffPlugin.actionPrevDiff.isEnabled()); + performAction(traceDiffPlugin.actionPrevDiff); + assertEquals(tb.addr(0x0040012a), traceDiffPlugin.getCurrentAddress()); + + assertFalse(traceDiffPlugin.actionPrevDiff.isEnabled()); + } + + @Test + public void testActionNextDiff() throws Throwable { + createAndOpenTrace(); + try (UndoableTransaction tid = tb.startTransaction()) { + DBTraceMemoryManager mm = tb.trace.getMemoryManager(); + mm.createRegion(".text", 0, tb.range(0x00400000, 0x0040ffff), + TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE); + + ByteBuffer buf = ByteBuffer.allocate(0x1000); // Yes, smaller than .text + buf.limit(0x1000); + mm.putBytes(0, tb.addr(0x00400000), buf); + buf.position(0); + buf.putLong(0x0123, 0x1122334455667788L); + buf.putLong(0x0321, 0x1122334455667788L); + mm.putBytes(1, tb.addr(0x00400000), buf); + } + traceManager.activateTrace(tb.trace); + waitForSwing(); + + waitOn(traceDiffPlugin.startComparison(TraceSchedule.snap(1))); + + assertTrue(traceDiffPlugin.actionNextDiff.isEnabled()); + performAction(traceDiffPlugin.actionNextDiff); + waitForPass(() -> assertEquals(tb.addr(0x00400123), traceDiffPlugin.getCurrentAddress())); + + assertTrue(traceDiffPlugin.actionNextDiff.isEnabled()); + performAction(traceDiffPlugin.actionNextDiff); + assertEquals(tb.addr(0x00400321), traceDiffPlugin.getCurrentAddress()); + + assertFalse(traceDiffPlugin.actionNextDiff.isEnabled()); + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java index 42bfe2da00..ed9bc6f32f 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java @@ -18,8 +18,7 @@ package ghidra.app.plugin.core.debug.gui.listing; import static ghidra.lifecycle.Unfinished.TODO; import static org.junit.Assert.*; -import java.awt.*; -import java.awt.image.BufferedImage; +import java.awt.Color; import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; @@ -37,14 +36,13 @@ import ghidra.app.plugin.core.debug.DebuggerCoordinates; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.AbstractFollowsCurrentThreadAction; -import ghidra.app.plugin.core.debug.gui.action.*; +import ghidra.app.plugin.core.debug.gui.action.DebuggerGoToDialog; import ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin; import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.BoundAction; import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.LogRow; import ghidra.app.plugin.core.debug.gui.modules.DebuggerMissingModuleActionContext; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingUtils; import ghidra.app.services.*; -import ghidra.app.util.viewer.listingpanel.ListingPanel; import ghidra.async.SwingExecutorService; import ghidra.framework.model.*; import ghidra.plugin.importer.ImporterPlugin; @@ -68,27 +66,6 @@ import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUITest { - static LocationTrackingSpec getLocationTrackingSpec(String name) { - return LocationTrackingSpec.fromConfigName(name); - } - - static AutoReadMemorySpec getAutoReadMemorySpec(String name) { - return AutoReadMemorySpec.fromConfigName(name); - } - - final LocationTrackingSpec trackNone = - getLocationTrackingSpec(NoneLocationTrackingSpec.CONFIG_NAME); - final LocationTrackingSpec trackPc = - getLocationTrackingSpec(PCLocationTrackingSpec.CONFIG_NAME); - final LocationTrackingSpec trackSp = - getLocationTrackingSpec(SPLocationTrackingSpec.CONFIG_NAME); - - final AutoReadMemorySpec readNone = - getAutoReadMemorySpec(NoneAutoReadMemorySpec.CONFIG_NAME); - final AutoReadMemorySpec readVisible = - getAutoReadMemorySpec(VisibleAutoReadMemorySpec.CONFIG_NAME); - final AutoReadMemorySpec readVisROOnce = - getAutoReadMemorySpec(VisibleROOnceAutoReadMemorySpec.CONFIG_NAME); protected DebuggerListingPlugin listingPlugin; protected DebuggerListingProvider listingProvider; @@ -110,12 +87,7 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI } protected void goToDyn(ProgramLocation location) { - waitForPass(() -> { - runSwing(() -> listingProvider.goTo(location.getProgram(), location)); - ProgramLocation confirm = listingProvider.getLocation(); - assertNotNull(confirm); - assertEquals(location.getAddress(), confirm.getAddress()); - }); + goTo(listingProvider.getListingPanel(), location); } protected static byte[] incBlock() { @@ -572,32 +544,6 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI assertEquals(ss.getAddress(0x00601234), loc.getAddress()); } - protected void assertListingBackgroundAt(Color expected, ListingPanel panel, - Address addr, int yAdjust) throws AWTException, InterruptedException { - ProgramLocation oneBack = new ProgramLocation(panel.getProgram(), addr.previous()); - runSwing(() -> panel.goTo(addr)); - runSwing(() -> panel.goTo(oneBack, false)); - waitForPass(() -> { - Rectangle r = panel.getBounds(); - // Capture off screen, so that focus/stacking doesn't matter - BufferedImage image = new BufferedImage(r.width, r.height, BufferedImage.TYPE_INT_ARGB); - Graphics g = image.getGraphics(); - try { - runSwing(() -> panel.paint(g)); - } - finally { - g.dispose(); - } - Point locP = panel.getLocationOnScreen(); - Point locFP = panel.getLocationOnScreen(); - locFP.translate(-locP.x, -locP.y); - Rectangle cursor = panel.getCursorBounds(); - Color actual = new Color(image.getRGB(locFP.x + cursor.x - 1, - locFP.y + cursor.y + cursor.height * 3 / 2 + yAdjust)); - assertEquals(expected, actual); - }); - } - @Test public void testDynamicListingMarksTrackedRegister() throws Exception { createAndOpenTrace(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java index 011d92f4c7..c0160ed447 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProviderTest.java @@ -66,26 +66,6 @@ import ghidra.util.database.UndoableTransaction; @Category(NightlyCategory.class) public class DebuggerMemoryBytesProviderTest extends AbstractGhidraHeadedDebuggerGUITest { - static LocationTrackingSpec getLocationTrackingSpec(String name) { - return LocationTrackingSpec.fromConfigName(name); - } - - static AutoReadMemorySpec getAutoReadMemorySpec(String name) { - return AutoReadMemorySpec.fromConfigName(name); - } - - final LocationTrackingSpec trackNone = - getLocationTrackingSpec(NoneLocationTrackingSpec.CONFIG_NAME); - final LocationTrackingSpec trackPc = - getLocationTrackingSpec(PCLocationTrackingSpec.CONFIG_NAME); - final LocationTrackingSpec trackSp = - getLocationTrackingSpec(SPLocationTrackingSpec.CONFIG_NAME); - - final AutoReadMemorySpec readNone = getAutoReadMemorySpec(NoneAutoReadMemorySpec.CONFIG_NAME); - final AutoReadMemorySpec readVisible = - getAutoReadMemorySpec(VisibleAutoReadMemorySpec.CONFIG_NAME); - final AutoReadMemorySpec readVisROOnce = - getAutoReadMemorySpec(VisibleROOnceAutoReadMemorySpec.CONFIG_NAME); protected DebuggerMemoryBytesPlugin memBytesPlugin; protected DebuggerMemoryBytesProvider memBytesProvider; diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java index c09ec92354..99105c3433 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeProviderTest.java @@ -23,7 +23,10 @@ import java.util.List; import org.junit.Before; import org.junit.Test; +import docking.widgets.dialogs.InputDialog; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; +import ghidra.trace.database.time.DBTraceSnapshot; import ghidra.trace.database.time.DBTraceTimeManager; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.time.TraceSnapshot; @@ -64,11 +67,11 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes } protected void assertProviderEmpty() { - assertTrue(timeProvider.snapshotTableModel.getModelData().isEmpty()); + assertTrue(timeProvider.mainPanel.snapshotTableModel.getModelData().isEmpty()); } protected void assertProviderPopulated() { - List snapsDisplayed = timeProvider.snapshotTableModel.getModelData(); + List snapsDisplayed = timeProvider.mainPanel.snapshotTableModel.getModelData(); // I should be able to assume this is sorted by key assertEquals(2, snapsDisplayed.size()); @@ -85,6 +88,42 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes // Timestamp is left unchecked, since default is current time } + @Test // TODO: Technically, this is a plugin action.... Different test case? + public void testActionRenameSnapshot() throws Exception { + // Need some docked provider to provide action context + + addPlugin(tool, DebuggerListingPlugin.class); + assertFalse(timePlugin.actionRenameSnapshot.isEnabled()); + + createSnaplessTrace(); + addSnapshots(); + assertFalse(timePlugin.actionRenameSnapshot.isEnabled()); + + traceManager.openTrace(tb.trace); + waitForSwing(); + assertFalse(timePlugin.actionRenameSnapshot.isEnabled()); + + traceManager.activateTrace(tb.trace); + waitForSwing(); + assertTrue(timePlugin.actionRenameSnapshot.isEnabled()); + traceManager.activateSnap(10); + waitForSwing(); + assertTrue(timePlugin.actionRenameSnapshot.isEnabled()); + + performAction(timePlugin.actionRenameSnapshot, false); + InputDialog dialog = waitForDialogComponent(InputDialog.class); + assertEquals("Snap 10", dialog.getValue()); + + dialog.setValue("My Snapshot"); + dialog.close(); // isCancelled (private) defaults to false + waitForSwing(); + + DBTraceSnapshot snapshot = tb.trace.getTimeManager().getSnapshot(10, false); + assertEquals("My Snapshot", snapshot.getDescription()); + + // TODO: Test cancelled has no effect + } + @Test public void testEmpty() { assertProviderEmpty(); @@ -158,7 +197,7 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes } waitForDomainObject(tb.trace); - assertEquals(1, timeProvider.snapshotTableModel.getModelData().size()); + assertEquals(1, timeProvider.mainPanel.snapshotTableModel.getModelData().size()); } @Test @@ -238,7 +277,7 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes traceManager.activateTrace(tb.trace); waitForSwing(); - SnapshotRow row = timeProvider.snapshotTableModel.getModelData().get(0); + SnapshotRow row = timeProvider.mainPanel.snapshotTableModel.getModelData().get(0); runSwing(() -> row.setDescription("Custom Description")); waitForDomainObject(tb.trace); @@ -258,14 +297,14 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes traceManager.activateTrace(tb.trace); waitForSwing(); - List data = timeProvider.snapshotTableModel.getModelData(); + List data = timeProvider.mainPanel.snapshotTableModel.getModelData(); - timeProvider.snapshotFilterPanel.setSelectedItem(data.get(0)); + timeProvider.mainPanel.snapshotFilterPanel.setSelectedItem(data.get(0)); waitForSwing(); assertEquals(0, traceManager.getCurrentSnap()); - timeProvider.snapshotFilterPanel.setSelectedItem(data.get(1)); + timeProvider.mainPanel.snapshotFilterPanel.setSelectedItem(data.get(1)); waitForSwing(); assertEquals(10, traceManager.getCurrentSnap()); @@ -283,22 +322,22 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes traceManager.activateTrace(tb.trace); waitForSwing(); - List data = timeProvider.snapshotTableModel.getModelData(); + List data = timeProvider.mainPanel.snapshotTableModel.getModelData(); traceManager.activateSnap(0); waitForSwing(); - assertEquals(data.get(0), timeProvider.snapshotFilterPanel.getSelectedItem()); + assertEquals(data.get(0), timeProvider.mainPanel.snapshotFilterPanel.getSelectedItem()); traceManager.activateSnap(10); waitForSwing(); - assertEquals(data.get(1), timeProvider.snapshotFilterPanel.getSelectedItem()); + assertEquals(data.get(1), timeProvider.mainPanel.snapshotFilterPanel.getSelectedItem()); traceManager.activateSnap(5); waitForSwing(); - assertNull(timeProvider.snapshotFilterPanel.getSelectedItem()); + assertNull(timeProvider.mainPanel.snapshotFilterPanel.getSelectedItem()); } @Test @@ -312,7 +351,7 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes traceManager.activateTrace(tb.trace); waitForSwing(); - List data = timeProvider.snapshotTableModel.getModelData(); + List data = timeProvider.mainPanel.snapshotTableModel.getModelData(); assertEquals(2, data.size()); for (SnapshotRow row : data) { assertTrue(row.getSnap() >= 0); @@ -329,12 +368,12 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes traceManager.activateTrace(tb.trace); waitForSwing(); - assertEquals(2, timeProvider.snapshotTableModel.getModelData().size()); + assertEquals(2, timeProvider.mainPanel.snapshotTableModel.getModelData().size()); addScratchSnapshot(); waitForDomainObject(tb.trace); - List data = timeProvider.snapshotTableModel.getModelData(); + List data = timeProvider.mainPanel.snapshotTableModel.getModelData(); assertEquals(2, data.size()); for (SnapshotRow row : data) { assertTrue(row.getSnap() >= 0); @@ -353,17 +392,17 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes waitForSwing(); assertEquals(true, timeProvider.hideScratch); - assertEquals(2, timeProvider.snapshotTableModel.getModelData().size()); + assertEquals(2, timeProvider.mainPanel.snapshotTableModel.getModelData().size()); performAction(timeProvider.actionHideScratch); assertEquals(false, timeProvider.hideScratch); - assertEquals(3, timeProvider.snapshotTableModel.getModelData().size()); + assertEquals(3, timeProvider.mainPanel.snapshotTableModel.getModelData().size()); performAction(timeProvider.actionHideScratch); assertEquals(true, timeProvider.hideScratch); - assertEquals(2, timeProvider.snapshotTableModel.getModelData().size()); + assertEquals(2, timeProvider.mainPanel.snapshotTableModel.getModelData().size()); } @Test @@ -380,6 +419,6 @@ public class DebuggerTimeProviderTest extends AbstractGhidraHeadedDebuggerGUITes waitForSwing(); assertEquals(false, timeProvider.hideScratch); - assertEquals(3, timeProvider.snapshotTableModel.getModelData().size()); + assertEquals(3, timeProvider.mainPanel.snapshotTableModel.getModelData().size()); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java index 1590f03fca..9fb62a9e82 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemoryManager.java @@ -327,6 +327,17 @@ public class DBTraceMemoryManager return delegateRead(start.getAddressSpace(), m -> m.getBufferAt(snap, start, byteOrder)); } + @Override + public Long getSnapOfMostRecentChangeToBlock(long snap, Address address) { + return delegateRead(address.getAddressSpace(), + m -> m.getSnapOfMostRecentChangeToBlock(snap, address)); + } + + @Override + public int getBlockSize() { + return DBTraceMemorySpace.BLOCK_SIZE; + } + @Override public void pack() { delegateWriteAll(getActiveSpaces(), m -> m.pack()); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java index c4ef09557d..60a5c08353 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceMemorySpace.java @@ -884,6 +884,26 @@ public class DBTraceMemorySpace implements Unfinished, TraceMemorySpace, DBTrace return false; } + @Override + public Long getSnapOfMostRecentChangeToBlock(long snap, Address address) { + assertInSpace(address); + try (LockHold hold = LockHold.lock(lock.readLock())) { + long offset = address.getOffset(); + long roundOffset = offset & BLOCK_MASK; + OffsetSnap loc = new OffsetSnap(roundOffset, snap); + DBTraceMemoryBlockEntry ent = findMostRecentBlockEntry(loc, true); + if (ent == null) { + return null; + } + return ent.getSnap(); + } + } + + @Override + public int getBlockSize() { + return BLOCK_SIZE; + } + public long getFirstChange(Range span, AddressRange range) { assertInSpace(range); long lower = DBTraceUtils.lowerEndpoint(span); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java index 0f5bc95cab..bb63c1d9b9 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java @@ -472,6 +472,32 @@ public interface TraceMemoryOperations { : ByteOrder.LITTLE_ENDIAN); } + /** + * Find the internal storage block that most-recently defines the value at the given snap and + * address, and return the block's snap. + * + *

+ * This method reveals portions of the internal storage so that clients can optimize difference + * computations by eliminating corresponding ranges defined by the same block. If the underlying + * implementation cannot answer this question, this returns the given snap. + * + * @param snap the time + * @param address the location + * @return the most snap for the most recent containing block + */ + Long getSnapOfMostRecentChangeToBlock(long snap, Address address); + + /** + * Get the block size used by internal storage. + * + *

+ * This method reveals portions of the internal storage so that clients can optimize searches. + * If the underlying implementation cannot answer this question, this returns 0. + * + * @return the block size + */ + int getBlockSize(); + /** * Optimize storage space * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java index 8e1e1c0a29..9b0b3def02 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Sequence.java @@ -368,7 +368,7 @@ public class Sequence implements Comparable { * * @param trace the trace to which the machine is bound * @param eventThread the thread for the first step, if it applies to the "last thread" - * @param machine the machine to step + * @param machine the machine to step, or null to validate the sequence * @param action the action to step each thread * @param monitor a monitor for cancellation and progress reports * @return the last trace thread stepped during execution @@ -384,6 +384,22 @@ public class Sequence implements Comparable { return thread; } + /** + * Validate this sequence for the given trace + * + * @param trace the trace + * @param eventThread the thread for the first step, if it applies to the "last thread" + * @return the last trace thread that would be stepped by this sequence + */ + public TraceThread validate(Trace trace, TraceThread eventThread) { + try { + return execute(trace, eventThread, null, null, null); + } + catch (CancelledException e) { + throw new AssertionError(e); + } + } + /** * Get the key of the last thread stepped * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Step.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Step.java index 2c87399a9d..88287559a0 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Step.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/Step.java @@ -91,8 +91,8 @@ public interface Step extends Comparable { TraceThread thread = isEventThread() ? eventThread : tm.getThread(getThreadKey()); if (thread == null) { if (isEventThread()) { - throw new IllegalArgumentException( - "Thread key -1 can only be used if last/event thread is given"); + throw new IllegalArgumentException("Thread must be given, e.g., 0:t1-3, " + + "since the last thread or snapshot event thread is not given."); } throw new IllegalArgumentException( "Thread with key " + getThreadKey() + " does not exist in given trace"); @@ -160,6 +160,10 @@ public interface Step extends Comparable { PcodeMachine machine, Consumer> stepAction, TaskMonitor monitor) throws CancelledException { TraceThread thread = getThread(tm, eventThread); + if (machine == null) { + // Just performing validation (specifically thread parts) + return thread; + } PcodeThread emuThread = machine.getThread(thread.getPath(), true); execute(emuThread, stepAction, monitor); return thread; diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java index 5d4d4e6eaf..0f8f77c54e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/time/schedule/TraceSchedule.java @@ -344,6 +344,22 @@ public class TraceSchedule implements Comparable { pSteps.execute(trace, lastThread, machine, PcodeThread::stepPcodeOp, monitor); } + /** + * Validate this schedule for the given trace + * + *

+ * This performs a dry run of the sequence on the given trace. If the schedule starts on the + * "last thread," it verifies the snapshot gives the event thread. It also checks that every + * thread key in the sequence exists in the trace. + * + * @param trace the trace against which to validate this schedule + */ + public void validate(Trace trace) { + TraceThread lastThread = getEventThread(trace); + lastThread = steps.validate(trace, lastThread); + lastThread = pSteps.validate(trace, lastThread); + } + /** * Realize the machine state for this schedule using the given trace and pre-positioned machine * @@ -385,13 +401,13 @@ public class TraceSchedule implements Comparable { * This schedule is left unmodified. If it had any p-code steps, those steps are dropped in the * resulting schedule. * - * @param thread the thread to step + * @param thread the thread to step, or null for the "last thread" * @param tickCount the number of ticks to take the thread forward * @return the resulting schedule */ public TraceSchedule steppedForward(TraceThread thread, long tickCount) { Sequence steps = this.steps.clone(); - steps.advance(new TickStep(thread.getKey(), tickCount)); + steps.advance(new TickStep(thread == null ? -1 : thread.getKey(), tickCount)); return new TraceSchedule(snap, steps, new Sequence()); } @@ -441,13 +457,13 @@ public class TraceSchedule implements Comparable { * Returns the equivalent of executing the schedule followed by stepping the given thread * {@code pTickCount} more p-code operations * - * @param thread the thread to step + * @param thread the thread to step, or null for the "last thread" * @param pTickCount the number of p-code ticks to take the thread forward * @return the resulting schedule */ public TraceSchedule steppedPcodeForward(TraceThread thread, int pTickCount) { Sequence pTicks = this.pSteps.clone(); - pTicks.advance(new TickStep(thread.getKey(), pTickCount)); + pTicks.advance(new TickStep(thread == null ? -1 : thread.getKey(), pTickCount)); return new TraceSchedule(snap, steps.clone(), pTicks); } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/plugintool/AutoService.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/plugintool/AutoService.java index a57158c83c..7245283f31 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/plugintool/AutoService.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/plugintool/AutoService.java @@ -75,14 +75,16 @@ public interface AutoService { } } - public static Wiring wireServicesConsumed(Plugin plugin, Object receiver) { - // TODO: Validate against PluginInfo? - + public static Wiring wireServicesConsumed(PluginTool tool, Object receiver) { AutoServiceListener listener = new AutoServiceListener<>(receiver); - PluginTool tool = plugin.getTool(); tool.addServiceListener(listener); listener.notifyCurrentServices(tool); return new WiringImpl(listener); } + + public static Wiring wireServicesConsumed(Plugin plugin, Object receiver) { + // TODO: Validate against PluginInfo? + return wireServicesConsumed(plugin.getTool(), receiver); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/framework/options/AutoOptionsTest.java b/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/framework/options/AutoOptionsTest.java index e296da32f6..2925e7642e 100644 --- a/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/framework/options/AutoOptionsTest.java +++ b/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/framework/options/AutoOptionsTest.java @@ -41,14 +41,14 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { protected static final String OPT2_NAME = "Test Option 2"; protected static final String OPT2_DEFAULT = "Default value"; protected static final String OPT2_DESC = "Another test option"; + protected static final String OPT2_NEW_VALUE = "A new value"; - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class",// - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsPlugin extends Plugin { @AutoOptionDefined(name = OPT1_NAME, description = OPT1_DESC) private int myIntOption = OPT1_DEFAULT; @@ -65,13 +65,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsNoParamPlugin extends AnnotatedWithOptionsPlugin { protected int updateNoParamCount; @@ -85,13 +84,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsNewOnlyParamDefaultPlugin extends AnnotatedWithOptionsPlugin { protected int updateNewOnlyParamDefaultNew; @@ -107,13 +105,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsNewOnlyParamAnnotatedPlugin extends AnnotatedWithOptionsPlugin { protected int updateNewOnlyParamAnnotatedNew; @@ -128,13 +125,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsOldOnlyParamAnnotatedPlugin extends AnnotatedWithOptionsPlugin { protected int updateOldOnlyParamAnnotatedOld; @@ -149,13 +145,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsNewOldParamDefaultPlugin extends AnnotatedWithOptionsPlugin { protected int updateNewOldParamDefaultNew; @@ -172,13 +167,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsNewOldParamNewAnnotPlugin extends AnnotatedWithOptionsPlugin { protected int updateNewOldParamNewAnnotNew; @@ -195,13 +189,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsNewOldParamOldAnnotPlugin extends AnnotatedWithOptionsPlugin { protected int updateNewOldParamOldAnnotNew; @@ -218,13 +211,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsNewOldParamNewOldAnnotPlugin extends AnnotatedWithOptionsPlugin { protected int updateNewOldParamNewOldAnnotNew; @@ -242,13 +234,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsOldNewParamNewAnnotPlugin extends AnnotatedWithOptionsPlugin { protected int updateOldNewParamNewAnnotNew; @@ -265,13 +256,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsOldNewParamOldAnnotPlugin extends AnnotatedWithOptionsPlugin { protected int updateOldNewParamOldAnnotNew; @@ -288,13 +278,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "A plugin class replete with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "An annotated plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "A plugin class replete with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "An annotated plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedWithOptionsOldNewParamOldNewAnnotPlugin extends AnnotatedWithOptionsPlugin { protected int updateOldNewParamOldNewAnnotNew; @@ -312,13 +301,12 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { } } - @PluginInfo(// - category = "Testing", // - description = "Consumer-only plugin class with auto option annotations", // - packageName = MiscellaneousPluginPackage.NAME, // - shortDescription = "A consumer-only plugin class", // - status = PluginStatus.HIDDEN // - ) + @PluginInfo( + category = "Testing", + description = "Consumer-only plugin class with auto option annotations", + packageName = MiscellaneousPluginPackage.NAME, + shortDescription = "A consumer-only plugin class", + status = PluginStatus.HIDDEN) public static class AnnotatedConsumerOnlyPlugin extends Plugin { @AutoOptionConsumed(name = OPT1_NAME) private int othersIntOption; @@ -387,7 +375,18 @@ public class AutoOptionsTest extends AbstractGhidraHeadedIntegrationTest { assertEquals(6, plugin.myIntOption); options.setInt(OPT1_NAME, OPT1_NEW_VALUE); - assertEquals(10, plugin.myIntOption); + assertEquals(OPT1_NEW_VALUE, plugin.myIntOption); + } + + @Test + public void testOptionsUpdatedExplicitCategory() throws PluginException { + AnnotatedWithOptionsPlugin plugin = addPlugin(tool, AnnotatedWithOptionsPlugin.class); + + ToolOptions options = tool.getOptions(OPT2_CATEGORY); + assertEquals(1, options.getOptionNames().size()); + options.setString(OPT2_NAME, OPT2_NEW_VALUE); + + assertEquals(OPT2_NEW_VALUE, plugin.myStringOption); } @Test diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java index b9b6c9ded9..03d4b85c03 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java @@ -705,6 +705,16 @@ public class CodeViewerProvider extends NavigatableComponentProviderAdapter return true; } + /** + * Extension point to specify titles when dual panels are active + * + * @param panelProgram the program assigned to the panel whose title is requested + * @return the title of the panel for the given program + */ + protected String computePanelTitle(Program panelProgram) { + return panelProgram.getDomainFile().toString(); + } + public void setPanel(ListingPanel lp) { Program myProgram = listingPanel.getListingModel().getProgram(); Program otherProgram = lp.getListingModel().getProgram(); @@ -712,10 +722,10 @@ public class CodeViewerProvider extends NavigatableComponentProviderAdapter String otherName = myName; if (myProgram != null) { - myName = myProgram.getDomainFile().toString(); + myName = computePanelTitle(myProgram); } if (otherProgram != null) { - otherName = otherProgram.getDomainFile().toString(); + otherName = computePanelTitle(otherProgram); } if (otherPanel != null) { removeHoverServices(otherPanel); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/InputDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/InputDialog.java index e8f4f1d6f3..e461cb0d87 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/InputDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/dialogs/InputDialog.java @@ -43,12 +43,12 @@ public class InputDialog extends DialogComponentProvider { private InputDialogListener listener; /** - * Creates a provider for a generic input dialog with the specified title, a text field, - * labeled by the specified label. The user should check the value of - * "isCanceled()" to know whether or not the user canceled the operation. - * Otherwise, use the "getValue()" or "getValues()" to get the value(s) - * entered by the user. Use the tool's "showDialog()" to display the dialog. + * Creates a provider for a generic input dialog with the specified title, a text field, labeled + * by the specified label. The user should check the value of "isCanceled()" to know whether or + * not the user canceled the operation. Otherwise, use the "getValue()" or "getValues()" to get + * the value(s) entered by the user. Use the tool's "showDialog()" to display the dialog. *

+ * * @param dialogTitle used as the name of the dialog's title bar * @param label value to use for the label of the text field */ @@ -57,12 +57,12 @@ public class InputDialog extends DialogComponentProvider { } /** - * Creates a generic input dialog with the specified title, a text field, - * labeled by the specified label. The user should check the value of - * "isCanceled()" to know whether or not the user canceled the operation. - * Otherwise, use the "getValue()" or "getValues()" to get the value(s) - * entered by the user. Use the tool's "showDialog()" to display the dialog. + * Creates a generic input dialog with the specified title, a text field, labeled by the + * specified label. The user should check the value of "isCanceled()" to know whether or not the + * user canceled the operation. Otherwise, use the "getValue()" or "getValues()" to get the + * value(s) entered by the user. Use the tool's "showDialog()" to display the dialog. *

+ * * @param dialogTitle used as the name of the dialog's title bar * @param label value to use for the label of the text field * @param initialValue initial value to use for the text field @@ -72,12 +72,12 @@ public class InputDialog extends DialogComponentProvider { } /** - * Creates a generic input dialog with the specified title, a text field, - * labeled by the specified label. The user should check the value of - * "isCanceled()" to know whether or not the user canceled the operation. - * Otherwise, use the "getValue()" or "getValues()" to get the value(s) - * entered by the user. Use the tool's "showDialog()" to display the dialog. + * Creates a generic input dialog with the specified title, a text field, labeled by the + * specified label. The user should check the value of "isCanceled()" to know whether or not the + * user canceled the operation. Otherwise, use the "getValue()" or "getValues()" to get the + * value(s) entered by the user. Use the tool's "showDialog()" to display the dialog. *

+ * * @param dialogTitle used as the name of the dialog's title bar * @param label value to use for the label of the text field * @param initialValue initial value to use for the text field @@ -89,12 +89,12 @@ public class InputDialog extends DialogComponentProvider { } /** - * Creates a generic input dialog with the specified title, a text field, - * labeled by the specified label. The user should check the value of - * "isCanceled()" to know whether or not the user canceled the operation. - * Otherwise, use the "getValue()" or "getValues()" to get the value(s) - * entered by the user. Use the tool's "showDialog()" to display the dialog. + * Creates a generic input dialog with the specified title, a text field, labeled by the + * specified label. The user should check the value of "isCanceled()" to know whether or not the + * user canceled the operation. Otherwise, use the "getValue()" or "getValues()" to get the + * value(s) entered by the user. Use the tool's "showDialog()" to display the dialog. *

+ * * @param dialogTitle used as the name of the dialog's title bar * @param label value to use for the label of the text field * @param initialValue initial value to use for the text field @@ -105,12 +105,12 @@ public class InputDialog extends DialogComponentProvider { } /** - * Creates a generic input dialog with the specified title, a text field, - * labeled by the specified label. The user should check the value of - * "isCanceled()" to know whether or not the user canceled the operation. - * Otherwise, use the "getValue()" or "getValues()" to get the value(s) - * entered by the user. Use the tool's "showDialog()" to display the dialog. + * Creates a generic input dialog with the specified title, a text field, labeled by the + * specified label. The user should check the value of "isCanceled()" to know whether or not the + * user canceled the operation. Otherwise, use the "getValue()" or "getValues()" to get the + * value(s) entered by the user. Use the tool's "showDialog()" to display the dialog. *

+ * * @param dialogTitle used as the name of the dialog's title bar * @param labels values to use for the labels of the text fields * @param initialValues initial values to use for the text fields @@ -120,12 +120,12 @@ public class InputDialog extends DialogComponentProvider { } /** - * Creates a generic input dialog with the specified title, a text field, - * labeled by the specified label. The user should check the value of - * "isCanceled()" to know whether or not the user canceled the operation. - * Otherwise, use the "getValue()" or "getValues()" to get the value(s) - * entered by the user. Use the tool's "showDialog()" to display the dialog. + * Creates a generic input dialog with the specified title, a text field, labeled by the + * specified label. The user should check the value of "isCanceled()" to know whether or not the + * user canceled the operation. Otherwise, use the "getValue()" or "getValues()" to get the + * value(s) entered by the user. Use the tool's "showDialog()" to display the dialog. *

+ * * @param dialogTitle used as the name of the dialog's title bar * @param labels values to use for the labels of the text fields * @param initialValues initial values to use for the text fields @@ -194,6 +194,7 @@ public class InputDialog extends DialogComponentProvider { textFields = new MyTextField[inputLabels.length]; for (int i = 0; i < inputValues.length; i++) { textFields[i] = new MyTextField(initialValues[i]); + inputValues[i] = initialValues[i]; textFields[i].addKeyListener(keyListener); textFields[i].setName("input.dialog.text.field." + i); panel.add(new GLabel(inputLabels[i], SwingConstants.RIGHT)); @@ -221,11 +222,16 @@ public class InputDialog extends DialogComponentProvider { @Override protected void cancelCallback() { isCanceled = true; + for (int v = 0; v < inputValues.length; v++) { + inputValues[v] = null; + } + close(); } /** * Returns if this dialog is cancelled + * * @return true if cancelled */ public boolean isCanceled() { @@ -234,6 +240,7 @@ public class InputDialog extends DialogComponentProvider { /** * Return the value of the first (and maybe only) text field + * * @return the text field value */ public String getValue() { @@ -242,6 +249,7 @@ public class InputDialog extends DialogComponentProvider { /** * Sets the text of the primary text field + * * @param text the text */ public void setValue(String text) { @@ -250,15 +258,18 @@ public class InputDialog extends DialogComponentProvider { /** * Sets the text of the text field at the given index + * * @param text the text * @param index the index of the text field */ public void setValue(String text, int index) { textFields[index].setText(text); + inputValues[index] = text; } /** * Return the values for all the text field(s) + * * @return the text field values */ public String[] getValues() { @@ -285,7 +296,8 @@ public class InputDialog extends DialogComponentProvider { } /** - * @see javax.swing.text.Document#insertString(int, java.lang.String, javax.swing.text.AttributeSet) + * @see javax.swing.text.Document#insertString(int, java.lang.String, + * javax.swing.text.AttributeSet) */ @Override public void insertString(int offs, String str, AttributeSet a)