mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 10:19:23 +02:00
GP-3818: Create TraceRMI launcher framework. Launch script for gdb.
This commit is contained in:
parent
4561e8335d
commit
eea90f49c9
379 changed files with 5180 additions and 1487 deletions
|
@ -0,0 +1,42 @@
|
|||
/* ###
|
||||
* 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.connection;
|
||||
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.services.TraceRmiService;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
|
||||
@PluginInfo(
|
||||
shortDescription = "GUI elements to manage Trace RMI connections",
|
||||
description = """
|
||||
Provides a panel for managing Trace RMI connections. The panel also allows users to
|
||||
control the Trace RMI server and/or create manual connections.
|
||||
""",
|
||||
category = PluginCategoryNames.DEBUGGER,
|
||||
packageName = DebuggerPluginPackage.NAME,
|
||||
status = PluginStatus.RELEASED,
|
||||
servicesRequired = {
|
||||
TraceRmiService.class,
|
||||
})
|
||||
public class TraceRmiConnectionManagerPlugin extends Plugin {
|
||||
public TraceRmiConnectionManagerPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
}
|
||||
|
||||
// TODO: Add the actual provider. This will probably replace DebuggerTargetsPlugin.
|
||||
}
|
|
@ -0,0 +1,553 @@
|
|||
/* ###
|
||||
* 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.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import org.jdom.Element;
|
||||
import org.jdom.JDOMException;
|
||||
|
||||
import db.Transaction;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
|
||||
import ghidra.app.plugin.core.terminal.TerminalListener;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.dbg.util.ShellUtils;
|
||||
import ghidra.debug.api.modules.*;
|
||||
import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry;
|
||||
import ghidra.debug.api.tracermi.*;
|
||||
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.util.ProgramLocation;
|
||||
import ghidra.pty.*;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.TraceLocation;
|
||||
import ghidra.trace.model.modules.TraceModule;
|
||||
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";
|
||||
|
||||
protected record PtyTerminalSession(Terminal terminal, Pty pty, PtySession session,
|
||||
Thread waiter) implements TerminalSession {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
terminate();
|
||||
terminal.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() throws IOException {
|
||||
terminal.terminated();
|
||||
session.destroyForcibly();
|
||||
pty.close();
|
||||
waiter.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
protected record NullPtyTerminalSession(Terminal terminal, Pty pty, String name)
|
||||
implements TerminalSession {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
terminate();
|
||||
terminal.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() throws IOException {
|
||||
terminal.terminated();
|
||||
pty.close();
|
||||
}
|
||||
}
|
||||
|
||||
static class TerminateSessionTask extends Task {
|
||||
private final TerminalSession session;
|
||||
|
||||
public TerminateSessionTask(TerminalSession session) {
|
||||
super("Terminate Session", false, false, false);
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(TaskMonitor monitor) throws CancelledException {
|
||||
try {
|
||||
session.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.error(this, "Could not terminate: " + e, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected final Program program;
|
||||
protected final PluginTool tool;
|
||||
protected final TerminalService terminalService;
|
||||
|
||||
public AbstractTraceRmiLaunchOffer(Program program, PluginTool tool) {
|
||||
this.program = Objects.requireNonNull(program);
|
||||
this.tool = Objects.requireNonNull(tool);
|
||||
this.terminalService = Objects.requireNonNull(tool.getService(TerminalService.class));
|
||||
}
|
||||
|
||||
protected int getTimeoutMillis() {
|
||||
return 10000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return DebuggerResources.ICON_DEBUGGER;
|
||||
}
|
||||
|
||||
protected Address getMappingProbeAddress() {
|
||||
AddressIterator eepi = program.getSymbolTable().getExternalEntryPointIterator();
|
||||
if (eepi.hasNext()) {
|
||||
return eepi.next();
|
||||
}
|
||||
InstructionIterator ii = program.getListing().getInstructions(true);
|
||||
if (ii.hasNext()) {
|
||||
return ii.next().getAddress();
|
||||
}
|
||||
AddressSetView es = program.getMemory().getExecuteSet();
|
||||
if (!es.isEmpty()) {
|
||||
return es.getMinAddress();
|
||||
}
|
||||
if (!program.getMemory().isEmpty()) {
|
||||
return program.getMinAddress();
|
||||
}
|
||||
return null; // I guess we won't wait for a mapping, then
|
||||
}
|
||||
|
||||
protected CompletableFuture<Void> listenForMapping(
|
||||
DebuggerStaticMappingService mappingService, TraceRmiConnection connection,
|
||||
Trace trace) {
|
||||
Address probeAddress = getMappingProbeAddress();
|
||||
if (probeAddress == null) {
|
||||
return AsyncUtils.NIL; // No need to wait on mapping of nothing
|
||||
}
|
||||
ProgramLocation probe = new ProgramLocation(program, probeAddress);
|
||||
var result = new CompletableFuture<Void>() {
|
||||
DebuggerStaticMappingChangeListener listener = (affectedTraces, affectedPrograms) -> {
|
||||
if (!affectedPrograms.contains(program) &&
|
||||
!affectedTraces.contains(trace)) {
|
||||
return;
|
||||
}
|
||||
check();
|
||||
};
|
||||
|
||||
protected void check() {
|
||||
long snap = connection.getLastSnapshot(trace);
|
||||
TraceLocation result = mappingService.getOpenMappedLocation(trace, probe, snap);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
complete(null);
|
||||
mappingService.removeChangeListener(listener);
|
||||
}
|
||||
};
|
||||
mappingService.addChangeListener(result.listener);
|
||||
result.check();
|
||||
result.exceptionally(ex -> {
|
||||
mappingService.removeChangeListener(result.listener);
|
||||
return null;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
protected Collection<ModuleMapEntry> invokeMapper(TaskMonitor monitor,
|
||||
DebuggerStaticMappingService mappingService, Trace trace) throws CancelledException {
|
||||
Map<TraceModule, ModuleMapProposal> map = mappingService
|
||||
.proposeModuleMaps(trace.getModuleManager().getAllModules(), List.of(program));
|
||||
Collection<ModuleMapEntry> proposal = MapProposal.flatten(map.values());
|
||||
mappingService.addModuleMappings(proposal, monitor, true);
|
||||
return proposal;
|
||||
}
|
||||
|
||||
private void saveLauncherArgs(Map<String, ?> args,
|
||||
Map<String, ParameterDescription<?>> params) {
|
||||
SaveState state = new SaveState();
|
||||
for (ParameterDescription<?> param : params.values()) {
|
||||
Object val = args.get(param.name);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the default launcher arguments
|
||||
*
|
||||
* <p>
|
||||
* It is not sufficient to simply take the defaults specified in the parameters. This must
|
||||
* populate the arguments necessary to launch the requested program.
|
||||
*
|
||||
* @param params the parameters
|
||||
* @return the default arguments
|
||||
*/
|
||||
@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()) {
|
||||
ParameterDescription<?> param = entry.getValue();
|
||||
map.put(entry.getKey(), param.defaultValue);
|
||||
if (PARAM_DISPLAY_IMAGE.equals(param.display)) {
|
||||
if (param.type != String.class) {
|
||||
Msg.warn(this, "'Image' parameter has unexpected type: " + paramImage.type);
|
||||
}
|
||||
paramImage = (ParameterDescription<String>) param;
|
||||
}
|
||||
}
|
||||
if (paramImage != null) {
|
||||
File imageFile = TraceRmiLauncherServicePlugin.getProgramPath(program);
|
||||
if (imageFile != null) {
|
||||
paramImage.set(map, imageFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user for arguments, showing those last used or defaults
|
||||
*
|
||||
* @param lastExc
|
||||
*
|
||||
* @param params the parameters of the model's launcher
|
||||
* @param lastExc if re-prompting, an error to display
|
||||
* @return the arguments given by the user, or null if cancelled
|
||||
*/
|
||||
protected Map<String, ?> promptLauncherArgs(LaunchConfigurator configurator,
|
||||
Throwable lastExc) {
|
||||
Map<String, ParameterDescription<?>> params = getParameters();
|
||||
DebuggerMethodInvocationDialog dialog =
|
||||
new DebuggerMethodInvocationDialog(tool, getTitle(), "Launch", getIcon());
|
||||
dialog.setDescription(getDescription());
|
||||
// NB. Do not invoke read/writeConfigState
|
||||
Map<String, ?> args;
|
||||
boolean reset = false;
|
||||
do {
|
||||
args =
|
||||
configurator.configureLauncher(this, loadLastLauncherArgs(true), RelPrompt.BEFORE);
|
||||
for (ParameterDescription<?> param : params.values()) {
|
||||
Object val = args.get(param.name);
|
||||
if (val != null) {
|
||||
dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class),
|
||||
val);
|
||||
}
|
||||
}
|
||||
if (lastExc != null) {
|
||||
dialog.setStatusText(lastExc.toString(), MessageType.ERROR);
|
||||
}
|
||||
else {
|
||||
dialog.setStatusText("");
|
||||
}
|
||||
args = dialog.promptArguments(params);
|
||||
if (args == null) {
|
||||
// Cancelled
|
||||
return null;
|
||||
}
|
||||
reset = dialog.isResetRequested();
|
||||
if (reset) {
|
||||
args = generateDefaultLauncherArgs(params);
|
||||
}
|
||||
saveLauncherArgs(args, params);
|
||||
}
|
||||
while (reset);
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the arguments last used for this offer, or give the defaults
|
||||
*
|
||||
* <p>
|
||||
* If there are no saved "last used" arguments, then this will return the defaults. If there are
|
||||
* saved arguments, but they cannot be loaded, then this will behave differently depending on
|
||||
* whether the user will be confirming the arguments. If there will be no prompt/confirmation,
|
||||
* then this method must throw an exception in order to avoid launching with defaults, when the
|
||||
* 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);
|
||||
saveLauncherArgs(args, params);
|
||||
return args;
|
||||
}
|
||||
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the launcher args
|
||||
*
|
||||
* <p>
|
||||
* This should either call {@link #promptLauncherArgs(Map))} or
|
||||
* {@link #loadLastLauncherArgs(Map, boolean))}. Note if choosing the latter, the user will not
|
||||
* be prompted to confirm.
|
||||
*
|
||||
* @param params the parameters of the model's launcher
|
||||
* @param configurator the rules for configuring the launcher
|
||||
* @param lastExc if retrying, the last exception to display as an error message
|
||||
* @return the chosen arguments, or null if the user cancels at the prompt
|
||||
*/
|
||||
public Map<String, ?> getLauncherArgs(boolean prompt, LaunchConfigurator configurator,
|
||||
Throwable lastExc) {
|
||||
return prompt
|
||||
? configurator.configureLauncher(this, promptLauncherArgs(configurator, lastExc),
|
||||
RelPrompt.AFTER)
|
||||
: configurator.configureLauncher(this, loadLastLauncherArgs(false), RelPrompt.NONE);
|
||||
}
|
||||
|
||||
public Map<String, ?> getLauncherArgs(boolean prompt) {
|
||||
return getLauncherArgs(prompt, LaunchConfigurator.NOP, null);
|
||||
}
|
||||
|
||||
protected PtyFactory getPtyFactory() {
|
||||
return PtyFactory.local();
|
||||
}
|
||||
|
||||
protected PtyTerminalSession runInTerminal(List<String> commandLine, Map<String, String> env,
|
||||
Collection<TerminalSession> subordinates)
|
||||
throws IOException {
|
||||
PtyFactory factory = getPtyFactory();
|
||||
Pty pty = factory.openpty();
|
||||
|
||||
PtyParent parent = pty.getParent();
|
||||
Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"),
|
||||
parent.getInputStream(), parent.getOutputStream());
|
||||
terminal.setSubTitle(ShellUtils.generateLine(commandLine));
|
||||
TerminalListener resizeListener = new TerminalListener() {
|
||||
@Override
|
||||
public void resized(short cols, short rows) {
|
||||
parent.setWindowSize(cols, rows);
|
||||
}
|
||||
};
|
||||
terminal.addTerminalListener(resizeListener);
|
||||
|
||||
env.put("TERM", "xterm-256color");
|
||||
PtySession session = pty.getChild().session(commandLine.toArray(String[]::new), env);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
try {
|
||||
session.waitExited();
|
||||
terminal.terminated();
|
||||
pty.close();
|
||||
|
||||
for (TerminalSession ss : subordinates) {
|
||||
ss.terminate();
|
||||
}
|
||||
}
|
||||
catch (InterruptedException | IOException e) {
|
||||
Msg.error(this, e);
|
||||
}
|
||||
}, "Waiter: " + getConfigName());
|
||||
waiter.start();
|
||||
|
||||
PtyTerminalSession terminalSession =
|
||||
new PtyTerminalSession(terminal, pty, session, waiter);
|
||||
terminal.setTerminateAction(() -> {
|
||||
tool.execute(new TerminateSessionTask(terminalSession));
|
||||
});
|
||||
return terminalSession;
|
||||
}
|
||||
|
||||
protected NullPtyTerminalSession nullPtyTerminal() throws IOException {
|
||||
PtyFactory factory = getPtyFactory();
|
||||
Pty pty = factory.openpty();
|
||||
|
||||
PtyParent parent = pty.getParent();
|
||||
Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"),
|
||||
parent.getInputStream(), parent.getOutputStream());
|
||||
TerminalListener resizeListener = new TerminalListener() {
|
||||
@Override
|
||||
public void resized(short cols, short rows) {
|
||||
parent.setWindowSize(cols, rows);
|
||||
}
|
||||
};
|
||||
terminal.addTerminalListener(resizeListener);
|
||||
|
||||
String name = pty.getChild().nullSession();
|
||||
terminal.setSubTitle(name);
|
||||
|
||||
NullPtyTerminalSession terminalSession = new NullPtyTerminalSession(terminal, pty, name);
|
||||
terminal.setTerminateAction(() -> {
|
||||
tool.execute(new TerminateSessionTask(terminalSession));
|
||||
});
|
||||
return terminalSession;
|
||||
}
|
||||
|
||||
protected abstract void launchBackEnd(TaskMonitor monitor,
|
||||
Map<String, TerminalSession> sessions, Map<String, ?> args, SocketAddress address)
|
||||
throws Exception;
|
||||
|
||||
@Override
|
||||
public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) {
|
||||
TraceRmiService service = tool.getService(TraceRmiService.class);
|
||||
DebuggerStaticMappingService mappingService =
|
||||
tool.getService(DebuggerStaticMappingService.class);
|
||||
DebuggerTraceManagerService traceManager =
|
||||
tool.getService(DebuggerTraceManagerService.class);
|
||||
final PromptMode mode = configurator.getPromptMode();
|
||||
boolean prompt = mode == PromptMode.ALWAYS;
|
||||
|
||||
TraceRmiAcceptor acceptor = null;
|
||||
Map<String, TerminalSession> sessions = new LinkedHashMap<>();
|
||||
TraceRmiConnection 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, connection, trace, lastExc);
|
||||
}
|
||||
acceptor = null;
|
||||
sessions.clear();
|
||||
connection = null;
|
||||
trace = null;
|
||||
lastExc = null;
|
||||
|
||||
try {
|
||||
monitor.setMessage("Listening for connection");
|
||||
acceptor = service.acceptOne(new InetSocketAddress("127.0.0.1", 0));
|
||||
monitor.setMessage("Launching back-end");
|
||||
launchBackEnd(monitor, sessions, args, acceptor.getAddress());
|
||||
monitor.setMessage("Waiting for connection");
|
||||
acceptor.setTimeout(getTimeoutMillis());
|
||||
connection = acceptor.accept();
|
||||
monitor.setMessage("Waiting for trace");
|
||||
trace = connection.waitForTrace(getTimeoutMillis());
|
||||
traceManager.openTrace(trace);
|
||||
traceManager.activateTrace(trace);
|
||||
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()) {
|
||||
monitor.setMessage(
|
||||
"Could not formulate a mapping with the target program. " +
|
||||
"Continuing without one.");
|
||||
Msg.showWarn(this, null, "Launch " + program,
|
||||
"The resulting target process has no mapping to the static image " +
|
||||
program + ". Intervention is required before static and dynamic " +
|
||||
"addresses can be translated. Check the target's module list.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
lastExc = e;
|
||||
prompt = mode != PromptMode.NEVER;
|
||||
if (prompt) {
|
||||
continue;
|
||||
}
|
||||
return new LaunchResult(program, sessions, connection, trace, lastExc);
|
||||
}
|
||||
return new LaunchResult(program, sessions, connection, trace, null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
/* ###
|
||||
* 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.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.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;
|
||||
|
||||
public class LaunchAction extends MultiActionDockingAction {
|
||||
public static final String NAME = "Launch";
|
||||
public static final Icon ICON = DebuggerResources.ICON_DEBUGGER;
|
||||
public static final String GROUP = DebuggerResources.GROUP_GENERAL;
|
||||
public static final String HELP_ANCHOR = "launch_tracermi";
|
||||
|
||||
private final TraceRmiLauncherServicePlugin plugin;
|
||||
private MenuActionDockingToolbarButton button;
|
||||
|
||||
public LaunchAction(TraceRmiLauncherServicePlugin plugin) {
|
||||
super(NAME, plugin.getName());
|
||||
this.plugin = plugin;
|
||||
setToolBarData(new ToolBarData(ICON, GROUP, "A"));
|
||||
setHelpLocation(new HelpLocation(plugin.getName(), HELP_ANCHOR));
|
||||
}
|
||||
|
||||
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;
|
||||
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
|
||||
public List<DockingActionIf> getActionList(ActionContext context) {
|
||||
Program program = plugin.currentProgram;
|
||||
Collection<TraceRmiLaunchOffer> offers = plugin.getOffers(program);
|
||||
|
||||
List<DockingActionIf> actions = new ArrayList<>();
|
||||
|
||||
ProgramUserData userData = program.getProgramUserData();
|
||||
Map<String, Long> saved = new HashMap<>();
|
||||
for (String propName : userData.getStringPropertyNames()) {
|
||||
ConfigLast check = checkSavedConfig(userData, propName);
|
||||
if (check == null) {
|
||||
continue;
|
||||
}
|
||||
saved.put(check.configName, check.last);
|
||||
}
|
||||
|
||||
for (TraceRmiLaunchOffer offer : offers) {
|
||||
actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName())
|
||||
.popupMenuPath(prependConfigAndLaunch(offer.getMenuPath()))
|
||||
.popupMenuGroup(offer.getMenuGroup(), offer.getMenuOrder())
|
||||
.popupMenuIcon(offer.getIcon())
|
||||
.helpLocation(offer.getHelpLocation())
|
||||
.enabledWhen(ctx -> true)
|
||||
.onAction(ctx -> plugin.configureAndLaunch(offer))
|
||||
.build());
|
||||
Long last = saved.get(offer.getConfigName());
|
||||
if (last == null) {
|
||||
continue;
|
||||
}
|
||||
actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName())
|
||||
.popupMenuPath("Re-launch " + program.getName() + " using " + offer.getTitle())
|
||||
.popupMenuGroup("0", "%016x".formatted(Long.MAX_VALUE - last))
|
||||
.popupMenuIcon(offer.getIcon())
|
||||
.helpLocation(offer.getHelpLocation())
|
||||
.enabledWhen(ctx -> true)
|
||||
.onAction(ctx -> plugin.relaunch(offer))
|
||||
.build());
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
class MenuActionDockingToolbarButton extends MultipleActionDockingToolbarButton {
|
||||
public MenuActionDockingToolbarButton(MultiActionDockingActionIf action) {
|
||||
super(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JPopupMenu doCreateMenu() {
|
||||
ActionContext context = getActionContext();
|
||||
List<DockingActionIf> actionList = getActionList(context);
|
||||
MenuHandler handler =
|
||||
new PopupMenuHandler(plugin.getTool().getWindowManager(), context);
|
||||
MenuManager manager =
|
||||
new MenuManager("Launch", (char) 0, GROUP, true, handler, null);
|
||||
for (DockingActionIf action : actionList) {
|
||||
manager.addAction(action);
|
||||
}
|
||||
return manager.getPopupMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JPopupMenu showPopup() {
|
||||
// Make accessible to this file
|
||||
return super.showPopup();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JButton doCreateButton() {
|
||||
return button = new MenuActionDockingToolbarButton(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
// See comment on super method about use of runLater
|
||||
ConfigLast last = findMostRecentConfig();
|
||||
if (last == null) {
|
||||
Swing.runLater(() -> button.showPopup());
|
||||
return;
|
||||
}
|
||||
for (TraceRmiLaunchOffer offer : plugin.getOffers(plugin.currentProgram)) {
|
||||
if (offer.getConfigName().equals(last.configName)) {
|
||||
plugin.relaunch(offer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Swing.runLater(() -> button.showPopup());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/* ###
|
||||
* 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.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
|
||||
import docking.DockingWindowManager;
|
||||
import docking.widgets.OptionDialog;
|
||||
import docking.widgets.filechooser.GhidraFileChooserMode;
|
||||
import docking.widgets.pathmanager.*;
|
||||
import utility.function.Callback;
|
||||
|
||||
public class ScriptPathsPropertyEditor extends AbstractTypedPropertyEditor<String> {
|
||||
|
||||
@Override
|
||||
protected String fromText(String text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJavaInitializationString() {
|
||||
return "\"" + StringEscapeUtils.escapeJava(getValue()) + "\"";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getCustomEditor() {
|
||||
return new ScriptPathsEditor();
|
||||
}
|
||||
|
||||
protected class ScriptPathsEditor extends JPanel {
|
||||
public ScriptPathsEditor() {
|
||||
super(new BorderLayout());
|
||||
JButton button = new JButton("Edit Paths");
|
||||
button.addActionListener(this::showDialog);
|
||||
add(button);
|
||||
}
|
||||
|
||||
protected void showDialog(ActionEvent evt) {
|
||||
DockingWindowManager.showDialog(this, new ScriptPathsDialog());
|
||||
}
|
||||
}
|
||||
|
||||
protected class ScriptPathsDialog extends AbstractPathsDialog {
|
||||
protected ScriptPathsDialog() {
|
||||
super("Debugger Launch Script Paths");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] loadPaths() {
|
||||
return getValue().lines().filter(d -> !d.isBlank()).toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void savePaths(String[] paths) {
|
||||
setValue(Stream.of(paths).collect(Collectors.joining("\n")));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PathnameTablePanel newPathnameTablePanel() {
|
||||
PathnameTablePanel tablePanel = new ScriptPathsPanel(this::reset);
|
||||
tablePanel.setFileChooserProperties(getTitle(), "DebuggerLaunchScriptDirectory",
|
||||
GhidraFileChooserMode.DIRECTORIES_ONLY, true, null);
|
||||
return tablePanel;
|
||||
}
|
||||
}
|
||||
|
||||
protected class ScriptPathsPanel extends PathnameTablePanel {
|
||||
public ScriptPathsPanel(Callback resetCallback) {
|
||||
// disable edits, top/bottom irrelevant, unordered
|
||||
super(null, resetCallback, false, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int promptConfirmReset() {
|
||||
String confirmation = """
|
||||
<html><body width="200px">
|
||||
Are you sure you would like to reload the Debugger's launcher script paths?
|
||||
This will reset any changes you've made so far.
|
||||
</html>""";
|
||||
String header = "Reset Script Paths?";
|
||||
|
||||
return OptionDialog.showYesNoDialog(this, header, confirmation);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
/* ###
|
||||
* 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.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.app.events.ProgramActivatedPluginEvent;
|
||||
import ghidra.app.events.ProgramClosedPluginEvent;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources.DebugProgramAction;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchConfigurator;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.PromptMode;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOpinion;
|
||||
import ghidra.framework.options.OptionsChangeListener;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.program.model.listing.Program;
|
||||
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;
|
||||
|
||||
@PluginInfo(
|
||||
shortDescription = "GUI elements to launch targets using Trace RMI",
|
||||
description = """
|
||||
Provides menus and toolbar actions to launch Trace RMI targets.
|
||||
""",
|
||||
category = PluginCategoryNames.DEBUGGER,
|
||||
packageName = DebuggerPluginPackage.NAME,
|
||||
status = PluginStatus.UNSTABLE,
|
||||
eventsConsumed = {
|
||||
ProgramActivatedPluginEvent.class,
|
||||
ProgramClosedPluginEvent.class,
|
||||
},
|
||||
servicesRequired = {
|
||||
TraceRmiService.class,
|
||||
TerminalService.class,
|
||||
},
|
||||
servicesProvided = {
|
||||
TraceRmiLauncherService.class,
|
||||
})
|
||||
public class TraceRmiLauncherServicePlugin extends Plugin
|
||||
implements TraceRmiLauncherService, OptionsChangeListener {
|
||||
protected static final String OPTION_NAME_SCRIPT_PATHS = "Script Paths";
|
||||
|
||||
private final static LaunchConfigurator PROMPT = new LaunchConfigurator() {
|
||||
@Override
|
||||
public PromptMode getPromptMode() {
|
||||
return PromptMode.ALWAYS;
|
||||
}
|
||||
};
|
||||
|
||||
private static abstract class AbstractLaunchTask extends Task {
|
||||
final TraceRmiLaunchOffer offer;
|
||||
|
||||
public AbstractLaunchTask(TraceRmiLaunchOffer offer) {
|
||||
super(offer.getTitle(), true, true, true);
|
||||
this.offer = offer;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReLaunchTask extends AbstractLaunchTask {
|
||||
public ReLaunchTask(TraceRmiLaunchOffer offer) {
|
||||
super(offer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(TaskMonitor monitor) throws CancelledException {
|
||||
offer.launchProgram(monitor);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConfigureAndLaunchTask extends AbstractLaunchTask {
|
||||
public ConfigureAndLaunchTask(TraceRmiLaunchOffer offer) {
|
||||
super(offer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(TaskMonitor monitor) throws CancelledException {
|
||||
offer.launchProgram(monitor, PROMPT);
|
||||
}
|
||||
}
|
||||
|
||||
public static File getProgramPath(Program program) {
|
||||
if (program == null) {
|
||||
return null;
|
||||
}
|
||||
String path = program.getExecutablePath();
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
File file = new File(path);
|
||||
try {
|
||||
if (!file.canExecute()) {
|
||||
return null;
|
||||
}
|
||||
return file.getCanonicalFile();
|
||||
}
|
||||
catch (SecurityException | IOException e) {
|
||||
Msg.error(TraceRmiLauncherServicePlugin.class, "Cannot examine file " + path, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected final ToolOptions options;
|
||||
|
||||
protected Program currentProgram;
|
||||
protected LaunchAction launchAction;
|
||||
protected List<DockingActionIf> currentLaunchers = new ArrayList<>();
|
||||
|
||||
public TraceRmiLauncherServicePlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
this.options = tool.getOptions(DebuggerPluginPackage.NAME);
|
||||
this.options.addOptionsChangeListener(this);
|
||||
createActions();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
super.init();
|
||||
for (TraceRmiLaunchOpinion opinion : ClassSearcher
|
||||
.getInstances(TraceRmiLaunchOpinion.class)) {
|
||||
opinion.registerOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
protected void createActions() {
|
||||
launchAction = new LaunchAction(this);
|
||||
tool.addAction(launchAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void optionsChanged(ToolOptions options, String optionName, Object oldValue,
|
||||
Object newValue) throws OptionsVetoException {
|
||||
for (TraceRmiLaunchOpinion opinion : ClassSearcher
|
||||
.getInstances(TraceRmiLaunchOpinion.class)) {
|
||||
if (opinion.requiresRefresh(optionName)) {
|
||||
updateLauncherMenu();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<TraceRmiLaunchOpinion> getOpinions() {
|
||||
return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<TraceRmiLaunchOffer> getOffers(Program program) {
|
||||
if (program == null) {
|
||||
return List.of();
|
||||
}
|
||||
return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class)
|
||||
.stream()
|
||||
.flatMap(op -> op.getOffers(program, getTool()).stream())
|
||||
.toList();
|
||||
}
|
||||
|
||||
protected void relaunch(TraceRmiLaunchOffer offer) {
|
||||
tool.execute(new ReLaunchTask(offer));
|
||||
}
|
||||
|
||||
protected void configureAndLaunch(TraceRmiLaunchOffer offer) {
|
||||
tool.execute(new ConfigureAndLaunchTask(offer));
|
||||
}
|
||||
|
||||
protected String[] constructLaunchMenuPrefix() {
|
||||
return new String[] {
|
||||
DebuggerPluginPackage.NAME,
|
||||
"Configure and Launch " + currentProgram.getName() + " using..." };
|
||||
}
|
||||
|
||||
protected String[] prependConfigAndLaunch(List<String> menuPath) {
|
||||
return Stream.concat(
|
||||
Stream.of(constructLaunchMenuPrefix()),
|
||||
menuPath.stream()).toArray(String[]::new);
|
||||
}
|
||||
|
||||
private void updateLauncherMenu() {
|
||||
Collection<TraceRmiLaunchOffer> offers = currentProgram == null
|
||||
? List.of()
|
||||
: getOffers(currentProgram);
|
||||
synchronized (currentLaunchers) {
|
||||
for (DockingActionIf launcher : currentLaunchers) {
|
||||
tool.removeAction(launcher);
|
||||
}
|
||||
currentLaunchers.clear();
|
||||
|
||||
if (!offers.isEmpty()) {
|
||||
tool.setMenuGroup(constructLaunchMenuPrefix(), DebugProgramAction.GROUP, "zz");
|
||||
}
|
||||
for (TraceRmiLaunchOffer offer : offers) {
|
||||
currentLaunchers.add(new ActionBuilder(offer.getConfigName(), getName())
|
||||
.menuPath(prependConfigAndLaunch(offer.getMenuPath()))
|
||||
.menuGroup(offer.getMenuGroup(), offer.getMenuOrder())
|
||||
.menuIcon(offer.getIcon())
|
||||
.helpLocation(offer.getHelpLocation())
|
||||
.enabledWhen(ctx -> true)
|
||||
.onAction(ctx -> configureAndLaunch(offer))
|
||||
.buildAndInstall(tool));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processEvent(PluginEvent event) {
|
||||
super.processEvent(event);
|
||||
if (event instanceof ProgramActivatedPluginEvent evt) {
|
||||
currentProgram = evt.getActiveProgram();
|
||||
updateLauncherMenu();
|
||||
}
|
||||
if (event instanceof ProgramClosedPluginEvent evt) {
|
||||
if (currentProgram == evt.getProgram()) {
|
||||
currentProgram = null;
|
||||
updateLauncherMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,633 @@
|
|||
/* ###
|
||||
* 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.io.*;
|
||||
import java.math.BigInteger;
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import generic.theme.GIcon;
|
||||
import generic.theme.Gui;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.dbg.util.ShellUtils;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
/**
|
||||
* A launcher implemented by a simple UNIX shell script.
|
||||
*
|
||||
* <p>
|
||||
* The script must start with an attributes header in a comment block. Some attributes are required.
|
||||
* Others are optional:
|
||||
* <ul>
|
||||
* <li>{@code @menu-path}: <b>(Required)</b></li>
|
||||
* </ul>
|
||||
*/
|
||||
public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer {
|
||||
public static final String SHEBANG = "#!";
|
||||
|
||||
public static final String AT_TITLE = "@title";
|
||||
public static final String AT_DESC = "@desc";
|
||||
public static final String AT_MENU_PATH = "@menu-path";
|
||||
public static final String AT_MENU_GROUP = "@menu-group";
|
||||
public static final String AT_MENU_ORDER = "@menu-order";
|
||||
public static final String AT_ICON = "@icon";
|
||||
public static final String AT_HELP = "@help";
|
||||
public static final String AT_ENUM = "@enum";
|
||||
public static final String AT_ENV = "@env";
|
||||
public static final String AT_ARG = "@arg";
|
||||
public static final String AT_ARGS = "@args";
|
||||
public static final String AT_TTY = "@tty";
|
||||
|
||||
public static final String PREFIX_ENV = "env:";
|
||||
public static final String PREFIX_ARG = "arg:";
|
||||
public static final String KEY_ARGS = "args";
|
||||
|
||||
public static final String MSGPAT_INVALID_HELP_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use Topic#anchor";
|
||||
public static final String MSGPAT_INVALID_ENUM_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use NAME:type Choice1 [ChoiceN...]";
|
||||
public static final String MSGPAT_INVALID_ENV_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use NAME:type=default \"Display\" \"Tool Tip\"";
|
||||
public static final String MSGPAT_INVALID_ARG_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use :type \"Display\" \"Tool Tip\"";
|
||||
public static final String MSGPAT_INVALID_ARGS_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use \"Display\" \"Tool Tip\"";
|
||||
|
||||
protected record Location(String fileName, int lineNo) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "%s:%d".formatted(fileName, lineNo);
|
||||
}
|
||||
}
|
||||
|
||||
protected interface OptType<T> {
|
||||
static OptType<?> parse(Location loc, String typeName,
|
||||
Map<String, UserType<?>> userEnums) {
|
||||
OptType<?> type = switch (typeName) {
|
||||
case "str" -> BaseType.STRING;
|
||||
case "int" -> BaseType.INT;
|
||||
case "bool" -> BaseType.BOOL;
|
||||
default -> userEnums.get(typeName);
|
||||
};
|
||||
if (type == null) {
|
||||
Msg.error(AttributesParser.class, "%s: Invalid type %s".formatted(loc, typeName));
|
||||
return null;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
default TypeAndDefault<T> withCastDefault(Object defaultValue) {
|
||||
return new TypeAndDefault<>(this, cls().cast(defaultValue));
|
||||
}
|
||||
|
||||
Class<T> cls();
|
||||
|
||||
T decode(Location loc, String str);
|
||||
|
||||
ParameterDescription<T> createParameter(String name, T defaultValue, String display,
|
||||
String description);
|
||||
}
|
||||
|
||||
protected interface BaseType<T> extends OptType<T> {
|
||||
static BaseType<?> parse(Location loc, String typeName) {
|
||||
BaseType<?> type = switch (typeName) {
|
||||
case "str" -> BaseType.STRING;
|
||||
case "int" -> BaseType.INT;
|
||||
case "bool" -> BaseType.BOOL;
|
||||
default -> null;
|
||||
};
|
||||
if (type == null) {
|
||||
Msg.error(AttributesParser.class,
|
||||
"%s: Invalid base type %s".formatted(loc, typeName));
|
||||
return null;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
public static final BaseType<String> STRING = new BaseType<>() {
|
||||
@Override
|
||||
public Class<String> cls() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decode(Location loc, String str) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
public static final BaseType<BigInteger> INT = new BaseType<>() {
|
||||
@Override
|
||||
public Class<BigInteger> cls() {
|
||||
return BigInteger.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger decode(Location loc, String str) {
|
||||
try {
|
||||
if (str.startsWith("0x")) {
|
||||
return new BigInteger(str.substring(2), 16);
|
||||
}
|
||||
return new BigInteger(str);
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
Msg.error(AttributesParser.class,
|
||||
("%s: Invalid int for %s: %s. You may prefix with 0x for hexadecimal. " +
|
||||
"Otherwise, decimal is used.").formatted(loc, AT_ENV, str));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
public static final BaseType<Boolean> BOOL = new BaseType<>() {
|
||||
@Override
|
||||
public Class<Boolean> cls() {
|
||||
return Boolean.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean decode(Location loc, String str) {
|
||||
Boolean result = switch (str) {
|
||||
case "true" -> true;
|
||||
case "false" -> false;
|
||||
default -> null;
|
||||
};
|
||||
if (result == null) {
|
||||
Msg.error(AttributesParser.class,
|
||||
"%s: Invalid bool for %s: %s. Only true or false (in lower case) is allowed."
|
||||
.formatted(loc, AT_ENV, str));
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
default UserType<T> withCastChoices(List<?> choices) {
|
||||
return new UserType<>(this, choices.stream().map(cls()::cast).toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
default ParameterDescription<T> createParameter(String name, T defaultValue, String display,
|
||||
String description) {
|
||||
return ParameterDescription.create(cls(), name, false, defaultValue, display,
|
||||
description);
|
||||
}
|
||||
}
|
||||
|
||||
protected record UserType<T> (BaseType<T> base, List<T> choices) implements OptType<T> {
|
||||
@Override
|
||||
public Class<T> cls() {
|
||||
return base.cls();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T decode(Location loc, String str) {
|
||||
return base.decode(loc, str);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParameterDescription<T> createParameter(String name, T defaultValue, String display,
|
||||
String description) {
|
||||
return ParameterDescription.choices(cls(), name, choices, defaultValue, display,
|
||||
description);
|
||||
}
|
||||
}
|
||||
|
||||
protected record TypeAndDefault<T> (OptType<T> type, T defaultValue) {
|
||||
public static TypeAndDefault<?> parse(Location loc, String typeName, String defaultString,
|
||||
Map<String, UserType<?>> userEnums) {
|
||||
OptType<?> tac = OptType.parse(loc, typeName, userEnums);
|
||||
if (tac == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = tac.decode(loc, defaultString);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return tac.withCastDefault(value);
|
||||
}
|
||||
|
||||
public ParameterDescription<T> createParameter(String name, String display,
|
||||
String description) {
|
||||
return type.createParameter(name, defaultValue, display, description);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class AttributesParser {
|
||||
protected int argc = 0;
|
||||
protected String title;
|
||||
protected StringBuilder description;
|
||||
protected List<String> menuPath;
|
||||
protected String menuGroup;
|
||||
protected String menuOrder;
|
||||
protected String iconId;
|
||||
protected HelpLocation helpLocation;
|
||||
protected final Map<String, UserType<?>> userTypes = new HashMap<>();
|
||||
protected final Map<String, ParameterDescription<?>> parameters = new LinkedHashMap<>();
|
||||
protected final Set<String> extraTtys = new LinkedHashSet<>();
|
||||
|
||||
/**
|
||||
* Process a line in the metadata comment block
|
||||
*
|
||||
* @param line the line, excluding any comment delimiters
|
||||
*/
|
||||
public void parseLine(Location loc, String line) {
|
||||
String afterHash = line.stripLeading().substring(1);
|
||||
if (afterHash.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String[] parts = afterHash.split("\\s+", 2);
|
||||
if (!parts[0].startsWith("@")) {
|
||||
return;
|
||||
}
|
||||
if (parts.length < 2) {
|
||||
Msg.error(this, "%s: Too few tokens: %s".formatted(loc, line));
|
||||
return;
|
||||
}
|
||||
switch (parts[0].trim()) {
|
||||
case AT_TITLE -> parseTitle(loc, parts[1]);
|
||||
case AT_DESC -> parseDesc(loc, parts[1]);
|
||||
case AT_MENU_PATH -> parseMenuPath(loc, parts[1]);
|
||||
case AT_MENU_GROUP -> parseMenuGroup(loc, parts[1]);
|
||||
case AT_MENU_ORDER -> parseMenuOrder(loc, parts[1]);
|
||||
case AT_ICON -> parseIcon(loc, parts[1]);
|
||||
case AT_HELP -> parseHelp(loc, parts[1]);
|
||||
case AT_ENUM -> parseEnum(loc, parts[1]);
|
||||
case AT_ENV -> parseEnv(loc, parts[1]);
|
||||
case AT_ARG -> parseArg(loc, parts[1], ++argc);
|
||||
case AT_ARGS -> parseArgs(loc, parts[1]);
|
||||
case AT_TTY -> parseTty(loc, parts[1]);
|
||||
default -> parseUnrecognized(loc, line);
|
||||
}
|
||||
}
|
||||
|
||||
protected void parseTitle(Location loc, String str) {
|
||||
if (title != null) {
|
||||
Msg.warn(this, "%s: Duplicate @title".formatted(loc));
|
||||
}
|
||||
title = str;
|
||||
}
|
||||
|
||||
protected void parseDesc(Location loc, String str) {
|
||||
if (description == null) {
|
||||
description = new StringBuilder();
|
||||
}
|
||||
description.append(str);
|
||||
description.append("\n");
|
||||
}
|
||||
|
||||
protected void parseMenuPath(Location loc, String str) {
|
||||
if (menuPath != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_PATH));
|
||||
}
|
||||
menuPath = List.of(str.trim().split("\\."));
|
||||
if (menuPath.isEmpty()) {
|
||||
Msg.error(this,
|
||||
"%s: Empty %s. Ignoring.".formatted(loc, AT_MENU_PATH));
|
||||
}
|
||||
}
|
||||
|
||||
protected void parseMenuGroup(Location loc, String str) {
|
||||
if (menuGroup != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_GROUP));
|
||||
}
|
||||
menuGroup = str;
|
||||
}
|
||||
|
||||
protected void parseMenuOrder(Location loc, String str) {
|
||||
if (menuOrder != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_ORDER));
|
||||
}
|
||||
menuOrder = str;
|
||||
}
|
||||
|
||||
protected void parseIcon(Location loc, String str) {
|
||||
if (iconId != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_ICON));
|
||||
}
|
||||
iconId = str.trim();
|
||||
if (!Gui.hasIcon(iconId)) {
|
||||
Msg.error(this,
|
||||
"%s: Icon id %s not registered in the theme".formatted(loc, iconId));
|
||||
}
|
||||
}
|
||||
|
||||
protected void parseHelp(Location loc, String str) {
|
||||
if (helpLocation != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_HELP));
|
||||
}
|
||||
String[] parts = str.trim().split("#", 2);
|
||||
if (parts.length != 2) {
|
||||
Msg.error(this, MSGPAT_INVALID_HELP_SYNTAX.formatted(loc, AT_HELP));
|
||||
return;
|
||||
}
|
||||
helpLocation = new HelpLocation(parts[0].trim(), parts[1].trim());
|
||||
}
|
||||
|
||||
protected void parseEnum(Location loc, String str) {
|
||||
List<String> parts = ShellUtils.parseArgs(str);
|
||||
if (parts.size() < 2) {
|
||||
Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM));
|
||||
return;
|
||||
}
|
||||
String[] nameParts = parts.get(0).split(":", 2);
|
||||
if (nameParts.length != 2) {
|
||||
Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM));
|
||||
return;
|
||||
}
|
||||
String name = nameParts[0].trim();
|
||||
BaseType<?> baseType = BaseType.parse(loc, nameParts[1]);
|
||||
if (baseType == null) {
|
||||
return;
|
||||
}
|
||||
List<?> choices = parts.stream().skip(1).map(s -> baseType.decode(loc, s)).toList();
|
||||
if (choices.contains(null)) {
|
||||
return;
|
||||
}
|
||||
UserType<?> userType = baseType.withCastChoices(choices);
|
||||
if (userTypes.put(name, userType) != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENUM, name));
|
||||
}
|
||||
}
|
||||
|
||||
protected void parseEnv(Location loc, String str) {
|
||||
List<String> parts = ShellUtils.parseArgs(str);
|
||||
if (parts.size() != 3) {
|
||||
Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
|
||||
return;
|
||||
}
|
||||
String[] nameParts = parts.get(0).split(":", 2);
|
||||
if (nameParts.length != 2) {
|
||||
Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
|
||||
return;
|
||||
}
|
||||
String trimmed = nameParts[0].trim();
|
||||
String name = PREFIX_ENV + trimmed;
|
||||
String[] tadParts = nameParts[1].split("=", 2);
|
||||
if (tadParts.length != 2) {
|
||||
Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
|
||||
return;
|
||||
}
|
||||
TypeAndDefault<?> tad =
|
||||
TypeAndDefault.parse(loc, tadParts[0].trim(), tadParts[1].trim(), userTypes);
|
||||
ParameterDescription<?> param = tad.createParameter(name, parts.get(1), parts.get(2));
|
||||
if (parameters.put(name, param) != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENV, trimmed));
|
||||
}
|
||||
}
|
||||
|
||||
protected void parseArg(Location loc, String str, int argNum) {
|
||||
List<String> parts = ShellUtils.parseArgs(str);
|
||||
if (parts.size() != 3) {
|
||||
Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG));
|
||||
return;
|
||||
}
|
||||
String colonType = parts.get(0).trim();
|
||||
if (!colonType.startsWith(":")) {
|
||||
Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG));
|
||||
return;
|
||||
}
|
||||
OptType<?> type = OptType.parse(loc, colonType.substring(1), userTypes);
|
||||
if (type == null) {
|
||||
return;
|
||||
}
|
||||
String name = PREFIX_ARG + argNum;
|
||||
parameters.put(name, ParameterDescription.create(type.cls(), name, true, null,
|
||||
parts.get(1), parts.get(2)));
|
||||
}
|
||||
|
||||
protected void parseArgs(Location loc, String str) {
|
||||
List<String> parts = ShellUtils.parseArgs(str);
|
||||
if (parts.size() != 2) {
|
||||
Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS));
|
||||
return;
|
||||
}
|
||||
ParameterDescription<String> parameter = ParameterDescription.create(String.class,
|
||||
"args", false, "", parts.get(0), parts.get(1));
|
||||
if (parameters.put(KEY_ARGS, parameter) != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s. Replaced".formatted(loc, AT_ARGS));
|
||||
}
|
||||
}
|
||||
|
||||
protected void parseTty(Location loc, String str) {
|
||||
if (!extraTtys.add(str)) {
|
||||
Msg.warn(this, "%s: Duplicate %s. Ignored".formatted(loc, AT_TTY));
|
||||
}
|
||||
}
|
||||
|
||||
protected void parseUnrecognized(Location loc, String line) {
|
||||
Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, line));
|
||||
}
|
||||
|
||||
protected void validate(String fileName) {
|
||||
if (title == null) {
|
||||
Msg.error(this, "%s is required. Using script file name.".formatted(AT_TITLE));
|
||||
title = fileName;
|
||||
}
|
||||
if (menuPath == null) {
|
||||
menuPath = List.of(title);
|
||||
}
|
||||
if (menuGroup == null) {
|
||||
menuGroup = "";
|
||||
}
|
||||
if (menuOrder == null) {
|
||||
menuOrder = "";
|
||||
}
|
||||
if (iconId == null) {
|
||||
iconId = "icon.debugger";
|
||||
}
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description == null ? null : description.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a launch offer from the given shell script.
|
||||
*
|
||||
* @param program the current program, usually the target image. In general, this should be used
|
||||
* for at least two purposes. 1) To populate the default command line. 2) To ensure
|
||||
* the target image is mapped in the resulting target trace.
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public static UnixShellScriptTraceRmiLaunchOffer create(Program program, PluginTool tool,
|
||||
File script) throws FileNotFoundException {
|
||||
try (BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(new FileInputStream(script)))) {
|
||||
AttributesParser attrs = new AttributesParser();
|
||||
String line;
|
||||
for (int lineNo = 1; (line = reader.readLine()) != null; lineNo++) {
|
||||
if (line.startsWith(SHEBANG) && lineNo == 1) {
|
||||
}
|
||||
else if (line.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
else if (line.stripLeading().startsWith("#")) {
|
||||
attrs.parseLine(new Location(script.getName(), lineNo), line);
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
attrs.validate(script.getName());
|
||||
return new UnixShellScriptTraceRmiLaunchOffer(program, tool, script,
|
||||
"UNIX_SHELL:" + script.getName(), attrs.title, attrs.getDescription(),
|
||||
attrs.menuPath, attrs.menuGroup, attrs.menuOrder, new GIcon(attrs.iconId),
|
||||
attrs.helpLocation, attrs.parameters, attrs.extraTtys);
|
||||
}
|
||||
catch (FileNotFoundException e) {
|
||||
// Avoid capture by IOException
|
||||
throw e;
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected final File script;
|
||||
protected final String configName;
|
||||
protected final String title;
|
||||
protected final String description;
|
||||
protected final List<String> menuPath;
|
||||
protected final String menuGroup;
|
||||
protected final String menuOrder;
|
||||
protected final Icon icon;
|
||||
protected final HelpLocation helpLocation;
|
||||
protected final Map<String, ParameterDescription<?>> parameters;
|
||||
protected final List<String> extraTtys;
|
||||
|
||||
public UnixShellScriptTraceRmiLaunchOffer(Program program, PluginTool tool, File script,
|
||||
String configName, String title, String description, List<String> menuPath,
|
||||
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
|
||||
Map<String, ParameterDescription<?>> parameters, Collection<String> extraTtys) {
|
||||
super(program, tool);
|
||||
this.script = script;
|
||||
this.configName = configName;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.menuPath = List.copyOf(menuPath);
|
||||
this.menuGroup = menuGroup;
|
||||
this.menuOrder = menuOrder;
|
||||
this.icon = icon;
|
||||
this.helpLocation = helpLocation;
|
||||
this.parameters = Collections.unmodifiableMap(new LinkedHashMap<>(parameters));
|
||||
this.extraTtys = List.copyOf(extraTtys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigName() {
|
||||
return configName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getMenuPath() {
|
||||
return menuPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMenuGroup() {
|
||||
return menuGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMenuOrder() {
|
||||
return menuOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpLocation getHelpLocation() {
|
||||
return helpLocation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ParameterDescription<?>> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
protected static String addrToString(InetAddress address) {
|
||||
if (address.isAnyLocalAddress()) {
|
||||
return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost.
|
||||
}
|
||||
return address.getHostAddress();
|
||||
}
|
||||
|
||||
protected static String sockToString(SocketAddress address) {
|
||||
if (address instanceof InetSocketAddress tcp) {
|
||||
return addrToString(tcp.getAddress()) + ":" + tcp.getPort();
|
||||
}
|
||||
throw new AssertionError("Unhandled address type " + address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void launchBackEnd(TaskMonitor monitor, Map<String, TerminalSession> sessions,
|
||||
Map<String, ?> args, SocketAddress address) throws Exception {
|
||||
List<String> commandLine = new ArrayList<>();
|
||||
Map<String, String> env = new HashMap<>(System.getenv());
|
||||
|
||||
commandLine.add(script.getAbsolutePath());
|
||||
env.put("GHIDRA_HOME", Application.getInstallationDirectory().getAbsolutePath());
|
||||
env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address));
|
||||
|
||||
ParameterDescription<?> paramDesc;
|
||||
for (int i = 1; (paramDesc = parameters.get("arg:" + i)) != null; i++) {
|
||||
commandLine.add(Objects.toString(paramDesc.get(args)));
|
||||
}
|
||||
|
||||
paramDesc = parameters.get("args");
|
||||
if (paramDesc != null) {
|
||||
commandLine.addAll(ShellUtils.parseArgs((String) paramDesc.get(args)));
|
||||
}
|
||||
|
||||
for (Entry<String, ParameterDescription<?>> ent : parameters.entrySet()) {
|
||||
String key = ent.getKey();
|
||||
if (key.startsWith(PREFIX_ENV)) {
|
||||
String varName = key.substring(PREFIX_ENV.length());
|
||||
env.put(varName, Objects.toString(ent.getValue().get(args)));
|
||||
}
|
||||
}
|
||||
|
||||
for (String tty : extraTtys) {
|
||||
NullPtyTerminalSession ns = nullPtyTerminal();
|
||||
env.put(tty, ns.name());
|
||||
sessions.put(ns.name(), ns);
|
||||
}
|
||||
|
||||
sessions.put("Shell", runInTerminal(commandLine, env, sessions.values()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/* ###
|
||||
* 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.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOpinion;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.framework.options.OptionType;
|
||||
import ghidra.framework.options.Options;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.framework.plugintool.util.PluginUtils;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class UnixShellScriptTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion {
|
||||
|
||||
@Override
|
||||
public void registerOptions(Options options) {
|
||||
String pluginName = PluginUtils.getPluginNameFromClass(TraceRmiLauncherServicePlugin.class);
|
||||
options.registerOption(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS,
|
||||
OptionType.STRING_TYPE, "", new HelpLocation(pluginName, "options"),
|
||||
"Paths to search for user-created debugger launchers", new ScriptPathsPropertyEditor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresRefresh(String optionName) {
|
||||
return TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS.equals(optionName);
|
||||
}
|
||||
|
||||
protected Stream<ResourceFile> getModuleScriptPaths() {
|
||||
return Application.findModuleSubDirectories("data/debugger-launchers").stream();
|
||||
}
|
||||
|
||||
protected Stream<ResourceFile> getUserScriptPaths(PluginTool tool) {
|
||||
Options options = tool.getOptions(DebuggerPluginPackage.NAME);
|
||||
String scriptPaths =
|
||||
options.getString(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS, "");
|
||||
return scriptPaths.lines().filter(d -> !d.isBlank()).map(ResourceFile::new);
|
||||
}
|
||||
|
||||
protected Stream<ResourceFile> getScriptPaths(PluginTool tool) {
|
||||
return Stream.concat(getModuleScriptPaths(), getUserScriptPaths(tool));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<TraceRmiLaunchOffer> getOffers(Program program, PluginTool tool) {
|
||||
return getScriptPaths(tool)
|
||||
.flatMap(rf -> Stream.of(rf.listFiles(crf -> crf.getName().endsWith(".sh"))))
|
||||
.flatMap(sf -> {
|
||||
try {
|
||||
return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(program, tool,
|
||||
sf.getFile(false)));
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Could not offer " + sf + ":" + e.getMessage(), e);
|
||||
return Stream.of();
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/* ###
|
||||
* 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.service.rmi.trace;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import ghidra.debug.api.tracermi.RemoteAsyncResult;
|
||||
import ghidra.util.Swing;
|
||||
|
||||
public class DefaultRemoteAsyncResult extends CompletableFuture<Object>
|
||||
implements RemoteAsyncResult {
|
||||
final ValueDecoder decoder;
|
||||
|
||||
public DefaultRemoteAsyncResult() {
|
||||
this.decoder = ValueDecoder.DEFAULT;
|
||||
}
|
||||
|
||||
public DefaultRemoteAsyncResult(OpenTrace open) {
|
||||
this.decoder = open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get() throws InterruptedException, ExecutionException {
|
||||
if (Swing.isSwingThread()) {
|
||||
throw new AssertionError("Refusing indefinite wait on Swing thread");
|
||||
}
|
||||
return super.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(long timeout, TimeUnit unit)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
if (Swing.isSwingThread() && unit.toSeconds(timeout) > 1) {
|
||||
throw new AssertionError("Refusing a timeout > 1 second on Swing thread");
|
||||
}
|
||||
return super.get(timeout, unit);
|
||||
}
|
||||
}
|
|
@ -17,9 +17,11 @@ package ghidra.app.plugin.core.debug.service.rmi.trace;
|
|||
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod.Action;
|
||||
import ghidra.debug.api.tracermi.RemoteMethod;
|
||||
import ghidra.debug.api.tracermi.RemoteMethodRegistry;
|
||||
import ghidra.debug.api.tracermi.RemoteMethod.Action;
|
||||
|
||||
public class RemoteMethodRegistry {
|
||||
public class DefaultRemoteMethodRegistry implements RemoteMethodRegistry {
|
||||
private final Map<String, RemoteMethod> map = new HashMap<>();
|
||||
private final Map<Action, Set<RemoteMethod>> byAction = new HashMap<>();
|
||||
|
||||
|
@ -30,18 +32,21 @@ public class RemoteMethodRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, RemoteMethod> all() {
|
||||
synchronized (map) {
|
||||
return Map.copyOf(map);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMethod get(String name) {
|
||||
synchronized (map) {
|
||||
return map.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RemoteMethod> getByAction(Action action) {
|
||||
synchronized (map) {
|
||||
return byAction.getOrDefault(action, Set.of());
|
|
@ -19,9 +19,11 @@ import java.io.IOException;
|
|||
import java.net.ServerSocket;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
public class TraceRmiAcceptor extends TraceRmiServer {
|
||||
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
|
||||
|
||||
public TraceRmiAcceptor(TraceRmiPlugin plugin, SocketAddress address) {
|
||||
public class DefaultTraceRmiAcceptor extends TraceRmiServer implements TraceRmiAcceptor {
|
||||
|
||||
public DefaultTraceRmiAcceptor(TraceRmiPlugin plugin, SocketAddress address) {
|
||||
super(plugin, address);
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
package ghidra.app.plugin.core.debug.service.rmi.trace;
|
||||
|
||||
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler.*;
|
||||
import ghidra.debug.api.tracermi.TraceRmiError;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.program.model.lang.Register;
|
||||
import ghidra.rmi.trace.TraceRmi.*;
|
||||
|
|
|
@ -15,8 +15,21 @@
|
|||
*/
|
||||
package ghidra.app.plugin.core.debug.service.rmi.trace;
|
||||
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import java.util.Map;
|
||||
|
||||
public record RemoteParameter(String name, SchemaName type, boolean required,
|
||||
ValueSupplier defaultValue, String display, String description) {
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import ghidra.debug.api.tracermi.RemoteMethod;
|
||||
import ghidra.debug.api.tracermi.RemoteParameter;
|
||||
import ghidra.debug.api.tracermi.RemoteMethod.Action;
|
||||
import ghidra.trace.model.Trace;
|
||||
|
||||
public record RecordRemoteMethod(TraceRmiHandler handler, String name, Action action,
|
||||
String description, Map<String, RemoteParameter> parameters, SchemaName retType)
|
||||
implements RemoteMethod {
|
||||
@Override
|
||||
public DefaultRemoteAsyncResult invokeAsync(Map<String, Object> arguments) {
|
||||
Trace trace = validate(arguments);
|
||||
OpenTrace open = handler.getOpenTrace(trace);
|
||||
return handler.invoke(open, name, arguments);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/* ###
|
||||
* 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.service.rmi.trace;
|
||||
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import ghidra.debug.api.tracermi.RemoteParameter;
|
||||
import ghidra.program.model.address.AddressOverflowException;
|
||||
import ghidra.trace.model.Trace;
|
||||
|
||||
public record RecordRemoteParameter(TraceRmiHandler handler, String name, SchemaName type,
|
||||
boolean required, ValueSupplier defaultValue, String display, String description)
|
||||
implements RemoteParameter {
|
||||
|
||||
public Object getDefaultValue(Trace trace) {
|
||||
OpenTrace open = handler.getOpenTrace(trace);
|
||||
if (open == null) {
|
||||
throw new IllegalArgumentException("Trace is not from this connection");
|
||||
}
|
||||
try {
|
||||
return defaultValue().get(open);
|
||||
}
|
||||
catch (AddressOverflowException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getDefaultValue() {
|
||||
try {
|
||||
return defaultValue().get(ValueDecoder.DEFAULT);
|
||||
}
|
||||
catch (AddressOverflowException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.service.rmi.trace;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
import ghidra.util.Swing;
|
||||
|
||||
/**
|
||||
* The future result of invoking a {@link RemoteMethod}.
|
||||
*
|
||||
* <p>
|
||||
* While this can technically result in an object, returning values from remote methods is highly
|
||||
* discouraged. This has led to several issues in the past, including duplication of information
|
||||
* (and a lot of it) over the connection. Instead, most methods should just update the trace
|
||||
* database, and the client can retrieve the relevant information from it. One exception might be
|
||||
* the {@code execute} method. This is typically for executing a CLI command with captured output.
|
||||
* There is generally no place for such output to go into the trace, and the use cases for such a
|
||||
* method to return the output are compelling. For other cases, perhaps the most you can do is
|
||||
* return a {@link TraceObject}, so that a client can quickly associate the trace changes with the
|
||||
* method. Otherwise, please return null/void/None for all methods.
|
||||
*
|
||||
* <b>NOTE:</b> To avoid the mistake of blocking the Swing thread on an asynchronous result, the
|
||||
* {@link #get()} methods have been overridden to check for the Swing thread. If invoked on the
|
||||
* Swing thread with a timeout greater than 1 second, an assertion error will be thrown. Please use
|
||||
* a non-swing thread, e.g., a task thread or script thread, to wait for results, or chain
|
||||
* callbacks.
|
||||
*/
|
||||
public class RemoteAsyncResult extends CompletableFuture<Object> {
|
||||
final ValueDecoder decoder;
|
||||
|
||||
public RemoteAsyncResult() {
|
||||
this.decoder = ValueDecoder.DEFAULT;
|
||||
}
|
||||
|
||||
public RemoteAsyncResult(OpenTrace open) {
|
||||
this.decoder = open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get() throws InterruptedException, ExecutionException {
|
||||
if (Swing.isSwingThread()) {
|
||||
throw new AssertionError("Refusing indefinite wait on Swing thread");
|
||||
}
|
||||
return super.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(long timeout, TimeUnit unit)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
if (Swing.isSwingThread() && unit.toSeconds(timeout) > 1) {
|
||||
throw new AssertionError("Refusing a timeout > 1 second on Swing thread");
|
||||
}
|
||||
return super.get(timeout, unit);
|
||||
}
|
||||
}
|
|
@ -1,330 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.service.rmi.trace;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.schema.*;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
|
||||
/**
|
||||
* A remote method registered by the back-end debugger.
|
||||
*
|
||||
* <p>
|
||||
* Remote methods must describe the parameters names and types at a minimum. They should also
|
||||
* provide a display name and description for the method itself and each of its parameters. These
|
||||
* methods should not return a result. Instead, any "result" should be recorded into a trace. The
|
||||
* invocation can result in an error, which is communicated by an exception that can carry only a
|
||||
* message string. Choice few methods should return a result, for example, the {@code execute}
|
||||
* method with output capture. That output generally does not belong in a trace, so the only way to
|
||||
* communicate it back to the front end is to return it.
|
||||
*/
|
||||
public interface RemoteMethod {
|
||||
|
||||
/**
|
||||
* A "hint" for how to map the method to a common action.
|
||||
*
|
||||
* <p>
|
||||
* Many common commands/actions have varying names across different back-end debuggers. We'd
|
||||
* like to present common idioms for these common actions, but allow them to keep the names used
|
||||
* by the back-end, because those names are probably better known to users of that back-end than
|
||||
* Ghidra's action names are known. The action hints will affect the icon and placement of the
|
||||
* action in the UI, but the display name will still reflect the name given by the back-end.
|
||||
* Note that the "stock" action names are not a fixed enumeration. These are just the ones that
|
||||
* might get special treatment from Ghidra. All methods should appear somewhere (at least, e.g.,
|
||||
* in context menus for applicable objects), even if the action name is unspecified or does not
|
||||
* match a stock name. This list may change over time, but that shouldn't matter much. Each
|
||||
* back-end should make its best effort to match its methods to these stock actions where
|
||||
* applicable, but ultimately, it is up to the UI to decide what is presented where.
|
||||
*/
|
||||
public record Action(String name) {
|
||||
public static final Action REFRESH = new Action("refresh");
|
||||
public static final Action ACTIVATE = new Action("activate");
|
||||
/**
|
||||
* A weaker form of activate.
|
||||
*
|
||||
* <p>
|
||||
* The user has expressed interest in an object, but has not activated it yet. This is often
|
||||
* used to communicate selection (i.e., highlight) of the object. Whereas, double-clicking
|
||||
* or pressing enter would more likely invoke 'activate.'
|
||||
*/
|
||||
public static final Action FOCUS = new Action("focus");
|
||||
public static final Action TOGGLE = new Action("toggle");
|
||||
public static final Action DELETE = new Action("delete");
|
||||
|
||||
/**
|
||||
* Forms: (cmd:STRING):STRING
|
||||
*
|
||||
* Optional arguments: capture:BOOL
|
||||
*/
|
||||
public static final Action EXECUTE = new Action("execute");
|
||||
|
||||
/**
|
||||
* Forms: (spec:STRING)
|
||||
*/
|
||||
public static final Action CONNECT = new Action("connect");
|
||||
|
||||
/**
|
||||
* Forms: (target:Attachable), (pid:INT), (spec:STRING)
|
||||
*/
|
||||
public static final Action ATTACH = new Action("attach");
|
||||
public static final Action DETACH = new Action("detach");
|
||||
|
||||
/**
|
||||
* Forms: (command_line:STRING), (file:STRING,args:STRING), (file:STRING,args:STRING_ARRAY),
|
||||
* (ANY*)
|
||||
*/
|
||||
public static final Action LAUNCH = new Action("launch");
|
||||
public static final Action KILL = new Action("kill");
|
||||
|
||||
public static final Action RESUME = new Action("resume");
|
||||
public static final Action INTERRUPT = new Action("interrupt");
|
||||
|
||||
/**
|
||||
* All of these will show in the "step" portion of the control toolbar, if present. The
|
||||
* difference in each "step_x" is minor. The icon will indicate which form, and the
|
||||
* positions will be shifted so they appear in a consistent order. The display name is
|
||||
* determined by the method name, not the action name. For stepping actions that don't fit
|
||||
* the standards, use {@link #STEP_EXT}. There should be at most one of each standard
|
||||
* applicable for any given context. (Multiple will appear, but may confuse the user.) You
|
||||
* can have as many extended step actions as you like. They will be ordered
|
||||
* lexicographically by name.
|
||||
*/
|
||||
public static final Action STEP_INTO = new Action("step_into");
|
||||
public static final Action STEP_OVER = new Action("step_over");
|
||||
public static final Action STEP_OUT = new Action("step_out");
|
||||
/**
|
||||
* Skip is not typically available, except in emulators. If the back-end debugger does not
|
||||
* have a command for this action out-of-the-box, we do not recommend trying to implement it
|
||||
* yourself. The purpose of these actions just to expose/map each command to the UI, not to
|
||||
* invent new features for the back-end debugger.
|
||||
*/
|
||||
public static final Action STEP_SKIP = new Action("step_skip");
|
||||
/**
|
||||
* Step back is not typically available, except in emulators and timeless (or time-travel)
|
||||
* debuggers.
|
||||
*/
|
||||
public static final Action STEP_BACK = new Action("step_back");
|
||||
/**
|
||||
* The action for steps that don't fit one of the common stepping actions.
|
||||
*/
|
||||
public static final Action STEP_EXT = new Action("step_ext");
|
||||
|
||||
/**
|
||||
* Forms: (addr:ADDRESS), R/W(rng:RANGE), set(expr:STRING)
|
||||
*
|
||||
* Optional arguments: condition:STRING, commands:STRING
|
||||
*/
|
||||
public static final Action BREAK_SW_EXECUTE = new Action("break_sw_execute");
|
||||
public static final Action BREAK_HW_EXECUTE = new Action("break_hw_execute");
|
||||
public static final Action BREAK_READ = new Action("break_read");
|
||||
public static final Action BREAK_WRITE = new Action("break_write");
|
||||
public static final Action BREAK_ACCESS = new Action("break_access");
|
||||
public static final Action BREAK_EXT = new Action("break_ext");
|
||||
|
||||
/**
|
||||
* Forms: (rng:RANGE)
|
||||
*/
|
||||
public static final Action READ_MEM = new Action("read_mem");
|
||||
/**
|
||||
* Forms: (addr:ADDRESS,data:BYTES)
|
||||
*/
|
||||
public static final Action WRITE_MEM = new Action("write_mem");
|
||||
|
||||
// NOTE: no read_reg. Use refresh(RegContainer), refresh(RegGroup), refresh(Register)
|
||||
/**
|
||||
* Forms: (frame:Frame,name:STRING,value:BYTES), (register:Register,value:BYTES)
|
||||
*/
|
||||
public static final Action WRITE_REG = new Action("write_reg");
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the method.
|
||||
*
|
||||
* @return the name
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* A string that hints at the UI action this method achieves.
|
||||
*
|
||||
* @return the action
|
||||
*/
|
||||
Action action();
|
||||
|
||||
/**
|
||||
* A description of the method.
|
||||
*
|
||||
* <p>
|
||||
* This is the text for tooltips or other information presented by actions whose purpose is to
|
||||
* invoke this method. If the back-end command name is well known to its users, this text should
|
||||
* include that name.
|
||||
*
|
||||
* @return the description
|
||||
*/
|
||||
String description();
|
||||
|
||||
/**
|
||||
* The methods parameters.
|
||||
*
|
||||
* <p>
|
||||
* Parameters are all keyword-style parameters. This returns a map of names to parameter
|
||||
* descriptions.
|
||||
*
|
||||
* @return the parameter map
|
||||
*/
|
||||
Map<String, RemoteParameter> parameters();
|
||||
|
||||
/**
|
||||
* Get the schema for the return type.
|
||||
*
|
||||
* <b>NOTE:</b> Most methods should return void, i.e., either they succeed, or they throw/raise
|
||||
* an error message. One notable exception is "execute," which may return the console output
|
||||
* from executing a command. In most cases, the method should only cause an update to the trace
|
||||
* database. That effect is its result.
|
||||
*
|
||||
* @return the schema name for the method's return type.
|
||||
*/
|
||||
SchemaName retType();
|
||||
|
||||
/**
|
||||
* Check the type of an argument.
|
||||
*
|
||||
* <p>
|
||||
* This is a hack, because {@link TargetObjectSchema} expects {@link TargetObject}, or a
|
||||
* primitive. We instead need {@link TraceObject}. I'd add the method to the schema, except that
|
||||
* trace stuff is not in its dependencies.
|
||||
*
|
||||
* @param name the name of the parameter
|
||||
* @param sch the type of the parameter
|
||||
* @param arg the argument
|
||||
*/
|
||||
static void checkType(String name, TargetObjectSchema sch, Object arg) {
|
||||
if (sch.getType() != TargetObject.class) {
|
||||
if (sch.getType().isInstance(arg)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (arg instanceof TraceObject obj) {
|
||||
if (sch.equals(obj.getTargetSchema())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"For parameter %s: argument %s is not a %s".formatted(name, arg, sch));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given argument.
|
||||
*
|
||||
* <p>
|
||||
* This method is for checking parameter sanity before they are marshalled to the back-end. This
|
||||
* is called automatically during invocation. Clients can use this method to pre-test or
|
||||
* validate in the UI, when invocation is not yet desired.
|
||||
*
|
||||
* @param arguments the arguments
|
||||
* @return the trace if any object arguments were given, or null
|
||||
* @throws IllegalArgumentException if the arguments are not valid
|
||||
*/
|
||||
default Trace validate(Map<String, Object> arguments) {
|
||||
Trace trace = null;
|
||||
SchemaContext ctx = EnumerableTargetObjectSchema.MinimalSchemaContext.INSTANCE;
|
||||
for (Map.Entry<String, RemoteParameter> ent : parameters().entrySet()) {
|
||||
if (!arguments.containsKey(ent.getKey())) {
|
||||
if (ent.getValue().required()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing required parameter '" + ent.getKey() + "'");
|
||||
}
|
||||
continue; // Should not need to check the default value
|
||||
}
|
||||
Object arg = arguments.get(ent.getKey());
|
||||
if (arg instanceof TraceObject obj) {
|
||||
if (trace == null) {
|
||||
trace = obj.getTrace();
|
||||
ctx = trace.getObjectManager().getRootSchema().getContext();
|
||||
}
|
||||
else if (trace != obj.getTrace()) {
|
||||
throw new IllegalArgumentException(
|
||||
"All TraceObject parameters must come from the same trace");
|
||||
}
|
||||
}
|
||||
TargetObjectSchema sch = ctx.getSchema(ent.getValue().type());
|
||||
checkType(ent.getKey(), sch, arg);
|
||||
}
|
||||
for (Map.Entry<String, Object> ent : arguments.entrySet()) {
|
||||
if (!parameters().containsKey(ent.getKey())) {
|
||||
throw new IllegalArgumentException("Extra argument '" + ent.getKey() + "'");
|
||||
}
|
||||
}
|
||||
return trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the remote method, getting a future result.
|
||||
*
|
||||
* <p>
|
||||
* This invokes the method asynchronously. The returned objects is a {@link CompletableFuture},
|
||||
* whose getters are overridden to prevent blocking the Swing thread for more than 1 second. Use
|
||||
* of this method is not recommended, if it can be avoided; however, you should not create a
|
||||
* thread whose sole purpose is to invoke this method. UI actions that need to invoke a remote
|
||||
* method should do so using this method, but they must be sure to handle errors using, e.g.,
|
||||
* using {@link CompletableFuture#exceptionally(Function)}, lest the actions fail silently.
|
||||
*
|
||||
* @param arguments the keyword arguments to the remote method
|
||||
* @return the future result
|
||||
* @throws IllegalArgumentException if the arguments are not valid
|
||||
*/
|
||||
RemoteAsyncResult invokeAsync(Map<String, Object> arguments);
|
||||
|
||||
/**
|
||||
* Invoke the remote method and wait for its completion.
|
||||
*
|
||||
* <p>
|
||||
* This method cannot be invoked from the Swing thread. This is to avoid locking up the user
|
||||
* interface. If you are on the Swing thread, consider {@link #invokeAsync(Map)} instead. You
|
||||
* can chain the follow-up actions and then schedule any UI updates on the Swing thread using
|
||||
* {@link AsyncUtils#SWING_EXECUTOR}.
|
||||
*
|
||||
* @param arguments the keyword arguments to the remote method
|
||||
* @throws IllegalArgumentException if the arguments are not valid
|
||||
*/
|
||||
default Object invoke(Map<String, Object> arguments) {
|
||||
try {
|
||||
return invokeAsync(arguments).get();
|
||||
}
|
||||
catch (InterruptedException | ExecutionException e) {
|
||||
throw new TraceRmiError(e);
|
||||
}
|
||||
}
|
||||
|
||||
record RecordRemoteMethod(TraceRmiHandler handler, String name, Action action,
|
||||
String description, Map<String, RemoteParameter> parameters, SchemaName retType)
|
||||
implements RemoteMethod {
|
||||
@Override
|
||||
public RemoteAsyncResult invokeAsync(Map<String, Object> arguments) {
|
||||
Trace trace = validate(arguments);
|
||||
OpenTrace open = handler.getOpenTrace(trace);
|
||||
return handler.invoke(open, name, arguments);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.service.rmi.trace;
|
||||
|
||||
public class TraceRmiError extends RuntimeException {
|
||||
public TraceRmiError() {
|
||||
}
|
||||
|
||||
public TraceRmiError(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public TraceRmiError(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public TraceRmiError(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -25,8 +25,7 @@ import java.nio.file.Paths;
|
|||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.*;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
@ -35,16 +34,16 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
|||
import com.google.protobuf.ByteString;
|
||||
|
||||
import db.Transaction;
|
||||
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
|
||||
import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin;
|
||||
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
|
||||
import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod.Action;
|
||||
import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod.RecordRemoteMethod;
|
||||
import ghidra.app.services.DebuggerTraceManagerService;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import ghidra.dbg.target.schema.XmlSchemaContext;
|
||||
import ghidra.dbg.util.PathPattern;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
import ghidra.debug.api.tracermi.*;
|
||||
import ghidra.debug.api.tracermi.RemoteMethod.Action;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.plugintool.AutoService;
|
||||
import ghidra.framework.plugintool.AutoService.Wiring;
|
||||
|
@ -69,7 +68,7 @@ import ghidra.util.exception.CancelledException;
|
|||
import ghidra.util.exception.DuplicateFileException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
public class TraceRmiHandler {
|
||||
public class TraceRmiHandler implements TraceRmiConnection {
|
||||
public static final String VERSION = "10.4";
|
||||
|
||||
protected static class VersionMismatchError extends TraceRmiError {
|
||||
|
@ -139,6 +138,7 @@ public class TraceRmiHandler {
|
|||
protected static class OpenTraceMap {
|
||||
private final Map<DoId, OpenTrace> byId = new HashMap<>();
|
||||
private final Map<Trace, OpenTrace> byTrace = new HashMap<>();
|
||||
private final CompletableFuture<OpenTrace> first = new CompletableFuture<>();
|
||||
|
||||
public synchronized boolean isEmpty() {
|
||||
return byId.isEmpty();
|
||||
|
@ -168,6 +168,11 @@ public class TraceRmiHandler {
|
|||
public synchronized void put(OpenTrace openTrace) {
|
||||
byId.put(openTrace.doId, openTrace);
|
||||
byTrace.put(openTrace.trace, openTrace);
|
||||
first.complete(openTrace);
|
||||
}
|
||||
|
||||
public CompletableFuture<OpenTrace> getFirstAsync() {
|
||||
return first;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,9 +186,9 @@ public class TraceRmiHandler {
|
|||
private final OpenTraceMap openTraces = new OpenTraceMap();
|
||||
private final Map<Tid, OpenTx> openTxes = new HashMap<>();
|
||||
|
||||
private final RemoteMethodRegistry methodRegistry = new RemoteMethodRegistry();
|
||||
private final DefaultRemoteMethodRegistry methodRegistry = new DefaultRemoteMethodRegistry();
|
||||
// The remote must service requests and reply in the order received.
|
||||
private final Deque<RemoteAsyncResult> xReqQueue = new ArrayDeque<>();
|
||||
private final Deque<DefaultRemoteAsyncResult> xReqQueue = new ArrayDeque<>();
|
||||
|
||||
@AutoServiceConsumed
|
||||
private DebuggerTraceManagerService traceManager;
|
||||
|
@ -201,6 +206,7 @@ public class TraceRmiHandler {
|
|||
*/
|
||||
public TraceRmiHandler(TraceRmiPlugin plugin, Socket socket) throws IOException {
|
||||
this.plugin = plugin;
|
||||
plugin.addHandler(this);
|
||||
this.socket = socket;
|
||||
this.in = socket.getInputStream();
|
||||
this.out = socket.getOutputStream();
|
||||
|
@ -211,17 +217,18 @@ public class TraceRmiHandler {
|
|||
}
|
||||
|
||||
protected void flushXReqQueue(Throwable exc) {
|
||||
List<RemoteAsyncResult> copy;
|
||||
List<DefaultRemoteAsyncResult> copy;
|
||||
synchronized (xReqQueue) {
|
||||
copy = List.copyOf(xReqQueue);
|
||||
xReqQueue.clear();
|
||||
}
|
||||
for (RemoteAsyncResult result : copy) {
|
||||
for (DefaultRemoteAsyncResult result : copy) {
|
||||
result.completeExceptionally(exc);
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() throws IOException {
|
||||
plugin.removeHandler(this);
|
||||
flushXReqQueue(new TraceRmiError("Socket closed"));
|
||||
socket.close();
|
||||
while (!openTxes.isEmpty()) {
|
||||
|
@ -248,12 +255,24 @@ public class TraceRmiHandler {
|
|||
closed.complete(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return socket.isClosed() && closed.isDone();
|
||||
}
|
||||
|
||||
public void waitClosed() throws InterruptedException, ExecutionException {
|
||||
closed.get();
|
||||
@Override
|
||||
public void waitClosed() {
|
||||
try {
|
||||
closed.get();
|
||||
}
|
||||
catch (InterruptedException | ExecutionException e) {
|
||||
throw new TraceRmiError(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected DomainFolder getOrCreateNewTracesFolder()
|
||||
|
@ -442,8 +461,9 @@ public class TraceRmiHandler {
|
|||
req.getRequestActivate().getObject().getPath().getPath());
|
||||
case REQUEST_END_TX -> "endTx(%d)".formatted(
|
||||
req.getRequestEndTx().getTxid().getId());
|
||||
case REQUEST_START_TX -> "startTx(%d)".formatted(
|
||||
req.getRequestStartTx().getTxid().getId());
|
||||
case REQUEST_START_TX -> "startTx(%d,%s)".formatted(
|
||||
req.getRequestStartTx().getTxid().getId(),
|
||||
req.getRequestStartTx().getDescription());
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
@ -728,7 +748,12 @@ public class TraceRmiHandler {
|
|||
OpenTrace open = requireOpenTrace(req.getOid());
|
||||
TraceObject object = open.getObject(req.getObject(), true);
|
||||
DebuggerCoordinates coords = traceManager.getCurrent();
|
||||
coords = coords.object(object);
|
||||
if (coords.getTrace() == object.getTrace()) {
|
||||
coords = coords.object(object);
|
||||
}
|
||||
else {
|
||||
coords = DebuggerCoordinates.NOWHERE.object(object);
|
||||
}
|
||||
if (open.lastSnapshot != null) {
|
||||
coords = coords.snap(open.lastSnapshot.getKey());
|
||||
}
|
||||
|
@ -738,7 +763,7 @@ public class TraceRmiHandler {
|
|||
}
|
||||
else {
|
||||
Trace currentTrace = traceManager.getCurrentTrace();
|
||||
if (currentTrace == null || currentTrace == open.trace) {
|
||||
if (currentTrace == null || openTraces.getByTrace(currentTrace) != null) {
|
||||
traceManager.activate(coords);
|
||||
}
|
||||
}
|
||||
|
@ -939,8 +964,7 @@ public class TraceRmiHandler {
|
|||
RemoteMethod rm = new RecordRemoteMethod(this, m.getName(), new Action(m.getAction()),
|
||||
m.getDescription(), m.getParametersList()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(MethodParameter::getName,
|
||||
TraceRmiHandler::makeParameter)),
|
||||
.collect(Collectors.toMap(MethodParameter::getName, this::makeParameter)),
|
||||
new SchemaName(m.getReturnType().getName()));
|
||||
methodRegistry.add(rm);
|
||||
}
|
||||
|
@ -978,8 +1002,8 @@ public class TraceRmiHandler {
|
|||
return ReplyPutRegisterValue.getDefaultInstance();
|
||||
}
|
||||
|
||||
protected static RemoteParameter makeParameter(MethodParameter mp) {
|
||||
return new RemoteParameter(mp.getName(), new SchemaName(mp.getType().getName()),
|
||||
protected RecordRemoteParameter makeParameter(MethodParameter mp) {
|
||||
return new RecordRemoteParameter(this, mp.getName(), new SchemaName(mp.getType().getName()),
|
||||
mp.getRequired(), ot -> ot.toValue(mp.getDefaultValue()), mp.getDisplay(),
|
||||
mp.getDescription());
|
||||
}
|
||||
|
@ -1084,7 +1108,7 @@ public class TraceRmiHandler {
|
|||
|
||||
protected RootMessage.Builder handleXInvokeMethod(XReplyInvokeMethod xrep) {
|
||||
String error = xrep.getError();
|
||||
RemoteAsyncResult result;
|
||||
DefaultRemoteAsyncResult result;
|
||||
synchronized (xReqQueue) {
|
||||
result = xReqQueue.poll();
|
||||
}
|
||||
|
@ -1102,11 +1126,13 @@ public class TraceRmiHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketAddress getRemoteAddress() {
|
||||
return socket.getRemoteSocketAddress();
|
||||
}
|
||||
|
||||
public RemoteMethodRegistry getMethods() {
|
||||
@Override
|
||||
public DefaultRemoteMethodRegistry getMethods() {
|
||||
return methodRegistry;
|
||||
}
|
||||
|
||||
|
@ -1121,20 +1147,20 @@ public class TraceRmiHandler {
|
|||
return open;
|
||||
}
|
||||
|
||||
protected RemoteAsyncResult invoke(OpenTrace open, String methodName,
|
||||
protected DefaultRemoteAsyncResult invoke(OpenTrace open, String methodName,
|
||||
Map<String, Object> arguments) {
|
||||
RootMessage.Builder req = RootMessage.newBuilder();
|
||||
XRequestInvokeMethod.Builder invoke = XRequestInvokeMethod.newBuilder()
|
||||
.setName(methodName)
|
||||
.addAllArguments(
|
||||
arguments.entrySet().stream().map(TraceRmiHandler::makeArgument).toList());
|
||||
RemoteAsyncResult result;
|
||||
DefaultRemoteAsyncResult result;
|
||||
if (open != null) {
|
||||
result = new RemoteAsyncResult(open);
|
||||
result = new DefaultRemoteAsyncResult(open);
|
||||
invoke.setOid(open.doId.toDomObjId());
|
||||
}
|
||||
else {
|
||||
result = new RemoteAsyncResult();
|
||||
result = new DefaultRemoteAsyncResult();
|
||||
}
|
||||
req.setXrequestInvokeMethod(invoke);
|
||||
synchronized (xReqQueue) {
|
||||
|
@ -1151,6 +1177,7 @@ public class TraceRmiHandler {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Internal
|
||||
public long getLastSnapshot(Trace trace) {
|
||||
TraceSnapshot lastSnapshot = openTraces.getByTrace(trace).lastSnapshot;
|
||||
|
@ -1159,4 +1186,14 @@ public class TraceRmiHandler {
|
|||
}
|
||||
return lastSnapshot.getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Trace waitForTrace(long timeoutMillis) throws TimeoutException {
|
||||
try {
|
||||
return openTraces.getFirstAsync().get(timeoutMillis, TimeUnit.MILLISECONDS).trace;
|
||||
}
|
||||
catch (InterruptedException | ExecutionException e) {
|
||||
throw new TraceRmiError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,14 @@ package ghidra.app.plugin.core.debug.service.rmi.trace;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
|
||||
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
|
||||
import ghidra.app.services.TraceRmiService;
|
||||
import ghidra.debug.api.tracermi.TraceRmiConnection;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.util.task.ConsoleTaskMonitor;
|
||||
|
@ -31,11 +33,8 @@ import ghidra.util.task.TaskMonitor;
|
|||
@PluginInfo(
|
||||
shortDescription = "Connect to back-end debuggers via Trace RMI",
|
||||
description = """
|
||||
Provides an alternative for connecting to back-end debuggers. The DebuggerModel has
|
||||
become a bit onerous to implement. Despite its apparent flexibility, the recorder at
|
||||
the front-end imposes many restrictions, and getting it to work turns into a lot of
|
||||
guess work and frustration. Trace RMI should offer a more direct means of recording a
|
||||
trace from a back-end.
|
||||
Provides a means for connecting to back-end debuggers.
|
||||
NOTE this is an alternative to the DebuggerModel and is meant to replace it.
|
||||
""",
|
||||
category = PluginCategoryNames.DEBUGGER,
|
||||
packageName = DebuggerPluginPackage.NAME,
|
||||
|
@ -54,6 +53,8 @@ public class TraceRmiPlugin extends Plugin implements TraceRmiService {
|
|||
private SocketAddress serverAddress = new InetSocketAddress("0.0.0.0", DEFAULT_PORT);
|
||||
private TraceRmiServer server;
|
||||
|
||||
private final Set<TraceRmiHandler> handlers = new LinkedHashSet<>();
|
||||
|
||||
public TraceRmiPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
}
|
||||
|
@ -107,13 +108,28 @@ public class TraceRmiPlugin extends Plugin implements TraceRmiService {
|
|||
public TraceRmiHandler connect(SocketAddress address) throws IOException {
|
||||
Socket socket = new Socket();
|
||||
socket.connect(address);
|
||||
return new TraceRmiHandler(this, socket);
|
||||
TraceRmiHandler handler = new TraceRmiHandler(this, socket);
|
||||
handler.start();
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TraceRmiAcceptor acceptOne(SocketAddress address) throws IOException {
|
||||
TraceRmiAcceptor acceptor = new TraceRmiAcceptor(this, address);
|
||||
public DefaultTraceRmiAcceptor acceptOne(SocketAddress address) throws IOException {
|
||||
DefaultTraceRmiAcceptor acceptor = new DefaultTraceRmiAcceptor(this, address);
|
||||
acceptor.start();
|
||||
return acceptor;
|
||||
}
|
||||
|
||||
void addHandler(TraceRmiHandler handler) {
|
||||
handlers.add(handler);
|
||||
}
|
||||
|
||||
void removeHandler(TraceRmiHandler handler) {
|
||||
handlers.remove(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<TraceRmiConnection> getAllConnections() {
|
||||
return List.copyOf(handlers);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.services;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiAcceptor;
|
||||
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
|
||||
|
||||
public interface TraceRmiService {
|
||||
SocketAddress getServerAddress();
|
||||
|
||||
/**
|
||||
* Set the server address and port
|
||||
*
|
||||
* @param serverAddress may be null to bind to ephemeral port
|
||||
*/
|
||||
void setServerAddress(SocketAddress serverAddress);
|
||||
|
||||
void startServer() throws IOException;
|
||||
|
||||
void stopServer();
|
||||
|
||||
boolean isServerStarted();
|
||||
|
||||
TraceRmiHandler connect(SocketAddress address) throws IOException;
|
||||
|
||||
/**
|
||||
* Accept a single connection by listening on the given address
|
||||
*
|
||||
* @param address the socket address to bind, or null for ephemeral
|
||||
* @return the acceptor, which can be used to retrieve the ephemeral address and accept the
|
||||
* actual connection
|
||||
* @throws IOException on error
|
||||
*/
|
||||
TraceRmiAcceptor acceptOne(SocketAddress address) throws IOException;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue