From 9cb38e493b3f5af60de086c3ae31d6813c805006 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Thu, 15 Feb 2024 14:33:42 -0500 Subject: [PATCH] GP-4323: Add gdb/ssh and gdbserver/ssh connectors --- .../data/debugger-launchers/local-gdb.sh | 11 +- .../data/debugger-launchers/ssh-gdb.sh | 64 +++++++ .../data/debugger-launchers/ssh-gdbserver.sh | 69 ++++++++ .../src/main/py/src/ghidragdb/commands.py | 13 ++ .../app/services/DebuggerConsoleService.java | 29 +++- .../AbstractScriptTraceRmiLaunchOffer.java | 13 +- .../launcher/AbstractTraceRmiLaunchOffer.java | 9 +- .../launcher/ScriptAttributesParser.java | 78 ++++++++- .../service/tracermi/TraceRmiTarget.java | 2 +- .../DebuggerBreakpointMarkerPlugin.java | 2 +- .../DebuggerBreakpointsProvider.java | 2 +- .../gui/console/DebuggerConsolePlugin.java | 11 ++ .../gui/console/DebuggerConsoleProvider.java | 158 +++++++++--------- .../debug/gui/control/TargetActionTask.java | 2 +- .../gui/objects/DebuggerObjectsPlugin.java | 4 +- .../gui/objects/DebuggerObjectsProvider.java | 6 +- .../objects/components/ObjectElementRow.java | 2 +- .../progress/DefaultMonitorReceiver.java | 11 +- .../listing/DebuggerListingProviderTest.java | 8 +- .../framework/plugintool/AutoConfigState.java | 17 ++ 20 files changed, 397 insertions(+), 114 deletions(-) create mode 100755 Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/ssh-gdb.sh create mode 100755 Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/ssh-gdbserver.sh diff --git a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/local-gdb.sh b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/local-gdb.sh index cd59acac6b..3878ab9c38 100755 --- a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/local-gdb.sh +++ b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/local-gdb.sh @@ -19,17 +19,18 @@ #@desc

Launch with gdb

#@desc

This will launch the target on the local machine using gdb. GDB must already #@desc be installed on your system, and it must embed the Python 3 interpreter. You will also -#@desc need protobuf and psutil installed for Python 3. +#@desc need protobuf and psutil installed for Python 3.

#@desc #@menu-group local #@icon icon.debugger #@help TraceRmiLauncherServicePlugin#gdb #@enum StartCmd:str run start starti -#@env OPT_GDB_PATH:str="gdb" "Path to gdb" "The path to gdb. Omit the full path to resolve using the system PATH." -#@env OPT_START_CMD:StartCmd="start" "Run command" "The gdb command to actually run the target." #@arg :str "Image" "The target binary executable image" #@args "Arguments" "Command-line arguments to pass to the target" -#@tty TTY_TARGET +#@env OPT_GDB_PATH:str="gdb" "Path to gdb" "The path to gdb. Omit the full path to resolve using the system PATH." +#@env OPT_START_CMD:StartCmd="start" "Run command" "The gdb command to actually run the target." +#@env OPT_EXTRA_TTY:bool=false "Inferior TTY" "Provide a separate terminal emulator for the target." +#@tty TTY_TARGET if env:OPT_EXTRA_TTY if [ -d ${GHIDRA_HOME}/ghidra/.git ] then @@ -48,6 +49,8 @@ target_image="$1" shift target_args="$@" +# NOTE: Ghidra will leave TTY_TARGET empty, which gdb takes for the same terminal. + "$OPT_GDB_PATH" \ -q \ -ex "set pagination off" \ diff --git a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/ssh-gdb.sh b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/ssh-gdb.sh new file mode 100755 index 0000000000..ee10af08ef --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/ssh-gdb.sh @@ -0,0 +1,64 @@ +#!/usr/bin/bash +## ### +# 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. +## +#@timeout 60000 +#@title gdb via ssh +#@desc +#@desc

Launch with gdb via ssh

+#@desc

This will launch the target on a remote machine using gdb via ssh. +#@desc GDB and an SSH server must already be installed and operational on the remote system, and +#@desc GDB must embed the Python 3 interpreter. The remote SSH server must be configured to allow +#@desc remote port forwarding. You will also need to install the following for GDB's embedded +#@desc version of Python:

+#@desc +#@desc +#@menu-group remote +#@icon icon.debugger +#@help TraceRmiLauncherServicePlugin#gdb +#@enum StartCmd:str run start starti +#@arg :str "Image" "The target binary executable image on the remote system" +#@args "Arguments" "Command-line arguments to pass to the target" +#@env OPT_HOST:str="localhost" "[User@]Host" "The hostname or user@host" +#@env OPT_REMOTE_PORT:int=12345 "Remote Trace RMI Port" "A free port on the remote end to receive and forward the Trace RMI connection." +#@env OPT_EXTRA_SSH_ARGS:str="" "Extra ssh arguments" "Extra arguments to pass to ssh. Use with care." +#@env OPT_GDB_PATH:str="gdb" "Path to gdb" "The path to gdb on the remote system. Omit the full path to resolve using the system PATH." +#@env OPT_START_CMD:StartCmd="start" "Run command" "The gdb command to actually run the target." + +target_image="$1" +shift +target_args="$@" + +ssh "-R$OPT_REMOTE_PORT:$GHIDRA_TRACE_RMI_ADDR" -t $OPT_EXTRA_SSH_ARGS "$OPT_HOST" "TERM='$TERM' '$OPT_GDB_PATH' \ + -q \ + -ex 'set pagination off' \ + -ex 'set confirm off' \ + -ex 'show version' \ + -ex 'python import ghidragdb' \ + -ex 'file \"$target_image\"' \ + -ex 'set args $target_args' \ + -ex 'ghidra trace connect \"localhost:$OPT_REMOTE_PORT\"' \ + -ex 'ghidra trace start' \ + -ex 'ghidra trace sync-enable' \ + -ex '$OPT_START_CMD' \ + -ex 'set confirm on' \ + -ex 'set pagination on'" diff --git a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/ssh-gdbserver.sh b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/ssh-gdbserver.sh new file mode 100755 index 0000000000..b4e62b8a8a --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/ssh-gdbserver.sh @@ -0,0 +1,69 @@ +#!/usr/bin/bash +## ### +# 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. +## +#@timeout 60000 +#@title gdb + gdbserver via ssh +#@desc +#@desc

Launch with local gdb and gdbserver via ssh

+#@desc

This will start gdb on the local system and then use it to connect and launch +#@desc the target in gdbserver on the remote system via ssh. The actual command +#@desc used is, e.g:

+#@desc
target remote | ssh user@host gdbserver - /path/to/image
+#@desc

It may be worth testing this manually to ensure everything is configured correctly. An +#@desc SSH server and gdbserver must already be installed and operational on the remote +#@desc system. GDB must be installed on your local system, it must be compatible with the +#@desc gdbserver on the remote system, and it must embed the Python 3 interpreter. You +#@desc will also need protobuf installed for Python 3 on the local system. There are no +#@desc Python requirements for the remote system.

