From d6c1c3cf85e68d85354ee942fdb68f4f49820d44 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Mon, 6 Dec 2021 14:42:35 -0500 Subject: [PATCH] GP-1222: Added comparison between times in a trace. --- Ghidra/Debug/Debugger/build.gradle | 1 + Ghidra/Debug/Debugger/certification.manifest | 4 + .../src/main/help/help/TOC_Source.xml | 4 + .../DebuggerTimePlugin.html | 19 +- .../DebuggerTraceViewDiffPlugin.html | 158 +++++ .../images/DebuggerTimeSelectionDialog.png | Bin 0 -> 21829 bytes .../images/DebuggerTraceViewDiffPlugin.png | Bin 0 -> 48968 bytes .../core/debug/DebuggerCoordinates.java | 4 + .../core/debug/gui/DebuggerResources.java | 78 +++ .../debug/gui/DebuggerSnapActionContext.java | 18 +- .../action/DebuggerTrackLocationTrait.java | 4 +- .../gui/diff/DebuggerTraceViewDiffPlugin.java | 630 ++++++++++++++++++ .../listing/CursorBackgroundColorModel.java | 3 +- .../gui/listing/DebuggerListingPlugin.java | 26 + .../gui/listing/DebuggerListingProvider.java | 68 +- ...emoryStateListingBackgroundColorModel.java | 3 +- .../gui/thread/DebuggerThreadsProvider.java | 4 +- .../gui/time/DebuggerSnapshotTablePanel.java | 262 ++++++++ .../debug/gui/time/DebuggerTimePlugin.java | 71 ++ .../debug/gui/time/DebuggerTimeProvider.java | 234 +------ .../gui/time/DebuggerTimeSelectionDialog.java | 193 ++++++ .../DebuggerTraceManagerServicePlugin.java | 60 +- .../core/debug/utils/BackgroundUtils.java | 60 +- .../app/services/DebuggerListingService.java | 70 ++ .../services/DebuggerTraceManagerService.java | 200 +++++- .../resources/images/table_relationship.png | Bin 0 -> 663 bytes ...ebuggerTraceViewDiffPluginScreenShots.java | 128 ++++ .../AbstractGhidraHeadedDebuggerGUITest.java | 62 ++ .../diff/DebuggerTraceViewDiffPluginTest.java | 233 +++++++ .../listing/DebuggerListingProviderTest.java | 60 +- .../DebuggerMemoryBytesProviderTest.java | 20 - .../gui/time/DebuggerTimeProviderTest.java | 75 ++- .../ghidra/trace/database/DBTraceUtils.java | 1 - .../database/memory/DBTraceMemoryManager.java | 11 + .../database/memory/DBTraceMemorySpace.java | 20 + .../model/memory/TraceMemoryOperations.java | 26 + .../trace/model/time/schedule/Sequence.java | 18 +- .../trace/model/time/schedule/Step.java | 8 +- .../model/time/schedule/TraceSchedule.java | 24 +- .../framework/plugintool/AutoService.java | 10 +- .../framework/options/AutoOptionsTest.java | 183 +++-- .../core/codebrowser/CodeViewerProvider.java | 14 +- .../docking/widgets/dialogs/InputDialog.java | 74 +- 43 files changed, 2622 insertions(+), 519 deletions(-) create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/DebuggerTraceViewDiffPlugin.html create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTimeSelectionDialog.png create mode 100644 Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerTraceViewDiffPlugin/images/DebuggerTraceViewDiffPlugin.png create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPlugin.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerSnapshotTablePanel.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/time/DebuggerTimeSelectionDialog.java create mode 100644 Ghidra/Debug/Debugger/src/main/resources/images/table_relationship.png create mode 100644 Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPluginScreenShots.java create mode 100644 Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/diff/DebuggerTraceViewDiffPluginTest.java 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 0000000000000000000000000000000000000000..e4a617d11049c3bc2d0d89e8e5b8051c4ce4c04d GIT binary patch literal 21829 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7056ec724d6f14da0b48e8d0dd7f118580abf7db GIT binary patch literal 48968 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 h#{P^C_)~Y-0Q??@(E}FU!5Sj(orFU{ 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..28b8505c0ee9a6b874becd1d1c0f9f18a0ef46de GIT binary patch literal 663 zcmV;I0%-k-P)^@R5;6} zllxCnVHn5HpVV5;by;h+VV6~?IE`7f*=DPitC_81F*JteShZ>uk`$7rj_5>VHAx6D zkWkV{?PeG30_TKWA}Cwu+c^g-4xQh8w(l?R^L;-1?s?yr06;89gx>xE9k(OUTHir4 zFo%=9uR&T~K9(|DMiS~CPh$Ss!#KKK47wgLATqh1C|1+;Syl>Q_AGoW_7bOkZP0qv z(0E9ka5W2ujmBq?0+`%ignsYzRhYw9&^>ukXo!k8?7uuE2`~;bLcg$xK5q<#;thK* zghYV4{z^r)c^h^Z%2C@@A@UPee^f+dQaPy32LA$-eR4&WX)D`0+=BLzR>;q75eE4h z7>(uuR3HXwEDFbX6mLGKv9ohb0MuBvD=LheS@M1pYK(HMKUIoy-9`-hVzBb^)Bh)U z^)ZENoex}OB^Q}(b~K`_V=cQ*Q?f~aXCA5SB5VO#@KS*#fRKVqVB^$a-2*N%>TlNJ zu>Alw=vK39)T?BCqITPRBr}V!2Wi|L&x;s(LL;61w^>z>5*jy&gH!__nJsNuDf_dJ zw!ybZeEorYp?m`8B=?XU6Eq4{YpCKQgNuxk<11u^x1sKz697ZppmRyYEk7;V!ShHG zD!otg9gsI(V1Wa;o-%^h9p(W1K(TgpKKdmEIBK3nJpCPXGKWv`EFyFUM5We?VwD|p xQc{2;rkT-0hHz%zr76^(4`KJ2;PSpQuixh%8O#OXSReoZ002ovPDHLkV1k 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/DBTraceUtils.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceUtils.java index b26963eaaa..95ab01c72a 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceUtils.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceUtils.java @@ -19,7 +19,6 @@ import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.util.Iterator; import java.util.Objects; import java.util.function.BiConsumer; 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 9045246d2e..fdef1e86f7 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 @@ -875,6 +875,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)