mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 10:19:23 +02:00
GP-406: Central Debug Console for problems and actions
This commit is contained in:
parent
2466f85ea8
commit
a1cfeebcc9
39 changed files with 2101 additions and 837 deletions
|
@ -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|
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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.</P>
|
||||
|
||||
|
||||
<H3><A name="interrupt"></A>Interrupt</H3>
|
||||
|
||||
|
||||
<P>This action is always available. It interrupts the current target's execution.</P>
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
|
|
@ -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>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue