diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index 1e12776852..9f50cd3e65 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -38,11 +38,12 @@ src/main/help/help/topics/DebuggerBreakpointsPlugin/images/breakpoint-mixed-ed.p src/main/help/help/topics/DebuggerBreakpointsPlugin/images/breakpoints-clear-all.png||GHIDRA||||END| src/main/help/help/topics/DebuggerBreakpointsPlugin/images/breakpoints-disable-all.png||GHIDRA||||END| src/main/help/help/topics/DebuggerBreakpointsPlugin/images/breakpoints-enable-all.png||GHIDRA||||END| +src/main/help/help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html||GHIDRA||||END| +src/main/help/help/topics/DebuggerConsolePlugin/images/DebuggerConsolePlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerListingPlugin/images/DebuggerGoToDialog.png||GHIDRA||||END| src/main/help/help/topics/DebuggerListingPlugin/images/DebuggerListingPlugin.png||GHIDRA||||END| -src/main/help/help/topics/DebuggerListingPlugin/images/DebuggerModuleImportDialog.png||GHIDRA||||END| src/main/help/help/topics/DebuggerMemviewPlugin/DebuggerMemviewPlugin.html||GHIDRA||||END| src/main/help/help/topics/DebuggerMemviewPlugin/images/DebuggerMemviewPlugin.png||GHIDRA||||END| src/main/help/help/topics/DebuggerMemviewPlugin/images/DebuggerMemviewPlugin_old.png||GHIDRA||||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 a9902c2ef8..d21c44c30c 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Debug/Debugger/src/main/help/help/TOC_Source.xml @@ -74,6 +74,10 @@ target="help/topics/DebuggerModelServicePlugin/DebuggerModelServicePlugin.html" /> + + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/Debugger/Troubleshooting.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/Debugger/Troubleshooting.html index c851714c51..0ed4ff62bc 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/Debugger/Troubleshooting.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/Debugger/Troubleshooting.html @@ -15,9 +15,10 @@

Error Console

-

The first place to look when you're having trouble is the error console. In Eclipse, this is - just the "Console" window. In Ghidra, it can be accessed from the main application window. - Sometimes it reports known issues; sometimes it reports unexpected behavior; etc., which may be +

The first place to look when you're having trouble is the Debug Console. Second, if you're + in Eclipse, you can check its "Console" window. Often, Ghidra's Debug Console will offer + actions to help you resolve a well-known issue or configuration problem. It also duplicates the + error log, when those messages are emitted from a debugger-related class. These typically offer clues to exactly what has gone wrong.

Settings and Toggles

@@ -38,9 +39,6 @@

In the Dynamic Listing:

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html new file mode 100644 index 0000000000..30e30a3393 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html @@ -0,0 +1,68 @@ + + + + + + + Debugger: Memory Regions + + + + + +

Debugger: Console

+ + + + + + + +
+ +

The console logs messages from Ghidra related to the debugger. Depending on the exact + configuration, this can comprise a wide range of components, including all GUI views, active + connectors, and running agents. Currently, it implements an appender to gather all Log4J + messages emitted by Ghidra and filters for debugger-related packages and a level in the range + INFO through and including FATAL. That feature will likely be removed as more components are + programmed to work directly with the console. Soon, it may also provide a command-line + interface to control Ghidra's debugging sessions and interact with traces.

+ +

Some log messages include an action context, allowing plug-ins to offer actions on that + message. These are said to be "actionable" messages. A noteworthy example is when navigating to + a module that could not be automatically mapped from the current project. Instead of displaying + a prompt, it will log a message and suggest actions to resolve the issue. A successful + resolution typically removes the message from the log. Note that additional actions may be + available from the context menu.

+ +

By default, the log is sorted so that actionable messages appear at the top. Then, it is + sorted by descending date, so that the most recent messages appear at the top. Like any other + Ghidra table, it can customized and filtered. Note that the filter box is at the top, because + we anticipate a command-line input in the future, which we'd like to place at the bottom.

+ +

Table Columns

+ +

The table has the following columns:

+ + + +

Actions

+ +

Not considering extension actions from other plugins, the console provides the + following:

+ +

Clear

+ +

Removes all messages, including actionable messages, from the log.

+ + diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerConsolePlugin/images/DebuggerConsolePlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerConsolePlugin/images/DebuggerConsolePlugin.png new file mode 100644 index 0000000000..79f8089b97 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerConsolePlugin/images/DebuggerConsolePlugin.png differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html index e697480995..16706c488c 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerInterpreterPlugin/DebuggerInterpreterPlugin.html @@ -42,9 +42,9 @@ immediately upon the associated target interpreter becoming invalid, i.e., the connection was closed. Pinning an interpreter keeps it open, but in a disabled state, so that the buffer can be examined after invalidation.

- +

Interrupt

- +

This action is always available. It interrupts the current target's execution.

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html index ad4215d60c..9769c18d68 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html @@ -131,7 +131,9 @@ computed using information about loaded modules reported by the debugger. For the finer details, see the Static Mappings - window.

+ window. When you navigate to a location contained by a module, but there is no corresponding + static location, the listing logs a "missing module" to the console, offering either to import + the module or map it to an existing program.

Capture Memory

@@ -162,29 +164,6 @@ neglect to capture read-only ranges that have been captured previously. -

Auto-Import Current Module

- -

This toggle is available whenever Sync to Static Listing is enabled. It causes Ghidra to - prompt the user to import unknown modules. Specifically, when Ghidra cannot map the dynamic - listing's location to a static location, but the debugger reports a module containing the - dynamic address, it prompts the user to import that module:

- - - - - - - -
- -

This non-modal dialog collects those prompts and appears whenever a new module is suggested. - To import a module, click the import icon to the suggestion's right. To remove an entry, click - the delete icon to the suggestions's left. To ignore an entry, check the box to the left of the - suggestion and dismiss the dialog. Note that a removed suggestion is not ignored. Ghidra may - suggest that module again. To import modules manually, see the Modules window.

-

Tool Options: Colors

The memory-state and tracked-location background colors can all be configured here.

diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/images/DebuggerModuleImportDialog.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/images/DebuggerModuleImportDialog.png deleted file mode 100644 index 0cab69707e..0000000000 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerListingPlugin/images/DebuggerModuleImportDialog.png and /dev/null differ diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html index d7a3eee373..445e0c380b 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html @@ -145,6 +145,16 @@ It behaves like Map Sections, except that it will propose the selected section be mapped to the block containing the cursor in the static listing.

+

Import Missing Module

+ +

This action is offered to resolve a "Missing Module" console message. It is equivalent to Import From File System on the missing module.

+ +

Map Missing Module

+ +

This action is offered to resolve a "Missing Module" console message. It is equivalent to Map Module To on the missing module.

+

Filter Sections by Module

