Merge remote-tracking branch 'origin/GP-4439_Dan_rawGdbConnector--SQUASHED'

This commit is contained in:
Ryan Kurtz 2024-03-27 07:48:24 -04:00
commit 11abf7553c
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):
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

View File

@ -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
@ -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<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<>();
Map<String, String> env = new HashMap<>(System.getenv());
prepareSubprocess(commandLine, env, args, address);
if (program != null) {
env.put("GHIDRA_LANGUAGE_ID", program.getLanguageID().toString());
}
for (Map.Entry<String, TtyCondition> 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();
}
}

View File

@ -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<ModuleMapEntry> invokeMapper(TaskMonitor monitor,
DebuggerStaticMappingService mappingService, Trace trace) throws CancelledException {
if (program == null) {
return List.of();
}
Map<TraceModule, ModuleMapProposal> map = mappingService
.proposeModuleMaps(trace.getModuleManager().getAllModules(), List.of(program));
Collection<ModuleMapEntry> proposal = MapProposal.flatten(map.values());
@ -227,7 +228,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
return proposal;
}
private void saveLauncherArgs(Map<String, ?> args,
protected SaveState saveLauncherArgsToState(Map<String, ?> args,
Map<String, ParameterDescription<?>> 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<String, ?> args,
Map<String, ParameterDescription<?>> params) {
saveState(saveLauncherArgsToState(args, params));
}
/**
@ -261,9 +267,6 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
@SuppressWarnings("unchecked")
protected Map<String, ?> generateDefaultLauncherArgs(
Map<String, ParameterDescription<?>> params) {
if (program == null) {
return Map.of();
}
Map<String, Object> map = new LinkedHashMap<String, Object>();
ParameterDescription<String> paramImage = null;
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);
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<String, ?> 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<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);
Map<String, ?> args = loadLauncherArgsFromState(loadState(forPrompt), 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,58 +517,24 @@ 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);
DebuggerTraceManagerService traceManager =
tool.getService(DebuggerTraceManagerService.class);
final PromptMode mode = configurator.getPromptMode();
boolean prompt = mode == PromptMode.ALWAYS;
DefaultTraceRmiAcceptor acceptor = null;
Map<String, TerminalSession> sessions = new LinkedHashMap<>();
TraceRmiHandler connection = null;
Trace trace = null;
Throwable lastExc = null;
monitor.setMaximum(5);
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 {
monitor.setMessage("Listening for connection");
monitor.increment();
acceptor = service.acceptOne(new InetSocketAddress("127.0.0.1", 0));
monitor.setMessage("Launching back-end");
monitor.increment();
launchBackEnd(monitor, sessions, args, acceptor.getAddress());
monitor.setMessage("Waiting for connection");
monitor.increment();
acceptor.setTimeout(getConnectionTimeoutMillis());
connection = acceptor.accept();
connection.registerTerminals(sessions.values());
monitor.setMessage("Waiting for trace");
monitor.increment();
trace = connection.waitForTrace(getTimeoutMillis());
traceManager.openTrace(trace);
traceManager.activate(traceManager.resolveTrace(trace),
ActivationCause.START_RECORDING);
monitor.setMessage("Waiting for module mapping");
monitor.increment();
try {
listenForMapping(mappingService, connection, trace).get(getTimeoutMillis(),
TimeUnit.MILLISECONDS);
@ -598,6 +554,80 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
"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();
boolean prompt = mode == PromptMode.ALWAYS;
DefaultTraceRmiAcceptor acceptor = null;
Map<String, TerminalSession> sessions = new LinkedHashMap<>();
TraceRmiHandler connection = null;
Trace trace = null;
Throwable lastExc = null;
initializeMonitor(monitor);
while (true) {
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");
acceptor = service.acceptOne(new InetSocketAddress("127.0.0.1", 0));
monitor.increment();
monitor.setMessage("Launching back-end");
launchBackEnd(monitor, sessions, args, acceptor.getAddress());
monitor.increment();
monitor.setMessage("Waiting for connection");
acceptor.setTimeout(getConnectionTimeoutMillis());
connection = acceptor.accept();
connection.registerTerminals(sessions.values());
monitor.increment();
monitor.setMessage("Waiting for trace");
trace = connection.waitForTrace(getTimeoutMillis());
traceManager.openTrace(trace);
traceManager.activate(traceManager.resolveTrace(trace),
ActivationCause.START_RECORDING);
monitor.increment();
waitForModuleMapping(monitor, connection, trace);
}
catch (CancelledException e) {
lastExc = e;
LaunchResult result =
new LaunchResult(program, sessions, acceptor, connection, trace, lastExc);
try {
result.close();
}
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 = """
<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();
return LaunchFailureDialog.show(result);
}
}

View File

@ -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<String> 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<DockingActionIf> actions = new ArrayList<>();
Map<String, Long> 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<String, Long> 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());
}
}

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;
/**
* Some attributes are required. Others are optional:
* <ul>
* <li>{@code @menu-path}: <b>(Required)</b></li>
* </ul>
*
* 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<String> menuPath,
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
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) {
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<String, ParameterDescription<?>> parameters = new LinkedHashMap<>();
private final Map<String, TtyCondition> 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,10 +395,13 @@ 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);
}
}
else {
switch (parts[0].trim()) {
case AT_TITLE -> parseTitle(loc, parts[1]);
case AT_DESC -> parseDesc(loc, parts[1]);
@ -418,6 +419,7 @@ public abstract class ScriptAttributesParser {
default -> parseUnrecognized(loc, comment);
}
}
}
protected void parseTitle(Location loc, String str) {
if (title != null) {
@ -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() {

View File

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

View File

@ -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):
@ -552,8 +553,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):

View File

@ -681,7 +681,7 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel
* <p>
* 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
* <p>
* 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.
*/

View File

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

View File

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

View File

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