From f9bea7720a033bec7cb438c5fe25158005e8728d Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Tue, 26 Mar 2024 08:50:53 -0400 Subject: [PATCH] GP-4439: Add raw-gdb.sh and raw-python.sh. Add @no-image tag. --- .../data/debugger-launchers/raw-gdb.sh | 57 +++ .../src/main/py/src/ghidragdb/hooks.py | 2 +- .../api/tracermi/TraceRmiLaunchOffer.java | 14 +- .../data/debugger-launchers/raw-python3.sh | 43 +++ .../data/support/raw-python3.py | 36 ++ .../AbstractScriptTraceRmiLaunchOffer.java | 9 +- .../launcher/AbstractTraceRmiLaunchOffer.java | 340 ++++++++---------- .../gui/tracermi/launcher/LaunchAction.java | 118 ++---- .../launcher/LaunchFailureDialog.java | 132 +++++++ .../launcher/ScriptAttributesParser.java | 58 +-- .../TraceRmiLauncherServicePlugin.java | 147 +++++++- .../service/tracermi/TraceRmiHandler.java | 4 +- .../src/main/py/src/ghidratrace/client.py | 15 +- .../plugin/core/terminal/TerminalPanel.java | 4 +- .../core/terminal/TerminalProvider.java | 3 + .../main/java/ghidra/pty/unix/UnixPty.java | 2 + .../java/ghidra/pty/unix/UnixPtyEndpoint.java | 8 +- 17 files changed, 655 insertions(+), 337 deletions(-) create mode 100755 Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/raw-gdb.sh create mode 100755 Ghidra/Debug/Debugger-rmi-trace/data/debugger-launchers/raw-python3.sh create mode 100644 Ghidra/Debug/Debugger-rmi-trace/data/support/raw-python3.py create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchFailureDialog.java diff --git a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/raw-gdb.sh b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/raw-gdb.sh new file mode 100755 index 0000000000..3a2b8224ec --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/raw-gdb.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env 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. +## +#@title raw gdb +#@no-image +#@desc +#@desc

Start gdb

+#@desc

This will start gdb and connect to it. It will not launch +#@desc a target, so you can (must) set up your target manually. +#@desc 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 +#@menu-group raw +#@icon icon.debugger +#@help TraceRmiLauncherServicePlugin#gdb +#@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_ARCH:str="i386:x86-64" "Architecture" "Target architecture" + +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 architecture $OPT_ARCH" \ + -ex "ghidra trace connect \"$GHIDRA_TRACE_RMI_ADDR\"" \ + -ex "ghidra trace start" \ + -ex "ghidra trace sync-enable" \ + -ex "set confirm on" \ + -ex "set pagination on" diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py index 8756f98195..060318dcd3 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py @@ -48,8 +48,8 @@ class HookState(object): def end_batch(self): if self.batch is None: return - commands.STATE.client.end_batch() self.batch = None + commands.STATE.client.end_batch() def check_skip_continue(self): skip = self.skip_continue diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java index 1f30063ab8..bd69e0310c 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java @@ -52,6 +52,7 @@ public interface TraceRmiLaunchOffer { * @param sessions any terminal sessions created while launching the back-end. If there are more * than one, they are distinguished by launcher-defined keys. If there are no * sessions, then there was likely a catastrophic error in the launcher. + * @param acceptor the acceptor if waiting for a connection * @param connection if the target connected back to Ghidra, that connection * @param trace if the connection started a trace, the (first) trace it created * @param exception optional error, if failed @@ -138,7 +139,7 @@ public interface TraceRmiLaunchOffer { /** * Re-write the launcher arguments, if desired * - * @param launcher the launcher that will create the target + * @param offer the offer that will create the target * @param arguments the arguments suggested by the offer or saved settings * @param relPrompt describes the timing of this callback relative to prompting the user * @return the adjusted arguments @@ -176,7 +177,7 @@ public interface TraceRmiLaunchOffer { * memorized. The opinion will generate each offer fresh each time, so it's important that the * "same offer" have the same configuration name. Note that the name cannot depend on * the program name, but can depend on the model factory and program language and/or compiler - * spec. This name cannot contain semicolons ({@ code ;}). + * spec. This name cannot contain semicolons ({@code ;}). * * @return the configuration name */ @@ -262,6 +263,8 @@ public interface TraceRmiLaunchOffer { * The order of entries in the quick-launch drop-down menu is always most-recently to * least-recently used. An entry that has never been used does not appear in the quick launch * menu. + * + * @return the sub-group name for ordering in the menu */ default String getMenuOrder() { return ""; @@ -285,4 +288,11 @@ public interface TraceRmiLaunchOffer { * @return the parameters */ Map> getParameters(); + + /** + * Check if this offer requires an open program + * + * @return true if required + */ + boolean requiresImage(); } diff --git a/Ghidra/Debug/Debugger-rmi-trace/data/debugger-launchers/raw-python3.sh b/Ghidra/Debug/Debugger-rmi-trace/data/debugger-launchers/raw-python3.sh new file mode 100755 index 0000000000..2290d7fb4a --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/data/debugger-launchers/raw-python3.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env 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. +## + +#@title raw python +#@no-image +#@desc +#@desc

Start gdb

+#@desc

This will start python, import ghidratrace and connect to it. +#@desc This connector is made for those wanting to explore the TraceRMI API and possibly develop +#@desc a new connector. You will need protobuf installed for Python 3.