+#@desc +#@menu-group remote +#@icon icon.debugger +#@help TraceRmiLauncherServicePlugin#gdb +#@arg :str "Image" "The target binary executable image on the remote system" +#@args "Arguments" "Command-line arguments to pass to the target" +#@env OPT_HOST:str="localhost" "[User@]Host" "The hostname or user@host" +#@env OPT_EXTRA_SSH_ARGS:str="" "Extra ssh arguments" "Extra arguments to pass to ssh. Use with care." +#@env OPT_GDBSERVER_PATH:str="gdbserver" "Path to gdbserver (remote)" "The path to gdbserver on the remote system. Omit the full path to resolve using the system PATH." +#@env OPT_EXTRA_GDBSERVER_ARGS:str="" "Extra gdbserver arguments" "Extra arguments to pass to gdbserver. Use with care." +#@env OPT_GDB_PATH:str="gdb" "Path to gdb" "The path to gdb on the local system. Omit the full path to resolve using the system PATH." + +if [ -d ${GHIDRA_HOME}/ghidra/.git ] +then + export PYTHONPATH=$GHIDRA_HOME/ghidra/Ghidra/Debug/Debugger-agent-gdb/build/pypkg/src:$PYTHONPATH + export PYTHONPATH=$GHIDRA_HOME/ghidra/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH +elif [ -d ${GHIDRA_HOME}/.git ] +then + export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-agent-gdb/build/pypkg/src:$PYTHONPATH + export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH +else + export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-agent-gdb/pypkg/src:$PYTHONPATH + export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/pypkg/src:$PYTHONPATH +fi + +"$OPT_GDB_PATH" \ + -q \ + -ex "set pagination off" \ + -ex "set confirm off" \ + -ex "show version" \ + -ex "python import ghidragdb" \ + -ex "set inferior-tty $TTY_TARGET" \ + -ex "target remote | ssh $OPT_EXTRA_SSH_ARGS '$OPT_HOST' '$OPT_GDBSERVER_PATH' $OPT_EXTRA_GDBSERVER_ARGS - $@" \ + -ex "ghidra trace connect \"$GHIDRA_TRACE_RMI_ADDR\"" \ + -ex "ghidra trace start" \ + -ex "ghidra trace sync-enable" \ + -ex "ghidra trace sync-synth-stopped" \ + -ex "set confirm on" \ + -ex "set pagination on" diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py index a706e13dc5..da1736c398 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py @@ -1507,6 +1507,19 @@ def ghidra_trace_sync_disable(*, is_mi, **kwargs): hooks.disable_current_inferior() +@cmd('ghidra trace sync-synth-stopped', '-ghidra-trace-sync-synth-stopped', + gdb.COMMAND_SUPPORT, False) +def ghidra_trace_sync_synth_stopped(*, is_mi, **kwargs): + """ + Act as though the target has just stopped. + + This may need to be invoked immediately after 'ghidra trace sync-enable', + to ensure the first snapshot displays the initial/current target state. + """ + + hooks.on_stop(object()) # Pass a fake event + + @cmd('ghidra util wait-stopped', '-ghidra-util-wait-stopped', gdb.COMMAND_NONE, False) def ghidra_util_wait_stopped(timeout='1', *, is_mi, **kwargs): """ diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerConsoleService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerConsoleService.java index 27db228776..f5b0865ad3 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerConsoleService.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerConsoleService.java @@ -31,26 +31,51 @@ public interface DebuggerConsoleService { * Log a message to the console * *

