GP-406: Central Debug Console for problems and actions

This commit is contained in:
Dan 2021-05-05 09:35:35 -04:00
parent 2466f85ea8
commit a1cfeebcc9
39 changed files with 2101 additions and 837 deletions

View file

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

View file

@ -74,6 +74,10 @@
target="help/topics/DebuggerModelServicePlugin/DebuggerModelServicePlugin.html" />
</tocdef>
<tocdef id="DebuggerConsolePlugin" text="Debug Console"
sortgroup="c1"
target="help/topics/DebuggerConsolePlugin/DebuggerConsolePlugin.html" />
<tocdef id="DebuggerObjectsPlugin" text="Commands and Objects"
sortgroup="d"
target="help/topics/DebuggerObjectsPlugin/DebuggerObjectsPlugin.html" />

View file

@ -15,9 +15,10 @@
<H2>Error Console</H2>
<P>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
<P>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.</P>
<H2>Settings and Toggles</H2>
@ -38,9 +39,6 @@
<P>In the Dynamic Listing:</P>
<UL>
<LI>"Auto-Import Current Module" will cause the user to be prompted for information to sync
static and dynamic listings.</LI>
<LI>"Sync to Static Listing" controls the tracking of the Static listing.</LI>
</UL>

View file

@ -0,0 +1,68 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<TITLE>Debugger: Memory Regions</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="../../shared/Frontpage.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="plugin"></A>Debugger: Console</H1>
<TABLE width="100%">
<TBODY>
<TR>
<TD align="center" width="100%"><IMG alt="" border="1" src=
"images/DebuggerConsolePlugin.png"></TD>
</TR>
</TBODY>
</TABLE>
<P>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.</P>
<P>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.</P>
<P>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.</P>
<H2>Table Columns</H2>
<P>The table has the following columns:</P>
<UL>
<LI>Icon - an icon to identify the type, topic, or source of a message.</LI>
<LI>Message - the message itself.</LI>
<LI>Actions - if actionable, a row of buttons for available actions.</LI>
<LI>Time - the time the message was generated in 24-hour HH:mm:ss.SSS format.</LI>
</UL>
<H2>Actions</H2>
<P>Not considering extension actions from other plugins, the console provides the
following:</P>
<H3><A name="clear"></A>Clear</H3>
<P>Removes all messages, including actionable messages, from the log.</P>
</BODY>
</HTML>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -131,7 +131,9 @@
computed using information about loaded modules reported by the debugger. For the finer
details, see the <A href=
"help/topics/DebuggerStaticMappingPlugin/DebuggerStaticMappingPlugin.html">Static Mappings</A>
window.</P>
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.</P>
<H3><A name="capture_memory"></A>Capture Memory</H3>
@ -162,29 +164,6 @@
neglect to capture read-only ranges that have been captured previously.</LI>
</UL>
<H3><A name="auto_import_module"></A>Auto-Import Current Module</H3>
<P>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:</P>
<TABLE width="100%">
<TBODY>
<TR>
<TD align="center" width="100%"><IMG alt="" src=
"images/DebuggerModuleImportDialog.png"></TD>
</TR>
</TBODY>
</TABLE>
<P>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 <A href=
"help/topics/DebuggerModulesPlugin/DebuggerModulesPlugin.html">Modules</A> window.</P>
<H2><A name="colors"></A>Tool Options: Colors</H2>
<P>The memory-state and tracked-location background colors can all be configured here.</P>

View file

@ -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.</P>
<H3><A name="import_missing_module"></A>Import Missing Module</H3>
<P>This action is offered to resolve a "Missing Module" console message. It is equivalent to <A
href="#import_from_fs">Import From File System</A> on the missing module.</P>
<H3><A name="map_missing_module"></A>Map Missing Module</H3>
<P>This action is offered to resolve a "Missing Module" console message. It is equivalent to <A
href="#map_module_to">Map Module To</A> on the missing module.</P>
<H3><A name="filter_by_module"></A>Filter Sections by Module</H3>
<P>This action is always available. By default the bottom table displays all sections in the

View file

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

View file

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

View file

@ -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<ActionList> {
static void configureBox(JPanel box) {
box.setLayout(new BoxLayout(box, BoxLayout.X_AXIS));
box.setOpaque(true);
box.setAlignmentX(0.5f);
}
static void ensureCacheSize(List<JButton> buttonCache, int size,
Consumer<JButton> 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<JButton> buttonCache, ActionList value,
Consumer<JButton> 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<JButton> 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;
}
}