+#@desc +#@menu-group raw +#@icon icon.debugger +#@help TraceRmiLauncherServicePlugin#gdb +#@env OPT_PYTHON_EXE:str="python" "Path to python" "The path to the Python 3 interpreter. Omit the full path to resolve using the system PATH." +#@env OPT_LANG:str="DATA:LE:64:default" "Ghidra Language" "The Ghidra LanguageID for the trace" +#@env OPT_COMP:str="pointer64" "Ghidra Compiler" "The Ghidra CompilerSpecID for the trace" + +if [ -d ${GHIDRA_HOME}/ghidra/.git ] +then + 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-rmi-trace/build/pypkg/src:$PYTHONPATH +else + export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/pypkg/src:$PYTHONPATH +fi + +"$OPT_PYTHON_EXE" -i ../support/raw-python3.py diff --git a/Ghidra/Debug/Debugger-rmi-trace/data/support/raw-python3.py b/Ghidra/Debug/Debugger-rmi-trace/data/support/raw-python3.py new file mode 100644 index 0000000000..db0f8f2bdc --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/data/support/raw-python3.py @@ -0,0 +1,36 @@ +## ### +# 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. +## +from concurrent.futures import ThreadPoolExecutor +import os +import socket +import sys + +from ghidratrace import * +from ghidratrace.client import * + + +REGISTRY = MethodRegistry(ThreadPoolExecutor(max_workers=1)) + +host = os.getenv("GHIDRA_TRACE_RMI_HOST") +port = int(os.getenv("GHIDRA_TRACE_RMI_PORT")) +c = socket.socket() +c.connect((host, port)) +client = Client( + c, f"python-{sys.version_info.major}.{sys.version_info.minor}", REGISTRY) +print(f"Connected to {client.description} at {host}:{port}") + +trace = client.create_trace("noname", os.getenv( + "OPT_LANG"), os.getenv("OPT_COMP")) 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 ee2e90b327..effa0aac33 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 @@ -102,7 +102,9 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi List commandLine = new ArrayList<>(); Map env = new HashMap<>(System.getenv()); prepareSubprocess(commandLine, env, args, address); - env.put("GHIDRA_LANGUAGE_ID", program.getLanguageID().toString()); + if (program != null) { + env.put("GHIDRA_LANGUAGE_ID", program.getLanguageID().toString()); + } for (Map.Entry ent : attrs.extraTtys().entrySet()) { if (!ent.getValue().isActive(args)) { @@ -116,4 +118,9 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi sessions.put("Shell", runInTerminal(commandLine, env, script.getParentFile(), sessions.values())); } + + @Override + public boolean requiresImage() { + return !attrs.noImage(); + } } 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 da4491b350..6aceb0290a 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 @@ -26,13 +26,9 @@ import java.util.concurrent.*; import javax.swing.Icon; -import org.jdom.Element; -import org.jdom.JDOMException; - -import db.Transaction; -import docking.widgets.OptionDialog; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.LaunchFailureDialog.ErrPromptResponse; import ghidra.app.plugin.core.debug.service.tracermi.DefaultTraceRmiAcceptor; import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler; import ghidra.app.plugin.core.terminal.TerminalListener; @@ -48,21 +44,20 @@ import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.AutoConfigState.ConfigStateField; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.*; -import ghidra.program.model.listing.*; +import ghidra.program.model.listing.InstructionIterator; +import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; import ghidra.pty.*; import ghidra.trace.model.Trace; import ghidra.trace.model.TraceLocation; import ghidra.trace.model.modules.TraceModule; -import ghidra.util.*; +import ghidra.util.MessageType; +import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.task.Task; import ghidra.util.task.TaskMonitor; -import ghidra.util.xml.XmlUtilities; public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer { - - public static final String PREFIX_DBGLAUNCH = "DBGLAUNCH_"; public static final String PARAM_DISPLAY_IMAGE = "Image"; public static final String PREFIX_PARAM_EXTTOOL = "env:GHIDRA_LANG_EXTTOOL_"; @@ -146,7 +141,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer public AbstractTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program) { this.plugin = Objects.requireNonNull(plugin); - this.program = Objects.requireNonNull(program); + this.program = program; this.tool = plugin.getTool(); this.terminalService = Objects.requireNonNull(tool.getService(TerminalService.class)); } @@ -165,6 +160,9 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer } protected Address getMappingProbeAddress() { + if (program == null) { + return null; + } AddressIterator eepi = program.getSymbolTable().getExternalEntryPointIterator(); if (eepi.hasNext()) { return eepi.next(); @@ -220,6 +218,9 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer protected Collection invokeMapper(TaskMonitor monitor, DebuggerStaticMappingService mappingService, Trace trace) throws CancelledException { + if (program == null) { + return List.of(); + } Map map = mappingService .proposeModuleMaps(trace.getModuleManager().getAllModules(), List.of(program)); Collection proposal = MapProposal.flatten(map.values()); @@ -227,7 +228,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer return proposal; } - private void saveLauncherArgs(Map args, + protected SaveState saveLauncherArgsToState(Map args, Map> params) { SaveState state = new SaveState(); for (ParameterDescription param : params.values()) { @@ -235,17 +236,22 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer if (val != null) { ConfigStateField.putState(state, param.type.asSubclass(Object.class), "param_" + param.name, val); - state.putLong("last", System.currentTimeMillis()); } } - if (program != null) { - ProgramUserData userData = program.getProgramUserData(); - try (Transaction tx = userData.openTransaction()) { - Element element = state.saveToXml(); - userData.setStringProperty(PREFIX_DBGLAUNCH + getConfigName(), - XmlUtilities.toString(element)); - } + return state; + } + + protected void saveState(SaveState state) { + if (program == null) { + plugin.writeToolLaunchConfig(getConfigName(), state); + return; } + plugin.writeProgramLaunchConfig(program, getConfigName(), state); + } + + protected void saveLauncherArgs(Map args, + Map> params) { + saveState(saveLauncherArgsToState(args, params)); } /** @@ -261,9 +267,6 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer @SuppressWarnings("unchecked") protected Map generateDefaultLauncherArgs( Map> params) { - if (program == null) { - return Map.of(); - } Map map = new LinkedHashMap(); ParameterDescription paramImage = null; for (Entry> entry : params.entrySet()) { @@ -285,7 +288,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer } } } - if (paramImage != null) { + if (paramImage != null && program != null) { File imageFile = TraceRmiLauncherServicePlugin.getProgramPath(program); if (imageFile != null) { paramImage.set(map, imageFile.getAbsolutePath()); @@ -354,56 +357,43 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer * user may be expecting a customized launch. If there will be a prompt, then this may safely * return the defaults, since the user will be given a chance to correct them. * - * @param params the parameters of the model's launcher * @param forPrompt true if the user will be confirming the arguments * @return the loaded arguments, or defaults */ protected Map loadLastLauncherArgs(boolean forPrompt) { - /** - * TODO: Supposedly, per-program, per-user config stuff is being generalized for analyzers. - * Re-examine this if/when that gets merged - */ - if (program != null) { - Map> params = getParameters(); - ProgramUserData userData = program.getProgramUserData(); - String property = - userData.getStringProperty(PREFIX_DBGLAUNCH + getConfigName(), null); - if (property != null) { - try { - Element element = XmlUtilities.fromString(property); - SaveState state = new SaveState(element); - List names = List.of(state.getNames()); - Map args = new LinkedHashMap<>(); - for (ParameterDescription param : params.values()) { - String key = "param_" + param.name; - if (names.contains(key)) { - Object configState = ConfigStateField.getState(state, param.type, key); - if (configState != null) { - args.put(param.name, configState); - } - } - } - if (!args.isEmpty()) { - return args; - } - } - catch (JDOMException | IOException e) { - if (!forPrompt) { - throw new RuntimeException( - "Saved launcher args are corrupt, or launcher parameters changed. Not launching.", - e); - } - Msg.error(this, - "Saved launcher args are corrupt, or launcher parameters changed. Defaulting.", - e); - } - } - Map args = generateDefaultLauncherArgs(params); - saveLauncherArgs(args, params); - return args; - } + Map> params = getParameters(); + Map args = loadLauncherArgsFromState(loadState(forPrompt), params); + saveLauncherArgs(args, params); + return args; + } - return new LinkedHashMap<>(); + protected Map loadLauncherArgsFromState(SaveState state, + Map> params) { + Map defaultArgs = generateDefaultLauncherArgs(params); + if (state == null) { + return defaultArgs; + } + List names = List.of(state.getNames()); + Map args = new LinkedHashMap<>(); + for (ParameterDescription param : params.values()) { + String key = "param_" + param.name; + Object configState = + names.contains(key) ? ConfigStateField.getState(state, param.type, key) : null; + if (configState != null) { + args.put(param.name, configState); + } + else { + args.put(param.name, defaultArgs.get(param.name)); + } + } + return args; + } + + protected SaveState loadState(boolean forPrompt) { + if (program == null) { + return plugin.readToolLaunchConfig(getConfigName()); + } + return plugin.readProgramLaunchConfig(program, getConfigName(), forPrompt); } /** @@ -527,11 +517,52 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer } } - @Override - public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) { - InternalTraceRmiService service = tool.getService(InternalTraceRmiService.class); + protected void initializeMonitor(TaskMonitor monitor) { + if (requiresImage()) { + monitor.setMaximum(6); + } + else { + monitor.setMaximum(5); + } + } + + protected void waitForModuleMapping(TaskMonitor monitor, TraceRmiHandler connection, + Trace trace) throws CancelledException, InterruptedException, ExecutionException, + NoStaticMappingException { + if (!requiresImage()) { + return; + } DebuggerStaticMappingService mappingService = tool.getService(DebuggerStaticMappingService.class); + monitor.setMessage("Waiting for module mapping"); + try { + listenForMapping(mappingService, connection, trace).get(getTimeoutMillis(), + TimeUnit.MILLISECONDS); + } + catch (TimeoutException e) { + monitor.setMessage( + "Timed out waiting for module mapping. Invoking the mapper."); + Collection mapped; + try { + mapped = invokeMapper(monitor, mappingService, trace); + } + catch (CancelledException ce) { + throw new CancellationException(e.getMessage()); + } + if (mapped.isEmpty()) { + throw new NoStaticMappingException( + "The resulting target process has no mapping to the static image."); + } + } + monitor.increment(); + } + + @Override + public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) { + if (requiresImage() && program == null) { + throw new IllegalStateException("Offer requires image, but no program given."); + } + InternalTraceRmiService service = tool.getService(InternalTraceRmiService.class); DebuggerTraceManagerService traceManager = tool.getService(DebuggerTraceManagerService.class); final PromptMode mode = configurator.getPromptMode(); @@ -543,61 +574,60 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer Trace trace = null; Throwable lastExc = null; - monitor.setMaximum(5); + initializeMonitor(monitor); while (true) { - monitor.setMessage("Gathering arguments"); - Map args = getLauncherArgs(prompt, configurator, lastExc); - if (args == null) { - if (lastExc == null) { - lastExc = new CancelledException(); - } - return new LaunchResult(program, sessions, acceptor, connection, trace, lastExc); - } - acceptor = null; - sessions.clear(); - connection = null; - trace = null; - lastExc = null; - try { + monitor.setMessage("Gathering arguments"); + Map args = getLauncherArgs(prompt, configurator, lastExc); + if (args == null) { + if (lastExc == null) { + lastExc = new CancelledException(); + } + return new LaunchResult(program, sessions, acceptor, connection, trace, + lastExc); + } + monitor.increment(); + + acceptor = null; + sessions.clear(); + connection = null; + trace = null; + lastExc = null; + monitor.setMessage("Listening for connection"); - monitor.increment(); acceptor = service.acceptOne(new InetSocketAddress("127.0.0.1", 0)); + monitor.increment(); + monitor.setMessage("Launching back-end"); - monitor.increment(); launchBackEnd(monitor, sessions, args, acceptor.getAddress()); - monitor.setMessage("Waiting for connection"); monitor.increment(); + + monitor.setMessage("Waiting for connection"); acceptor.setTimeout(getConnectionTimeoutMillis()); connection = acceptor.accept(); connection.registerTerminals(sessions.values()); - monitor.setMessage("Waiting for trace"); monitor.increment(); + + monitor.setMessage("Waiting for trace"); trace = connection.waitForTrace(getTimeoutMillis()); traceManager.openTrace(trace); traceManager.activate(traceManager.resolveTrace(trace), ActivationCause.START_RECORDING); - monitor.setMessage("Waiting for module mapping"); monitor.increment(); + + waitForModuleMapping(monitor, connection, trace); + } + catch (CancelledException e) { + lastExc = e; + LaunchResult result = + new LaunchResult(program, sessions, acceptor, connection, trace, lastExc); try { - listenForMapping(mappingService, connection, trace).get(getTimeoutMillis(), - TimeUnit.MILLISECONDS); + result.close(); } - catch (TimeoutException e) { - monitor.setMessage( - "Timed out waiting for module mapping. Invoking the mapper."); - Collection mapped; - try { - mapped = invokeMapper(monitor, mappingService, trace); - } - catch (CancelledException ce) { - throw new CancellationException(e.getMessage()); - } - if (mapped.isEmpty()) { - throw new NoStaticMappingException( - "The resulting target process has no mapping to the static image."); - } + catch (Exception e1) { + Msg.error(this, "Could not close", e1); } + return new LaunchResult(program, Map.of(), null, null, null, lastExc); } catch (Exception e) { DebuggerConsoleService consoleService = @@ -635,101 +665,11 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer } return result; } - return new LaunchResult(program, sessions, null, connection, trace, null); + return new LaunchResult(program, sessions, acceptor, connection, trace, null); } } - enum ErrPromptResponse { - KEEP, RETRY, TERMINATE; - } - protected ErrPromptResponse promptError(LaunchResult result) { - String message = """ - -