This action is always available. By default the bottom table displays all sections in the 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 a254a77e34..13a613fac6 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 @@ -32,6 +32,7 @@ import docking.widgets.table.*; import docking.widgets.tree.GTreeNode; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.gui.breakpoint.DebuggerBreakpointsPlugin; +import ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; import ghidra.app.plugin.core.debug.gui.memory.DebuggerRegionsPlugin; import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesPlugin; @@ -118,6 +119,7 @@ public interface DebuggerResources { ImageIcon ICON_CLOSE = ResourceManager.loadImage("images/x.gif"); ImageIcon ICON_ADD = ResourceManager.loadImage("images/add.png"); ImageIcon ICON_DELETE = ResourceManager.loadImage("images/delete.png"); + ImageIcon ICON_CLEAR = ResourceManager.loadImage("images/erase16.png"); ImageIcon ICON_REFRESH = ResourceManager.loadImage("images/view-refresh.png"); ImageIcon ICON_FILTER = ResourceManager.loadImage("images/filter_off.png"); // Eww. ImageIcon ICON_SELECT_ROWS = ResourceManager.loadImage("images/table_go.png"); @@ -128,7 +130,7 @@ public interface DebuggerResources { //ResourceManager.loadImage("images/capture-memory.png"); // TODO: Draw an icon - ImageIcon ICON_MAP_MODULES = ResourceManager.loadImage("images/map-modules.png"); + ImageIcon ICON_MAP_MODULES = ResourceManager.loadImage("images/modules.png"); ImageIcon ICON_MAP_SECTIONS = ICON_MAP_MODULES; // TODO ImageIcon ICON_BLOCK = ICON_MAP_SECTIONS; // TODO // TODO: Draw an icon @@ -138,6 +140,10 @@ public interface DebuggerResources { // TODO: Draw an icon? ImageIcon ICON_CAPTURE_SYMBOLS = ResourceManager.loadImage("images/closedFolderLabels.png"); + ImageIcon ICON_LOG_FATAL = ResourceManager.loadImage("images/edit-bomg.png"); + ImageIcon ICON_LOG_ERROR = ResourceManager.loadImage("images/dialog-warning_red.png"); + ImageIcon ICON_LOG_WARN = ResourceManager.loadImage("images/dialog-warning.png"); + ImageIcon ICON_SYNC = ResourceManager.loadImage("images/sync_enabled.png"); ImageIcon ICON_VISIBILITY = ResourceManager.loadImage("images/format-text-bold.png"); @@ -156,6 +162,11 @@ public interface DebuggerResources { HelpLocation HELP_PROVIDER_BREAKPOINTS = new HelpLocation( PluginUtils.getPluginNameFromClass(DebuggerBreakpointsPlugin.class), HELP_ANCHOR_PLUGIN); + String TITLE_PROVIDER_CONSOLE = "Debug Console"; + ImageIcon ICON_PROVIDER_CONSOLE = ICON_CONSOLE; + HelpLocation HELP_PROVIDER_CONSOLE = new HelpLocation( + PluginUtils.getPluginNameFromClass(DebuggerConsolePlugin.class), HELP_ANCHOR_PLUGIN); + String TITLE_PROVIDER_LISTING = "Dynamic"; ImageIcon ICON_PROVIDER_LISTING = ICON_LISTING; HelpLocation HELP_PROVIDER_LISTING = new HelpLocation( @@ -310,6 +321,9 @@ public interface DebuggerResources { "Colors.Ineffective Disabled Breakpoint Markers Have Background"; boolean DEFAULT_COLOR_INEFFECTIVE_D_BREAKPOINT_COLORING_BACKGROUND = false; + String OPTION_NAME_LOG_BUFFER_LIMIT = "Log Buffer Size"; + int DEFAULT_LOG_BUFFER_LIMIT = 100; + // TODO: Re-assign/name groups String GROUP_GENERAL = "Dbg1. General"; String GROUP_CONNECTION = "Dbg2. Connection"; @@ -645,7 +659,7 @@ public interface DebuggerResources { .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); } } - + interface InterpreterInterruptAction { String NAME = "Interpreter Interrupt"; String DESCRIPTION = "Send an interrupt through this Interpreter"; @@ -733,17 +747,36 @@ public interface DebuggerResources { } } - interface AutoImportCurrentModuleAction { - String NAME = "Auto-Import Current Module"; - String DESCRIPTION = "Import missing module at the cursor"; + interface ImportMissingModuleAction { + String NAME = "Import Missing Module"; + String DESCRIPTION = "Import the missing module from disk"; Icon ICON = ICON_IMPORT; - String HELP_ANCHOR = "auto_import_module"; + String HELP_ANCHOR = "import_missing_module"; - static ToggleActionBuilder builder(Plugin owner) { + static ActionBuilder builder(Plugin owner) { String ownerName = owner.getName(); - return new ToggleActionBuilder(NAME, ownerName).description(DESCRIPTION) - .menuIcon(ICON) - .menuPath(NAME) + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .popupMenuIcon(ICON) + .popupMenuPath(NAME) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface MapMissingModuleAction { + String NAME = "Map Missing Module"; + String DESCRIPTION = "Map the missing module to an existing import"; + Icon ICON = ICON_MAP_MODULES; + String HELP_ANCHOR = "map_missing_module"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .description(DESCRIPTION) + .toolBarIcon(ICON) + .popupMenuIcon(ICON) + .popupMenuPath(NAME) .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); } } @@ -862,7 +895,8 @@ public interface DebuggerResources { static ActionBuilder builder(Plugin owner) { String ownerName = owner.getName(); - return new ActionBuilder(NAME, ownerName).toolBarGroup(GROUP) + return new ActionBuilder(NAME, ownerName) + .toolBarGroup(GROUP) .toolBarIcon(ICON) .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); } @@ -879,7 +913,26 @@ public interface DebuggerResources { } static ActionBuilder builder(String ownerName) { - return new ActionBuilder(NAME, ownerName).toolBarGroup(GROUP) + return new ActionBuilder(NAME, ownerName) + .toolBarGroup(GROUP) + .toolBarIcon(ICON) + .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); + } + } + + interface ClearAction { + String NAME = "Clear"; + String GROUP = "yyyy"; + Icon ICON = ICON_CLEAR; + String HELP_ANCHOR = "clear"; + + static ActionBuilder builder(Plugin owner) { + return builder(owner.getName()); + } + + static ActionBuilder builder(String ownerName) { + return new ActionBuilder(NAME, ownerName) + .toolBarGroup(GROUP) .toolBarIcon(ICON) .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)); } @@ -892,7 +945,21 @@ public interface DebuggerResources { static ToggleActionBuilder builder(Plugin owner) { String ownerName = owner.getName(); - return new ToggleActionBuilder(NAME, ownerName).toolBarGroup(GROUP).toolBarIcon(ICON); + return new ToggleActionBuilder(NAME, ownerName) + .toolBarGroup(GROUP) + .toolBarIcon(ICON); + } + } + + interface SelectNoneAction { + String NAME = "Select None"; + String GROUP = "Select"; + + static ActionBuilder builder(Plugin owner) { + String ownerName = owner.getName(); + return new ActionBuilder(NAME, ownerName) + .popupMenuGroup(GROUP) + .popupMenuPath(NAME); } } @@ -904,7 +971,8 @@ public interface DebuggerResources { static ActionBuilder builder(Plugin owner) { String ownerName = owner.getName(); - return new ActionBuilder(NAME, ownerName).toolBarGroup(GROUP) + return new ActionBuilder(NAME, ownerName) + .toolBarGroup(GROUP) .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR)) .toolBarIcon(ICON); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellEditor.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellEditor.java new file mode 100644 index 0000000000..866fb4f53b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellEditor.java @@ -0,0 +1,67 @@ +/* ### + * 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.console; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; +import javax.swing.table.TableCellEditor; + +import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.ActionList; +import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.BoundAction; + +public class ConsoleActionsCellEditor extends AbstractCellEditor + implements TableCellEditor, ActionListener { + private static final ActionList EMPTY_ACTION_LIST = new ActionList(); + + protected final JPanel box = new JPanel(); + protected final List buttonCache = new ArrayList<>(); + + protected ActionList value; + + public ConsoleActionsCellEditor() { + ConsoleActionsCellRenderer.configureBox(box); + } + + @Override + public Object getCellEditorValue() { + return EMPTY_ACTION_LIST; + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object v, boolean isSelected, + int row, int column) { + // I can't think of when you'd be "editing" a non-selected cell. + box.setBackground(table.getSelectionBackground()); + + value = (ActionList) v; + ConsoleActionsCellRenderer.populateBox(box, buttonCache, value, + button -> button.addActionListener(this)); + return box; + } + + @Override + public void actionPerformed(ActionEvent e) { + int index = buttonCache.indexOf(e.getSource()); + BoundAction action = value.get(index); + stopCellEditing(); + action.perform(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java new file mode 100644 index 0000000000..dba02d6ef0 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java @@ -0,0 +1,89 @@ +/* ### + * 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.console; + +import java.awt.Component; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.swing.*; + +import docking.widgets.table.GTableCellRenderingData; +import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.ActionList; +import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.BoundAction; +import ghidra.docking.settings.Settings; +import ghidra.util.table.column.AbstractGhidraColumnRenderer; + +public class ConsoleActionsCellRenderer extends AbstractGhidraColumnRenderer { + + static void configureBox(JPanel box) { + box.setLayout(new BoxLayout(box, BoxLayout.X_AXIS)); + box.setOpaque(true); + box.setAlignmentX(0.5f); + } + + static void ensureCacheSize(List buttonCache, int size, + Consumer extraConfig) { + int diff = size - buttonCache.size(); + for (int i = 0; i < diff; i++) { + JButton button = new JButton(); + button.setMinimumSize(DebuggerConsoleProvider.ACTION_BUTTON_DIM); + button.setMaximumSize(DebuggerConsoleProvider.ACTION_BUTTON_DIM); + extraConfig.accept(button); + buttonCache.add(button); + } + } + + static void populateBox(JPanel box, List buttonCache, ActionList value, + Consumer extraConfig) { + box.removeAll(); + ensureCacheSize(buttonCache, value.size(), extraConfig); + int i = 0; + for (BoundAction a : value) { + JButton button = buttonCache.get(i); + button.setToolTipText(a.getTooltipText()); + button.setIcon(a.getIcon()); + button.setEnabled(a.isEnabled()); + box.add(button); + i++; + } + } + + protected final JPanel box = new JPanel(); + protected final List buttonCache = new ArrayList<>(); + + public ConsoleActionsCellRenderer() { + configureBox(box); + } + + @Override + public String getFilterString(ActionList t, Settings settings) { + return t.stream().map(a -> a.getName()).collect(Collectors.joining(" ")); + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + super.getTableCellRendererComponent(data); // A bit of a waste, but sets the background + box.setBackground(getBackground()); + + ActionList value = (ActionList) data.getValue(); + populateBox(box, buttonCache, value, button -> { + }); + return box; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java new file mode 100644 index 0000000000..5fd781ca61 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java @@ -0,0 +1,130 @@ +/* ### + * 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.console; + +import javax.swing.Icon; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.filter.LevelRangeFilter; + +import docking.ActionContext; +import docking.action.DockingActionIf; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.services.DebuggerConsoleService; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; + +@PluginInfo( + shortDescription = "Debugger console panel plugin", + description = "A tool-global console for controlling a debug/trace session", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + servicesRequired = {}, + servicesProvided = { + DebuggerConsoleService.class, + }) +public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleService { + protected static final String APPENDER_NAME = "debuggerAppender"; + + protected class ConsolePluginAppender extends AbstractAppender { + + public ConsolePluginAppender() { + super(APPENDER_NAME, null, null, true, Property.EMPTY_ARRAY); + + addFilter(LevelRangeFilter.createFilter(Level.FATAL, Level.INFO, null, null)); + } + + @Override + public void append(LogEvent event) { + String loggerName = event.getLoggerName(); + if (loggerName.contains(".debug") || + loggerName.contains(".dbg.") || + loggerName.contains("agent.")) { + provider.logEvent(event); + } + } + } + + protected DebuggerConsoleProvider provider; + + protected final ConsolePluginAppender appender; + + protected Logger rootLogger; + + public DebuggerConsolePlugin(PluginTool tool) { + super(tool); + + appender = new ConsolePluginAppender(); + } + + @Override + protected void init() { + super.init(); + provider = new DebuggerConsoleProvider(this); + + rootLogger = (Logger) LogManager.getRootLogger(); + appender.start(); + rootLogger.addAppender(appender); + } + + @Override + protected void dispose() { + if (rootLogger != null) { + rootLogger.removeAppender(appender); + appender.stop(); + + provider.dispose(); + tool.removeComponentProvider(provider); + } + super.dispose(); + } + + @Override + public void log(Icon icon, String message, ActionContext context) { + provider.log(icon, message, context); + } + + @Override + public void remove(ActionContext context) { + provider.remove(context); + } + + @Override + public void addResolutionAction(DockingActionIf action) { + provider.addResolutionAction(action); + } + + @Override + public void removeResolutionAction(DockingActionIf action) { + provider.removeResolutionAction(action); + } + + /** + * For testing: get the number of rows having a given class of action context + * + * @param ctxCls the context class + */ + public long getRowCount(Class ctxCls) { + return provider.getRowCount(ctxCls); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java new file mode 100644 index 0000000000..6c0828b153 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java @@ -0,0 +1,487 @@ +/* ### + * 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.console; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.MouseEvent; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.TableModelEvent; +import javax.swing.table.*; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LogEvent; + +import docking.*; +import docking.action.DockingAction; +import docking.action.DockingActionIf; +import docking.actions.PopupActionProvider; +import docking.widgets.table.ColumnSortState.SortDirection; +import docking.widgets.table.CustomToStringCellRenderer; +import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.ClearAction; +import ghidra.app.plugin.core.debug.gui.DebuggerResources.SelectNoneAction; +import ghidra.app.plugin.core.debug.utils.DebouncedRowWrappedEnumeratedColumnTableModel; +import ghidra.framework.options.AutoOptions; +import ghidra.framework.options.annotation.*; +import ghidra.framework.plugintool.AutoService; +import ghidra.framework.plugintool.ComponentProviderAdapter; +import ghidra.util.*; +import ghidra.util.table.GhidraTable; +import ghidra.util.table.GhidraTableFilterPanel; + +public class DebuggerConsoleProvider extends ComponentProviderAdapter + implements PopupActionProvider { + static final int ACTION_BUTTON_SIZE = 32; + static final Dimension ACTION_BUTTON_DIM = + new Dimension(ACTION_BUTTON_SIZE, ACTION_BUTTON_SIZE); + static final int MAX_ROW_HEIGHT = 300; + + protected enum LogTableColumns implements EnumeratedTableColumn { + LEVEL("Level", Icon.class, LogRow::getIcon, SortDirection.ASCENDING, false), + MESSAGE("Message", String.class, LogRow::getMessage, SortDirection.ASCENDING, false), + ACTIONS("Actions", ActionList.class, LogRow::getActions, SortDirection.DESCENDING, true), + TIME("Time", Date.class, LogRow::getDate, SortDirection.DESCENDING, false); + + private final String header; + private final Function getter; + private final Class cls; + private final SortDirection defaultSortDirection; + private final boolean editable; + + LogTableColumns(String header, Class cls, Function getter, + SortDirection defaultSortDirection, boolean editable) { + this.header = header; + this.cls = cls; + this.getter = getter; + this.defaultSortDirection = defaultSortDirection; + this.editable = editable; + } + + @Override + public String getHeader() { + return header; + } + + @Override + public Class getValueClass() { + return cls; + } + + @Override + public Object getValueOf(LogRow row) { + return getter.apply(row); + } + + @Override + public boolean isEditable(LogRow row) { + return editable; + } + + @Override + public void setValueOf(LogRow row, Object value) { + } + + @Override + public SortDirection defaultSortDirection() { + return defaultSortDirection; + } + } + + protected static class BoundAction { + protected final DockingActionIf action; + protected final ActionContext context; + + public BoundAction(DockingActionIf action, ActionContext context) { + this.action = action; + this.context = context; + } + + @Override + public String toString() { + return getName(); + } + + public String getName() { + return action.getName(); + } + + public Icon getIcon() { + return action.getToolBarData().getIcon(); + } + + public boolean isEnabled() { + return action.isEnabledForContext(context); + } + + public String getTooltipText() { + return action.getDescription(); + } + + public void perform() { + action.actionPerformed(context); + } + } + + protected static class ActionList extends ArrayList { + } + + protected static class LogRow { + private final Icon icon; + private final String message; + private final Date date; + private final ActionContext context; + private final ActionList actions; + + public LogRow(Icon icon, String message, Date date, ActionContext context, + ActionList actions) { + this.icon = icon; + this.message = message; + this.date = date; + this.context = context; + this.actions = actions; + } + + public Icon getIcon() { + return icon; + } + + public String getMessage() { + return message; + } + + public Date getDate() { + return date; + } + + public ActionContext getActionContext() { + return context; + } + + public ActionList getActions() { + return actions; + } + } + + protected static class LogTableModel extends DebouncedRowWrappedEnumeratedColumnTableModel< // + LogTableColumns, ActionContext, LogRow, LogRow> { + + public LogTableModel() { + super("Log", LogTableColumns.class, r -> r.getActionContext(), r -> r); + } + + @Override + public java.util.List defaultSortOrder() { + return java.util.List.of(LogTableColumns.ACTIONS, LogTableColumns.TIME); + } + } + + protected static class LogTable extends GhidraTable { + + public LogTable(LogTableModel model) { + super(model); + } + + @Override + public void tableChanged(TableModelEvent e) { + super.tableChanged(e); + Swing.runIfSwingOrRunLater(() -> updateRowHeights()); + } + + @Override + public void columnMarginChanged(ChangeEvent e) { + super.columnMarginChanged(e); + // TODO: Debounce or otherwise delay this + Swing.runIfSwingOrRunLater(() -> updateRowHeights()); + } + + protected void updateRowHeights() { + // TODO: Be more selective in which rows + // Those changed + // Those visible? + TableModel model = getModel(); + int rows = model.getRowCount(); + int cols = getColumnCount(); + for (int r = 0; r < rows; r++) { + int height = 0; + for (int c = 0; c < cols; c++) { + height = Math.max(height, computePreferredHeight(r, c)); + } + setRowHeight(r, height); + } + } + + protected int computePreferredHeight(int r, int c) { + TableCellRenderer renderer = getCellRenderer(r, c); + if (renderer instanceof ConsoleActionsCellRenderer) { + ActionList actions = (ActionList) getModel().getValueAt(r, c); + if (!actions.isEmpty()) { + return ACTION_BUTTON_SIZE; + } + return 0; + } + if (renderer instanceof CustomToStringCellRenderer) { + CustomToStringCellRenderer custom = (CustomToStringCellRenderer) renderer; + int colWidth = getColumnModel().getColumn(c).getWidth(); + prepareRenderer(renderer, r, c); + return custom.getRowHeight(colWidth); + } + return 0; + } + } + + private final DebuggerConsolePlugin plugin; + + @SuppressWarnings("unused") + private final AutoService.Wiring autoServiceWiring; + + @AutoOptionDefined( + name = DebuggerResources.OPTION_NAME_LOG_BUFFER_LIMIT, + description = "The maximum number of entries in the console log (0 or less for unlimited)", + help = @HelpInfo(anchor = "buffer_limit")) + private int logBufferLimit = DebuggerResources.DEFAULT_LOG_BUFFER_LIMIT; + @SuppressWarnings("unused") + private final AutoOptions.Wiring autoOptionsWiring; + + protected final Map> actionsByOwnerThenName = + new LinkedHashMap<>(); + + protected final LogTableModel logTableModel = new LogTableModel(); + protected GhidraTable logTable; + private GhidraTableFilterPanel logFilterPanel; + + private Deque buffer = new ArrayDeque<>(); + + private final JPanel mainPanel = new JPanel(new BorderLayout()); + + DockingAction actionClear; + DockingAction actionSelectNone; + + public DebuggerConsoleProvider(DebuggerConsolePlugin plugin) { + super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_CONSOLE, plugin.getName()); + this.plugin = plugin; + + tool.addPopupActionProvider(this); + + setIcon(DebuggerResources.ICON_PROVIDER_CONSOLE); + setHelpLocation(DebuggerResources.HELP_PROVIDER_CONSOLE); + setWindowMenuGroup(DebuggerPluginPackage.NAME); + + buildMainPanel(); + + this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); + this.autoOptionsWiring = AutoOptions.wireOptions(plugin, this); + + setDefaultWindowPosition(WindowPosition.BOTTOM); + setVisible(true); + createActions(); + } + + protected void dispose() { + tool.removePopupActionProvider(this); + } + + protected void buildMainPanel() { + logTable = new LogTable(logTableModel); + logTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + mainPanel.add(new JScrollPane(logTable)); + logFilterPanel = new GhidraTableFilterPanel<>(logTable, logTableModel); + mainPanel.add(logFilterPanel, BorderLayout.NORTH); + + logTable.setRowHeight(ACTION_BUTTON_SIZE); + TableColumnModel columnModel = logTable.getColumnModel(); + + TableColumn levelCol = columnModel.getColumn(LogTableColumns.LEVEL.ordinal()); + levelCol.setMaxWidth(24); + levelCol.setMinWidth(24); + + TableColumn msgCol = columnModel.getColumn(LogTableColumns.MESSAGE.ordinal()); + msgCol.setPreferredWidth(150); + msgCol.setCellRenderer(CustomToStringCellRenderer.HTML); + + TableColumn actCol = columnModel.getColumn(LogTableColumns.ACTIONS.ordinal()); + actCol.setPreferredWidth(50); + actCol.setCellRenderer(new ConsoleActionsCellRenderer()); + actCol.setCellEditor(new ConsoleActionsCellEditor()); + + TableColumn timeCol = columnModel.getColumn(LogTableColumns.TIME.ordinal()); + timeCol.setCellRenderer(CustomToStringCellRenderer.TIME_24HMSms); + timeCol.setPreferredWidth(15); + } + + protected void createActions() { + actionClear = ClearAction.builder(plugin) + .onAction(this::activatedClear) + .buildAndInstallLocal(this); + actionSelectNone = SelectNoneAction.builder(plugin) + .popupWhen(ctx -> ctx.getSourceComponent() == logTable) + .onAction(this::activatedSelectNone) + .buildAndInstallLocal(this); + } + + private void activatedClear(ActionContext ctx) { + synchronized (buffer) { + logTableModel.clear(); + buffer.clear(); + } + } + + private void activatedSelectNone(ActionContext ctx) { + logTable.clearSelection(); + } + + @Override + public ActionContext getActionContext(MouseEvent event) { + if (logTable.getSelectedRowCount() != 1) { + return super.getActionContext(event); + } + LogRow sel = logFilterPanel.getSelectedItem(); + if (sel == null) { + // I guess this can happen because of timing? + return super.getActionContext(event); + } + return sel.getActionContext(); + } + + @AutoOptionConsumed(name = DebuggerResources.OPTION_NAME_LOG_BUFFER_LIMIT) + private void setLogBufferLimit(int logBufferLimit) { + truncateLog(); + } + + @Override + public JComponent getComponent() { + return mainPanel; + } + + protected void truncateLog() { + synchronized (buffer) { + while (logBufferLimit > 0 && buffer.size() > logBufferLimit) { + logTableModel.deleteItem(buffer.removeFirst()); + } + } + } + + protected void log(Icon icon, String message, ActionContext context) { + logRow(new LogRow(icon, message, new Date(), context, computeToolbarActions(context))); + } + + protected void logRow(LogRow row) { + synchronized (buffer) { + LogRow old = logTableModel.deleteKey(row.getActionContext()); + if (old != null) { + buffer.remove(old); + } + logTableModel.addItem(row); + buffer.addLast(row); + truncateLog(); + } + //logTable.scrollRectToVisible(new Rectangle(0, Integer.MAX_VALUE - 1, 1, 1)); + } + + protected Icon iconForLevel(Level level) { + if (level == Level.FATAL) { + return DebuggerResources.ICON_LOG_FATAL; + } + else if (level == Level.ERROR) { + return DebuggerResources.ICON_LOG_ERROR; + } + else if (level == Level.WARN) { + return DebuggerResources.ICON_LOG_WARN; + } + return null; + } + + protected void logEvent(LogEvent event) { + ActionContext context = new LogRowConsoleActionContext(); + logRow(new LogRow(iconForLevel(event.getLevel()), + "" + HTMLUtilities.escapeHTML(event.getMessage().getFormattedMessage()), + new Date(event.getTimeMillis()), context, computeToolbarActions(context))); + } + + protected void remove(ActionContext context) { + synchronized (buffer) { + LogRow r = logTableModel.deleteKey(context); + buffer.remove(r); + } + } + + protected void addResolutionAction(DockingActionIf action) { + DockingActionIf replaced = + actionsByOwnerThenName.computeIfAbsent(action.getOwner(), o -> new LinkedHashMap<>()) + .put(action.getName(), action); + if (replaced != null) { + Msg.warn(this, "Duplicate resolution action registered: " + action.getFullName()); + } + } + + protected void removeResolutionAction(DockingActionIf action) { + Map byName = actionsByOwnerThenName.get(action.getOwner()); + if (byName == null) { + Msg.warn(this, "Action to remove was never added: " + action.getFullName()); + return; + } + DockingActionIf removed = byName.get(action.getName()); + if (removed != action) { + if (removed != null) { + Msg.warn(this, + "Action to remove did not match that added: " + action.getFullName()); + } + else { + Msg.warn(this, "Action to removed was never added: " + action.getFullName()); + } + return; + } + if (byName.isEmpty()) { + actionsByOwnerThenName.remove(action.getOwner()); + } + } + + protected Stream streamActions(ActionContext context) { + return actionsByOwnerThenName.values() + .stream() + .flatMap(m -> m.values().stream()) + .filter(a -> a.isValidContext(context)); + } + + protected ActionList computeToolbarActions(ActionContext context) { + return streamActions(context) + .filter(a -> a.getToolBarData() != null) + .map(a -> new BoundAction(a, context)) + .collect(Collectors.toCollection(ActionList::new)); + } + + @Override + public java.util.List getPopupActions(Tool tool, ActionContext context) { + return streamActions(context) + .filter(a -> a.isAddToPopup(context)) + .collect(Collectors.toList()); + } + + protected long getRowCount(Class ctxCls) { + return logTableModel.getModelData() + .stream() + .filter(r -> ctxCls.isInstance(r.context)) + .count(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/LogRowConsoleActionContext.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/LogRowConsoleActionContext.java new file mode 100644 index 0000000000..db0658b1c0 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/LogRowConsoleActionContext.java @@ -0,0 +1,22 @@ +/* ### + * 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.console; + +import docking.ActionContext; + +public class LogRowConsoleActionContext extends ActionContext { + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPlugin.java index 21fa362e84..3e09ff8168 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPlugin.java @@ -31,19 +31,18 @@ import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.framework.plugintool.util.PluginStatus; -@PluginInfo( // - shortDescription = "Debugger interpreter panel service", // - description = "Manage interpreter panels within debug sessions", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - servicesRequired = { // - InterpreterPanelService.class // - }, // +@PluginInfo( + shortDescription = "Debugger interpreter panel service", + description = "Manage interpreter panels within debug sessions", + category = PluginCategoryNames.DEBUGGER, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.RELEASED, + servicesRequired = { + InterpreterPanelService.class, + }, servicesProvided = { - DebuggerInterpreterService.class // - } // -) + DebuggerInterpreterService.class, + }) public class DebuggerInterpreterPlugin extends AbstractDebuggerPlugin implements DebuggerInterpreterService { 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 f4ff8ca1b3..39dd501520 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 @@ -342,7 +342,7 @@ public class DebuggerListingPlugin extends CodeBrowserPlugin implements Debugger //cbGoTo.invoke(() -> { DebuggerListingProvider provider = getConnectedProvider(); provider.doSyncToStatic(location); - provider.doAutoImportCurrentModule(); + provider.doCheckCurrentModuleMissing(); //}); return true; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java index 932fc64903..cdc6637163 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java @@ -19,7 +19,6 @@ import static ghidra.app.plugin.core.debug.gui.DebuggerResources.ICON_REGISTER_M import static ghidra.app.plugin.core.debug.gui.DebuggerResources.OPTION_NAME_COLORS_REGISTER_MARKERS; import java.awt.Color; -import java.io.File; import java.lang.invoke.MethodHandles; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -50,6 +49,7 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import ghidra.app.plugin.core.debug.gui.action.*; import ghidra.app.plugin.core.debug.gui.action.AutoReadMemorySpec.AutoReadMemorySpecConfigFieldCodec; import ghidra.app.plugin.core.debug.gui.action.LocationTrackingSpec.TrackingSpecConfigFieldCodec; +import ghidra.app.plugin.core.debug.gui.modules.DebuggerMissingModuleActionContext; import ghidra.app.plugin.core.debug.utils.BackgroundUtils; import ghidra.app.plugin.core.debug.utils.ProgramURLUtils; import ghidra.app.plugin.core.exporter.ExporterDialog; @@ -85,8 +85,7 @@ import ghidra.trace.model.stack.TraceStack; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.util.TraceAddressSpace; -import ghidra.util.Msg; -import ghidra.util.Swing; +import ghidra.util.*; import utilities.util.SuppressableCallback; import utilities.util.SuppressableCallback.Suppression; @@ -323,6 +322,8 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi //@AutoServiceConsumed via method private DebuggerStaticMappingService mappingService; @AutoServiceConsumed + private DebuggerConsoleService consoleService; + @AutoServiceConsumed private ProgramManager programManager; @AutoServiceConsumed private FileImporterService importerService; @@ -349,12 +350,10 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi protected MultiStateDockingAction actionTrackLocation; protected DockingAction actionGoTo; protected SyncToStaticListingAction actionSyncToStaticListing; - protected ToggleDockingAction actionAutoImportCurrentModule; protected FollowsCurrentThreadAction actionFollowsCurrentThread; protected MultiStateDockingAction actionAutoReadMemory; protected DockingAction actionExportView; - protected final DebuggerModuleImportDialog importDialog; protected final DebuggerGoToDialog goToDialog; @AutoConfigStateField(codec = TrackingSpecConfigFieldCodec.class) @@ -362,8 +361,6 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi @AutoConfigStateField protected boolean syncToStaticListing; @AutoConfigStateField - protected boolean autoImportCurrentModule; - @AutoConfigStateField protected boolean followsCurrentThread = true; @AutoConfigStateField(codec = AutoReadMemorySpecConfigFieldCodec.class) protected AutoReadMemorySpec autoReadMemorySpec = defaultReadMemorySpec; @@ -389,7 +386,6 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi super(plugin, formatManager, isConnected); this.plugin = plugin; - importDialog = new DebuggerModuleImportDialog(tool); goToDialog = new DebuggerGoToDialog(this); ListingPanel listingPanel = getListingPanel(); @@ -403,7 +399,6 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi autoOptionsWiring = AutoOptions.wireOptionsConsumed(plugin, this); syncToStaticListing = isConnected; - autoImportCurrentModule = isConnected; setVisible(true); createActions(); @@ -473,12 +468,10 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi actionTrackLocation.setCurrentActionStateByUserData(trackingSpec); if (isConnected()) { actionSyncToStaticListing.setSelected(syncToStaticListing); - actionAutoImportCurrentModule.setSelected(autoImportCurrentModule); followsCurrentThread = true; } else { syncToStaticListing = false; - autoImportCurrentModule = false; actionFollowsCurrentThread.setSelected(followsCurrentThread); updateBorder(); } @@ -744,11 +737,6 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi if (isConnected()) { actionSyncToStaticListing = new SyncToStaticListingAction(); - actionAutoImportCurrentModule = AutoImportCurrentModuleAction.builder(plugin) - .enabledWhen(ctx -> syncToStaticListing) - .onAction(this::autoImportCurrentModuleActionToggled) - .selected(autoImportCurrentModule) - .buildAndInstallLocal(this); } else { actionFollowsCurrentThread = new FollowsCurrentThreadAction(); @@ -803,10 +791,6 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi doSetTrackingSpec(newState.getUserData()); } - protected void autoImportCurrentModuleActionToggled(ActionContext ctx) { - doSetAutoImportCurrentModule(actionAutoImportCurrentModule.isSelected()); - } - protected void activatedAutoReadMemory(ActionContext ctx) { doAutoReadMemory(); } @@ -891,7 +875,7 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi super.programLocationChanged(location, trigger); if (trigger == EventTrigger.GUI_ACTION) { doSyncToStatic(location); - doAutoImportCurrentModule(); + doCheckCurrentModuleMissing(); } } @@ -928,11 +912,8 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi } } - protected void doAutoImportCurrentModule() { - if (!autoImportCurrentModule) { - return; - } - if (importerService == null) { + protected void doCheckCurrentModuleMissing() { + if (importerService == null || consoleService == null) { return; } Trace trace = current.getTrace(); @@ -962,8 +943,8 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi } } - Map missing = new LinkedHashMap<>(); - Set toOpen = new LinkedHashSet<>(); + Set missing = new HashSet<>(); + Set toOpen = new HashSet<>(); TraceModuleManager modMan = trace.getModuleManager(); Collection modules = Stream.concat( modMan.getModulesAt(snap, address).stream().filter(m -> m.getSections().isEmpty()), @@ -975,7 +956,7 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi for (TraceModule mod : modules) { Set matches = mappingService.findProbableModulePrograms(mod); if (matches.isEmpty()) { - missing.put(mod, new File(mod.getName())); + missing.add(mod); } else { toOpen.addAll(matches); @@ -988,7 +969,12 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi ProgramManager.OPEN_VISIBLE); } } - importDialog.addFiles(missing.values()); + for (TraceModule mod : missing) { + consoleService.log(DebuggerResources.ICON_LOG_ERROR, + "The module " + HTMLUtilities.escapeHTML(mod.getName()) + + " was not found in the project", + new DebuggerMissingModuleActionContext(mod)); + } /** * Once the programs are opened, including those which are successfully imported, the * section mapper should take over, eventually invoking callbacks to our mapping change @@ -1027,11 +1013,6 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi doSyncToStatic(getLocation()); } - protected void doSetAutoImportCurrentModule(boolean autoImport) { - this.autoImportCurrentModule = autoImport; - doAutoImportCurrentModule(); - } - public boolean isSyncToStaticListing() { return syncToStaticListing; } @@ -1117,13 +1098,13 @@ public class DebuggerListingProvider extends CodeViewerProvider implements Listi if (!syncToStaticListing || trackedStatic == null) { Swing.runIfSwingOrRunLater(() -> { goTo(curView, loc); - doAutoImportCurrentModule(); + doCheckCurrentModuleMissing(); }); } else { Swing.runIfSwingOrRunLater(() -> { goTo(curView, loc); - doAutoImportCurrentModule(); + doCheckCurrentModuleMissing(); plugin.fireStaticLocationEvent(trackedStatic); }); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerModuleImportDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerModuleImportDialog.java deleted file mode 100644 index 870984f473..0000000000 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerModuleImportDialog.java +++ /dev/null @@ -1,251 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.plugin.core.debug.gui.listing; - -import java.awt.BorderLayout; -import java.io.File; -import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.Function; - -import javax.swing.*; -import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; - -import docking.DialogComponentProvider; -import docking.widgets.filechooser.GhidraFileChooser; -import docking.widgets.table.CellEditorUtils; -import docking.widgets.table.DefaultEnumeratedColumnTableModel; -import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; -import ghidra.app.plugin.core.debug.gui.DebuggerResources; -import ghidra.app.services.FileImporterService; -import ghidra.framework.main.AppInfo; -import ghidra.framework.model.DomainFolder; -import ghidra.framework.model.Project; -import ghidra.framework.plugintool.PluginTool; -import ghidra.util.MessageType; -import ghidra.util.table.GhidraTable; -import ghidra.util.table.GhidraTableFilterPanel; - -public class DebuggerModuleImportDialog extends DialogComponentProvider { - static final String BLANK = ""; - static final int BUTTON_SIZE = 32; - - protected static class FileRow { - private final File file; - private boolean isIgnored; - - public FileRow(File file) { - this.file = file; - } - - public File getFile() { - return file; - } - - public boolean isIgnored() { - return isIgnored; - } - - public void setIgnored(boolean isIgnored) { - this.isIgnored = isIgnored; - } - } - - protected static enum FileTableColumns - implements EnumeratedTableColumn { - REMOVE("Remove", String.class, m -> BLANK, (m, v) -> nop()), - IGNORE("Ignore", Boolean.class, FileRow::isIgnored, FileRow::setIgnored), - PATH("Path", File.class, FileRow::getFile), - IMPORT("Import", String.class, m -> BLANK, (m, v) -> nop()); - - private String header; - private Class cls; - private Function getter; - private BiConsumer setter; - - private static void nop() { - } - - @SuppressWarnings("unchecked") - FileTableColumns(String header, Class cls, - Function getter, BiConsumer setter) { - this.header = header; - this.cls = cls; - this.getter = getter; - this.setter = (BiConsumer) setter; - } - - FileTableColumns(String header, Class cls, - Function getter) { - this(header, cls, getter, null); - } - - @Override - public String getHeader() { - return header; - } - - @Override - public Class getValueClass() { - return cls; - } - - @Override - public Object getValueOf(FileRow row) { - return getter.apply(row); - } - - @Override - public boolean isEditable(FileRow row) { - return setter != null; - } - - @Override - public void setValueOf(FileRow row, Object value) { - setter.accept(row, value); - } - } - - protected static class FileTableModel - extends DefaultEnumeratedColumnTableModel { - public FileTableModel() { - super("Suggested Files to Import", FileTableColumns.class); - } - } - - private final PluginTool tool; - - final FileTableModel fileTableModel = new FileTableModel(); - private final Map map = new HashMap<>(); - - private GhidraTable fileTable; - private GhidraTableFilterPanel fileFilterPanel; - - protected DebuggerModuleImportDialog(PluginTool tool) { - super("Suggested Modules to Import", false, true, false, false); - this.tool = tool; - - populateComponents(); - } - - protected void populateComponents() { - JPanel panel = new JPanel(new BorderLayout()); - - fileTable = new GhidraTable(fileTableModel); - fileTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - panel.add(new JScrollPane(fileTable)); - - fileFilterPanel = new GhidraTableFilterPanel<>(fileTable, fileTableModel); - panel.add(fileFilterPanel, BorderLayout.SOUTH); - - TableColumnModel columnModel = fileTable.getColumnModel(); - - TableColumn removeCol = columnModel.getColumn(FileTableColumns.REMOVE.ordinal()); - CellEditorUtils.installButton(fileTable, fileFilterPanel, removeCol, - DebuggerResources.ICON_DELETE, BUTTON_SIZE, this::removeFile); - - TableColumn ignoreCol = columnModel.getColumn(FileTableColumns.IGNORE.ordinal()); - ignoreCol.setPreferredWidth(30); - - TableColumn importCol = columnModel.getColumn(FileTableColumns.IMPORT.ordinal()); - CellEditorUtils.installButton(fileTable, fileFilterPanel, importCol, - DebuggerResources.ICON_IMPORT, BUTTON_SIZE, this::importFile); - - addWorkPanel(panel); - } - - private void importFile(FileRow mod) { - FileImporterService importerService = tool.getService(FileImporterService.class); - if (importerService == null) { - setStatusText("No FileImporterService!", MessageType.ERROR); - return; - } - GhidraFileChooser chooser = new GhidraFileChooser(getComponent()); - chooser.setSelectedFile(mod.getFile()); - File file = chooser.getSelectedFile(); // Shows modal - if (file == null) { // Includes cancelled case - return; - } - Project activeProject = Objects.requireNonNull(AppInfo.getActiveProject()); - DomainFolder root = activeProject.getProjectData().getRootFolder(); - importerService.importFile(root, file); - removeFile(mod); - } - - private void removeFile(FileRow mod) { - removeFiles(Set.of(mod.getFile())); - } - - public void show() { - tool.showDialog(this); - } - - /** - * Suggest files to import. - * - *

- * If this causes a change to the suggested file list, or the list is not currently showing, the - * dialog will be shown. The user may leave the list in the background to avoid being pestered - * again. - * - * @param files the collection of files to suggest importing - */ - public void addFiles(Collection files) { - synchronized (map) { - List mods = new ArrayList<>(); - for (File file : files) { - map.computeIfAbsent(file, f -> { - FileRow mod = new FileRow(f); - mods.add(mod); - return mod; - }); - } - fileTableModel.addAll(mods); - // Do not steal focus if suggested files are already on screen, or ignored - boolean anyNotIgnored = - fileTableModel.getModelData().stream().anyMatch(r -> !r.isIgnored()); - if (!mods.isEmpty() || (!isShowing() && anyNotIgnored)) { - show(); - } - } - } - - /** - * Remove suggested files from the dialog. - * - *

- * If this causes the list to become empty, the dialog is automatically hidden. - * - * @param files the collection of files to no longer suggest - */ - public void removeFiles(Collection files) { - synchronized (map) { - Set mods = new HashSet<>(); - for (File file : files) { - FileRow mod = map.remove(file); - if (mod != null) { - mods.add(mod); - } - } - fileTableModel.deleteWith(mods::contains); - - if (fileTableModel.getModelData().isEmpty()) { - close(); - } - } - } -} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerMissingModuleActionContext.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerMissingModuleActionContext.java new file mode 100644 index 0000000000..86c31cc0fb --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerMissingModuleActionContext.java @@ -0,0 +1,55 @@ +/* ### + * 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.modules; + +import java.util.Objects; + +import docking.ActionContext; +import ghidra.trace.model.modules.TraceModule; + +public class DebuggerMissingModuleActionContext extends ActionContext { + private final TraceModule module; + private final int hashCode; + + public DebuggerMissingModuleActionContext(TraceModule module) { + this.module = Objects.requireNonNull(module); + this.hashCode = Objects.hash(getClass(), module); + } + + public TraceModule getModule() { + return module; + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DebuggerMissingModuleActionContext)) { + return false; + } + DebuggerMissingModuleActionContext that = (DebuggerMissingModuleActionContext) obj; + if (!this.module.equals(that.module)) { + return false; + } + return true; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java index cdf0674a13..4bd84e7aea 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPlugin.java @@ -25,23 +25,23 @@ import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; @PluginInfo( // - shortDescription = "Debugger module and section manager", // - description = "GUI to manage modules and sections", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { - ProgramActivatedPluginEvent.class, // - ProgramLocationPluginEvent.class, // - ProgramClosedPluginEvent.class, // - TraceActivatedPluginEvent.class, // - }, // - servicesRequired = { // - DebuggerModelService.class, // - DebuggerStaticMappingService.class, // - DebuggerTraceManagerService.class, // - ProgramManager.class, // - } // + shortDescription = "Debugger module and section manager", // + description = "GUI to manage modules and sections", // + category = PluginCategoryNames.DEBUGGER, // + packageName = DebuggerPluginPackage.NAME, // + status = PluginStatus.RELEASED, // + eventsConsumed = { + ProgramActivatedPluginEvent.class, // + ProgramLocationPluginEvent.class, // + ProgramClosedPluginEvent.class, // + TraceActivatedPluginEvent.class, // + }, // + servicesRequired = { // + DebuggerModelService.class, // + DebuggerStaticMappingService.class, // + DebuggerTraceManagerService.class, // + ProgramManager.class, // + } // ) public class DebuggerModulesPlugin extends AbstractDebuggerPlugin { protected DebuggerModulesProvider provider; @@ -58,6 +58,7 @@ public class DebuggerModulesPlugin extends AbstractDebuggerPlugin { @Override protected void dispose() { + provider.dispose(); tool.removeComponentProvider(provider); super.dispose(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java index 146041c632..a7cfc0bd23 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java @@ -458,15 +458,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { return; } TraceModule mod = modules.iterator().next(); - GhidraFileChooser chooser = new GhidraFileChooser(getComponent()); - chooser.setSelectedFile(new File(mod.getName())); - File file = chooser.getSelectedFile(); - if (file == null) { // Perhaps cancelled - return; - } - Project activeProject = Objects.requireNonNull(AppInfo.getActiveProject()); - DomainFolder root = activeProject.getProjectData().getRootFolder(); - importerService.importFile(root, file); + importModuleFromFileSystem(mod); } @Override @@ -508,6 +500,8 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { @AutoServiceConsumed private DebuggerListingService listingService; @AutoServiceConsumed + private DebuggerConsoleService consoleService; + @AutoServiceConsumed ProgramManager programManager; @AutoServiceConsumed private GoToService goToService; @@ -551,6 +545,9 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { DockingAction actionMapSectionTo; DockingAction actionMapSectionsTo; + DockingAction actionImportMissingModule; + DockingAction actionMapMissingModule; + SelectAddressesAction actionSelectAddresses; CaptureTypesAction actionCaptureTypes; CaptureSymbolsAction actionCaptureSymbols; @@ -580,6 +577,18 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { createActions(); } + private void importModuleFromFileSystem(TraceModule module) { + GhidraFileChooser chooser = new GhidraFileChooser(getComponent()); + chooser.setSelectedFile(new File(module.getName())); + File file = chooser.getSelectedFile(); + if (file == null) { // Perhaps cancelled + return; + } + Project activeProject = Objects.requireNonNull(AppInfo.getActiveProject()); + DomainFolder root = activeProject.getProjectData().getRootFolder(); + importerService.importFile(root, file); + } + @AutoServiceConsumed private void setModelService(DebuggerModelService modelService) { if (this.modelService != null) { @@ -592,6 +601,29 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { contextChanged(); } + @AutoServiceConsumed + private void setConsoleService(DebuggerConsoleService consoleService) { + if (consoleService != null) { + if (actionImportMissingModule != null) { + consoleService.addResolutionAction(actionImportMissingModule); + } + if (actionMapMissingModule != null) { + consoleService.addResolutionAction(actionMapMissingModule); + } + } + } + + protected void dispose() { + if (consoleService != null) { + if (actionImportMissingModule != null) { + consoleService.removeResolutionAction(actionImportMissingModule); + } + if (actionMapMissingModule != null) { + consoleService.removeResolutionAction(actionMapMissingModule); + } + } + } + @Override public ActionContext getActionContext(MouseEvent event) { if (myActionContext == null) { @@ -743,6 +775,15 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { .onAction(this::activatedMapSectionsTo) .buildAndInstallLocal(this); + actionImportMissingModule = ImportMissingModuleAction.builder(plugin) + .withContext(DebuggerMissingModuleActionContext.class) + .onAction(this::activatedImportMissingModule) + .build(); + actionMapMissingModule = MapMissingModuleAction.builder(plugin) + .withContext(DebuggerMissingModuleActionContext.class) + .onAction(this::activatedMapMissingModule) + .build(); + actionSelectAddresses = new SelectAddressesAction(); actionCaptureTypes = new CaptureTypesAction(); actionCaptureSymbols = new CaptureSymbolsAction(); @@ -818,6 +859,19 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { mapSectionTo(sel.iterator().next()); } + private void activatedImportMissingModule(DebuggerMissingModuleActionContext context) { + if (importerService == null) { + Msg.error(this, "Import service is not present"); + } + importModuleFromFileSystem(context.getModule()); + consoleService.remove(context); // TODO: Should remove when mapping is created + } + + private void activatedMapMissingModule(DebuggerMissingModuleActionContext context) { + mapModuleTo(context.getModule()); + consoleService.remove(context); // TODO: Should remove when mapping is created + } + private void toggledFilter(ActionContext ignored) { if (actionFilterSectionsByModules.isSelected()) { sectionFilterPanel.setSecondaryFilter(filterSectionsBySelectedModules); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java index 4243d8b94c..38fe49708d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java @@ -159,14 +159,14 @@ public class DebuggerModelServicePlugin extends Plugin protected class ListenerOnRecorders implements TraceRecorderListener { @Override public void snapAdvanced(TraceRecorder recorder, long snap) { - TimedMsg.info(this, "Got snapAdvanced callback"); + TimedMsg.debug(this, "Got snapAdvanced callback"); fireSnapEvent(recorder, snap); List copy; synchronized (proxies) { copy = List.copyOf(proxies); } for (DebuggerModelServiceProxyPlugin proxy : copy) { - TimedMsg.info(this, "Firing SnapEvent on " + proxy); + TimedMsg.debug(this, "Firing SnapEvent on " + proxy); proxy.fireSnapEvent(recorder, snap); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultThreadRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultThreadRecorder.java index 7adfeef18f..cfc9d349f8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultThreadRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultThreadRecorder.java @@ -264,7 +264,7 @@ public class DefaultThreadRecorder implements ManagedThreadRecorder { int frameLevel = stackRecorder.getSuccessorFrameLevel(bank); long snap = recorder.getSnap(); String path = bank.getJoinedPath("."); - TimedMsg.info(this, "Reg values changed: " + updates.keySet()); + TimedMsg.debug(this, "Reg values changed: " + updates.keySet()); recorder.parTx.execute("Registers " + path + " changed", () -> { TraceCodeManager codeManager = trace.getCodeManager(); TraceCodeRegisterSpace codeRegisterSpace = @@ -377,7 +377,7 @@ public class DefaultThreadRecorder implements ManagedThreadRecorder { if (targetRange == null) { return AsyncUtils.NIL; } - TimedMsg.info(this, + TimedMsg.debug(this, " Reading memory at " + name + " (" + targetAddress + " -> " + targetRange + ")"); // NOTE: Recorder takes data via memoryUpdated callback // TODO: In that callback, sort out process memory from thread memory? diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java index a589345113..1d2f5ffd4d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java @@ -119,7 +119,7 @@ public class TraceEventListener extends AnnotatedDebuggerAttributeListener { if (!valid) { return; } - TimedMsg.info(this, "Event: " + type + " thread=" + eventThread + " description=" + + TimedMsg.debug(this, "Event: " + type + " thread=" + eventThread + " description=" + description + " params=" + parameters); // Just use this to step the snaps. Creation/destruction still handled in add/remove if (eventThread == null) { @@ -162,7 +162,7 @@ public class TraceEventListener extends AnnotatedDebuggerAttributeListener { if (!valid) { return; } - TimedMsg.info(this, "State " + state + " for " + stateful); + TimedMsg.debug(this, "State " + state + " for " + stateful); TargetObject x = recorder.objectManager.findThreadOrProcess(stateful); if (x != null) { if (x == target && state == TargetExecutionState.TERMINATED) { @@ -204,7 +204,7 @@ public class TraceEventListener extends AnnotatedDebuggerAttributeListener { } Address traceAddr = recorder.getMemoryMapper().targetToTrace(address); long snap = recorder.getSnap(); - TimedMsg.info(this, "Memory updated: " + address + " (" + data.length + ")"); + TimedMsg.debug(this, "Memory updated: " + address + " (" + data.length + ")"); String path = memory.getJoinedPath("."); recorder.parTx.execute("Memory observed: " + path, () -> { memoryManager.putBytes(snap, traceAddr, ByteBuffer.wrap(data)); 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 9347b14967..f8f2a03f07 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 @@ -618,7 +618,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } else if (event instanceof TraceRecorderAdvancedPluginEvent) { TraceRecorderAdvancedPluginEvent ev = (TraceRecorderAdvancedPluginEvent) event; - TimedMsg.info(this, "Processing trace-advanced event"); + TimedMsg.debug(this, "Processing trace-advanced event"); doTraceRecorderAdvanced(ev.getRecorder(), ev.getSnap()); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerConsoleService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerConsoleService.java new file mode 100644 index 0000000000..ad1654599b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerConsoleService.java @@ -0,0 +1,87 @@ +/* ### + * 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.services; + +import javax.swing.Icon; + +import docking.ActionContext; +import docking.action.DockingActionIf; +import ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin; +import ghidra.dbg.DebuggerConsoleLogger; +import ghidra.framework.plugintool.ServiceInfo; +import ghidra.util.HTMLUtilities; + +@ServiceInfo(defaultProvider = DebuggerConsolePlugin.class) +public interface DebuggerConsoleService extends DebuggerConsoleLogger { + + /** + * Log an actionable message to the console + * + *

+ * WARNING: The log accepts and will interpret HTML in its messages, allowing a rich and + * flexible display; however, you MUST sanitize any content derived from the user or target. We + * recommend using {@link HTMLUtilities#escapeHTML(String)}. + * + * @param icon an icon for the message + * @param message the HTML-formatted message + * @param context an (immutable) context for actions + */ + void log(Icon icon, String message, ActionContext context); + + /** + * Remove an actionable message from the console + * + *

+ * It is common courtesy to remove the entry when the user has resolved the issue, whether via + * the presented actions, or some other means. The framework does not do this automatically, + * because simply activating an action does not imply the issue will be resolved. + * + * @param context the context of the entry to remove + */ + void remove(ActionContext context); + + /** + * Add an action which might be applied to an actionable log message + * + *

+ * Please invoke this method from the Swing thread. Only toolbar and pop-up menu placement is + * considered. Toolbar actions are placed as icon-only buttons in the "Actions" column for any + * log message where the action is applicable to the context given for that message. Pop-up + * actions are placed in the context menu when a single message is selected and the action is + * applicable to its given context. In most cases, the action should be presented both as a + * button and as a pop-up menu. Less commonly, an action may be presented only as a pop-up, + * likely because it is an uncommon resolution, or because you don't want the user to activated + * it accidentally. Rarely, if ever, should an action be a button, but not in the menu. The user + * may expect the menu to give more complete descriptions of actions presented as buttons. + * + *

+ * IMPORTANT: Unlike other action managers, you are required to remove your actions upon + * plugin disposal. + * + * @param action the action + */ + void addResolutionAction(DockingActionIf action); + + /** + * Remove an action + * + *

+ * Please invoke this method from the Swing thread. + * + * @param action the action + */ + void removeResolutionAction(DockingActionIf action); +} diff --git a/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool b/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool index d24e0a8845..37a33dd7a4 100644 --- a/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool +++ b/Ghidra/Debug/Debugger/src/main/resources/defaultTools/Debugger.tool @@ -7,145 +7,154 @@ + - + - - - + + + - + - + - - - - + + + + + - - + + - + - + - - - - - - - - - - - - + + + + + + + + + + + + + + - + - + - + - + - - - - - + + + + - - + + + - + - + - + - + - + - + - + - + - + - + - + + + + + + - + @@ -155,54 +164,26 @@ - + + + + + + - + - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -233,32 +214,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -311,52 +266,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -369,56 +278,15 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + @@ -439,19 +307,6 @@ - - - - - - - - - - - - - @@ -467,15 +322,15 @@ - - + + - - - - - - + + + + + + @@ -483,6 +338,24 @@ + + + + + + + + + + + + + + + + + + @@ -496,38 +369,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -629,12 +470,12 @@ - - - - - - + + + + + + @@ -642,6 +483,430 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -670,41 +935,15 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + @@ -727,23 +966,6 @@ - - - - - - - - - - - - - - - - - @@ -795,54 +1017,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -879,6 +1053,21 @@ + + + + + + + + + + + + + + + diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePluginScreenShots.java new file mode 100644 index 0000000000..e4a8165e28 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePluginScreenShots.java @@ -0,0 +1,77 @@ +/* ### + * 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.console; + +import static org.junit.Assert.assertEquals; + +import org.junit.*; +import org.junit.rules.TestName; + +import docking.ActionContext; +import docking.action.builder.ActionBuilder; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.util.Msg; +import help.screenshot.GhidraScreenShotGenerator; + +public class DebuggerConsolePluginScreenShots extends GhidraScreenShotGenerator { + + public static class ScreenShotActionContext extends ActionContext { + } + + DebuggerConsolePlugin consolePlugin; + DebuggerConsoleProvider consoleProvider; + + @Rule + public TestName name = new TestName(); + + @Before + public void setUpMine() throws Throwable { + consolePlugin = addPlugin(tool, DebuggerConsolePlugin.class); + consoleProvider = waitForComponentProvider(DebuggerConsoleProvider.class); + + consolePlugin.addResolutionAction(new ActionBuilder("Import", name.getMethodName()) + .toolBarIcon(DebuggerResources.ICON_IMPORT) + .popupMenuIcon(DebuggerResources.ICON_IMPORT) + .popupMenuPath("Map") + .description("Import") + .withContext(ScreenShotActionContext.class) + .onAction(ctx -> Msg.info(this, "Import clicked")) + .build()); + consolePlugin.addResolutionAction(new ActionBuilder("Map", name.getMethodName()) + .toolBarIcon(DebuggerResources.ICON_MODULES) + .popupMenuIcon(DebuggerResources.ICON_MODULES) + .popupMenuPath("Map") + .description("Map") + .withContext(ScreenShotActionContext.class) + .onAction(ctx -> Msg.info(this, "Map clicked")) + .build()); + } + + @Test + public void testCaptureDebuggerConsolePlugin() throws Throwable { + Msg.warn(this, "This is a warning message"); + Msg.error(this, "This is an error message"); + consolePlugin.log(DebuggerResources.ICON_DEBUGGER, + "You can take action to resolve this message", + new ScreenShotActionContext()); + + AbstractGhidraHeadedDebuggerGUITest + .waitForPass(() -> assertEquals(3, consolePlugin.getRowCount(ActionContext.class))); + + captureIsolatedProvider(consoleProvider, 600, 300); + } +} diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPluginScreenShots.java index f70f4dea2c..91a77409ab 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingPluginScreenShots.java @@ -31,7 +31,6 @@ import ghidra.test.ToyProgramBuilder; import ghidra.trace.database.ToyDBTraceBuilder; import ghidra.trace.model.memory.TraceMemoryFlag; import ghidra.trace.model.memory.TraceMemoryRegisterSpace; -import ghidra.trace.model.modules.TraceModule; import ghidra.trace.model.symbol.*; import ghidra.trace.model.thread.TraceThread; import ghidra.util.database.UndoableTransaction; @@ -140,36 +139,4 @@ public class DebuggerListingPluginScreenShots extends GhidraScreenShotGenerator captureDialog(dialog); } - - @Test - public void testCaptureDebuggerModuleImportDialog() throws Throwable { - try (UndoableTransaction tid = tb.startTransaction()) { - long snap = tb.trace.getTimeManager().createSnapshot("First").getKey(); - tb.trace.getMemoryManager() - .addRegion("bash:.text", Range.atLeast(0L), tb.range(0x00400000, 0x0040ffff), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); - tb.trace.getMemoryManager() - .addRegion("libc:.text", Range.atLeast(0L), tb.range(0x7fac0000, 0x7facffff), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); - - TraceModule bin = tb.trace.getModuleManager() - .addLoadedModule("/bin/bash", "/bin/bash", - tb.range(0x00400000, 0x0040ffff), snap); - bin.addSection("bash[.text]", tb.range(0x00400000, 0x0040ffff)); - TraceModule lib = tb.trace.getModuleManager() - .addLoadedModule("/lib/libc.so.6", "/lib/libc.so.6", - tb.range(0x7fac0000, 0x7facffff), snap); - lib.addSection("libc[.text]", tb.range(0x7fac0000, 0x7facffff)); - - traceManager.openTrace(tb.trace); - traceManager.activateTrace(tb.trace); - - listingPlugin.goTo(tb.addr(0x7fac1234), true); - waitForSwing(); - listingPlugin.goTo(tb.addr(0x00401234), true); - waitForSwing(); - - captureDialog(DebuggerModuleImportDialog.class); - } - } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProviderTest.java new file mode 100644 index 0000000000..055bebd24d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProviderTest.java @@ -0,0 +1,78 @@ +/* ### + * 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.console; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +import docking.ActionContext; +import docking.action.builder.ActionBuilder; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.util.Msg; + +public class DebuggerConsoleProviderTest extends AbstractGhidraHeadedDebuggerGUITest { + DebuggerConsolePlugin consolePlugin; + DebuggerConsoleProvider consoleProvider; + + @Before + public void setUpConsoleProviderTest() throws Exception { + consolePlugin = addPlugin(tool, DebuggerConsolePlugin.class); + consoleProvider = waitForComponentProvider(DebuggerConsoleProvider.class); + } + + public static class TestConsoleActionContext extends ActionContext { + + } + + @Test + public void testActions() throws Exception { + consolePlugin.addResolutionAction(new ActionBuilder("Add", name.getMethodName()) + .toolBarIcon(DebuggerResources.ICON_ADD) + .description("Add") + .withContext(TestConsoleActionContext.class) + .onAction(ctx -> Msg.info(this, "Add clicked")) + .build()); + consolePlugin.addResolutionAction(new ActionBuilder("Delete", name.getMethodName()) + .popupMenuIcon(DebuggerResources.ICON_DELETE) + .popupMenuPath("Delete") + .description("Delete") + .withContext(TestConsoleActionContext.class) + .onAction(ctx -> Msg.info(this, "Delete clicked")) + .build()); + + consolePlugin.log(DebuggerResources.ICON_DEBUGGER, "Test message", + new TestConsoleActionContext()); + consolePlugin.log(DebuggerResources.ICON_DEBUGGER, "Test message 2", + new TestConsoleActionContext()); + + waitForPass(() -> assertEquals(2, consoleProvider.logTable.getRowCount())); + } + + @Test + public void testHTMLLabel() throws Exception { + consolePlugin.log(DebuggerResources.ICON_DEBUGGER, + "A rather lengthy test message. " + + "Here's some more text just to prove it!", + new TestConsoleActionContext()); + consolePlugin.log(DebuggerResources.ICON_DEBUGGER, "Test message 2", + new TestConsoleActionContext()); + + waitForPass(() -> assertEquals(2, consoleProvider.logTable.getRowCount())); + } +} 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 32f4ff4dbb..902cc2fadd 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 @@ -36,6 +36,8 @@ 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.console.DebuggerConsolePlugin; +import ghidra.app.plugin.core.debug.gui.modules.DebuggerMissingModuleActionContext; import ghidra.app.services.*; import ghidra.app.util.viewer.listingpanel.ListingPanel; import ghidra.async.SwingExecutorService; @@ -1078,9 +1080,9 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI } @Test - public void testActionAutoImportCurrentModuleWithSections() throws Exception { + public void testPromptImportCurrentModuleWithSections() throws Exception { addPlugin(tool, ImporterPlugin.class); - assertTrue(listingProvider.actionAutoImportCurrentModule.isEnabled()); + DebuggerConsolePlugin consolePlugin = addPlugin(tool, DebuggerConsolePlugin.class); createAndOpenTrace(); try (UndoableTransaction tid = tb.startTransaction()) { @@ -1099,16 +1101,19 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI // In the module, but not in its section listingPlugin.goTo(tb.addr(0x00411234), true); waitForSwing(); - assertFalse(listingProvider.importDialog.isVisible()); + waitForPass(() -> assertEquals(0, + consolePlugin.getRowCount(DebuggerMissingModuleActionContext.class))); listingPlugin.goTo(tb.addr(0x00401234), true); - waitForDialogComponent(DebuggerModuleImportDialog.class); + waitForSwing(); + waitForPass(() -> assertEquals(1, + consolePlugin.getRowCount(DebuggerMissingModuleActionContext.class))); } @Test - public void testActionAutoImportCurrentModuleWithoutSections() throws Exception { + public void testPromptImportCurrentModuleWithoutSections() throws Exception { addPlugin(tool, ImporterPlugin.class); - assertTrue(listingProvider.actionAutoImportCurrentModule.isEnabled()); + DebuggerConsolePlugin consolePlugin = addPlugin(tool, DebuggerConsolePlugin.class); createAndOpenTrace(); try (UndoableTransaction tid = tb.startTransaction()) { @@ -1116,7 +1121,7 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI .addRegion("bash:.text", Range.atLeast(0L), tb.range(0x00400000, 0x0041ffff), Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); - TraceModule bin = tb.trace.getModuleManager() + tb.trace.getModuleManager() .addLoadedModule("/bin/bash", "/bin/bash", tb.range(0x00400000, 0x0041ffff), 0); @@ -1125,7 +1130,9 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerGUI // In the module, but not in its section listingPlugin.goTo(tb.addr(0x00411234), true); - waitForDialogComponent(DebuggerModuleImportDialog.class); + waitForSwing(); + waitForPass(() -> assertEquals(1, + consolePlugin.getRowCount(DebuggerMissingModuleActionContext.class))); } @Test diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerConsoleLogger.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerConsoleLogger.java new file mode 100644 index 0000000000..95b53b58e4 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerConsoleLogger.java @@ -0,0 +1,20 @@ +/* ### + * 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.dbg; + +public interface DebuggerConsoleLogger { + +} diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceDomainObjectListener.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceDomainObjectListener.java index 6f91f6447c..c79a4fea13 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceDomainObjectListener.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/TraceDomainObjectListener.java @@ -165,7 +165,7 @@ public class TraceDomainObjectListener implements DomainObjectListener { for (DomainObjectChangeRecord rec : ev) { if (rec.getEventType() == DomainObject.DO_OBJECT_RESTORED) { restoredHandler.accept(rec); - TimedMsg.info(this, " Done: OBJECT_RESTORED"); + TimedMsg.debug(this, " Done: OBJECT_RESTORED"); return; } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DefaultTraceTimeViewport.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DefaultTraceTimeViewport.java index d91720a553..8a23e06dfb 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DefaultTraceTimeViewport.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/util/DefaultTraceTimeViewport.java @@ -328,14 +328,16 @@ public class DefaultTraceTimeViewport implements TraceTimeViewport { @Override public T getTop(Function func) { - synchronized (ordered) { - for (Range rng : ordered) { - T t = func.apply(rng.upperEndpoint()); - if (t != null) { - return t; + try (LockHold hold = trace.lockRead()) { + synchronized (ordered) { + for (Range rng : ordered) { + T t = func.apply(rng.upperEndpoint()); + if (t != null) { + return t; + } } + return null; } - return null; } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java index 2691408886..e0fadc6477 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/CustomToStringCellRenderer.java @@ -18,10 +18,15 @@ package docking.widgets.table; import java.awt.Component; import java.awt.Font; import java.math.BigInteger; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.function.BiFunction; -import javax.swing.JTable; +import javax.swing.*; +import javax.swing.plaf.basic.BasicHTML; import javax.swing.table.TableModel; +import javax.swing.text.View; import ghidra.docking.settings.Settings; import ghidra.util.table.column.AbstractGColumnRenderer; @@ -40,6 +45,14 @@ public class CustomToStringCellRenderer extends AbstractGColumnRenderer { return v.signum() < 0 ? "-0x" + v.negate().toString(16) : "0x" + v.toString(16); } + public static final DateFormat TIME_FORMAT_24HMSms = new SimpleDateFormat("HH:mm:ss.SSS"); + + public static final CustomToStringCellRenderer TIME_24HMSms = + new CustomToStringCellRenderer<>(CustomFont.DEFAULT, Date.class, + (v, s) -> v == null ? "" : TIME_FORMAT_24HMSms.format(v), false); + public static final CustomToStringCellRenderer HTML = + new CustomToStringCellRenderer(CustomFont.DEFAULT, String.class, + (v, s) -> v == null ? "" : v, true); public static final CustomToStringCellRenderer MONO_OBJECT = new CustomToStringCellRenderer<>(CustomFont.MONOSPACED, Object.class, (v, s) -> v == null ? "" : v.toString(), false); @@ -60,6 +73,9 @@ public class CustomToStringCellRenderer extends AbstractGColumnRenderer { private final Class cls; private final BiFunction toString; + private final JPanel panelForSize = new JPanel(); + private final BoxLayout layoutForSize = new BoxLayout(panelForSize, BoxLayout.Y_AXIS); + public CustomToStringCellRenderer(Class cls, BiFunction toString, boolean enableHtml) { this(null, cls, toString, enableHtml); @@ -71,6 +87,8 @@ public class CustomToStringCellRenderer extends AbstractGColumnRenderer { this.customFont = font; this.cls = cls; this.toString = toString; + + panelForSize.setLayout(layoutForSize); } @Override @@ -100,6 +118,21 @@ public class CustomToStringCellRenderer extends AbstractGColumnRenderer { public Component getTableCellRendererComponent(GTableCellRenderingData data) { super.getTableCellRendererComponent(data); setText(toString.apply(cls.cast(data.getValue()), data.getColumnSettings())); + if (getHTMLRenderingEnabled()) { + setVerticalAlignment(SwingConstants.TOP); + } + else { + setVerticalAlignment(SwingConstants.CENTER); + } return this; } + + public int getRowHeight(int colWidth) { + View v = (View) getClientProperty(BasicHTML.propertyKey); + if (v == null) { + return 0; + } + v.setSize(colWidth, Short.MAX_VALUE); + return (int) v.getPreferredSpan(View.Y_AXIS); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/DefaultEnumeratedColumnTableModel.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/DefaultEnumeratedColumnTableModel.java index a78eac755c..02408c4c2f 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/DefaultEnumeratedColumnTableModel.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/DefaultEnumeratedColumnTableModel.java @@ -163,18 +163,22 @@ public class DefaultEnumeratedColumnTableModel & EnumeratedTab @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { - R row = modelData.get(rowIndex); - C col = cols[columnIndex]; - Class cls = col.getValueClass(); - col.setValueOf(row, cls.cast(aValue)); + synchronized (modelData) { + R row = modelData.get(rowIndex); + C col = cols[columnIndex]; + Class cls = col.getValueClass(); + col.setValueOf(row, cls.cast(aValue)); + } fireTableCellUpdated(rowIndex, columnIndex); } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { - R row = modelData.get(rowIndex); - C col = cols[columnIndex]; - return col.isEditable(row); + synchronized (modelData) { + R row = modelData.get(rowIndex); + C col = cols[columnIndex]; + return col.isEditable(row); + } } @Override @@ -200,35 +204,47 @@ public class DefaultEnumeratedColumnTableModel & EnumeratedTab @Override public void add(R row) { - int rowIndex = modelData.size(); - modelData.add(row); + int rowIndex; + synchronized (modelData) { + rowIndex = modelData.size(); + modelData.add(row); + } fireTableRowsInserted(rowIndex, rowIndex); } @Override public void addAll(Collection c) { - int startIndex = modelData.size(); - modelData.addAll(c); - int endIndex = modelData.size() - 1; + int startIndex; + int endIndex; + synchronized (modelData) { + startIndex = modelData.size(); + modelData.addAll(c); + endIndex = modelData.size() - 1; + } fireTableRowsInserted(startIndex, endIndex); } @Override public void notifyUpdated(R row) { - int rowIndex = modelData.indexOf(row); + int rowIndex; + synchronized (modelData) { + rowIndex = modelData.indexOf(row); + } fireTableRowsUpdated(rowIndex, rowIndex); } @Override public List notifyUpdatedWith(Predicate predicate) { int lastIndexUpdated = 0; - ListIterator rit = modelData.listIterator(); List updated = new ArrayList<>(); - while (rit.hasNext()) { - R row = rit.next(); - if (predicate.test(row)) { - lastIndexUpdated = rit.previousIndex(); - updated.add(row); + ListIterator rit = modelData.listIterator(); + synchronized (modelData) { + while (rit.hasNext()) { + R row = rit.next(); + if (predicate.test(row)) { + lastIndexUpdated = rit.previousIndex(); + updated.add(row); + } } } int size = updated.size(); @@ -245,25 +261,30 @@ public class DefaultEnumeratedColumnTableModel & EnumeratedTab @Override public void delete(R row) { - int rowIndex = modelData.indexOf(row); - if (rowIndex == -1) { - return; + int rowIndex; + synchronized (modelData) { + rowIndex = modelData.indexOf(row); + if (rowIndex == -1) { + return; + } + modelData.remove(rowIndex); } - modelData.remove(rowIndex); fireTableRowsDeleted(rowIndex, rowIndex); } @Override public List deleteWith(Predicate predicate) { int lastIndexRemoved = 0; - ListIterator rit = modelData.listIterator(); List removed = new ArrayList<>(); - while (rit.hasNext()) { - R row = rit.next(); - if (predicate.test(row)) { - lastIndexRemoved = rit.previousIndex(); - rit.remove(); - removed.add(row); + synchronized (modelData) { + ListIterator rit = modelData.listIterator(); + while (rit.hasNext()) { + R row = rit.next(); + if (predicate.test(row)) { + lastIndexRemoved = rit.previousIndex(); + rit.remove(); + removed.add(row); + } } } int size = removed.size(); @@ -280,17 +301,28 @@ public class DefaultEnumeratedColumnTableModel & EnumeratedTab @Override public R findFirst(Predicate predicate) { - for (R row : modelData) { - if (predicate.test(row)) { - return row; + synchronized (modelData) { + for (R row : modelData) { + if (predicate.test(row)) { + return row; + } } + return null; } - return null; } @Override public void clear() { - modelData.clear(); + synchronized (modelData) { + modelData.clear(); + } fireTableDataChanged(); } + + @Override + protected void sort(List data, TableSortingContext sortingContext) { + synchronized (data) { + super.sort(data, sortingContext); + } + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java index 9eb08aa349..42359588fb 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java @@ -47,8 +47,12 @@ public class RowWrappedEnumeratedColumnTableModel & Enumerated return map.computeIfAbsent(keyFunc.apply(t), k -> wrapper.apply(t)); } - protected synchronized R delFor(T t) { - return map.remove(keyFunc.apply(t)); + protected R delFor(T t) { + return delKey(keyFunc.apply(t)); + } + + protected synchronized R delKey(K k) { + return map.remove(k); } protected synchronized List rowsFor(Collection c) { @@ -79,9 +83,15 @@ public class RowWrappedEnumeratedColumnTableModel & Enumerated delete(delFor(t)); } + public R deleteKey(K k) { + R r = delKey(k); + delete(r); + return r; + } + public synchronized void deleteAllItems(Collection c) { deleteWith(rowsFor(c)::contains); - map.keySet().removeAll(c); + map.keySet().removeAll(c.stream().map(keyFunc).collect(Collectors.toList())); } public synchronized Map getMap() { diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/TimedMsg.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/TimedMsg.java index a4a9f18f18..cb97b77ab7 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/TimedMsg.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/TimedMsg.java @@ -34,7 +34,7 @@ public class TimedMsg { } } - public static void info(Object originator, String message) { - doMsg(Msg::info, originator, message); + public static void debug(Object originator, String message) { + doMsg(Msg::debug, originator, message); } }