View file

@ -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<? extends ActionContext> ctxCls) {
return provider.getRowCount(ctxCls);
}
}

View file

@ -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<LogTableColumns, LogRow> {
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<LogRow, ?> getter;
private final Class<?> cls;
private final SortDirection defaultSortDirection;
private final boolean editable;
<T> LogTableColumns(String header, Class<T> cls, Function<LogRow, T> 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<BoundAction> {
}
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<LogTableColumns> 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<String, Map<String, DockingActionIf>> actionsByOwnerThenName =
new LinkedHashMap<>();
protected final LogTableModel logTableModel = new LogTableModel();
protected GhidraTable logTable;
private GhidraTableFilterPanel<LogRow> logFilterPanel;
private Deque<LogRow> 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()),
"<html>" + 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<String, DockingActionIf> 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<DockingActionIf> 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<DockingActionIf> getPopupActions(Tool tool, ActionContext context) {
return streamActions(context)
.filter(a -> a.isAddToPopup(context))
.collect(Collectors.toList());
}
protected long getRowCount(Class<? extends ActionContext> ctxCls) {
return logTableModel.getModelData()
.stream()
.filter(r -> ctxCls.isInstance(r.context))
.count();
}
}

View file

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

View file

@ -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 {

View file

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

View file

@ -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<LocationTrackingSpec> actionTrackLocation;
protected DockingAction actionGoTo;
protected SyncToStaticListingAction actionSyncToStaticListing;
protected ToggleDockingAction actionAutoImportCurrentModule;
protected FollowsCurrentThreadAction actionFollowsCurrentThread;
protected MultiStateDockingAction<AutoReadMemorySpec> 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<TraceModule, File> missing = new LinkedHashMap<>();
Set<DomainFile> toOpen = new LinkedHashSet<>();
Set<TraceModule> missing = new HashSet<>();
Set<DomainFile> toOpen = new HashSet<>();
TraceModuleManager modMan = trace.getModuleManager();
Collection<TraceModule> 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<DomainFile> 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,
"<html>The module <b><tt>" + HTMLUtilities.escapeHTML(mod.getName()) +
"</tt></b> 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);
});
}

View file

@ -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<FileTableColumns, FileRow> {
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<FileRow, ?> getter;
private BiConsumer<FileRow, Object> setter;
private static void nop() {
}
@SuppressWarnings("unchecked")
<T> FileTableColumns(String header, Class<T> cls,
Function<FileRow, T> getter, BiConsumer<FileRow, T> setter) {
this.header = header;
this.cls = cls;
this.getter = getter;
this.setter = (BiConsumer<FileRow, Object>) setter;
}
<T> FileTableColumns(String header, Class<T> cls,
Function<FileRow, T> 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<FileTableColumns, FileRow> {
public FileTableModel() {
super("Suggested Files to Import", FileTableColumns.class);
}
}
private final PluginTool tool;
final FileTableModel fileTableModel = new FileTableModel();
private final Map<File, FileRow> map = new HashMap<>();
private GhidraTable fileTable;
private GhidraTableFilterPanel<FileRow> 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.
*
* <p>
* 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<File> files) {
synchronized (map) {
List<FileRow> 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.
*
* <p>
* 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<File> files) {
synchronized (map) {
Set<FileRow> 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();
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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?

View file

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

View file

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

View file

@ -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
*
* <p>
* <b>WARNING:</b> 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
*
* <p>
* 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
*
* <p>
* 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.
*
* <p>
* <b>IMPORTANT:</b> 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
*
* <p>
* Please invoke this method from the Swing thread.
*
* @param action the action
*/
void removeResolutionAction(DockingActionIf action);
}

View file

@ -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,
"<html>You can take <b>action</b> to resolve this message</html>",
new ScreenShotActionContext());
AbstractGhidraHeadedDebuggerGUITest
.waitForPass(() -> assertEquals(3, consolePlugin.getRowCount(ActionContext.class)));
captureIsolatedProvider(consoleProvider, 600, 300);
}
}

View file

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

View file