Failed to launch %s due to an exception:

- - %s - -

Troubleshooting

-

- Check the Terminal! - If no terminal is visible, check the menus: Window → Terminals → - .... - A path or other configuration parameter may be incorrect. - The back-end debugger may have paused for user input. - There may be a missing dependency. - There may be an incorrect version, etc.

- -

These resources remain after the failed launch:

-
    - %s -
- -

Do you want to keep these resources?

-
    -
  • Choose Yes to stop here and diagnose or complete the launch manually. -
  • -
  • Choose No to clean up and retry at the launch dialog.
  • -
  • Choose Cancel to clean up without retrying.
  • - """.formatted( - htmlProgramName(result), htmlExceptionMessage(result), htmlResources(result)); - return LaunchFailureDialog.show(message); - } - - static class LaunchFailureDialog extends OptionDialog { - public LaunchFailureDialog(String message) { - super("Launch Failed", message, "&Yes", "&No", OptionDialog.ERROR_MESSAGE, null, - true, "No"); - } - - static ErrPromptResponse show(String message) { - return switch (new LaunchFailureDialog(message).show()) { - case OptionDialog.YES_OPTION -> ErrPromptResponse.KEEP; - case OptionDialog.NO_OPTION -> ErrPromptResponse.RETRY; - case OptionDialog.CANCEL_OPTION -> ErrPromptResponse.TERMINATE; - default -> throw new AssertionError(); - }; - } - } - - protected String htmlProgramName(LaunchResult result) { - if (result.program() == null) { - return ""; - } - return "" + HTMLUtilities.escapeHTML(result.program().getName()) + ""; - } - - protected String htmlExceptionMessage(LaunchResult result) { - if (result.exception() == null) { - return "(No exception)"; - } - return HTMLUtilities.escapeHTML(result.exception().toString()); - } - - protected String htmlResources(LaunchResult result) { - StringBuilder sb = new StringBuilder(); - for (Entry ent : result.sessions().entrySet()) { - TerminalSession session = ent.getValue(); - sb.append("
  • Terminal: %s → %s".formatted( - HTMLUtilities.escapeHTML(ent.getKey()), - HTMLUtilities.escapeHTML(session.description()))); - if (session.isTerminated()) { - sb.append(" (Terminated)"); - } - sb.append("
  • \n"); - } - if (result.acceptor() != null) { - sb.append("
  • Acceptor: %s
  • \n".formatted( - HTMLUtilities.escapeHTML(result.acceptor().getAddress().toString()))); - } - if (result.connection() != null) { - sb.append("
  • Connection: %s
  • \n".formatted( - HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString()))); - } - if (result.trace() != null) { - sb.append("
  • Trace: %s
  • \n".formatted( - HTMLUtilities.escapeHTML(result.trace().getName()))); - } - return sb.toString(); + return LaunchFailureDialog.show(result); } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java index c4a9f1bb96..8ec4b59839 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java @@ -15,27 +15,22 @@ */ package ghidra.app.plugin.core.debug.gui.tracermi.launcher; -import java.io.IOException; import java.util.*; import java.util.stream.Stream; import javax.swing.*; -import org.jdom.Element; -import org.jdom.JDOMException; - import docking.ActionContext; import docking.PopupMenuHandler; import docking.action.*; import docking.action.builder.ActionBuilder; import docking.menu.*; import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin.ConfigLast; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; -import ghidra.framework.options.SaveState; import ghidra.program.model.listing.Program; -import ghidra.program.model.listing.ProgramUserData; -import ghidra.util.*; -import ghidra.util.xml.XmlUtilities; +import ghidra.util.HelpLocation; +import ghidra.util.Swing; public class LaunchAction extends MultiActionDockingAction { public static final String NAME = "Launch"; @@ -55,58 +50,10 @@ public class LaunchAction extends MultiActionDockingAction { protected String[] prependConfigAndLaunch(List menuPath) { Program program = plugin.currentProgram; - return Stream.concat( - Stream.of("Configure and Launch " + program.getName() + " using..."), - menuPath.stream()).toArray(String[]::new); - } - - record ConfigLast(String configName, long last) { - } - - ConfigLast checkSavedConfig(ProgramUserData userData, String propName) { - if (!propName.startsWith(AbstractTraceRmiLaunchOffer.PREFIX_DBGLAUNCH)) { - return null; - } - String configName = - propName.substring(AbstractTraceRmiLaunchOffer.PREFIX_DBGLAUNCH.length()); - String propVal = Objects.requireNonNull( - userData.getStringProperty(propName, null)); - Element element; - try { - element = XmlUtilities.fromString(propVal); - } - catch (JDOMException | IOException e) { - Msg.error(this, "Could not load launcher config for " + configName + ": " + e, e); - return null; - } - SaveState state = new SaveState(element); - if (!state.hasValue("last")) { - return null; - } - return new ConfigLast(configName, state.getLong("last", 0)); - } - - ConfigLast findMostRecentConfig() { - Program program = plugin.currentProgram; - if (program == null) { - return null; - } - ConfigLast best = null; - - ProgramUserData userData = program.getProgramUserData(); - for (String propName : userData.getStringPropertyNames()) { - ConfigLast candidate = checkSavedConfig(userData, propName); - if (candidate == null) { - continue; - } - else if (best == null) { - best = candidate; - } - else if (candidate.last > best.last) { - best = candidate; - } - } - return best; + String title = program == null + ? "Configure and Launch ..." + : "Configure and Launch %s using...".formatted(program.getName()); + return Stream.concat(Stream.of(title), menuPath.stream()).toArray(String[]::new); } @Override @@ -116,17 +63,7 @@ public class LaunchAction extends MultiActionDockingAction { List actions = new ArrayList<>(); - Map saved = new HashMap<>(); - if (program != null) { - ProgramUserData userData = program.getProgramUserData(); - for (String propName : userData.getStringPropertyNames()) { - ConfigLast check = checkSavedConfig(userData, propName); - if (check == null) { - continue; - } - saved.put(check.configName, check.last); - } - } + Map saved = plugin.loadSavedConfigs(program); for (TraceRmiLaunchOffer offer : offers) { actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName()) @@ -134,7 +71,7 @@ public class LaunchAction extends MultiActionDockingAction { .popupMenuGroup(offer.getMenuGroup(), offer.getMenuOrder()) .popupMenuIcon(offer.getIcon()) .helpLocation(offer.getHelpLocation()) - .enabledWhen(ctx -> true) + .enabledWhen(ctx -> !offer.requiresImage() || program != null) .onAction(ctx -> plugin.configureAndLaunch(offer)) .build()); Long last = saved.get(offer.getConfigName()); @@ -143,8 +80,11 @@ public class LaunchAction extends MultiActionDockingAction { // Thus, no worries about program.getName() below. continue; } + String title = program == null + ? "Re-launch " + offer.getTitle() + : "Re-launch %s using %s".formatted(program.getName(), offer.getTitle()); actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName()) - .popupMenuPath("Re-launch " + program.getName() + " using " + offer.getTitle()) + .popupMenuPath(title) .popupMenuGroup("0", "%016x".formatted(Long.MAX_VALUE - last)) .popupMenuIcon(offer.getIcon()) .helpLocation(offer.getHelpLocation()) @@ -169,6 +109,7 @@ public class LaunchAction extends MultiActionDockingAction { MenuManager manager = new MenuManager("Launch", (char) 0, GROUP, true, handler, null); for (DockingActionIf action : actionList) { + action.setEnabled(action.isEnabledForContext(context)); manager.addAction(action); } return manager.getPopupMenu(); @@ -193,26 +134,14 @@ public class LaunchAction extends MultiActionDockingAction { @Override public boolean isEnabledForContext(ActionContext context) { - return plugin.currentProgram != null; - } - - protected TraceRmiLaunchOffer findOffer(ConfigLast last) { - if (last == null) { - return null; - } - for (TraceRmiLaunchOffer offer : plugin.getOffers(plugin.currentProgram)) { - if (offer.getConfigName().equals(last.configName)) { - return offer; - } - } - return null; + return !plugin.getOffers(plugin.currentProgram).isEmpty(); } @Override public void actionPerformed(ActionContext context) { // See comment on super method about use of runLater - ConfigLast last = findMostRecentConfig(); - TraceRmiLaunchOffer offer = findOffer(last); + ConfigLast last = plugin.findMostRecentConfig(plugin.currentProgram); + TraceRmiLaunchOffer offer = plugin.findOffer(last); if (offer == null) { Swing.runLater(() -> button.showPopup()); return; @@ -223,14 +152,17 @@ public class LaunchAction extends MultiActionDockingAction { @Override public String getDescription() { Program program = plugin.currentProgram; - if (program == null) { - return "Launch (program required)"; + ConfigLast last = plugin.findMostRecentConfig(program); + TraceRmiLaunchOffer offer = plugin.findOffer(last); + if (offer == null && program == null) { + return "Configure and launch"; } - ConfigLast last = findMostRecentConfig(); - TraceRmiLaunchOffer offer = findOffer(last); if (offer == null) { return "Configure and launch " + program.getName(); } - return "Re-launch " + program.getName() + " using " + offer.getTitle(); + if (program == null) { + return "Re-launch " + offer.getTitle(); + } + return "Re-launch %s using %s".formatted(program.getName(), offer.getTitle()); } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchFailureDialog.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchFailureDialog.java new file mode 100644 index 0000000000..6bce1afeef --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchFailureDialog.java @@ -0,0 +1,132 @@ +/* ### + * 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.tracermi.launcher; + +import java.util.Map.Entry; + +import docking.widgets.OptionDialog; +import ghidra.debug.api.tracermi.TerminalSession; +import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchResult; +import ghidra.util.HTMLUtilities; + +public class LaunchFailureDialog extends OptionDialog { + private static final String MSGPAT_PART_TOP = """ + +

    Failed to launch %s due to an exception:

    + + %s + +

    Troubleshooting

    +

    + Check the Terminal! + If no terminal is visible, check the menus: Window → Terminals → + .... + A path or other configuration parameter may be incorrect. + The back-end debugger may have paused for user input. + There may be a missing dependency. + There may be an incorrect version, etc.

    + + """; + private static final String MSGPAT_PART_RESOURCES = """ +

    These resources remain after the failed launch:

    +
      + %s +
    + +

    How do you want to proceed?

    +
      +
    • Choose Keep to stop here and diagnose or complete the launch manually.
    • +
    • Choose Retry to clean up and retry at the launch dialog.
    • +
    • Choose Cancel to clean up without retrying.
    • +
    + """; + private static final String MSGPAT_WITH_RESOURCES = MSGPAT_PART_TOP + MSGPAT_PART_RESOURCES; + private static final String MSGPAT_WITHOUT_RESOURCES = MSGPAT_PART_TOP; + + public enum ErrPromptResponse { + KEEP, RETRY, TERMINATE; + } + + protected static String formatMessage(LaunchResult result) { + return hasResources(result) + ? MSGPAT_WITH_RESOURCES.formatted(htmlProgramName(result), + htmlExceptionMessage(result), htmlResources(result)) + : MSGPAT_WITHOUT_RESOURCES.formatted(htmlProgramName(result), + htmlExceptionMessage(result)); + } + + protected static String htmlProgramName(LaunchResult result) { + if (result.program() == null) { + return ""; + } + return "" + HTMLUtilities.escapeHTML(result.program().getName()) + ""; + } + + protected static String htmlExceptionMessage(LaunchResult result) { + if (result.exception() == null) { + return "(No exception)"; + } + return HTMLUtilities.escapeHTML(result.exception().toString()); + } + + protected static boolean hasResources(LaunchResult result) { + return !result.sessions().isEmpty() || + result.acceptor() != null || + result.connection() != null || + result.trace() != null; + } + + protected static String htmlResources(LaunchResult result) { + StringBuilder sb = new StringBuilder(); + for (Entry ent : result.sessions().entrySet()) { + TerminalSession session = ent.getValue(); + sb.append("
  • Terminal: %s → %s".formatted( + HTMLUtilities.escapeHTML(ent.getKey()), + HTMLUtilities.escapeHTML(session.description()))); + if (session.isTerminated()) { + sb.append(" (Terminated)"); + } + sb.append("
  • \n"); + } + if (result.acceptor() != null) { + sb.append("
  • Acceptor: %s
  • \n".formatted( + HTMLUtilities.escapeHTML(result.acceptor().getAddress().toString()))); + } + if (result.connection() != null) { + sb.append("
  • Connection: %s
  • \n".formatted( + HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString()))); + } + if (result.trace() != null) { + sb.append("
  • Trace: %s
  • \n".formatted( + HTMLUtilities.escapeHTML(result.trace().getName()))); + } + return sb.toString(); + } + + public static ErrPromptResponse show(LaunchResult result) { + return switch (new LaunchFailureDialog(result).show()) { + case OptionDialog.YES_OPTION -> ErrPromptResponse.KEEP; + case OptionDialog.NO_OPTION -> ErrPromptResponse.RETRY; + case OptionDialog.CANCEL_OPTION -> ErrPromptResponse.TERMINATE; + default -> throw new AssertionError(); + }; + } + + protected LaunchFailureDialog(LaunchResult result) { + super("Launch Failed", formatMessage(result), hasResources(result) ? "&Keep" : null, + "&Retry", OptionDialog.ERROR_MESSAGE, null, true, "Retry"); + } +} 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 160d6e35eb..6ddc255ac7 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 @@ -32,11 +32,7 @@ import ghidra.util.HelpLocation; import ghidra.util.Msg; /** - * Some attributes are required. Others are optional: - *
      - *
    • {@code @menu-path}: (Required)
    • - *
    - * + * A parser for reading attributes from a script header */ public abstract class ScriptAttributesParser { public static final String AT_TITLE = "@title"; @@ -52,6 +48,7 @@ public abstract class ScriptAttributesParser { 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 AT_NOIMAGE = "@no-image"; public static final String PREFIX_ENV = "env:"; public static final String PREFIX_ARG = "arg:"; @@ -277,7 +274,7 @@ public abstract class ScriptAttributesParser { public record ScriptAttributes(String title, String description, List menuPath, String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation, Map> parameters, Map extraTtys, - int timeoutMillis) { + int timeoutMillis, boolean noImage) { } /** @@ -301,7 +298,7 @@ public abstract class ScriptAttributesParser { if (address != null) { env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address)); if (address instanceof InetSocketAddress tcp) { - env.put("GHIDRA_TRACE_RMI_HOST", tcp.getAddress().toString()); + env.put("GHIDRA_TRACE_RMI_HOST", tcp.getAddress().getHostAddress()); env.put("GHIDRA_TRACE_RMI_PORT", Integer.toString(tcp.getPort())); } } @@ -337,6 +334,7 @@ public abstract class ScriptAttributesParser { private final Map> parameters = new LinkedHashMap<>(); private final Map extraTtys = new LinkedHashMap<>(); private int timeoutMillis = AbstractTraceRmiLaunchOffer.DEFAULT_TIMEOUT_MILLIS; + private boolean noImage = false; /** * Check if a line should just be ignored, e.g., blank lines, or the "shebang" line on UNIX. @@ -397,25 +395,29 @@ public abstract class ScriptAttributesParser { if (!parts[0].startsWith("@")) { return; } - if (parts.length < 2) { - Msg.error(this, "%s: Too few tokens: %s".formatted(loc, comment)); - return; + if (parts.length == 1) { + switch (parts[0].trim()) { + case AT_NOIMAGE -> parseNoImage(loc); + default -> parseUnrecognized(loc, comment); + } } - switch (parts[0].trim()) { - case AT_TITLE -> parseTitle(loc, parts[1]); - case AT_DESC -> parseDesc(loc, parts[1]); - case AT_MENU_PATH -> parseMenuPath(loc, parts[1]); - case AT_MENU_GROUP -> parseMenuGroup(loc, parts[1]); - case AT_MENU_ORDER -> parseMenuOrder(loc, parts[1]); - case AT_ICON -> parseIcon(loc, parts[1]); - case AT_HELP -> parseHelp(loc, parts[1]); - case AT_ENUM -> parseEnum(loc, parts[1]); - case AT_ENV -> parseEnv(loc, parts[1]); - 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); + else { + switch (parts[0].trim()) { + case AT_TITLE -> parseTitle(loc, parts[1]); + case AT_DESC -> parseDesc(loc, parts[1]); + case AT_MENU_PATH -> parseMenuPath(loc, parts[1]); + case AT_MENU_GROUP -> parseMenuGroup(loc, parts[1]); + case AT_MENU_ORDER -> parseMenuOrder(loc, parts[1]); + case AT_ICON -> parseIcon(loc, parts[1]); + case AT_HELP -> parseHelp(loc, parts[1]); + case AT_ENUM -> parseEnum(loc, parts[1]); + case AT_ENV -> parseEnv(loc, parts[1]); + 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); + } } } @@ -602,6 +604,10 @@ public abstract class ScriptAttributesParser { } } + protected void parseNoImage(Location loc) { + noImage = true; + } + protected void parseUnrecognized(Location loc, String line) { Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, line)); } @@ -626,7 +632,7 @@ public abstract class ScriptAttributesParser { return new ScriptAttributes(title, getDescription(), List.copyOf(menuPath), menuGroup, menuOrder, new GIcon(iconId), helpLocation, Collections.unmodifiableMap(new LinkedHashMap<>(parameters)), - Collections.unmodifiableMap(new LinkedHashMap<>(extraTtys)), timeoutMillis); + Collections.unmodifiableMap(new LinkedHashMap<>(extraTtys)), timeoutMillis, noImage); } private String getDescription() { diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePlugin.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePlugin.java index 4a9c0853c2..738732e270 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePlugin.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePlugin.java @@ -18,8 +18,13 @@ package ghidra.app.plugin.core.debug.gui.tracermi.launcher; import java.io.File; import java.io.IOException; import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jdom.Element; +import org.jdom.JDOMException; + +import db.Transaction; import docking.action.DockingActionIf; import docking.action.builder.ActionBuilder; import ghidra.app.events.ProgramActivatedPluginEvent; @@ -32,17 +37,18 @@ import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchConfigurator; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.PromptMode; import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion; -import ghidra.framework.options.OptionsChangeListener; -import ghidra.framework.options.ToolOptions; +import ghidra.framework.options.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.ProgramUserData; import ghidra.util.Msg; import ghidra.util.bean.opteditor.OptionsVetoException; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.exception.CancelledException; import ghidra.util.task.Task; import ghidra.util.task.TaskMonitor; +import ghidra.util.xml.XmlUtilities; @PluginInfo( shortDescription = "GUI elements to launch targets using Trace RMI", @@ -65,6 +71,10 @@ import ghidra.util.task.TaskMonitor; }) public class TraceRmiLauncherServicePlugin extends Plugin implements TraceRmiLauncherService, OptionsChangeListener { + protected static final String KEY_DBGLAUNCH = "DBGLAUNCH"; + protected static final String PREFIX_DBGLAUNCH = "DBGLAUNCH_"; + protected static final String KEY_LAST = "last"; + protected static final String OPTION_NAME_SCRIPT_PATHS = "Script Paths"; private final static LaunchConfigurator RELAUNCH = new LaunchConfigurator() { @@ -139,6 +149,8 @@ public class TraceRmiLauncherServicePlugin extends Plugin protected LaunchAction launchAction; protected List currentLaunchers = new ArrayList<>(); + protected SaveState toolLaunchConfigs = new SaveState(); + public TraceRmiLauncherServicePlugin(PluginTool tool) { super(tool); this.options = tool.getOptions(DebuggerPluginPackage.NAME); @@ -174,9 +186,6 @@ public class TraceRmiLauncherServicePlugin extends Plugin @Override public Collection getOffers(Program program) { - if (program == null) { - return List.of(); - } return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class) .stream() .flatMap(op -> op.getOffers(this, program).stream()) @@ -253,4 +262,132 @@ public class TraceRmiLauncherServicePlugin extends Plugin } } } + + @Override + public void readConfigState(SaveState saveState) { + super.readConfigState(saveState); + SaveState read = saveState.getSaveState(KEY_DBGLAUNCH); + if (read != null) { + toolLaunchConfigs = read; + } + } + + @Override + public void writeConfigState(SaveState saveState) { + super.writeConfigState(saveState); + if (toolLaunchConfigs != null) { + saveState.putSaveState(KEY_DBGLAUNCH, toolLaunchConfigs); + } + } + + protected SaveState readProgramLaunchConfig(Program program, String name, boolean forPrompt) { + /** + * TODO: Supposedly, per-program, per-user config stuff is being generalized for analyzers. + * Re-examine this if/when that gets merged + */ + ProgramUserData userData = program.getProgramUserData(); + String property = userData.getStringProperty(PREFIX_DBGLAUNCH + name, null); + if (property == null) { + return new SaveState(); + } + try { + Element element = XmlUtilities.fromString(property); + return new SaveState(element); + } + catch (JDOMException | IOException e) { + if (forPrompt) { + Msg.error(this, + "Saved launcher args are corrupt, or launcher parameters changed. Defaulting.", + e); + return new SaveState(); + } + throw new RuntimeException( + "Saved launcher args are corrupt, or launcher parameters changed. Not launching.", + e); + } + } + + protected SaveState readToolLaunchConfig(String name) { + if (!toolLaunchConfigs.hasValue(name)) { + return new SaveState(); + } + return toolLaunchConfigs.getSaveState(name); + } + + protected void writeProgramLaunchConfig(Program program, String name, SaveState state) { + ProgramUserData userData = program.getProgramUserData(); + state.putLong(KEY_LAST, System.currentTimeMillis()); + try (Transaction tx = userData.openTransaction()) { + Element element = state.saveToXml(); + userData.setStringProperty(PREFIX_DBGLAUNCH + name, XmlUtilities.toString(element)); + } + } + + protected void writeToolLaunchConfig(String name, SaveState state) { + state.putLong(KEY_LAST, System.currentTimeMillis()); + toolLaunchConfigs.putSaveState(name, state); + } + + protected record ConfigLast(String configName, long last, Program program) { + } + + protected ConfigLast checkSavedConfig(Program program, ProgramUserData userData, + String propName) { + if (!propName.startsWith(PREFIX_DBGLAUNCH)) { + return null; + } + String configName = propName.substring(PREFIX_DBGLAUNCH.length()); + String propVal = Objects.requireNonNull( + userData.getStringProperty(propName, null)); + Element element; + try { + element = XmlUtilities.fromString(propVal); + } + catch (JDOMException | IOException e) { + Msg.error(this, "Could not load launcher config for " + configName + ": " + e, e); + return null; + } + return checkSavedConfig(program, configName, new SaveState(element)); + } + + protected ConfigLast checkSavedConfig(Program program, String name, SaveState state) { + if (!state.hasValue(KEY_LAST)) { + return null; + } + return new ConfigLast(name, state.getLong(KEY_LAST, 0), program); + } + + protected Stream streamSavedConfigs(Program program) { + if (program == null) { + return Stream.of(toolLaunchConfigs.getNames()) + .map(n -> checkSavedConfig(null, n, toolLaunchConfigs.getSaveState(n))) + .filter(c -> c != null); + } + ProgramUserData userData = program.getProgramUserData(); + return userData.getStringPropertyNames() + .stream() + .map(n -> checkSavedConfig(program, userData, n)) + .filter(c -> c != null); + } + + protected ConfigLast findMostRecentConfig(Program program) { + return streamSavedConfigs(program).max(Comparator.comparing(c -> c.last)).orElse(null); + } + + protected TraceRmiLaunchOffer findOffer(ConfigLast last) { + if (last == null) { + return null; + } + for (TraceRmiLaunchOffer offer : getOffers(last.program)) { + if (offer.getConfigName().equals(last.configName)) { + return offer; + } + } + return null; + } + + protected Map loadSavedConfigs(Program program) { + return streamSavedConfigs(program) + .collect(Collectors.toMap(c -> c.configName(), c -> c.last())); + } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java index b1e6847435..92766db32e 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java @@ -27,7 +27,6 @@ import java.util.concurrent.*; import java.util.stream.*; import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import com.google.protobuf.ByteString; @@ -493,8 +492,9 @@ public class TraceRmiHandler implements TraceRmiConnection { return rep == null ? null : rep.build(); } catch (Throwable e) { + Msg.error(this, "Exception caused by back end", e); return rep.setError(ReplyError.newBuilder() - .setMessage(e.getMessage() + "\n" + ExceptionUtils.getStackTrace(e))) + .setMessage(e.getMessage())) .build(); } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py index 0c9dda8d7b..7a4e2b6728 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py @@ -62,8 +62,9 @@ class Receiver(Thread): Client._write_value( reply.xreply_invoke_method.return_value, result) except BaseException as e: - reply.xreply_invoke_method.error = ''.join( - traceback.format_exc()) + print("Error caused by front end") + traceback.print_exc() + reply.xreply_invoke_method.error = repr(e) self.client._send(reply) def _handle_reply(self, reply): @@ -540,8 +541,16 @@ class Batch(object): def append(self, fut): self.futures.append(fut) + @staticmethod + def _get_result(f, timeout): + try: + return f.result(timeout) + except BaseException as e: + print(f"Exception in batch operation: {repr(e)}") + return e + def results(self, timeout=None): - return [f.result(timeout) for f in self.futures] + return [self._get_result(f, timeout) for f in self.futures] class Client(object): diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java index 97ef5b0806..1cdfee0c9a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java @@ -681,7 +681,7 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel *

    * The terminal will no longer respond to the window resizing, and scrollbars are displayed as * needed. If the terminal size changes as a result of this call, - * {@link TerminalListener#resized(int, int)} is invoked. + * {@link TerminalListener#resized(short, short)} is invoked. * * @param cols the number of columns * @param rows the number of rows @@ -699,7 +699,7 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel *

    * Immediately fit the terminal to the window. It will also respond to the window resizing by * recalculating the rows and columns and adjusting the buffer's contents to fit. Whenever the - * terminal size changes {@link TerminalListener#resized(int, int)} is invoked. The bottom + * terminal size changes {@link TerminalListener#resized(short, short)} is invoked. The bottom * scrollbar is disabled, and the vertical scrollbar is always displayed, to avoid frenetic * horizontal resizing. */ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalProvider.java index e76a82a865..25f7fa0d04 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalProvider.java @@ -365,6 +365,9 @@ public class TerminalProvider extends ComponentProviderAdapter { terminated = true; removeLocalAction(actionTerminate); panel.terminalListeners.clear(); + panel.setOutputCallback(buf -> { + }); + panel.getFieldPanel().setCursorOn(false); setTitle("[Terminal]"); setSubTitle("Terminated"); if (!isVisible()) { diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPty.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPty.java index 37f7019cac..66cc987f71 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPty.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPty.java @@ -68,6 +68,8 @@ public class UnixPty implements Pty { if (closed) { return; } + child.closeStreams(); + parent.closeStreams(); LIB_POSIX.close(achild); LIB_POSIX.close(aparent); closed = true; diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyEndpoint.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyEndpoint.java index dc2ba76cde..90ff133d70 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyEndpoint.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyEndpoint.java @@ -15,8 +15,7 @@ */ package ghidra.pty.unix; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; import ghidra.pty.PtyEndpoint; import ghidra.pty.unix.PosixC.Ioctls; @@ -43,4 +42,9 @@ public class UnixPtyEndpoint implements PtyEndpoint { public InputStream getInputStream() { return inputStream; } + + protected void closeStreams() throws IOException { + outputStream.close(); + inputStream.close(); + } }