- * WARNING: See {@link #log(Icon, String, ActionContext)} regarding HTML. + * WARNING: See {@link #log(Icon, String, Throwable, ActionContext)} regarding HTML. * * @param icon an icon for the message * @param message the HTML-formatted message */ void log(Icon icon, String message); + /** + * Log an error message to the console + * + *

+ * WARNING: See {@link #log(Icon, String, Throwable, ActionContext)} regarding HTML. + * + * @param icon an icon for the message + * @param message the HTML-formatted message + * @param error an exception, if applicable + */ + void log(Icon icon, String message, Throwable error); + /** * Log an actionable message to the console * *

+ * WARNING: See {@link #log(Icon, String, Throwable, ActionContext)} regarding HTML. + * + * @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); + + /** + * Log an actionable error message to the console + * + *

* WARNING: The log accepts and will interpret HTML in its messages, allowing a rich and * flexible display; however, you MUST sanitize any content derived from the user or target. We * recommend using {@link HTMLUtilities#escapeHTML(String)}. * * @param icon an icon for the message * @param message the HTML-formatted message + * @param error an exception, if applicable * @param context an (immutable) context for actions */ - void log(Icon icon, String message, ActionContext context); + void log(Icon icon, String message, Throwable error, ActionContext context); /** * Remove an actionable message from the console diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java index ce12b293c9..fcd6bd8e60 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java @@ -22,6 +22,7 @@ import java.util.*; import javax.swing.Icon; import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.TtyCondition; import ghidra.dbg.target.TargetMethod.ParameterDescription; import ghidra.debug.api.tracermi.TerminalSession; import ghidra.program.model.listing.Program; @@ -87,6 +88,11 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi return attrs.parameters(); } + @Override + protected int getConnectionTimeoutMillis() { + return attrs.timeoutMillis(); + } + protected abstract void prepareSubprocess(List commandLine, Map env, Map args, SocketAddress address); @@ -97,9 +103,12 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi Map env = new HashMap<>(System.getenv()); prepareSubprocess(commandLine, env, args, address); - for (String tty : attrs.extraTtys()) { + for (Map.Entry ent : attrs.extraTtys().entrySet()) { + if (!ent.getValue().isActive(args)) { + continue; + } NullPtyTerminalSession ns = nullPtyTerminal(); - env.put(tty, ns.name()); + env.put(ent.getKey(), ns.name()); sessions.put(ns.name(), ns); } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java index 2f5ffc5a69..ab6ac8dc99 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java @@ -64,6 +64,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer public static final String PREFIX_DBGLAUNCH = "DBGLAUNCH_"; public static final String PARAM_DISPLAY_IMAGE = "Image"; + public static final int DEFAULT_TIMEOUT_MILLIS = 10000; protected record PtyTerminalSession(Terminal terminal, Pty pty, PtySession session, Thread waiter) implements TerminalSession { @@ -149,7 +150,11 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer } protected int getTimeoutMillis() { - return 10000; + return DEFAULT_TIMEOUT_MILLIS; + } + + protected int getConnectionTimeoutMillis() { + return getTimeoutMillis(); } @Override @@ -550,7 +555,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer launchBackEnd(monitor, sessions, args, acceptor.getAddress()); monitor.setMessage("Waiting for connection"); monitor.increment(); - acceptor.setTimeout(getTimeoutMillis()); + acceptor.setTimeout(getConnectionTimeoutMillis()); connection = acceptor.accept(); connection.registerTerminals(sessions.values()); monitor.setMessage("Waiting for trace"); diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java index cf22772c62..160d6e35eb 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java @@ -51,6 +51,7 @@ public abstract class ScriptAttributesParser { public static final String AT_ARG = "@arg"; public static final String AT_ARGS = "@args"; public static final String AT_TTY = "@tty"; + public static final String AT_TIMEOUT = "@timeout"; public static final String PREFIX_ENV = "env:"; public static final String PREFIX_ARG = "arg:"; @@ -66,6 +67,10 @@ public abstract class ScriptAttributesParser { "%s: Invalid %s syntax. Use :type \"Display\" \"Tool Tip\""; public static final String MSGPAT_INVALID_ARGS_SYNTAX = "%s: Invalid %s syntax. Use \"Display\" \"Tool Tip\""; + public static final String MSGPAT_INVALID_TTY_SYNTAX = + "%s: Invalid %s syntax. Use TTY_TARGET [if env:OPT_EXTRA_TTY]"; + public static final String MSGPAT_INVALID_TIMEOUT_SYNTAX = "" + + "%s: Invalid %s syntax. Use [milliseconds]"; protected record Location(String fileName, int lineNo) { @Override @@ -228,6 +233,33 @@ public abstract class ScriptAttributesParser { } } + public interface TtyCondition { + boolean isActive(Map args); + } + + enum ConstTtyCondition implements TtyCondition { + ALWAYS { + @Override + public boolean isActive(Map args) { + return true; + } + }, + } + + record EqualsTtyCondition(String key, String repr) implements TtyCondition { + @Override + public boolean isActive(Map args) { + return Objects.toString(args.get(key)).equals(repr); + } + } + + record BoolTtyCondition(String key) implements TtyCondition { + @Override + public boolean isActive(Map args) { + return args.get(key) instanceof Boolean b && b.booleanValue(); + } + } + protected static String addrToString(InetAddress address) { if (address.isAnyLocalAddress()) { return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost. @@ -244,7 +276,8 @@ public abstract class ScriptAttributesParser { public record ScriptAttributes(String title, String description, List menuPath, String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation, - Map> parameters, Collection extraTtys) { + Map> parameters, Map extraTtys, + int timeoutMillis) { } /** @@ -302,7 +335,8 @@ public abstract class ScriptAttributesParser { private HelpLocation helpLocation; private final Map> userTypes = new HashMap<>(); private final Map> parameters = new LinkedHashMap<>(); - private final Set extraTtys = new LinkedHashSet<>(); + private final Map extraTtys = new LinkedHashMap<>(); + private int timeoutMillis = AbstractTraceRmiLaunchOffer.DEFAULT_TIMEOUT_MILLIS; /** * Check if a line should just be ignored, e.g., blank lines, or the "shebang" line on UNIX. @@ -352,7 +386,8 @@ public abstract class ScriptAttributesParser { /** * Process a line in the metadata comment block * - * @param line the line, excluding any comment delimiters + * @param loc the location, for error reporting + * @param comment the comment, excluding any comment delimiters */ public void parseComment(Location loc, String comment) { if (comment.isBlank()) { @@ -379,6 +414,7 @@ public abstract class ScriptAttributesParser { case AT_ARG -> parseArg(loc, parts[1], ++argc); case AT_ARGS -> parseArgs(loc, parts[1]); case AT_TTY -> parseTty(loc, parts[1]); + case AT_TIMEOUT -> parseTimeout(loc, parts[1]); default -> parseUnrecognized(loc, comment); } } @@ -531,12 +567,41 @@ public abstract class ScriptAttributesParser { } } - protected void parseTty(Location loc, String str) { - if (!extraTtys.add(str)) { + protected void putTty(Location loc, String name, TtyCondition condition) { + if (extraTtys.put(name, condition) != null) { Msg.warn(this, "%s: Duplicate %s. Ignored".formatted(loc, AT_TTY)); } } + protected void parseTty(Location loc, String str) { + List parts = ShellUtils.parseArgs(str); + switch (parts.size()) { + case 1: + putTty(loc, parts.get(0), ConstTtyCondition.ALWAYS); + return; + case 3: + if ("if".equals(parts.get(1))) { + putTty(loc, parts.get(0), new BoolTtyCondition(parts.get(2))); + return; + } + case 5: + if ("if".equals(parts.get(1)) && "==".equals(parts.get(3))) { + putTty(loc, parts.get(0), new EqualsTtyCondition(parts.get(2), parts.get(4))); + return; + } + } + Msg.error(this, MSGPAT_INVALID_TTY_SYNTAX.formatted(loc, AT_TTY)); + } + + protected void parseTimeout(Location loc, String str) { + try { + timeoutMillis = Integer.parseInt(str); + } + catch (NumberFormatException e) { + Msg.error(this, MSGPAT_INVALID_TIMEOUT_SYNTAX.formatted(loc, AT_TIMEOUT)); + } + } + protected void parseUnrecognized(Location loc, String line) { Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, line)); } @@ -560,7 +625,8 @@ public abstract class ScriptAttributesParser { } return new ScriptAttributes(title, getDescription(), List.copyOf(menuPath), menuGroup, menuOrder, new GIcon(iconId), helpLocation, - Collections.unmodifiableMap(new LinkedHashMap<>(parameters)), List.copyOf(extraTtys)); + Collections.unmodifiableMap(new LinkedHashMap<>(parameters)), + Collections.unmodifiableMap(new LinkedHashMap<>(extraTtys)), timeoutMillis); } private String getDescription() { diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java index e5c442266e..6b0909e64d 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java @@ -190,7 +190,7 @@ public class TraceRmiTarget extends AbstractTarget { Msg.trace(this, "No root schema, yet: " + trace); return null; } - TargetObjectSchema schema = ctx.getSchema(type); + TargetObjectSchema schema = ctx.getSchemaOrNull(type); if (schema == null) { Msg.error(this, "Schema " + type + " not in trace! " + trace); return null; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java index 6c654c0789..d66d4e6ccd 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPlugin.java @@ -1145,6 +1145,6 @@ public class DebuggerBreakpointMarkerPlugin extends Plugin return; } Msg.error(this, message, ex); - consoleService.log(DebuggerResources.ICON_LOG_ERROR, message + " (" + ex + ")"); + consoleService.log(DebuggerResources.ICON_LOG_ERROR, message, ex); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java index 8ccfa1680d..e7e4a2bbbd 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java @@ -1379,6 +1379,6 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter return; } Msg.error(this, message, ex); - consoleService.log(DebuggerResources.ICON_LOG_ERROR, message + " (" + ex + ")"); + consoleService.log(DebuggerResources.ICON_LOG_ERROR, message, ex); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java index d17a64a152..9bb5f8d8a4 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java @@ -107,11 +107,21 @@ public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleServ provider.log(icon, message); } + @Override + public void log(Icon icon, String message, Throwable error) { + provider.log(icon, message, error); + } + @Override public void log(Icon icon, String message, ActionContext context) { provider.log(icon, message, context); } + @Override + public void log(Icon icon, String message, Throwable error, ActionContext context) { + provider.log(icon, message, error, context); + } + @Override public void removeFromLog(ActionContext context) { provider.removeFromLog(context); @@ -141,6 +151,7 @@ public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleServ * For testing: get the number of rows having a given class of action context * * @param ctxCls the context class + * @return the row count */ public long getRowCount(Class ctxCls) { return provider.getRowCount(ctxCls); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java index 6ddddd172f..1408905fdb 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java @@ -17,7 +17,7 @@ package ghidra.app.plugin.core.debug.gui.console; import java.awt.BorderLayout; import java.awt.Dimension; -import java.awt.event.MouseEvent; +import java.awt.event.*; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -64,14 +64,14 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter static final int MIN_ROW_HEIGHT = 16; protected enum LogTableColumns implements EnumeratedTableColumn> { - ICON("Icon", Icon.class, LogRow::getIcon, SortDirection.ASCENDING, false), - MESSAGE("Message", Object.class, LogRow::getMessage, SortDirection.ASCENDING, false) { + ICON("Icon", Icon.class, LogRow::icon, SortDirection.ASCENDING, false), + MESSAGE("Message", Object.class, LogRow::message, SortDirection.ASCENDING, false) { @Override public GColumnRenderer getRenderer() { return HtmlOrProgressCellRenderer.INSTANCE; } }, - ACTIONS("Actions", ActionList.class, LogRow::getActions, SortDirection.DESCENDING, true) { + ACTIONS("Actions", ActionList.class, LogRow::actions, SortDirection.DESCENDING, true) { private static final ConsoleActionsCellRenderer RENDERER = new ConsoleActionsCellRenderer(); @@ -80,7 +80,7 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter return RENDERER; } }, - TIME("Time", Date.class, LogRow::getDate, SortDirection.DESCENDING, false) { + TIME("Time", Date.class, LogRow::date, SortDirection.DESCENDING, false) { @Override public GColumnRenderer getRenderer() { return CustomToStringCellRenderer.TIME_24HMSms; @@ -191,99 +191,57 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter * @param the type of the message */ public interface LogRow { - Icon getIcon(); + Icon icon(); - T getMessage(); + T message(); - ActionList getActions(); + ActionList actions(); - Date getDate(); + Date date(); - ActionContext getActionContext(); + ActionContext actionContext(); + + default boolean activated() { + return false; + } } - static class MessageLogRow implements LogRow { - private final Icon icon; - private final String message; - private final Date date; - private final ActionContext context; - private final ActionList actions; - - public MessageLogRow(Icon icon, String message, Date date, ActionContext context, - ActionList actions) { + record MessageLogRow(Icon icon, String message, Date date, Throwable error, + ActionContext actionContext, ActionList actions) implements LogRow { + public MessageLogRow(Icon icon, String message, Date date, Throwable error, + ActionContext actionContext, ActionList actions) { this.icon = icon; this.message = message; this.date = date; - this.context = context; + this.error = error; + this.actionContext = actionContext; this.actions = Objects.requireNonNull(actions); } @Override - public Icon getIcon() { - return icon; - } - - @Override - public String getMessage() { - return message; - } - - @Override - public Date getDate() { - return date; - } - - @Override - public ActionContext getActionContext() { - return context; - } - - @Override - public ActionList getActions() { - return actions; + public boolean activated() { + Msg.showError(this, null, "Inspect error", message, error); + return true; } } - static class MonitorLogRow implements LogRow { + record MonitorLogRow(MonitorReceiver message, Date date, ActionContext actionContext, + ActionList actions) implements LogRow { + static final GIcon ICON = new GIcon("icon.pending"); - private final MonitorReceiver monitor; - private final Date date; - private final ActionContext context; - private final ActionList actions; - - public MonitorLogRow(MonitorReceiver monitor, Date date, ActionContext context, + public MonitorLogRow(MonitorReceiver message, Date date, ActionContext actionContext, ActionList actions) { - this.monitor = monitor; + this.message = message; this.date = date; - this.context = context; + this.actionContext = actionContext; this.actions = Objects.requireNonNull(actions); } @Override - public Icon getIcon() { + public Icon icon() { return ICON; } - - @Override - public MonitorReceiver getMessage() { - return monitor; - } - - @Override - public ActionList getActions() { - return actions; - } - - @Override - public Date getDate() { - return date; - } - - @Override - public ActionContext getActionContext() { - return context; - } } private class ListenerForProgress implements ProgressListener { @@ -323,7 +281,7 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter @Override public void errorReported(MonitorReceiver monitor, Throwable error) { - log(DebuggerResources.ICON_LOG_ERROR, error.getMessage()); + log(DebuggerResources.ICON_LOG_ERROR, error.getMessage(), error); } @Override @@ -371,7 +329,7 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter LogTableColumns, ActionContext, LogRow, LogRow> { public LogTableModel(PluginTool tool) { - super(tool, "Log", LogTableColumns.class, r -> r == null ? null : r.getActionContext(), + super(tool, "Log", LogTableColumns.class, r -> r == null ? null : r.actionContext(), r -> r, r -> r); } @@ -499,6 +457,27 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter logFilterPanel = new GhidraTableFilterPanel<>(logTable, logTableModel); mainPanel.add(logFilterPanel, BorderLayout.NORTH); + logTable.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1 & e.getClickCount() == 2) { + if (activateSelectedRow()) { + e.consume(); + } + } + } + }); + logTable.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + if (activateSelectedRow()) { + e.consume(); + } + } + } + }); + logTable.setRowHeight(ACTION_BUTTON_SIZE + 2); TableColumnModel columnModel = logTable.getColumnModel(); @@ -517,6 +496,14 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter timeCol.setPreferredWidth(15); } + protected boolean activateSelectedRow() { + LogRow row = logFilterPanel.getSelectedItem(); + if (row == null) { + return false; + } + return row.activated(); + } + protected void createActions() { actionClear = ClearAction.builder(plugin) .onAction(this::activatedClear) @@ -548,7 +535,7 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter // I guess this can happen because of timing? return super.getActionContext(event); } - return sel.getActionContext(); + return sel.actionContext(); } @AutoOptionConsumed(name = DebuggerResources.OPTION_NAME_LOG_BUFFER_LIMIT) @@ -570,17 +557,25 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter } protected void log(Icon icon, String message) { - log(icon, message, new LogRowConsoleActionContext()); + log(icon, message, null, new LogRowConsoleActionContext()); } protected void log(Icon icon, String message, ActionContext context) { - logRow( - new MessageLogRow(icon, message, new Date(), context, computeToolbarActions(context))); + log(icon, message, null, context); + } + + protected void log(Icon icon, String message, Throwable error) { + log(icon, message, error, new LogRowConsoleActionContext()); + } + + protected void log(Icon icon, String message, Throwable error, ActionContext context) { + logRow(new MessageLogRow(icon, message, new Date(), error, context, + computeToolbarActions(context))); } protected void logRow(LogRow row) { synchronized (buffer) { - LogRow old = logTableModel.deleteKey(row.getActionContext()); + LogRow old = logTableModel.deleteKey(row.actionContext()); if (old != null) { buffer.remove(old); } @@ -608,7 +603,8 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter ActionContext context = new LogRowConsoleActionContext(); logRow(new MessageLogRow(iconForLevel(event.getLevel()), "" + HTMLUtilities.escapeHTML(event.getMessage().getFormattedMessage()), - new Date(event.getTimeMillis()), context, computeToolbarActions(context))); + new Date(event.getTimeMillis()), event.getThrown(), context, + computeToolbarActions(context))); } protected void removeFromLog(ActionContext context) { @@ -686,7 +682,7 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter synchronized (buffer) { return logTableModel.getModelData() .stream() - .filter(r -> ctxCls.isInstance(r.getActionContext())) + .filter(r -> ctxCls.isInstance(r.actionContext())) .count(); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/TargetActionTask.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/TargetActionTask.java index 5508e2577f..084aa06d01 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/TargetActionTask.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/TargetActionTask.java @@ -63,7 +63,7 @@ public class TargetActionTask extends Task { private void reportError(Throwable error) { DebuggerConsoleService consoleService = tool.getService(DebuggerConsoleService.class); if (consoleService != null) { - consoleService.log(DebuggerResources.ICON_LOG_ERROR, error.getMessage()); + consoleService.log(DebuggerResources.ICON_LOG_ERROR, error.getMessage(), error); } else { Msg.showError(this, null, "Control Error", error.getMessage(), error); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java index 2ca4f917d9..344a1d1073 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java @@ -275,12 +275,12 @@ public class DebuggerObjectsPlugin extends AbstractDebuggerPlugin providers.get(0).readConfigState(saveState); } - public void objectError(String message) { + public void objectError(String message, Throwable ex) { if (consoleService == null) { Msg.error(this, message); return; } - consoleService.log(DebuggerResources.ICON_LOG_ERROR, message); + consoleService.log(DebuggerResources.ICON_LOG_ERROR, message, ex); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java index b34d2d2003..440dcbf5da 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java @@ -351,7 +351,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter if (pane != null) { if (currentModel != null) { currentModel.fetchModelRoot().thenAccept(this::refresh).exceptionally(ex -> { - plugin.objectError("Error refreshing model root"); + plugin.objectError("Error refreshing model root", ex); return null; }); } @@ -554,7 +554,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter table.setColumns(); // TODO: What with attrs? }).exceptionally(ex -> { - plugin.objectError("Failed to fetch attributes"); + plugin.objectError("Failed to fetch attributes", ex); return null; }); } @@ -569,7 +569,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter public void addTargetToMap(ObjectContainer container) { DebuggerObjectsProvider provider = container.getProvider(); if (!this.equals(provider)) { - plugin.objectError("TargetMap corrupted"); + plugin.objectError("TargetMap corrupted", null); } TargetObject targetObject = container.getTargetObject(); if (targetObject != null && !container.isLink()) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectElementRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectElementRow.java index 6bdf6132e6..b5bcd818ff 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectElementRow.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectElementRow.java @@ -35,7 +35,7 @@ public class ObjectElementRow { map = attributes; }).exceptionally(ex -> { DebuggerObjectsPlugin plugin = provider.getPlugin(); - plugin.objectError("Failed to fetch attributes"); + plugin.objectError("Failed to fetch attributes", ex); return null; }); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/DefaultMonitorReceiver.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/DefaultMonitorReceiver.java index f5d57182ef..c11a58605b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/DefaultMonitorReceiver.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/DefaultMonitorReceiver.java @@ -33,7 +33,7 @@ public class DefaultMonitorReceiver implements MonitorReceiver { private boolean valid = true; - private String message; + private String message = ""; private long maximum; private long progress; @@ -70,9 +70,14 @@ public class DefaultMonitorReceiver implements MonitorReceiver { void setMessage(String message) { synchronized (lock) { - this.message = message; + if (message == null) { + this.message = ""; + } + else { + this.message = message; + } } - plugin.listeners.invoke().messageUpdated(this, message); + plugin.listeners.invoke().messageUpdated(this, this.message); } void reportError(Throwable error) { diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java index f23956d211..f00fec5dcb 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java @@ -1598,7 +1598,7 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerTes DebuggerOpenProgramActionContext ctx = new DebuggerOpenProgramActionContext(df); waitForPass(() -> assertTrue(consolePlugin.logContains(ctx))); - assertTrue(consolePlugin.getLogRow(ctx).getMessage() instanceof String message && + assertTrue(consolePlugin.getLogRow(ctx).message() instanceof String message && message.contains("recovery")); } @@ -1627,7 +1627,7 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerTes DebuggerOpenProgramActionContext ctx = new DebuggerOpenProgramActionContext(df); waitForPass(() -> assertTrue(consolePlugin.logContains(ctx))); - assertTrue(consolePlugin.getLogRow(ctx).getMessage() instanceof String message && + assertTrue(consolePlugin.getLogRow(ctx).message() instanceof String message && message.contains("version")); } @@ -1646,8 +1646,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerTes waitForSwing(); LogRow row = consolePlugin.getLogRow(ctx); - assertEquals(1, row.getActions().size()); - BoundAction boundAction = row.getActions().get(0); + assertEquals(1, row.actions().size()); + BoundAction boundAction = row.actions().get(0); assertEquals(listingProvider.actionOpenProgram, boundAction.action); boundAction.perform(); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/plugintool/AutoConfigState.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/plugintool/AutoConfigState.java index b27601471f..84bb0704dc 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/plugintool/AutoConfigState.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/plugintool/AutoConfigState.java @@ -18,6 +18,7 @@ package ghidra.framework.plugintool; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles.Lookup; import java.lang.reflect.*; +import java.math.BigInteger; import java.util.*; import ghidra.framework.options.SaveState; @@ -258,6 +259,20 @@ public interface AutoConfigState { } } + static class BigIntegerConfigFieldCodec implements ConfigFieldCodec { + public static final BigIntegerConfigFieldCodec INSTANCE = new BigIntegerConfigFieldCodec(); + + @Override + public BigInteger read(SaveState state, String name, BigInteger current) { + return new BigInteger(state.getBytes(name, new byte[] { 0 })); + } + + @Override + public void write(SaveState state, String name, BigInteger value) { + state.putBytes(name, value == null ? null : value.toByteArray()); + } + } + static class EnumConfigFieldCodec implements ConfigFieldCodec> { public static final EnumConfigFieldCodec INSTANCE = new EnumConfigFieldCodec(); @@ -301,6 +316,8 @@ public interface AutoConfigState { addCodec(float[].class, FloatArrayConfigFieldCodec.INSTANCE); addCodec(double[].class, DoubleArrayConfigFieldCodec.INSTANCE); addCodec(String[].class, StringArrayConfigFieldCodec.INSTANCE); + + addCodec(BigInteger.class, BigIntegerConfigFieldCodec.INSTANCE); } private static void addCodec(Class cls, ConfigFieldCodec codec) {