GP-4439: Add raw-gdb.sh and raw-python.sh. Add @no-image tag.

This commit is contained in:
Dan 2024-03-26 08:50:53 -04:00
parent 523f6e4cbe
commit f9bea7720a
17 changed files with 655 additions and 337 deletions

View file

@ -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 <html><body width="300px">
#@desc <h3>Start <tt>gdb</tt></h3>
#@desc <p>This will start <tt>gdb</tt> 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 <tt>protobuf</tt> and <tt>psutil</tt> installed for Python 3.</p>
#@desc </body></html>
#@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"

View file

@ -48,8 +48,8 @@ class HookState(object):
def end_batch(self): def end_batch(self):
if self.batch is None: if self.batch is None:
return return
commands.STATE.client.end_batch()
self.batch = None self.batch = None
commands.STATE.client.end_batch()
def check_skip_continue(self): def check_skip_continue(self):
skip = self.skip_continue skip = self.skip_continue

View file

@ -52,6 +52,7 @@ public interface TraceRmiLaunchOffer {
* @param sessions any terminal sessions created while launching the back-end. If there are more * @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 * than one, they are distinguished by launcher-defined keys. If there are no
* sessions, then there was likely a catastrophic error in the launcher. * 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 connection if the target connected back to Ghidra, that connection
* @param trace if the connection started a trace, the (first) trace it created * @param trace if the connection started a trace, the (first) trace it created
* @param exception optional error, if failed * @param exception optional error, if failed
@ -138,7 +139,7 @@ public interface TraceRmiLaunchOffer {
/** /**
* Re-write the launcher arguments, if desired * 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 arguments the arguments suggested by the offer or saved settings
* @param relPrompt describes the timing of this callback relative to prompting the user * @param relPrompt describes the timing of this callback relative to prompting the user
* @return the adjusted arguments * @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 * 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 <em>cannot</em> depend on * "same offer" have the same configuration name. Note that the name <em>cannot</em> depend on
* the program name, but can depend on the model factory and program language and/or compiler * 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 * @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 * 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 * least-recently used. An entry that has never been used does not appear in the quick launch
* menu. * menu.
*
* @return the sub-group name for ordering in the menu
*/ */
default String getMenuOrder() { default String getMenuOrder() {
return ""; return "";
@ -285,4 +288,11 @@ public interface TraceRmiLaunchOffer {
* @return the parameters * @return the parameters
*/ */
Map<String, ParameterDescription<?>> getParameters(); Map<String, ParameterDescription<?>> getParameters();
/**
* Check if this offer requires an open program
*
* @return true if required
*/
boolean requiresImage();
} }

View file

@ -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 <html><body width="300px">
#@desc <h3>Start <tt>gdb</tt></h3>
#@desc <p>This will start <tt>python</tt>, import <tt>ghidratrace</tt> 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 <tt>protobuf</tt> installed for Python 3.</p>
#@desc </body></html>
#@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

View file

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

View file

@ -102,7 +102,9 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi
List<String> commandLine = new ArrayList<>(); List<String> commandLine = new ArrayList<>();
Map<String, String> env = new HashMap<>(System.getenv()); Map<String, String> env = new HashMap<>(System.getenv());
prepareSubprocess(commandLine, env, args, address); 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<String, TtyCondition> ent : attrs.extraTtys().entrySet()) { for (Map.Entry<String, TtyCondition> ent : attrs.extraTtys().entrySet()) {
if (!ent.getValue().isActive(args)) { if (!ent.getValue().isActive(args)) {
@ -116,4 +118,9 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi
sessions.put("Shell", sessions.put("Shell",
runInTerminal(commandLine, env, script.getParentFile(), sessions.values())); runInTerminal(commandLine, env, script.getParentFile(), sessions.values()));
} }
@Override
public boolean requiresImage() {
return !attrs.noImage();
}
} }

View file

@ -26,13 +26,9 @@ import java.util.concurrent.*;
import javax.swing.Icon; 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.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog; 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.DefaultTraceRmiAcceptor;
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler; import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler;
import ghidra.app.plugin.core.terminal.TerminalListener; 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.AutoConfigState.ConfigStateField;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.*; 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.program.util.ProgramLocation;
import ghidra.pty.*; import ghidra.pty.*;
import ghidra.trace.model.Trace; import ghidra.trace.model.Trace;
import ghidra.trace.model.TraceLocation; import ghidra.trace.model.TraceLocation;
import ghidra.trace.model.modules.TraceModule; 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.exception.CancelledException;
import ghidra.util.task.Task; import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
import ghidra.util.xml.XmlUtilities;
public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer { 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 PARAM_DISPLAY_IMAGE = "Image";
public static final String PREFIX_PARAM_EXTTOOL = "env:GHIDRA_LANG_EXTTOOL_"; 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) { public AbstractTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program) {
this.plugin = Objects.requireNonNull(plugin); this.plugin = Objects.requireNonNull(plugin);
this.program = Objects.requireNonNull(program); this.program = program;
this.tool = plugin.getTool(); this.tool = plugin.getTool();
this.terminalService = Objects.requireNonNull(tool.getService(TerminalService.class)); this.terminalService = Objects.requireNonNull(tool.getService(TerminalService.class));
} }
@ -165,6 +160,9 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
} }
protected Address getMappingProbeAddress() { protected Address getMappingProbeAddress() {
if (program == null) {
return null;
}
AddressIterator eepi = program.getSymbolTable().getExternalEntryPointIterator(); AddressIterator eepi = program.getSymbolTable().getExternalEntryPointIterator();
if (eepi.hasNext()) { if (eepi.hasNext()) {
return eepi.next(); return eepi.next();
@ -220,6 +218,9 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
protected Collection<ModuleMapEntry> invokeMapper(TaskMonitor monitor, protected Collection<ModuleMapEntry> invokeMapper(TaskMonitor monitor,
DebuggerStaticMappingService mappingService, Trace trace) throws CancelledException { DebuggerStaticMappingService mappingService, Trace trace) throws CancelledException {
if (program == null) {
return List.of();
}
Map<TraceModule, ModuleMapProposal> map = mappingService Map<TraceModule, ModuleMapProposal> map = mappingService
.proposeModuleMaps(trace.getModuleManager().getAllModules(), List.of(program)); .proposeModuleMaps(trace.getModuleManager().getAllModules(), List.of(program));
Collection<ModuleMapEntry> proposal = MapProposal.flatten(map.values()); Collection<ModuleMapEntry> proposal = MapProposal.flatten(map.values());
@ -227,7 +228,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
return proposal; return proposal;
} }
private void saveLauncherArgs(Map<String, ?> args, protected SaveState saveLauncherArgsToState(Map<String, ?> args,
Map<String, ParameterDescription<?>> params) { Map<String, ParameterDescription<?>> params) {
SaveState state = new SaveState(); SaveState state = new SaveState();
for (ParameterDescription<?> param : params.values()) { for (ParameterDescription<?> param : params.values()) {
@ -235,17 +236,22 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
if (val != null) { if (val != null) {
ConfigStateField.putState(state, param.type.asSubclass(Object.class), ConfigStateField.putState(state, param.type.asSubclass(Object.class),
"param_" + param.name, val); "param_" + param.name, val);
state.putLong("last", System.currentTimeMillis());
} }
} }
if (program != null) { return state;
ProgramUserData userData = program.getProgramUserData(); }
try (Transaction tx = userData.openTransaction()) {
Element element = state.saveToXml(); protected void saveState(SaveState state) {
userData.setStringProperty(PREFIX_DBGLAUNCH + getConfigName(), if (program == null) {
XmlUtilities.toString(element)); plugin.writeToolLaunchConfig(getConfigName(), state);
} return;
} }
plugin.writeProgramLaunchConfig(program, getConfigName(), state);
}
protected void saveLauncherArgs(Map<String, ?> args,
Map<String, ParameterDescription<?>> params) {
saveState(saveLauncherArgsToState(args, params));
} }
/** /**
@ -261,9 +267,6 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected Map<String, ?> generateDefaultLauncherArgs( protected Map<String, ?> generateDefaultLauncherArgs(
Map<String, ParameterDescription<?>> params) { Map<String, ParameterDescription<?>> params) {
if (program == null) {
return Map.of();
}
Map<String, Object> map = new LinkedHashMap<String, Object>(); Map<String, Object> map = new LinkedHashMap<String, Object>();
ParameterDescription<String> paramImage = null; ParameterDescription<String> paramImage = null;
for (Entry<String, ParameterDescription<?>> entry : params.entrySet()) { for (Entry<String, ParameterDescription<?>> 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); File imageFile = TraceRmiLauncherServicePlugin.getProgramPath(program);
if (imageFile != null) { if (imageFile != null) {
paramImage.set(map, imageFile.getAbsolutePath()); 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 * 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. * 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 * @param forPrompt true if the user will be confirming the arguments
* @return the loaded arguments, or defaults * @return the loaded arguments, or defaults
*/ */
protected Map<String, ?> loadLastLauncherArgs(boolean forPrompt) { protected Map<String, ?> loadLastLauncherArgs(boolean forPrompt) {
/** Map<String, ParameterDescription<?>> params = getParameters();
* TODO: Supposedly, per-program, per-user config stuff is being generalized for analyzers. Map<String, ?> args = loadLauncherArgsFromState(loadState(forPrompt), params);
* Re-examine this if/when that gets merged saveLauncherArgs(args, params);
*/ return args;
if (program != null) { }
Map<String, ParameterDescription<?>> 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<String> names = List.of(state.getNames());
Map<String, Object> 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<String, ?> args = generateDefaultLauncherArgs(params);
saveLauncherArgs(args, params);
return args;
}
return new LinkedHashMap<>(); protected Map<String, ?> loadLauncherArgsFromState(SaveState state,
Map<String, ParameterDescription<?>> params) {
Map<String, ?> defaultArgs = generateDefaultLauncherArgs(params);
if (state == null) {
return defaultArgs;
}
List<String> names = List.of(state.getNames());
Map<String, Object> 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 protected void initializeMonitor(TaskMonitor monitor) {
public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) { if (requiresImage()) {
InternalTraceRmiService service = tool.getService(InternalTraceRmiService.class); 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 = DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class); 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<ModuleMapEntry> 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 = DebuggerTraceManagerService traceManager =
tool.getService(DebuggerTraceManagerService.class); tool.getService(DebuggerTraceManagerService.class);
final PromptMode mode = configurator.getPromptMode(); final PromptMode mode = configurator.getPromptMode();
@ -543,61 +574,60 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
Trace trace = null; Trace trace = null;
Throwable lastExc = null; Throwable lastExc = null;
monitor.setMaximum(5); initializeMonitor(monitor);
while (true) { while (true) {
monitor.setMessage("Gathering arguments");
Map<String, ?> 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 { try {
monitor.setMessage("Gathering arguments");
Map<String, ?> 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.setMessage("Listening for connection");
monitor.increment();
acceptor = service.acceptOne(new InetSocketAddress("127.0.0.1", 0)); acceptor = service.acceptOne(new InetSocketAddress("127.0.0.1", 0));
monitor.increment();
monitor.setMessage("Launching back-end"); monitor.setMessage("Launching back-end");
monitor.increment();
launchBackEnd(monitor, sessions, args, acceptor.getAddress()); launchBackEnd(monitor, sessions, args, acceptor.getAddress());
monitor.setMessage("Waiting for connection");
monitor.increment(); monitor.increment();
monitor.setMessage("Waiting for connection");
acceptor.setTimeout(getConnectionTimeoutMillis()); acceptor.setTimeout(getConnectionTimeoutMillis());
connection = acceptor.accept(); connection = acceptor.accept();
connection.registerTerminals(sessions.values()); connection.registerTerminals(sessions.values());
monitor.setMessage("Waiting for trace");
monitor.increment(); monitor.increment();
monitor.setMessage("Waiting for trace");
trace = connection.waitForTrace(getTimeoutMillis()); trace = connection.waitForTrace(getTimeoutMillis());
traceManager.openTrace(trace); traceManager.openTrace(trace);
traceManager.activate(traceManager.resolveTrace(trace), traceManager.activate(traceManager.resolveTrace(trace),
ActivationCause.START_RECORDING); ActivationCause.START_RECORDING);
monitor.setMessage("Waiting for module mapping");
monitor.increment(); monitor.increment();
waitForModuleMapping(monitor, connection, trace);
}
catch (CancelledException e) {
lastExc = e;
LaunchResult result =
new LaunchResult(program, sessions, acceptor, connection, trace, lastExc);
try { try {
listenForMapping(mappingService, connection, trace).get(getTimeoutMillis(), result.close();
TimeUnit.MILLISECONDS);
} }
catch (TimeoutException e) { catch (Exception e1) {
monitor.setMessage( Msg.error(this, "Could not close", e1);
"Timed out waiting for module mapping. Invoking the mapper.");
Collection<ModuleMapEntry> 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.");
}
} }
return new LaunchResult(program, Map.of(), null, null, null, lastExc);
} }
catch (Exception e) { catch (Exception e) {
DebuggerConsoleService consoleService = DebuggerConsoleService consoleService =
@ -635,101 +665,11 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
} }
return result; 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) { protected ErrPromptResponse promptError(LaunchResult result) {
String message = """ return LaunchFailureDialog.show(result);
<html><body width="400px">
<h3>Failed to launch %s due to an exception:</h3>
<tt>%s</tt>
<h3>Troubleshooting</h3>
<p>
<b>Check the Terminal!</b>
If no terminal is visible, check the menus: <b>Window &rarr; Terminals &rarr;
...</b>.
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.</p>
<h3>These resources remain after the failed launch:</h3>
<ul>
%s
</ul>
<h3>Do you want to keep these resources?</h3>
<ul>
<li>Choose <b>Yes</b> to stop here and diagnose or complete the launch manually.
</li>
<li>Choose <b>No</b> to clean up and retry at the launch dialog.</li>
<li>Choose <b>Cancel</b> to clean up without retrying.</li>
""".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 "<tt>" + HTMLUtilities.escapeHTML(result.program().getName()) + "</tt>";
}
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<String, TerminalSession> ent : result.sessions().entrySet()) {
TerminalSession session = ent.getValue();
sb.append("<li>Terminal: %s &rarr; <tt>%s</tt>".formatted(
HTMLUtilities.escapeHTML(ent.getKey()),
HTMLUtilities.escapeHTML(session.description())));
if (session.isTerminated()) {
sb.append(" (Terminated)");
}
sb.append("</li>\n");
}
if (result.acceptor() != null) {
sb.append("<li>Acceptor: <tt>%s</tt></li>\n".formatted(
HTMLUtilities.escapeHTML(result.acceptor().getAddress().toString())));
}
if (result.connection() != null) {
sb.append("<li>Connection: <tt>%s</tt></li>\n".formatted(
HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString())));
}
if (result.trace() != null) {
sb.append("<li>Trace: %s</li>\n".formatted(
HTMLUtilities.escapeHTML(result.trace().getName())));
}
return sb.toString();
} }
} }

View file

@ -15,27 +15,22 @@
*/ */
package ghidra.app.plugin.core.debug.gui.tracermi.launcher; package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.swing.*; import javax.swing.*;
import org.jdom.Element;
import org.jdom.JDOMException;
import docking.ActionContext; import docking.ActionContext;
import docking.PopupMenuHandler; import docking.PopupMenuHandler;
import docking.action.*; import docking.action.*;
import docking.action.builder.ActionBuilder; import docking.action.builder.ActionBuilder;
import docking.menu.*; import docking.menu.*;
import ghidra.app.plugin.core.debug.gui.DebuggerResources; 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.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.framework.options.SaveState;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.ProgramUserData; import ghidra.util.HelpLocation;
import ghidra.util.*; import ghidra.util.Swing;
import ghidra.util.xml.XmlUtilities;
public class LaunchAction extends MultiActionDockingAction { public class LaunchAction extends MultiActionDockingAction {
public static final String NAME = "Launch"; public static final String NAME = "Launch";
@ -55,58 +50,10 @@ public class LaunchAction extends MultiActionDockingAction {
protected String[] prependConfigAndLaunch(List<String> menuPath) { protected String[] prependConfigAndLaunch(List<String> menuPath) {
Program program = plugin.currentProgram; Program program = plugin.currentProgram;
return Stream.concat( String title = program == null
Stream.of("Configure and Launch " + program.getName() + " using..."), ? "Configure and Launch ..."
menuPath.stream()).toArray(String[]::new); : "Configure and Launch %s using...".formatted(program.getName());
} return Stream.concat(Stream.of(title), 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;
} }
@Override @Override
@ -116,17 +63,7 @@ public class LaunchAction extends MultiActionDockingAction {
List<DockingActionIf> actions = new ArrayList<>(); List<DockingActionIf> actions = new ArrayList<>();
Map<String, Long> saved = new HashMap<>(); Map<String, Long> saved = plugin.loadSavedConfigs(program);
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);
}
}
for (TraceRmiLaunchOffer offer : offers) { for (TraceRmiLaunchOffer offer : offers) {
actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName()) actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName())
@ -134,7 +71,7 @@ public class LaunchAction extends MultiActionDockingAction {
.popupMenuGroup(offer.getMenuGroup(), offer.getMenuOrder()) .popupMenuGroup(offer.getMenuGroup(), offer.getMenuOrder())
.popupMenuIcon(offer.getIcon()) .popupMenuIcon(offer.getIcon())
.helpLocation(offer.getHelpLocation()) .helpLocation(offer.getHelpLocation())
.enabledWhen(ctx -> true) .enabledWhen(ctx -> !offer.requiresImage() || program != null)
.onAction(ctx -> plugin.configureAndLaunch(offer)) .onAction(ctx -> plugin.configureAndLaunch(offer))
.build()); .build());
Long last = saved.get(offer.getConfigName()); Long last = saved.get(offer.getConfigName());
@ -143,8 +80,11 @@ public class LaunchAction extends MultiActionDockingAction {
// Thus, no worries about program.getName() below. // Thus, no worries about program.getName() below.
continue; 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()) 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)) .popupMenuGroup("0", "%016x".formatted(Long.MAX_VALUE - last))
.popupMenuIcon(offer.getIcon()) .popupMenuIcon(offer.getIcon())
.helpLocation(offer.getHelpLocation()) .helpLocation(offer.getHelpLocation())
@ -169,6 +109,7 @@ public class LaunchAction extends MultiActionDockingAction {
MenuManager manager = MenuManager manager =
new MenuManager("Launch", (char) 0, GROUP, true, handler, null); new MenuManager("Launch", (char) 0, GROUP, true, handler, null);
for (DockingActionIf action : actionList) { for (DockingActionIf action : actionList) {
action.setEnabled(action.isEnabledForContext(context));
manager.addAction(action); manager.addAction(action);
} }
return manager.getPopupMenu(); return manager.getPopupMenu();
@ -193,26 +134,14 @@ public class LaunchAction extends MultiActionDockingAction {
@Override @Override
public boolean isEnabledForContext(ActionContext context) { public boolean isEnabledForContext(ActionContext context) {
return plugin.currentProgram != null; return !plugin.getOffers(plugin.currentProgram).isEmpty();
}
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;
} }
@Override @Override
public void actionPerformed(ActionContext context) { public void actionPerformed(ActionContext context) {
// See comment on super method about use of runLater // See comment on super method about use of runLater
ConfigLast last = findMostRecentConfig(); ConfigLast last = plugin.findMostRecentConfig(plugin.currentProgram);
TraceRmiLaunchOffer offer = findOffer(last); TraceRmiLaunchOffer offer = plugin.findOffer(last);
if (offer == null) { if (offer == null) {
Swing.runLater(() -> button.showPopup()); Swing.runLater(() -> button.showPopup());
return; return;
@ -223,14 +152,17 @@ public class LaunchAction extends MultiActionDockingAction {
@Override @Override
public String getDescription() { public String getDescription() {
Program program = plugin.currentProgram; Program program = plugin.currentProgram;
if (program == null) { ConfigLast last = plugin.findMostRecentConfig(program);
return "Launch (program required)"; TraceRmiLaunchOffer offer = plugin.findOffer(last);
if (offer == null && program == null) {
return "Configure and launch";
} }
ConfigLast last = findMostRecentConfig();
TraceRmiLaunchOffer offer = findOffer(last);
if (offer == null) { if (offer == null) {
return "Configure and launch " + program.getName(); 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());
} }
} }

View file

@ -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 = """
<html><body width="400px">
<h3>Failed to launch %s due to an exception:</h3>
<tt>%s</tt>
<h3>Troubleshooting</h3>
<p>
<b>Check the Terminal!</b>
If no terminal is visible, check the menus: <b>Window &rarr; Terminals &rarr;
...</b>.
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.</p>
""";
private static final String MSGPAT_PART_RESOURCES = """
<h3>These resources remain after the failed launch:</h3>
<ul>
%s
</ul>
<h3>How do you want to proceed?</h3>
<ul>
<li>Choose <b>Keep</b> to stop here and diagnose or complete the launch manually.</li>
<li>Choose <b>Retry</b> to clean up and retry at the launch dialog.</li>
<li>Choose <b>Cancel</b> to clean up without retrying.</li>
</ul>
""";
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 "<tt>" + HTMLUtilities.escapeHTML(result.program().getName()) + "</tt>";
}
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<String, TerminalSession> ent : result.sessions().entrySet()) {
TerminalSession session = ent.getValue();
sb.append("<li>Terminal: %s &rarr; <tt>%s</tt>".formatted(
HTMLUtilities.escapeHTML(ent.getKey()),
HTMLUtilities.escapeHTML(session.description())));
if (session.isTerminated()) {
sb.append(" (Terminated)");
}
sb.append("</li>\n");
}
if (result.acceptor() != null) {
sb.append("<li>Acceptor: <tt>%s</tt></li>\n".formatted(
HTMLUtilities.escapeHTML(result.acceptor().getAddress().toString())));
}
if (result.connection() != null) {
sb.append("<li>Connection: <tt>%s</tt></li>\n".formatted(
HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString())));
}
if (result.trace() != null) {
sb.append("<li>Trace: %s</li>\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");
}
}

View file

@ -32,11 +32,7 @@ import ghidra.util.HelpLocation;
import ghidra.util.Msg; import ghidra.util.Msg;
/** /**
* Some attributes are required. Others are optional: * A parser for reading attributes from a script header
* <ul>
* <li>{@code @menu-path}: <b>(Required)</b></li>
* </ul>
*
*/ */
public abstract class ScriptAttributesParser { public abstract class ScriptAttributesParser {
public static final String AT_TITLE = "@title"; 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_ARGS = "@args";
public static final String AT_TTY = "@tty"; public static final String AT_TTY = "@tty";
public static final String AT_TIMEOUT = "@timeout"; 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_ENV = "env:";
public static final String PREFIX_ARG = "arg:"; public static final String PREFIX_ARG = "arg:";
@ -277,7 +274,7 @@ public abstract class ScriptAttributesParser {
public record ScriptAttributes(String title, String description, List<String> menuPath, public record ScriptAttributes(String title, String description, List<String> menuPath,
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation, String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
Map<String, ParameterDescription<?>> parameters, Map<String, TtyCondition> extraTtys, Map<String, ParameterDescription<?>> parameters, Map<String, TtyCondition> extraTtys,
int timeoutMillis) { int timeoutMillis, boolean noImage) {
} }
/** /**
@ -301,7 +298,7 @@ public abstract class ScriptAttributesParser {
if (address != null) { if (address != null) {
env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address)); env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address));
if (address instanceof InetSocketAddress tcp) { 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())); env.put("GHIDRA_TRACE_RMI_PORT", Integer.toString(tcp.getPort()));
} }
} }
@ -337,6 +334,7 @@ public abstract class ScriptAttributesParser {
private final Map<String, ParameterDescription<?>> parameters = new LinkedHashMap<>(); private final Map<String, ParameterDescription<?>> parameters = new LinkedHashMap<>();
private final Map<String, TtyCondition> extraTtys = new LinkedHashMap<>(); private final Map<String, TtyCondition> extraTtys = new LinkedHashMap<>();
private int timeoutMillis = AbstractTraceRmiLaunchOffer.DEFAULT_TIMEOUT_MILLIS; 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. * 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("@")) { if (!parts[0].startsWith("@")) {
return; return;
} }
if (parts.length < 2) { if (parts.length == 1) {
Msg.error(this, "%s: Too few tokens: %s".formatted(loc, comment)); switch (parts[0].trim()) {
return; case AT_NOIMAGE -> parseNoImage(loc);
default -> parseUnrecognized(loc, comment);
}
} }
switch (parts[0].trim()) { else {
case AT_TITLE -> parseTitle(loc, parts[1]); switch (parts[0].trim()) {
case AT_DESC -> parseDesc(loc, parts[1]); case AT_TITLE -> parseTitle(loc, parts[1]);
case AT_MENU_PATH -> parseMenuPath(loc, parts[1]); case AT_DESC -> parseDesc(loc, parts[1]);
case AT_MENU_GROUP -> parseMenuGroup(loc, parts[1]); case AT_MENU_PATH -> parseMenuPath(loc, parts[1]);
case AT_MENU_ORDER -> parseMenuOrder(loc, parts[1]); case AT_MENU_GROUP -> parseMenuGroup(loc, parts[1]);
case AT_ICON -> parseIcon(loc, parts[1]); case AT_MENU_ORDER -> parseMenuOrder(loc, parts[1]);
case AT_HELP -> parseHelp(loc, parts[1]); case AT_ICON -> parseIcon(loc, parts[1]);
case AT_ENUM -> parseEnum(loc, parts[1]); case AT_HELP -> parseHelp(loc, parts[1]);
case AT_ENV -> parseEnv(loc, parts[1]); case AT_ENUM -> parseEnum(loc, parts[1]);
case AT_ARG -> parseArg(loc, parts[1], ++argc); case AT_ENV -> parseEnv(loc, parts[1]);
case AT_ARGS -> parseArgs(loc, parts[1]); case AT_ARG -> parseArg(loc, parts[1], ++argc);
case AT_TTY -> parseTty(loc, parts[1]); case AT_ARGS -> parseArgs(loc, parts[1]);
case AT_TIMEOUT -> parseTimeout(loc, parts[1]); case AT_TTY -> parseTty(loc, parts[1]);
default -> parseUnrecognized(loc, comment); 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) { protected void parseUnrecognized(Location loc, String line) {
Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, 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, return new ScriptAttributes(title, getDescription(), List.copyOf(menuPath), menuGroup,
menuOrder, new GIcon(iconId), helpLocation, menuOrder, new GIcon(iconId), helpLocation,
Collections.unmodifiableMap(new LinkedHashMap<>(parameters)), Collections.unmodifiableMap(new LinkedHashMap<>(parameters)),
Collections.unmodifiableMap(new LinkedHashMap<>(extraTtys)), timeoutMillis); Collections.unmodifiableMap(new LinkedHashMap<>(extraTtys)), timeoutMillis, noImage);
} }
private String getDescription() { private String getDescription() {

View file

@ -18,8 +18,13 @@ package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.jdom.Element;
import org.jdom.JDOMException;
import db.Transaction;
import docking.action.DockingActionIf; import docking.action.DockingActionIf;
import docking.action.builder.ActionBuilder; import docking.action.builder.ActionBuilder;
import ghidra.app.events.ProgramActivatedPluginEvent; 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.LaunchConfigurator;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.PromptMode; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.PromptMode;
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion; import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
import ghidra.framework.options.OptionsChangeListener; import ghidra.framework.options.*;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus; import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.ProgramUserData;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.bean.opteditor.OptionsVetoException; import ghidra.util.bean.opteditor.OptionsVetoException;
import ghidra.util.classfinder.ClassSearcher; import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task; import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
import ghidra.util.xml.XmlUtilities;
@PluginInfo( @PluginInfo(
shortDescription = "GUI elements to launch targets using Trace RMI", shortDescription = "GUI elements to launch targets using Trace RMI",
@ -65,6 +71,10 @@ import ghidra.util.task.TaskMonitor;
}) })
public class TraceRmiLauncherServicePlugin extends Plugin public class TraceRmiLauncherServicePlugin extends Plugin
implements TraceRmiLauncherService, OptionsChangeListener { 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"; protected static final String OPTION_NAME_SCRIPT_PATHS = "Script Paths";
private final static LaunchConfigurator RELAUNCH = new LaunchConfigurator() { private final static LaunchConfigurator RELAUNCH = new LaunchConfigurator() {
@ -139,6 +149,8 @@ public class TraceRmiLauncherServicePlugin extends Plugin
protected LaunchAction launchAction; protected LaunchAction launchAction;
protected List<DockingActionIf> currentLaunchers = new ArrayList<>(); protected List<DockingActionIf> currentLaunchers = new ArrayList<>();
protected SaveState toolLaunchConfigs = new SaveState();
public TraceRmiLauncherServicePlugin(PluginTool tool) { public TraceRmiLauncherServicePlugin(PluginTool tool) {
super(tool); super(tool);
this.options = tool.getOptions(DebuggerPluginPackage.NAME); this.options = tool.getOptions(DebuggerPluginPackage.NAME);
@ -174,9 +186,6 @@ public class TraceRmiLauncherServicePlugin extends Plugin
@Override @Override
public Collection<TraceRmiLaunchOffer> getOffers(Program program) { public Collection<TraceRmiLaunchOffer> getOffers(Program program) {
if (program == null) {
return List.of();
}
return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class) return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class)
.stream() .stream()
.flatMap(op -> op.getOffers(this, program).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<ConfigLast> 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<String, Long> loadSavedConfigs(Program program) {
return streamSavedConfigs(program)
.collect(Collectors.toMap(c -> c.configName(), c -> c.last()));
}
} }

View file

@ -27,7 +27,6 @@ import java.util.concurrent.*;
import java.util.stream.*; import java.util.stream.*;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
@ -493,8 +492,9 @@ public class TraceRmiHandler implements TraceRmiConnection {
return rep == null ? null : rep.build(); return rep == null ? null : rep.build();
} }
catch (Throwable e) { catch (Throwable e) {
Msg.error(this, "Exception caused by back end", e);
return rep.setError(ReplyError.newBuilder() return rep.setError(ReplyError.newBuilder()
.setMessage(e.getMessage() + "\n" + ExceptionUtils.getStackTrace(e))) .setMessage(e.getMessage()))
.build(); .build();
} }
} }

View file

@ -62,8 +62,9 @@ class Receiver(Thread):
Client._write_value( Client._write_value(
reply.xreply_invoke_method.return_value, result) reply.xreply_invoke_method.return_value, result)
except BaseException as e: except BaseException as e:
reply.xreply_invoke_method.error = ''.join( print("Error caused by front end")
traceback.format_exc()) traceback.print_exc()
reply.xreply_invoke_method.error = repr(e)
self.client._send(reply) self.client._send(reply)
def _handle_reply(self, reply): def _handle_reply(self, reply):
@ -540,8 +541,16 @@ class Batch(object):
def append(self, fut): def append(self, fut):
self.futures.append(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): 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): class Client(object):

View file

@ -681,7 +681,7 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel
* <p> * <p>
* The terminal will no longer respond to the window resizing, and scrollbars are displayed as * 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, * 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 cols the number of columns
* @param rows the number of rows * @param rows the number of rows
@ -699,7 +699,7 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel
* <p> * <p>
* Immediately fit the terminal to the window. It will also respond to the window resizing by * 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 * 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 * scrollbar is disabled, and the vertical scrollbar is always displayed, to avoid frenetic
* horizontal resizing. * horizontal resizing.
*/ */

View file

@ -365,6 +365,9 @@ public class TerminalProvider extends ComponentProviderAdapter {
terminated = true; terminated = true;
removeLocalAction(actionTerminate); removeLocalAction(actionTerminate);
panel.terminalListeners.clear(); panel.terminalListeners.clear();
panel.setOutputCallback(buf -> {
});
panel.getFieldPanel().setCursorOn(false);
setTitle("[Terminal]"); setTitle("[Terminal]");
setSubTitle("Terminated"); setSubTitle("Terminated");
if (!isVisible()) { if (!isVisible()) {

View file

@ -68,6 +68,8 @@ public class UnixPty implements Pty {
if (closed) { if (closed) {
return; return;
} }
child.closeStreams();
parent.closeStreams();
LIB_POSIX.close(achild); LIB_POSIX.close(achild);
LIB_POSIX.close(aparent); LIB_POSIX.close(aparent);
closed = true; closed = true;

View file

@ -15,8 +15,7 @@
*/ */
package ghidra.pty.unix; package ghidra.pty.unix;
import java.io.InputStream; import java.io.*;
import java.io.OutputStream;
import ghidra.pty.PtyEndpoint; import ghidra.pty.PtyEndpoint;
import ghidra.pty.unix.PosixC.Ioctls; import ghidra.pty.unix.PosixC.Ioctls;
@ -43,4 +42,9 @@ public class UnixPtyEndpoint implements PtyEndpoint {
public InputStream getInputStream() { public InputStream getInputStream() {
return inputStream; return inputStream;
} }
protected void closeStreams() throws IOException {
outputStream.close();
inputStream.close();
}
} }