@ -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, "<html><b>Test message</b></html>",
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,
"<html><b>A rather lengthy test message. " +
"Here's some more text just to prove it!</b></html>",
new TestConsoleActionContext());
consolePlugin.log(DebuggerResources.ICON_DEBUGGER, "Test message 2",
new TestConsoleActionContext());
waitForPass(() -> assertEquals(2, consoleProvider.logTable.getRowCount()));
}
}

View file

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

View file

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

View file

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

View file

@ -328,14 +328,16 @@ public class DefaultTraceTimeViewport implements TraceTimeViewport {
@Override
public <T> T getTop(Function<Long, T> func) {
synchronized (ordered) {
for (Range<Long> rng : ordered) {
T t = func.apply(rng.upperEndpoint());
if (t != null) {
return t;
try (LockHold hold = trace.lockRead()) {
synchronized (ordered) {
for (Range<Long> rng : ordered) {
T t = func.apply(rng.upperEndpoint());
if (t != null) {
return t;
}
}
return null;
}
return null;
}
}

View file

@ -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<T> extends AbstractGColumnRenderer<T> {
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<Date> TIME_24HMSms =
new CustomToStringCellRenderer<>(CustomFont.DEFAULT, Date.class,
(v, s) -> v == null ? "<null>" : TIME_FORMAT_24HMSms.format(v), false);
public static final CustomToStringCellRenderer<String> HTML =
new CustomToStringCellRenderer<String>(CustomFont.DEFAULT, String.class,
(v, s) -> v == null ? "<null>" : v, true);
public static final CustomToStringCellRenderer<Object> MONO_OBJECT =
new CustomToStringCellRenderer<>(CustomFont.MONOSPACED, Object.class,
(v, s) -> v == null ? "<null>" : v.toString(), false);
@ -60,6 +73,9 @@ public class CustomToStringCellRenderer<T> extends AbstractGColumnRenderer<T> {
private final Class<T> cls;
private final BiFunction<T, Settings, String> toString;
private final JPanel panelForSize = new JPanel();
private final BoxLayout layoutForSize = new BoxLayout(panelForSize, BoxLayout.Y_AXIS);
public CustomToStringCellRenderer(Class<T> cls, BiFunction<T, Settings, String> toString,
boolean enableHtml) {
this(null, cls, toString, enableHtml);
@ -71,6 +87,8 @@ public class CustomToStringCellRenderer<T> extends AbstractGColumnRenderer<T> {
this.customFont = font;
this.cls = cls;
this.toString = toString;
panelForSize.setLayout(layoutForSize);
}
@Override
@ -100,6 +118,21 @@ public class CustomToStringCellRenderer<T> extends AbstractGColumnRenderer<T> {
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);
}
}

View file

@ -163,18 +163,22 @@ public class DefaultEnumeratedColumnTableModel<C extends Enum<C> & 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<C extends Enum<C> & 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<R> 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<R> notifyUpdatedWith(Predicate<R> predicate) {
int lastIndexUpdated = 0;
ListIterator<R> rit = modelData.listIterator();
List<R> updated = new ArrayList<>();
while (rit.hasNext()) {
R row = rit.next();
if (predicate.test(row)) {
lastIndexUpdated = rit.previousIndex();
updated.add(row);
ListIterator<R> 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<C extends Enum<C> & 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<R> deleteWith(Predicate<R> predicate) {
int lastIndexRemoved = 0;
ListIterator<R> rit = modelData.listIterator();
List<R> 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<R> 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<C extends Enum<C> & EnumeratedTab
@Override
public R findFirst(Predicate<R> 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<R> data, TableSortingContext<R> sortingContext) {
synchronized (data) {
super.sort(data, sortingContext);
}
}
}

View file

@ -47,8 +47,12 @@ public class RowWrappedEnumeratedColumnTableModel<C extends Enum<C> & 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<R> rowsFor(Collection<? extends T> c) {
@ -79,9 +83,15 @@ public class RowWrappedEnumeratedColumnTableModel<C extends Enum<C> & Enumerated
delete(delFor(t));
}
public R deleteKey(K k) {
R r = delKey(k);
delete(r);
return r;
}
public synchronized void deleteAllItems(Collection<T> c) {
deleteWith(rowsFor(c)::contains);
map.keySet().removeAll(c);
map.keySet().removeAll(c.stream().map(keyFunc).collect(Collectors.toList()));
}
public synchronized Map<K, R> getMap() {

View file

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