GP-3818: Create TraceRMI launcher framework. Launch script for gdb.

This commit is contained in:
Dan 2023-09-20 15:17:37 -04:00
parent 4561e8335d
commit eea90f49c9
379 changed files with 5180 additions and 1487 deletions

View file

@ -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.
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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