GP-3857: Port most Debugger components to TraceRmi.

This commit is contained in:
Dan 2023-11-02 10:43:31 -04:00
parent 7e4d2bcfaa
commit fd4380c07a
222 changed files with 7241 additions and 3752 deletions

View file

@ -0,0 +1,387 @@
/* ###
* 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;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.beans.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.jdom.Element;
import docking.DialogComponentProvider;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils;
import ghidra.dbg.target.schema.SchemaContext;
import ghidra.debug.api.tracermi.RemoteParameter;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.Msg;
import ghidra.util.layout.PairLayout;
public class RemoteMethodInvocationDialog extends DialogComponentProvider
implements PropertyChangeListener {
private static final String KEY_MEMORIZED_ARGUMENTS = "memorizedArguments";
static class ChoicesPropertyEditor implements PropertyEditor {
private final List<?> choices;
private final String[] tags;
private final List<PropertyChangeListener> listeners = new ArrayList<>();
private Object value;
public ChoicesPropertyEditor(Set<?> choices) {
this.choices = List.copyOf(choices);
this.tags = choices.stream().map(Objects::toString).toArray(String[]::new);
}
@Override
public void setValue(Object value) {
if (Objects.equals(value, this.value)) {
return;
}
if (!choices.contains(value)) {
throw new IllegalArgumentException("Unsupported value: " + value);
}
Object oldValue;
List<PropertyChangeListener> listeners;
synchronized (this.listeners) {
oldValue = this.value;
this.value = value;
if (this.listeners.isEmpty()) {
return;
}
listeners = List.copyOf(this.listeners);
}
PropertyChangeEvent evt = new PropertyChangeEvent(this, null, oldValue, value);
for (PropertyChangeListener l : listeners) {
l.propertyChange(evt);
}
}
@Override
public Object getValue() {
return value;
}
@Override
public boolean isPaintable() {
return false;
}
@Override
public void paintValue(Graphics gfx, Rectangle box) {
// Not paintable
}
@Override
public String getJavaInitializationString() {
if (value == null) {
return "null";
}
if (value instanceof String str) {
return "\"" + StringEscapeUtils.escapeJava(str) + "\"";
}
return Objects.toString(value);
}
@Override
public String getAsText() {
return Objects.toString(value);
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
int index = ArrayUtils.indexOf(tags, text);
if (index < 0) {
throw new IllegalArgumentException("Unsupported value: " + text);
}
setValue(choices.get(index));
}
@Override
public String[] getTags() {
return tags.clone();
}
@Override
public Component getCustomEditor() {
return null;
}
@Override
public boolean supportsCustomEditor() {
return false;
}
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
}
record NameTypePair(String name, Class<?> type) {
public static NameTypePair fromParameter(SchemaContext ctx, RemoteParameter parameter) {
return new NameTypePair(parameter.name(), ctx.getSchema(parameter.type()).getType());
}
public static NameTypePair fromString(String name) throws ClassNotFoundException {
String[] parts = name.split(",", 2);
if (parts.length != 2) {
// This appears to be a bad assumption - empty fields results in solitary labels
return new NameTypePair(parts[0], String.class);
//throw new IllegalArgumentException("Could not parse name,type");
}
return new NameTypePair(parts[0], Class.forName(parts[1]));
}
}
private final BidiMap<RemoteParameter, PropertyEditor> paramEditors =
new DualLinkedHashBidiMap<>();
private JPanel panel;
private JLabel descriptionLabel;
private JPanel pairPanel;
private PairLayout layout;
protected JButton invokeButton;
protected JButton resetButton;
private final PluginTool tool;
private SchemaContext ctx;
private Map<String, RemoteParameter> parameters;
private Map<String, Object> defaults;
// TODO: Not sure this is the best keying, but I think it works.
private Map<NameTypePair, Object> memorized = new HashMap<>();
private Map<String, Object> arguments;
public RemoteMethodInvocationDialog(PluginTool tool, String title, String buttonText,
Icon buttonIcon) {
super(title, true, true, true, false);
this.tool = tool;
populateComponents(buttonText, buttonIcon);
setRememberSize(false);
}
protected Object computeMemorizedValue(RemoteParameter parameter) {
return memorized.computeIfAbsent(NameTypePair.fromParameter(ctx, parameter),
ntp -> parameter.getDefaultValue());
}
public Map<String, Object> promptArguments(SchemaContext ctx,
Map<String, RemoteParameter> parameterMap, Map<String, Object> defaults) {
setParameters(ctx, parameterMap);
setDefaults(defaults);
tool.showDialog(this);
return getArguments();
}
public void setParameters(SchemaContext ctx, Map<String, RemoteParameter> parameterMap) {
this.ctx = ctx;
this.parameters = parameterMap;
populateOptions();
}
public void setDefaults(Map<String, Object> defaults) {
this.defaults = defaults;
}
private void populateComponents(String buttonText, Icon buttonIcon) {
panel = new JPanel(new BorderLayout());
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
layout = new PairLayout(5, 5);
pairPanel = new JPanel(layout);
JPanel centering = new JPanel(new FlowLayout(FlowLayout.CENTER));
JScrollPane scrolling = new JScrollPane(centering, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
//scrolling.setPreferredSize(new Dimension(100, 130));
panel.add(scrolling, BorderLayout.CENTER);
centering.add(pairPanel);
descriptionLabel = new JLabel();
descriptionLabel.setMaximumSize(new Dimension(300, 100));
panel.add(descriptionLabel, BorderLayout.NORTH);
addWorkPanel(panel);
invokeButton = new JButton(buttonText, buttonIcon);
addButton(invokeButton);
resetButton = new JButton("Reset", DebuggerResources.ICON_REFRESH);
addButton(resetButton);
addCancelButton();
invokeButton.addActionListener(this::invoke);
resetButton.addActionListener(this::reset);
}
@Override
protected void cancelCallback() {
this.arguments = null;
close();
}
protected void invoke(ActionEvent evt) {
this.arguments = collectArguments();
close();
}
private void reset(ActionEvent evt) {
this.arguments = new HashMap<>();
for (RemoteParameter param : parameters.values()) {
if (defaults.containsKey(param.name())) {
arguments.put(param.name(), defaults.get(param.name()));
}
else {
arguments.put(param.name(), param.getDefaultValue());
}
}
populateValues();
}
protected PropertyEditor createEditor(RemoteParameter param) {
Class<?> type = ctx.getSchema(param.type()).getType();
PropertyEditor editor = PropertyEditorManager.findEditor(type);
if (editor != null) {
return editor;
}
Msg.warn(this, "No editor for " + type + "? Trying String instead");
return PropertyEditorManager.findEditor(String.class);
}
void populateOptions() {
pairPanel.removeAll();
paramEditors.clear();
for (RemoteParameter param : parameters.values()) {
JLabel label = new JLabel(param.display());
label.setToolTipText(param.description());
pairPanel.add(label);
PropertyEditor editor = createEditor(param);
Object val = computeMemorizedValue(param);
editor.setValue(val);
editor.addPropertyChangeListener(this);
pairPanel.add(MiscellaneousUtils.getEditorComponent(editor));
paramEditors.put(param, editor);
}
}
void populateValues() {
for (Map.Entry<String, Object> ent : arguments.entrySet()) {
RemoteParameter param = parameters.get(ent.getKey());
if (param == null) {
Msg.warn(this, "No parameter for argument: " + ent);
continue;
}
PropertyEditor editor = paramEditors.get(param);
editor.setValue(ent.getValue());
}
}
protected Map<String, Object> collectArguments() {
Map<String, Object> map = new LinkedHashMap<>();
for (RemoteParameter param : paramEditors.keySet()) {
Object val = memorized.get(NameTypePair.fromParameter(ctx, param));
if (val != null) {
map.put(param.name(), val);
}
}
return map;
}
public Map<String, Object> getArguments() {
return arguments;
}
public <T> void setMemorizedArgument(String name, Class<T> type, T value) {
if (value == null) {
return;
}
memorized.put(new NameTypePair(name, type), value);
}
public <T> T getMemorizedArgument(String name, Class<T> type) {
return type.cast(memorized.get(new NameTypePair(name, type)));
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
PropertyEditor editor = (PropertyEditor) evt.getSource();
RemoteParameter param = paramEditors.getKey(editor);
memorized.put(NameTypePair.fromParameter(ctx, param), editor.getValue());
}
public void writeConfigState(SaveState saveState) {
SaveState subState = new SaveState();
for (Map.Entry<NameTypePair, Object> ent : memorized.entrySet()) {
NameTypePair ntp = ent.getKey();
ConfigStateField.putState(subState, ntp.type().asSubclass(Object.class), ntp.name(),
ent.getValue());
}
saveState.putXmlElement(KEY_MEMORIZED_ARGUMENTS, subState.saveToXml());
}
public void readConfigState(SaveState saveState) {
Element element = saveState.getXmlElement(KEY_MEMORIZED_ARGUMENTS);
if (element == null) {
return;
}
SaveState subState = new SaveState(element);
for (String name : subState.getNames()) {
try {
NameTypePair ntp = NameTypePair.fromString(name);
memorized.put(ntp, ConfigStateField.getState(subState, ntp.type(), ntp.name()));
}
catch (Exception e) {
Msg.error(this, "Error restoring memorized parameter " + name, e);
}
}
}
public void setDescription(String htmlDescription) {
if (htmlDescription == null) {
descriptionLabel.setBorder(BorderFactory.createEmptyBorder());
descriptionLabel.setText("");
}
else {
descriptionLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
descriptionLabel.setText(htmlDescription);
}
}
}

View file

@ -30,10 +30,14 @@ import org.jdom.Element;
import org.jdom.JDOMException;
import db.Transaction;
import docking.widgets.OptionDialog;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
import ghidra.app.plugin.core.debug.service.rmi.trace.DefaultTraceRmiAcceptor;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
import ghidra.app.plugin.core.terminal.TerminalListener;
import ghidra.app.services.*;
import ghidra.app.services.DebuggerTraceManagerService.ActivationCause;
import ghidra.async.AsyncUtils;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.util.ShellUtils;
@ -50,8 +54,7 @@ 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.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
@ -77,6 +80,16 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
pty.close();
waiter.interrupt();
}
@Override
public boolean isTerminated() {
return terminal.isTerminated();
}
@Override
public String description() {
return session.description();
}
}
protected record NullPtyTerminalSession(Terminal terminal, Pty pty, String name)
@ -92,6 +105,16 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
terminal.terminated();
pty.close();
}
@Override
public boolean isTerminated() {
return terminal.isTerminated();
}
@Override
public String description() {
return name;
}
}
static class TerminateSessionTask extends Task {
@ -113,13 +136,15 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
}
}
protected final TraceRmiLauncherServicePlugin plugin;
protected final Program program;
protected final PluginTool tool;
protected final TerminalService terminalService;
public AbstractTraceRmiLaunchOffer(Program program, PluginTool tool) {
public AbstractTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program) {
this.plugin = Objects.requireNonNull(plugin);
this.program = Objects.requireNonNull(program);
this.tool = Objects.requireNonNull(tool);
this.tool = plugin.getTool();
this.terminalService = Objects.requireNonNull(tool.getService(TerminalService.class));
}
@ -151,9 +176,8 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
return null; // I guess we won't wait for a mapping, then
}
protected CompletableFuture<Void> listenForMapping(
DebuggerStaticMappingService mappingService, TraceRmiConnection connection,
Trace trace) {
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
@ -469,9 +493,20 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
Map<String, TerminalSession> sessions, Map<String, ?> args, SocketAddress address)
throws Exception;
static class NoStaticMappingException extends Exception {
public NoStaticMappingException(String message) {
super(message);
}
@Override
public String toString() {
return getMessage();
}
}
@Override
public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) {
TraceRmiService service = tool.getService(TraceRmiService.class);
InternalTraceRmiService service = tool.getService(InternalTraceRmiService.class);
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
DebuggerTraceManagerService traceManager =
@ -479,9 +514,9 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
final PromptMode mode = configurator.getPromptMode();
boolean prompt = mode == PromptMode.ALWAYS;
TraceRmiAcceptor acceptor = null;
DefaultTraceRmiAcceptor acceptor = null;
Map<String, TerminalSession> sessions = new LinkedHashMap<>();
TraceRmiConnection connection = null;
TraceRmiHandler connection = null;
Trace trace = null;
Throwable lastExc = null;
@ -509,10 +544,12 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
monitor.setMessage("Waiting for connection");
acceptor.setTimeout(getTimeoutMillis());
connection = acceptor.accept();
connection.registerTerminals(sessions.values());
monitor.setMessage("Waiting for trace");
trace = connection.waitForTrace(getTimeoutMillis());
traceManager.openTrace(trace);
traceManager.activateTrace(trace);
traceManager.activate(traceManager.resolveTrace(trace),
ActivationCause.START_RECORDING);
monitor.setMessage("Waiting for module mapping");
try {
listenForMapping(mappingService, connection, trace).get(getTimeoutMillis(),
@ -529,25 +566,132 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
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.");
throw new NoStaticMappingException(
"The resulting target process has no mapping to the static image.");
}
}
}
catch (Exception e) {
lastExc = e;
prompt = mode != PromptMode.NEVER;
LaunchResult result =
new LaunchResult(program, sessions, connection, trace, lastExc);
if (prompt) {
switch (promptError(result)) {
case KEEP:
return result;
case RETRY:
try {
result.close();
}
catch (Exception e1) {
Msg.error(this, "Could not close", e1);
}
continue;
case TERMINATE:
try {
result.close();
}
catch (Exception e1) {
Msg.error(this, "Could not close", e1);
}
return new LaunchResult(program, Map.of(), null, null, lastExc);
}
continue;
}
return new LaunchResult(program, sessions, connection, trace, lastExc);
return result;
}
return new LaunchResult(program, sessions, connection, trace, null);
}
}
enum ErrPromptResponse {
KEEP, RETRY, TERMINATE;
}
protected ErrPromptResponse promptError(LaunchResult result) {
String message = """
<html><body width="400px">
<h3>Failed to launch %s due to an exception:</h3>
<tt>%s</tt>
<h3>Troubleshooting</h3>
<p>
<b>Check the Terminal!</b>
If no terminal is visible, check the menus: <b>Window &rarr; Terminals &rarr;
...</b>.
A path or other configuration parameter may be incorrect.
The back-end debugger may have paused for user input.
There may be a missing dependency.
There may be an incorrect version, etc.</p>
<h3>These resources remain after the failed launch:</h3>
<ul>
%s
</ul>
<h3>Do you want to keep these resources?</h3>
<ul>
<li>Choose <b>Yes</b> to stop here and diagnose or complete the launch manually.
</li>
<li>Choose <b>No</b> to clean up and retry at the launch dialog.</li>
<li>Choose <b>Cancel</b> to clean up without retrying.</li>
""".formatted(
htmlProgramName(result), htmlExceptionMessage(result), htmlResources(result));
return LaunchFailureDialog.show(message);
}
static class LaunchFailureDialog extends OptionDialog {
public LaunchFailureDialog(String message) {
super("Launch Failed", message, "&Yes", "&No", OptionDialog.ERROR_MESSAGE, null,
true, "No");
}
static ErrPromptResponse show(String message) {
return switch (new LaunchFailureDialog(message).show()) {
case OptionDialog.YES_OPTION -> ErrPromptResponse.KEEP;
case OptionDialog.NO_OPTION -> ErrPromptResponse.RETRY;
case OptionDialog.CANCEL_OPTION -> ErrPromptResponse.TERMINATE;
default -> throw new AssertionError();
};
}
}
protected String htmlProgramName(LaunchResult result) {
if (result.program() == null) {
return "";
}
return "<tt>" + HTMLUtilities.escapeHTML(result.program().getName()) + "</tt>";
}
protected String htmlExceptionMessage(LaunchResult result) {
if (result.exception() == null) {
return "(No exception)";
}
return HTMLUtilities.escapeHTML(result.exception().toString());
}
protected String htmlResources(LaunchResult result) {
StringBuilder sb = new StringBuilder();
for (Entry<String, TerminalSession> ent : result.sessions().entrySet()) {
TerminalSession session = ent.getValue();
sb.append("<li>Terminal: " + HTMLUtilities.escapeHTML(ent.getKey()) + " &rarr; <tt>" +
HTMLUtilities.escapeHTML(session.description()) + "</tt>");
if (session.isTerminated()) {
sb.append(" (Terminated)");
}
sb.append("</li>\n");
}
if (result.connection() != null) {
sb.append("<li>Connection: <tt>" +
HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString()) +
"</tt></li>\n");
}
if (result.trace() != null) {
sb.append(
"<li>Trace: " + HTMLUtilities.escapeHTML(result.trace().getName()) + "</li>\n");
}
return sb.toString();
}
}

View file

@ -31,7 +31,7 @@ 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.debug.spi.tracermi.TraceRmiLaunchOpinion;
import ghidra.framework.options.OptionsChangeListener;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.*;
@ -67,6 +67,13 @@ public class TraceRmiLauncherServicePlugin extends Plugin
implements TraceRmiLauncherService, OptionsChangeListener {
protected static final String OPTION_NAME_SCRIPT_PATHS = "Script Paths";
private final static LaunchConfigurator RELAUNCH = new LaunchConfigurator() {
@Override
public PromptMode getPromptMode() {
return PromptMode.ON_ERROR;
}
};
private final static LaunchConfigurator PROMPT = new LaunchConfigurator() {
@Override
public PromptMode getPromptMode() {
@ -90,7 +97,7 @@ public class TraceRmiLauncherServicePlugin extends Plugin
@Override
public void run(TaskMonitor monitor) throws CancelledException {
offer.launchProgram(monitor);
offer.launchProgram(monitor, RELAUNCH);
}
}
@ -165,11 +172,6 @@ public class TraceRmiLauncherServicePlugin extends Plugin
}
}
@Override
public Collection<TraceRmiLaunchOpinion> getOpinions() {
return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class);
}
@Override
public Collection<TraceRmiLaunchOffer> getOffers(Program program) {
if (program == null) {
@ -177,7 +179,7 @@ public class TraceRmiLauncherServicePlugin extends Plugin
}
return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class)
.stream()
.flatMap(op -> op.getOffers(program, getTool()).stream())
.flatMap(op -> op.getOffers(this, program).stream())
.toList();
}

View file

@ -27,8 +27,8 @@ import generic.theme.GIcon;
import generic.theme.Gui;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.util.ShellUtils;
import ghidra.debug.api.tracermi.TerminalSession;
import ghidra.framework.Application;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
@ -471,8 +471,8 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
* the target image is mapped in the resulting target trace.
* @throws FileNotFoundException
*/
public static UnixShellScriptTraceRmiLaunchOffer create(Program program, PluginTool tool,
File script) throws FileNotFoundException {
public static UnixShellScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin,
Program program, File script) throws FileNotFoundException {
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(new FileInputStream(script)))) {
AttributesParser attrs = new AttributesParser();
@ -491,7 +491,7 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
}
}
attrs.validate(script.getName());
return new UnixShellScriptTraceRmiLaunchOffer(program, tool, script,
return new UnixShellScriptTraceRmiLaunchOffer(plugin, program, 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);
@ -517,11 +517,11 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
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,
public UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program,
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);
super(plugin, program);
this.script = script;
this.configName = configName;
this.title = title;

View file

@ -22,7 +22,7 @@ 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.debug.spi.tracermi.TraceRmiLaunchOpinion;
import ghidra.framework.Application;
import ghidra.framework.options.OptionType;
import ghidra.framework.options.Options;
@ -63,12 +63,13 @@ public class UnixShellScriptTraceRmiLaunchOpinion implements TraceRmiLaunchOpini
}
@Override
public Collection<TraceRmiLaunchOffer> getOffers(Program program, PluginTool tool) {
return getScriptPaths(tool)
public Collection<TraceRmiLaunchOffer> getOffers(TraceRmiLauncherServicePlugin plugin,
Program program) {
return getScriptPaths(plugin.getTool())
.flatMap(rf -> Stream.of(rf.listFiles(crf -> crf.getName().endsWith(".sh"))))
.flatMap(sf -> {
try {
return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(program, tool,
return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(plugin, program,
sf.getFile(false)));
}
catch (Exception e) {

View file

@ -36,11 +36,14 @@ import com.google.protobuf.ByteString;
import db.Transaction;
import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.services.DebuggerControlService;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.app.services.DebuggerTraceManagerService.ActivationCause;
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.control.ControlMode;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.debug.api.tracermi.*;
@ -48,7 +51,6 @@ import ghidra.framework.model.*;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.AutoService.Wiring;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.lifecycle.Internal;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.*;
import ghidra.program.util.DefaultLanguageService;
@ -158,6 +160,16 @@ public class TraceRmiHandler implements TraceRmiConnection {
return removed;
}
public synchronized OpenTrace removeByTrace(Trace trace) {
OpenTrace removed = byTrace.remove(trace);
if (removed == null) {
return null;
}
byId.remove(removed.doId);
plugin.withdrawTarget(removed.target);
return removed;
}
public synchronized OpenTrace getById(DoId doId) {
return byId.get(doId);
}
@ -185,6 +197,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
private final OutputStream out;
private final CompletableFuture<Void> negotiate = new CompletableFuture<>();
private final CompletableFuture<Void> closed = new CompletableFuture<>();
private final Set<TerminalSession> terminals = new LinkedHashSet<>();
private final OpenTraceMap openTraces = new OpenTraceMap();
private final Map<Tid, OpenTx> openTxes = new HashMap<>();
@ -195,6 +208,8 @@ public class TraceRmiHandler implements TraceRmiConnection {
@AutoServiceConsumed
private DebuggerTraceManagerService traceManager;
@AutoServiceConsumed
private DebuggerControlService controlService;
@SuppressWarnings("unused")
private final Wiring autoServiceWiring;
@ -230,9 +245,30 @@ public class TraceRmiHandler implements TraceRmiConnection {
}
}
protected void terminateTerminals() {
List<TerminalSession> terminals;
synchronized (this.terminals) {
terminals = List.copyOf(this.terminals);
this.terminals.clear();
}
for (TerminalSession term : terminals) {
CompletableFuture.runAsync(() -> {
try {
term.terminate();
}
catch (Exception e) {
Msg.error(this, "Could not terminate " + term + ": " + e);
}
});
}
}
public void dispose() throws IOException {
plugin.removeHandler(this);
flushXReqQueue(new TraceRmiError("Socket closed"));
terminateTerminals();
socket.close();
while (!openTxes.isEmpty()) {
Tid nextKey = openTxes.keySet().iterator().next();
@ -467,6 +503,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
case REQUEST_START_TX -> "startTx(%d,%s)".formatted(
req.getRequestStartTx().getTxid().getId(),
req.getRequestStartTx().getDescription());
case REQUEST_SET_VALUE -> "setValue(%d,%s,%s)".formatted(
req.getRequestSetValue().getValue().getParent().getId(),
req.getRequestSetValue().getValue().getParent().getPath().getPath(),
req.getRequestSetValue().getValue().getKey());
default -> null;
};
}
@ -751,25 +791,26 @@ public class TraceRmiHandler implements TraceRmiConnection {
OpenTrace open = requireOpenTrace(req.getOid());
TraceObject object = open.getObject(req.getObject(), true);
DebuggerCoordinates coords = traceManager.getCurrent();
if (coords.getTrace() == object.getTrace()) {
coords = coords.object(object);
if (coords.getTrace() != open.trace) {
coords = DebuggerCoordinates.NOWHERE;
}
else {
coords = DebuggerCoordinates.NOWHERE.object(object);
}
if (open.lastSnapshot != null) {
ControlMode mode = controlService.getCurrentMode(open.trace);
if (open.lastSnapshot != null && mode.followsPresent()) {
coords = coords.snap(open.lastSnapshot.getKey());
}
if (!traceManager.getOpenTraces().contains(open.trace)) {
traceManager.openTrace(open.trace);
traceManager.activate(coords);
}
else {
Trace currentTrace = traceManager.getCurrentTrace();
if (currentTrace == null || openTraces.getByTrace(currentTrace) != null) {
traceManager.activate(coords);
DebuggerCoordinates finalCoords = coords.object(object);
Swing.runLater(() -> {
if (!traceManager.getOpenTraces().contains(open.trace)) {
traceManager.openTrace(open.trace);
traceManager.activate(finalCoords, ActivationCause.SYNC_MODEL);
}
}
else {
Trace currentTrace = traceManager.getCurrentTrace();
if (currentTrace == null || openTraces.getByTrace(currentTrace) != null) {
traceManager.activate(finalCoords, ActivationCause.SYNC_MODEL);
}
}
});
return ReplyActivate.getDefaultInstance();
}
@ -965,7 +1006,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
}
for (Method m : req.getMethodsList()) {
RemoteMethod rm = new RecordRemoteMethod(this, m.getName(),
new ActionName(m.getAction()),
ActionName.name(m.getAction()),
m.getDescription(), m.getParametersList()
.stream()
.collect(Collectors.toMap(MethodParameter::getName, this::makeParameter)),
@ -996,7 +1037,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
for (RegVal rv : req.getValuesList()) {
Register register = open.getRegister(rv.getName(), false);
if (register == null) {
Msg.warn(this, "Ignoring unrecognized register: " + rv.getName());
Msg.trace(this, "Ignoring unrecognized register: " + rv.getName());
rep.addSkippedNames(rv.getName());
continue;
}
@ -1182,9 +1223,12 @@ public class TraceRmiHandler implements TraceRmiConnection {
}
@Override
@Internal
public long getLastSnapshot(Trace trace) {
TraceSnapshot lastSnapshot = openTraces.getByTrace(trace).lastSnapshot;
OpenTrace byTrace = openTraces.getByTrace(trace);
if (byTrace == null) {
throw new NoSuchElementException();
}
TraceSnapshot lastSnapshot = byTrace.lastSnapshot;
if (lastSnapshot == null) {
return 0;
}
@ -1200,4 +1244,21 @@ public class TraceRmiHandler implements TraceRmiConnection {
throw new TraceRmiError(e);
}
}
@Override
public void forceCloseTrace(Trace trace) {
OpenTrace open = openTraces.removeByTrace(trace);
open.trace.release(this);
}
@Override
public boolean isTarget(Trace trace) {
return openTraces.getByTrace(trace) != null;
}
public void registerTerminals(Collection<TerminalSession> terminals) {
synchronized (this.terminals) {
this.terminals.addAll(terminals);
}
}
}

View file

@ -23,8 +23,7 @@ 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.DebuggerTargetService;
import ghidra.app.services.TraceRmiService;
import ghidra.app.services.*;
import ghidra.debug.api.tracermi.TraceRmiConnection;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.AutoService.Wiring;
@ -52,8 +51,9 @@ import ghidra.util.task.TaskMonitor;
},
servicesProvided = {
TraceRmiService.class,
InternalTraceRmiService.class,
})
public class TraceRmiPlugin extends Plugin implements TraceRmiService {
public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
private static final int DEFAULT_PORT = 15432;
@AutoServiceConsumed

View file

@ -17,10 +17,8 @@ package ghidra.app.plugin.core.debug.service.rmi.trace;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
import java.util.function.*;
import java.util.stream.Collectors;
import org.apache.commons.lang3.exception.ExceptionUtils;
@ -28,7 +26,9 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import docking.ActionContext;
import ghidra.app.context.ProgramLocationActionContext;
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
import ghidra.app.plugin.core.debug.gui.tracermi.RemoteMethodInvocationDialog;
import ghidra.app.plugin.core.debug.service.target.AbstractTarget;
import ghidra.app.services.DebuggerConsoleService;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.async.*;
import ghidra.dbg.target.*;
@ -49,7 +49,9 @@ import ghidra.program.model.lang.RegisterValue;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.breakpoint.*;
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
import ghidra.trace.model.guest.TracePlatform;
import ghidra.trace.model.memory.TraceObjectMemoryRegion;
import ghidra.trace.model.stack.*;
import ghidra.trace.model.target.*;
import ghidra.trace.model.thread.TraceObjectThread;
@ -63,10 +65,12 @@ public class TraceRmiTarget extends AbstractTarget {
private static final String BREAK_READ = "breakRead";
private static final String BREAK_WRITE = "breakWrite";
private static final String BREAK_ACCESS = "breakAccess";
private final TraceRmiConnection connection;
private final Trace trace;
private final Matches matches = new Matches();
private final RequestCaches requestCaches = new RequestCaches();
private final Set<TraceBreakpointKind> supportedBreakpointKinds;
public TraceRmiTarget(PluginTool tool, TraceRmiConnection connection, Trace trace) {
@ -78,7 +82,7 @@ public class TraceRmiTarget extends AbstractTarget {
@Override
public boolean isValid() {
return !connection.isClosed();
return !connection.isClosed() && connection.isTarget(trace);
}
@Override
@ -88,7 +92,12 @@ public class TraceRmiTarget extends AbstractTarget {
@Override
public long getSnap() {
return connection.getLastSnapshot(trace);
try {
return connection.getLastSnapshot(trace);
}
catch (NoSuchElementException e) {
return 0;
}
}
@Override
@ -122,52 +131,72 @@ public class TraceRmiTarget extends AbstractTarget {
.orElse(null);
}
protected TraceObject findObject(ActionContext context) {
if (context instanceof DebuggerObjectActionContext ctx) {
List<TraceObjectValue> values = ctx.getObjectValues();
if (values.size() == 1) {
TraceObjectValue ov = values.get(0);
if (ov.isObject()) {
return ov.getChild();
protected TraceObject findObject(ActionContext context, boolean allowContextObject,
boolean allowCoordsObject) {
if (allowContextObject) {
if (context instanceof DebuggerObjectActionContext ctx) {
List<TraceObjectValue> values = ctx.getObjectValues();
if (values.size() == 1) {
TraceObjectValue ov = values.get(0);
if (ov.isObject()) {
return ov.getChild();
}
}
}
}
DebuggerTraceManagerService traceManager =
tool.getService(DebuggerTraceManagerService.class);
if (traceManager != null) {
return traceManager.getCurrentObject();
if (allowCoordsObject) {
DebuggerTraceManagerService traceManager =
tool.getService(DebuggerTraceManagerService.class);
if (traceManager == null) {
return null;
}
return traceManager.getCurrentFor(trace).getObject();
}
return null;
}
protected Object findArgumentForSchema(ActionContext context, TargetObjectSchema schema) {
protected Object findArgumentForSchema(ActionContext context, TargetObjectSchema schema,
boolean allowContextObject, boolean allowCoordsObject, boolean allowSuitableObject) {
if (schema instanceof EnumerableTargetObjectSchema prim) {
return switch (prim) {
case OBJECT -> findObject(context);
case OBJECT -> findObject(context, allowContextObject, allowCoordsObject);
case ADDRESS -> findAddress(context);
case RANGE -> findRange(context);
default -> null;
};
}
TraceObject object = findObject(context);
TraceObject object = findObject(context, allowContextObject, allowCoordsObject);
if (object == null) {
return null;
}
return object.querySuitableSchema(schema);
if (allowSuitableObject) {
return object.querySuitableSchema(schema);
}
if (object.getTargetSchema() == schema) {
return object;
}
return null;
}
private enum Missing {
MISSING; // The argument requires a prompt
}
protected Object findArgument(RemoteParameter parameter, ActionContext context) {
protected Object findArgument(RemoteParameter parameter, ActionContext context,
boolean allowContextObject, boolean allowCoordsObject, boolean allowSuitableObject) {
SchemaName type = parameter.type();
TargetObjectSchema schema = getSchemaContext().getSchema(type);
SchemaContext ctx = getSchemaContext();
if (ctx == null) {
Msg.trace(this, "No root schema, yet: " + trace);
return null;
}
TargetObjectSchema schema = ctx.getSchema(type);
if (schema == null) {
Msg.error(this, "Schema " + type + " not in trace! " + trace);
return null;
}
Object arg = findArgumentForSchema(context, schema);
Object arg = findArgumentForSchema(context, schema, allowContextObject, allowCoordsObject,
allowSuitableObject);
if (arg != null) {
return arg;
}
@ -177,27 +206,46 @@ public class TraceRmiTarget extends AbstractTarget {
return Missing.MISSING;
}
protected Map<String, Object> collectArguments(RemoteMethod method, ActionContext context) {
return method.parameters()
.entrySet()
.stream()
.collect(
Collectors.toMap(Entry::getKey, e -> findArgument(e.getValue(), context)));
protected Map<String, Object> collectArguments(RemoteMethod method, ActionContext context,
boolean allowContextObject, boolean allowCoordsObject, boolean allowSuitableObject) {
Map<String, Object> args = new HashMap<>();
for (RemoteParameter param : method.parameters().values()) {
Object found = findArgument(param, context, allowContextObject, allowCoordsObject,
allowSuitableObject);
if (found != null) {
args.put(param.name(), found);
}
}
return args;
}
private TargetExecutionState getStateOf(TraceObject object) {
return object.getExecutionState(getSnap());
try {
return object.getExecutionState(getSnap());
}
catch (NoSuchElementException e) {
return TargetExecutionState.TERMINATED;
}
}
private boolean stateOrNull(TraceObject object,
private boolean whenState(TraceObject object,
Predicate<TargetExecutionState> predicate) {
TargetExecutionState state = getStateOf(object);
return state == null || predicate.test(state);
try {
TargetExecutionState state = getStateOf(object);
return state == null || predicate.test(state);
}
catch (Exception e) {
Msg.error(this, "Could not get state: " + e);
return false;
}
}
protected BooleanSupplier chooseEnabler(RemoteMethod method, Map<String, Object> args) {
ActionName name = method.action();
SchemaContext ctx = getSchemaContext();
if (ctx == null) {
return () -> true;
}
RemoteParameter firstParam = method.parameters()
.values()
.stream()
@ -207,7 +255,12 @@ public class TraceRmiTarget extends AbstractTarget {
if (firstParam == null) {
return () -> true;
}
TraceObject firstArg = (TraceObject) args.get(firstParam.name());
Object firstArg = args.get(firstParam.name());
if (firstArg == null || firstArg == Missing.MISSING) {
Msg.trace(this, "MISSING first argument for " + method + "(" + firstParam + ")");
return () -> false;
}
TraceObject obj = (TraceObject) firstArg;
if (ActionName.RESUME.equals(name) ||
ActionName.STEP_BACK.equals(name) ||
ActionName.STEP_EXT.equals(name) ||
@ -215,29 +268,74 @@ public class TraceRmiTarget extends AbstractTarget {
ActionName.STEP_OUT.equals(name) ||
ActionName.STEP_OVER.equals(name) ||
ActionName.STEP_SKIP.equals(name)) {
return () -> stateOrNull(firstArg, TargetExecutionState::isStopped);
return () -> whenState(obj, state -> state != null && state.isStopped());
}
else if (ActionName.INTERRUPT.equals(name)) {
return () -> stateOrNull(firstArg, TargetExecutionState::isRunning);
return () -> whenState(obj, state -> state == null || state.isRunning());
}
else if (ActionName.KILL.equals(name)) {
return () -> stateOrNull(firstArg, TargetExecutionState::isAlive);
return () -> whenState(obj, state -> state == null || state.isAlive());
}
return () -> true;
}
protected ActionEntry createEntry(RemoteMethod method, ActionContext context) {
Map<String, Object> args = collectArguments(method, context);
private Map<String, Object> promptArgs(RemoteMethod method, Map<String, Object> defaults) {
SchemaContext ctx = getSchemaContext();
RemoteMethodInvocationDialog dialog = new RemoteMethodInvocationDialog(tool,
method.name(), method.name(), null);
while (true) {
for (RemoteParameter param : method.parameters().values()) {
Object val = defaults.get(param.name());
if (val != null) {
Class<?> type = ctx.getSchema(param.type()).getType();
dialog.setMemorizedArgument(param.name(), type.asSubclass(Object.class),
val);
}
}
Map<String, Object> args = dialog.promptArguments(ctx, method.parameters(), defaults);
if (args == null) {
// Cancelled
return null;
}
return args;
}
}
private CompletableFuture<?> invokeMethod(boolean prompt, RemoteMethod method,
Map<String, Object> arguments) {
Map<String, Object> chosenArgs;
if (prompt) {
chosenArgs = promptArgs(method, arguments);
}
else {
chosenArgs = arguments;
}
return method.invokeAsync(chosenArgs).thenAccept(result -> {
DebuggerConsoleService consoleService =
tool.getService(DebuggerConsoleService.class);
Class<?> retType = getSchemaContext().getSchema(method.retType()).getType();
if (consoleService != null && retType != Void.class && retType != Object.class) {
consoleService.log(null, method.name() + " returned " + result);
}
}).toCompletableFuture();
}
protected ActionEntry createEntry(RemoteMethod method, ActionContext context,
boolean allowContextObject, boolean allowCoordsObject, boolean allowSuitableObject) {
Map<String, Object> args = collectArguments(method, context, allowContextObject,
allowCoordsObject, allowSuitableObject);
boolean requiresPrompt = args.values().contains(Missing.MISSING);
return new ActionEntry(method.name(), method.action(), method.description(), requiresPrompt,
chooseEnabler(method, args), () -> method.invokeAsync(args).toCompletableFuture());
chooseEnabler(method, args), prompt -> invokeMethod(prompt, method, args));
}
protected Map<String, ActionEntry> collectFromMethods(Collection<RemoteMethod> methods,
ActionContext context) {
ActionContext context, boolean allowContextObject, boolean allowCoordsObject,
boolean allowSuitableObject) {
Map<String, ActionEntry> result = new HashMap<>();
for (RemoteMethod m : methods) {
result.put(m.name(), createEntry(m, context));
result.put(m.name(), createEntry(m, context, allowContextObject, allowCoordsObject,
allowSuitableObject));
}
return result;
}
@ -246,7 +344,15 @@ public class TraceRmiTarget extends AbstractTarget {
return method.parameters()
.values()
.stream()
.filter(p -> ctx.getSchema(p.type()).getType() == Address.class)
.filter(p -> {
TargetObjectSchema schema = ctx.getSchemaOrNull(p.type());
if (schema == null) {
Msg.error(this,
"Method " + method + " refers to invalid schema name: " + p.type());
return false;
}
return schema.getType() == Address.class;
})
.count() == 1;
}
@ -258,18 +364,20 @@ public class TraceRmiTarget extends AbstractTarget {
if (!isAddressMethod(m, ctx)) {
continue;
}
result.put(m.name(), createEntry(m, context));
result.put(m.name(), createEntry(m, context, true, true, true));
}
return result;
}
@Override
protected Map<String, ActionEntry> collectAllActions(ActionContext context) {
return collectFromMethods(connection.getMethods().all().values(), context);
return collectFromMethods(connection.getMethods().all().values(), context, true, false,
false);
}
protected Map<String, ActionEntry> collectByName(ActionName name, ActionContext context) {
return collectFromMethods(connection.getMethods().getByAction(name), context);
return collectFromMethods(connection.getMethods().getByAction(name), context, false, true,
true);
}
@Override
@ -311,7 +419,7 @@ public class TraceRmiTarget extends AbstractTarget {
public boolean isSupportsFocus() {
TargetObjectSchema schema = trace.getObjectManager().getRootSchema();
if (schema == null) {
Msg.warn(this, "Checked for focus support before root schema is available");
Msg.trace(this, "Checked for focus support before root schema is available");
return false;
}
return schema
@ -370,10 +478,17 @@ public class TraceRmiTarget extends AbstractTarget {
}
}
protected static boolean typeMatches(RemoteParameter param, SchemaContext ctx, Class<?> type) {
TargetObjectSchema sch = ctx.getSchema(param.type());
protected static boolean typeMatches(RemoteMethod method, RemoteParameter param,
SchemaContext ctx, Class<?> type) {
TargetObjectSchema sch = ctx.getSchemaOrNull(param.type());
if (sch == null) {
throw new RuntimeException(
"The parameter '%s' of method '%s' refers to a non-existent schema '%s'"
.formatted(param.name(), method.name(), param.type()));
}
if (type == TargetObject.class) {
return sch.getType() == type;
// The method cannot impose any further restriction. It must accept any object.
return sch == EnumerableTargetObjectSchema.OBJECT;
}
else if (TargetObject.class.isAssignableFrom(type)) {
return sch.getInterfaces().contains(type);
@ -389,13 +504,28 @@ public class TraceRmiTarget extends AbstractTarget {
RemoteParameter find(RemoteMethod method, SchemaContext ctx);
}
record SchemaParamSpec(String name, SchemaName schema) implements ParamSpec {
@Override
public RemoteParameter find(RemoteMethod method, SchemaContext ctx) {
List<RemoteParameter> withType = method.parameters()
.values()
.stream()
.filter(p -> schema.equals(p.type()))
.toList();
if (withType.size() != 1) {
return null;
}
return withType.get(0);
}
}
record TypeParamSpec(String name, Class<?> type) implements ParamSpec {
@Override
public RemoteParameter find(RemoteMethod method, SchemaContext ctx) {
List<RemoteParameter> withType = method.parameters()
.values()
.stream()
.filter(p -> typeMatches(p, ctx, type))
.filter(p -> typeMatches(method, p, ctx, type))
.toList();
if (withType.size() != 1) {
return null;
@ -408,16 +538,15 @@ public class TraceRmiTarget extends AbstractTarget {
@Override
public RemoteParameter find(RemoteMethod method, SchemaContext ctx) {
RemoteParameter param = method.parameters().get(name);
if (typeMatches(param, ctx, type)) {
if (param != null && typeMatches(method, param, ctx, type)) {
return param;
}
return null;
}
}
@SafeVarargs
protected static <T extends MethodMatcher> List<T> matchers(T... list) {
List<T> result = List.of(list);
protected static <T extends MethodMatcher> List<T> matchers(List<T> list) {
List<T> result = new ArrayList<>(list);
result.sort(Comparator.comparing(MethodMatcher::score).reversed());
if (result.isEmpty()) {
throw new AssertionError("empty matchers list?");
@ -429,33 +558,60 @@ public class TraceRmiTarget extends AbstractTarget {
throw new AssertionError("duplicate scores: " + curScore);
}
}
return result;
return List.copyOf(result);
}
@SafeVarargs
protected static <T extends MethodMatcher> List<T> matchers(T... list) {
return matchers(Arrays.asList(list));
}
record ActivateMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
static final ActivateMatcher HAS_FOCUS_TIME = new ActivateMatcher(3, List.of(
new TypeParamSpec("focus", TargetObject.class),
new TypeParamSpec("time", String.class)));
static final ActivateMatcher HAS_FOCUS_SNAP = new ActivateMatcher(2, List.of(
new TypeParamSpec("focus", TargetObject.class),
new TypeParamSpec("snap", Long.class)));
static final ActivateMatcher HAS_FOCUS = new ActivateMatcher(1, List.of(
new TypeParamSpec("focus", TargetObject.class)));
static final List<ActivateMatcher> ALL =
matchers(HAS_FOCUS_TIME, HAS_FOCUS_SNAP, HAS_FOCUS);
static List<ActivateMatcher> makeAllFor(int addScore, ParamSpec focusSpec) {
ActivateMatcher hasFocusTime = new ActivateMatcher(addScore + 3, List.of(
focusSpec,
new TypeParamSpec("time", String.class)));
ActivateMatcher hasFocusSnap = new ActivateMatcher(addScore + 2, List.of(
focusSpec,
new TypeParamSpec("snap", Long.class)));
ActivateMatcher hasFocus = new ActivateMatcher(addScore + 1, List.of(
focusSpec));
return matchers(hasFocusTime, hasFocusSnap, hasFocus);
}
static List<ActivateMatcher> makeBySpecificity(TargetObjectSchema rootSchema,
TraceObjectKeyPath path) {
List<ActivateMatcher> result = new ArrayList<>();
List<String> keyList = path.getKeyList();
result.addAll(makeAllFor((keyList.size() + 1) * 3,
new TypeParamSpec("focus", TargetObject.class)));
List<TargetObjectSchema> schemas = rootSchema.getSuccessorSchemas(keyList);
for (int i = keyList.size(); i > 0; i--) { // Inclusive on both ends
result.addAll(
makeAllFor(i * 3, new SchemaParamSpec("focus", schemas.get(i).getName())));
}
return matchers(result);
}
}
record ReadMemMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
static final ReadMemMatcher HAS_PROC_RANGE = new ReadMemMatcher(2, List.of(
new TypeParamSpec("process", TargetProcess.class),
new TypeParamSpec("range", AddressRange.class)));
static final ReadMemMatcher HAS_RANGE = new ReadMemMatcher(1, List.of(
new TypeParamSpec("range", AddressRange.class)));
static final List<ReadMemMatcher> ALL = matchers(HAS_RANGE);
static final List<ReadMemMatcher> ALL = matchers(HAS_PROC_RANGE, HAS_RANGE);
}
record WriteMemMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
static final WriteMemMatcher HAS_RANGE = new WriteMemMatcher(1, List.of(
static final WriteMemMatcher HAS_PROC_START_DATA = new WriteMemMatcher(2, List.of(
new TypeParamSpec("process", TargetProcess.class),
new TypeParamSpec("start", Address.class),
new TypeParamSpec("data", byte[].class)));
static final List<WriteMemMatcher> ALL = matchers(HAS_RANGE);
static final WriteMemMatcher HAS_START_DATA = new WriteMemMatcher(1, List.of(
new TypeParamSpec("start", Address.class),
new TypeParamSpec("data", byte[].class)));
static final List<WriteMemMatcher> ALL = matchers(HAS_PROC_START_DATA, HAS_START_DATA);
}
record ReadRegsMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
@ -469,10 +625,14 @@ public class TraceRmiTarget extends AbstractTarget {
}
record WriteRegMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
static final WriteRegMatcher HAS_FRAME_NAME_VALUE = new WriteRegMatcher(2, List.of(
static final WriteRegMatcher HAS_FRAME_NAME_VALUE = new WriteRegMatcher(3, List.of(
new TypeParamSpec("frame", TargetStackFrame.class),
new TypeParamSpec("name", String.class),
new TypeParamSpec("value", byte[].class)));
static final WriteRegMatcher HAS_THREAD_NAME_VALUE = new WriteRegMatcher(2, List.of(
new TypeParamSpec("thread", TargetThread.class),
new TypeParamSpec("name", String.class),
new TypeParamSpec("value", byte[].class)));
static final WriteRegMatcher HAS_REG_VALUE = new WriteRegMatcher(1, List.of(
new TypeParamSpec("register", TargetRegister.class),
new TypeParamSpec("value", byte[].class)));
@ -480,6 +640,22 @@ public class TraceRmiTarget extends AbstractTarget {
}
record BreakExecMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
static final BreakExecMatcher HAS_PROC_ADDR_COND_CMDS = new BreakExecMatcher(8, List.of(
new TypeParamSpec("process", TargetProcess.class),
new TypeParamSpec("address", Address.class),
new NameParamSpec("condition", String.class),
new NameParamSpec("commands", String.class)));
static final BreakExecMatcher HAS_PROC_ADDR_COND = new BreakExecMatcher(7, List.of(
new TypeParamSpec("process", TargetProcess.class),
new TypeParamSpec("address", Address.class),
new NameParamSpec("condition", String.class)));
static final BreakExecMatcher HAS_PROC_ADDR_CMDS = new BreakExecMatcher(6, List.of(
new TypeParamSpec("process", TargetProcess.class),
new TypeParamSpec("address", Address.class),
new NameParamSpec("commands", String.class)));
static final BreakExecMatcher HAS_PROC_ADDR = new BreakExecMatcher(5, List.of(
new TypeParamSpec("process", TargetProcess.class),
new TypeParamSpec("address", Address.class)));
static final BreakExecMatcher HAS_ADDR_COND_CMDS = new BreakExecMatcher(4, List.of(
new TypeParamSpec("address", Address.class),
new NameParamSpec("condition", String.class),
@ -493,10 +669,28 @@ public class TraceRmiTarget extends AbstractTarget {
static final BreakExecMatcher HAS_ADDR = new BreakExecMatcher(1, List.of(
new TypeParamSpec("address", Address.class)));
static final List<BreakExecMatcher> ALL =
matchers(HAS_ADDR_COND_CMDS, HAS_ADDR_COND, HAS_ADDR_CMDS, HAS_ADDR);
matchers(HAS_PROC_ADDR_COND_CMDS, HAS_PROC_ADDR_COND, HAS_PROC_ADDR_CMDS, HAS_PROC_ADDR,
HAS_ADDR_COND_CMDS, HAS_ADDR_COND, HAS_ADDR_CMDS, HAS_ADDR);
}
// TODO: Probably need a better way to deal with optional requirements
record BreakAccMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
static final BreakAccMatcher HAS_PROC_RNG_COND_CMDS = new BreakAccMatcher(8, List.of(
new TypeParamSpec("process", TargetProcess.class),
new TypeParamSpec("range", AddressRange.class),
new NameParamSpec("condition", String.class),
new NameParamSpec("commands", String.class)));
static final BreakAccMatcher HAS_PROC_RNG_COND = new BreakAccMatcher(7, List.of(
new TypeParamSpec("process", TargetProcess.class),
new TypeParamSpec("range", AddressRange.class),
new NameParamSpec("condition", String.class)));
static final BreakAccMatcher HAS_PROC_RNG_CMDS = new BreakAccMatcher(6, List.of(
new TypeParamSpec("process", TargetProcess.class),
new TypeParamSpec("range", AddressRange.class),
new NameParamSpec("commands", String.class)));
static final BreakAccMatcher HAS_PROC_RNG = new BreakAccMatcher(5, List.of(
new TypeParamSpec("process", TargetProcess.class),
new TypeParamSpec("range", AddressRange.class)));
static final BreakAccMatcher HAS_RNG_COND_CMDS = new BreakAccMatcher(4, List.of(
new TypeParamSpec("range", AddressRange.class),
new NameParamSpec("condition", String.class),
@ -510,7 +704,8 @@ public class TraceRmiTarget extends AbstractTarget {
static final BreakAccMatcher HAS_RNG = new BreakAccMatcher(1, List.of(
new TypeParamSpec("range", AddressRange.class)));
static final List<BreakAccMatcher> ALL =
matchers(HAS_RNG_COND_CMDS, HAS_RNG_COND, HAS_RNG_CMDS, HAS_RNG);
matchers(HAS_PROC_RNG_COND_CMDS, HAS_PROC_RNG_COND, HAS_PROC_RNG_CMDS, HAS_PROC_RNG,
HAS_RNG_COND_CMDS, HAS_RNG_COND, HAS_RNG_CMDS, HAS_RNG);
}
record DelBreakMatcher(int score, List<ParamSpec> spec) implements MethodMatcher {
@ -536,6 +731,11 @@ public class TraceRmiTarget extends AbstractTarget {
protected class Matches {
private final Map<String, MatchedMethod> map = new HashMap<>();
public MatchedMethod getBest(String name, ActionName action,
Supplier<List<? extends MethodMatcher>> preferredSupplier) {
return map.computeIfAbsent(name, n -> chooseBest(action, preferredSupplier.get()));
}
public MatchedMethod getBest(String name, ActionName action,
List<? extends MethodMatcher> preferred) {
return map.computeIfAbsent(name, n -> chooseBest(action, preferred));
@ -554,24 +754,57 @@ public class TraceRmiTarget extends AbstractTarget {
.max(MatchedMethod::compareTo)
.orElse(null);
if (best == null) {
Msg.error(this, "No suitable " + name + " method");
Msg.debug(this, "No suitable " + name + " method");
}
return best;
}
}
protected static class RequestCaches {
final Map<TraceObject, CompletableFuture<Void>> readRegs = new HashMap<>();
final Map<Address, CompletableFuture<Void>> readBlock = new HashMap<>();
public synchronized void invalidate() {
readRegs.clear();
readBlock.clear();
}
public synchronized CompletableFuture<Void> readRegs(TraceObject obj, RemoteMethod method,
Map<String, Object> args) {
return readRegs.computeIfAbsent(obj,
o -> method.invokeAsync(args).toCompletableFuture().thenApply(__ -> null));
}
public synchronized CompletableFuture<Void> readBlock(Address min, RemoteMethod method,
Map<String, Object> args) {
return readBlock.computeIfAbsent(min,
m -> method.invokeAsync(args).toCompletableFuture().thenApply(__ -> null));
}
}
@Override
public CompletableFuture<Void> activateAsync(DebuggerCoordinates prev,
DebuggerCoordinates coords) {
MatchedMethod activate =
matches.getBest("activate", ActionName.ACTIVATE, ActivateMatcher.ALL);
if (prev.getSnap() != coords.getSnap()) {
requestCaches.invalidate();
}
TraceObject object = coords.getObject();
if (object == null) {
return AsyncUtils.nil();
}
SchemaName name = object.getTargetSchema().getName();
MatchedMethod activate = matches.getBest("activate_" + name, ActionName.ACTIVATE,
() -> ActivateMatcher.makeBySpecificity(trace.getObjectManager().getRootSchema(),
object.getCanonicalPath()));
if (activate == null) {
return AsyncUtils.nil();
}
Map<String, Object> args = new HashMap<>();
RemoteParameter paramFocus = activate.params.get("focus");
args.put(paramFocus.name(), coords.getObject());
args.put(paramFocus.name(),
object.querySuitableSchema(getSchemaContext().getSchema(paramFocus.type())));
RemoteParameter paramTime = activate.params.get("time");
if (paramTime != null) {
args.put(paramTime.name(), coords.getTime().toString());
@ -605,7 +838,29 @@ public class TraceRmiTarget extends AbstractTarget {
}
protected SchemaContext getSchemaContext() {
return trace.getObjectManager().getRootSchema().getContext();
TargetObjectSchema rootSchema = trace.getObjectManager().getRootSchema();
if (rootSchema == null) {
return null;
}
return rootSchema.getContext();
}
protected TraceObject getProcessForSpace(AddressSpace space) {
for (TraceObjectValue objVal : trace.getObjectManager()
.getValuesIntersecting(Lifespan.at(getSnap()),
new AddressRangeImpl(space.getMinAddress(), space.getMaxAddress()))) {
if (!TargetMemoryRegion.RANGE_ATTRIBUTE_NAME.equals(objVal.getEntryKey())) {
continue;
}
TraceObject obj = objVal.getParent();
if (!obj.getInterfaces().contains(TraceObjectMemoryRegion.class)) {
continue;
}
return obj.queryCanonicalAncestorsTargetInterface(TargetProcess.class)
.findFirst()
.orElse(null);
}
return null;
}
@Override
@ -619,6 +874,15 @@ public class TraceRmiTarget extends AbstractTarget {
return AsyncUtils.nil();
}
RemoteParameter paramRange = readMem.params.get("range");
RemoteParameter paramProcess = readMem.params.get("process");
final Map<AddressSpace, TraceObject> procsBySpace;
if (paramProcess != null) {
procsBySpace = new HashMap<>();
}
else {
procsBySpace = null;
}
int total = 0;
AddressSetView quantized = quantize(set);
@ -630,10 +894,32 @@ public class TraceRmiTarget extends AbstractTarget {
// NOTE: Don't read in parallel, lest we overload the connection
return AsyncUtils.each(TypeSpec.VOID, quantized.iterator(), (r, loop) -> {
AddressRangeChunker blocks = new AddressRangeChunker(r, BLOCK_SIZE);
if (r.getAddressSpace().isRegisterSpace()) {
Msg.warn(this, "Request to read registers via readMemory: " + r + ". Ignoring.");
loop.repeatWhile(!monitor.isCancelled());
return;
}
AsyncUtils.each(TypeSpec.VOID, blocks.iterator(), (blk, inner) -> {
monitor.incrementProgress(1);
RemoteAsyncResult future =
readMem.method.invokeAsync(Map.of(paramRange.name(), blk));
final Map<String, Object> args;
if (paramProcess != null) {
TraceObject process = procsBySpace.computeIfAbsent(blk.getAddressSpace(),
this::getProcessForSpace);
if (process == null) {
Msg.warn(this, "Cannot find process containing " + blk.getMinAddress());
inner.repeatWhile(!monitor.isCancelled());
return;
}
args = Map.ofEntries(
Map.entry(paramProcess.name(), process),
Map.entry(paramRange.name(), blk));
}
else {
args = Map.ofEntries(
Map.entry(paramRange.name(), blk));
}
CompletableFuture<Void> future =
requestCaches.readBlock(blk.getMinAddress(), readMem.method, args);
future.exceptionally(e -> {
Msg.error(this, "Could not read " + blk + ": " + e);
return null; // Continue looping on errors
@ -652,6 +938,14 @@ public class TraceRmiTarget extends AbstractTarget {
Map<String, Object> args = new HashMap<>();
args.put(writeMem.params.get("start").name(), address);
args.put(writeMem.params.get("data").name(), data);
RemoteParameter paramProcess = writeMem.params.get("process");
if (paramProcess != null) {
TraceObject process = getProcessForSpace(address.getAddressSpace());
if (process == null) {
throw new IllegalStateException("Cannot find process containing " + address);
}
args.put(paramProcess.name(), process);
}
return writeMem.method.invokeAsync(args).toCompletableFuture().thenApply(__ -> null);
}
@ -668,11 +962,15 @@ public class TraceRmiTarget extends AbstractTarget {
return AsyncUtils.nil();
}
TraceObject container = tot.getObject().queryRegisterContainer(frame);
if (container == null) {
Msg.error(this,
"Cannot find register container for thread,frame: " + thread + "," + frame);
return AsyncUtils.nil();
}
RemoteParameter paramContainer = readRegs.params.get("container");
if (paramContainer != null) {
return readRegs.method.invokeAsync(Map.of(paramContainer.name(), container))
.toCompletableFuture()
.thenApply(__ -> null);
return requestCaches.readRegs(container, readRegs.method, Map.of(
paramContainer.name(), container));
}
Set<String> keys = new HashSet<>();
for (Register r : registers) {
@ -695,8 +993,8 @@ public class TraceRmiTarget extends AbstractTarget {
.collect(Collectors.toSet());
AsyncFence fence = new AsyncFence();
banks.stream().forEach(b -> {
fence.include(
readRegs.method.invokeAsync(Map.of(paramBank.name(), b)).toCompletableFuture());
fence.include(requestCaches.readRegs(b, readRegs.method, Map.of(
paramBank.name(), b)));
});
return fence.ready();
}
@ -704,8 +1002,8 @@ public class TraceRmiTarget extends AbstractTarget {
if (paramRegister != null) {
AsyncFence fence = new AsyncFence();
regs.stream().forEach(r -> {
fence.include(readRegs.method.invokeAsync(Map.of(paramRegister.name(), r))
.toCompletableFuture());
fence.include(requestCaches.readRegs(r, readRegs.method, Map.of(
paramRegister.name(), r)));
});
return fence.ready();
}
@ -733,7 +1031,7 @@ public class TraceRmiTarget extends AbstractTarget {
@Override
public CompletableFuture<Void> writeRegisterAsync(TracePlatform platform, TraceThread thread,
int frame, RegisterValue value) {
int frameLevel, RegisterValue value) {
MatchedMethod writeReg =
matches.getBest("writeReg", ActionName.WRITE_REG, WriteRegMatcher.ALL);
if (writeReg == null) {
@ -748,24 +1046,38 @@ public class TraceRmiTarget extends AbstractTarget {
byte[] data =
Utils.bigIntegerToBytes(value.getUnsignedValue(), register.getMinimumByteSize(), true);
RemoteParameter paramFrame = writeReg.params.get("frame");
if (paramFrame != null) {
TraceStack stack = trace.getStackManager().getLatestStack(thread, getSnap());
TraceStackFrame frameObj = stack.getFrame(frame, false);
RemoteParameter paramThread = writeReg.params.get("thread");
if (paramThread != null) {
return writeReg.method.invokeAsync(Map.ofEntries(
Map.entry(paramFrame.name(), frameObj),
Map.entry(paramThread.name(), tot.getObject()),
Map.entry(writeReg.params.get("name").name(), regName),
Map.entry(writeReg.params.get("data").name(), data)))
Map.entry(writeReg.params.get("value").name(), data)))
.toCompletableFuture()
.thenApply(__ -> null);
}
TraceObject regObj = findRegisterObject(tot, frame, regName);
RemoteParameter paramFrame = writeReg.params.get("frame");
if (paramFrame != null) {
TraceStack stack = trace.getStackManager().getLatestStack(thread, getSnap());
TraceStackFrame frame = stack.getFrame(frameLevel, false);
if (!(frame instanceof TraceObjectStackFrame tof)) {
Msg.error(this, "Non-object trace with TraceRmi!");
return AsyncUtils.nil();
}
return writeReg.method.invokeAsync(Map.ofEntries(
Map.entry(paramFrame.name(), tof.getObject()),
Map.entry(writeReg.params.get("name").name(), regName),
Map.entry(writeReg.params.get("value").name(), data)))
.toCompletableFuture()
.thenApply(__ -> null);
}
TraceObject regObj = findRegisterObject(tot, frameLevel, regName);
if (regObj == null) {
return AsyncUtils.nil();
}
return writeReg.method.invokeAsync(Map.ofEntries(
Map.entry(writeReg.params.get("frame").name(), regObj),
Map.entry(writeReg.params.get("data").name(), data)))
Map.entry(writeReg.params.get("value").name(), data)))
.toCompletableFuture()
.thenApply(__ -> null);
}
@ -830,6 +1142,18 @@ public class TraceRmiTarget extends AbstractTarget {
protected void putOptionalBreakArgs(Map<String, Object> args, MatchedMethod brk,
String condition, String commands) {
RemoteParameter paramProc = brk.params.get("process");
if (paramProc != null) {
Object proc =
findArgumentForSchema(null, getSchemaContext().getSchema(paramProc.type()), true,
true, true);
if (proc == null) {
Msg.error(this, "Cannot find required process argument for " + brk.method);
}
else {
args.put(paramProc.name(), proc);
}
}
if (condition != null && !condition.isBlank()) {
RemoteParameter paramCond = brk.params.get("condition");
if (paramCond == null) {
@ -920,21 +1244,21 @@ public class TraceRmiTarget extends AbstractTarget {
public CompletableFuture<Void> placeBreakpointAsync(AddressRange range,
Set<TraceBreakpointKind> kinds, String condition, String commands) {
Set<TraceBreakpointKind> copyKinds = Set.copyOf(kinds);
if (copyKinds.equals(Set.of(TraceBreakpointKind.HW_EXECUTE))) {
if (copyKinds.equals(TraceBreakpointKindSet.HW_EXECUTE)) {
return placeHwExecBreakAsync(expectSingleAddr(range, TraceBreakpointKind.HW_EXECUTE),
condition, commands);
}
if (copyKinds.equals(Set.of(TraceBreakpointKind.SW_EXECUTE))) {
if (copyKinds.equals(TraceBreakpointKindSet.SW_EXECUTE)) {
return placeSwExecBreakAsync(expectSingleAddr(range, TraceBreakpointKind.SW_EXECUTE),
condition, commands);
}
if (copyKinds.equals(Set.of(TraceBreakpointKind.READ))) {
if (copyKinds.equals(TraceBreakpointKindSet.READ)) {
return placeReadBreakAsync(range, condition, commands);
}
if (copyKinds.equals(Set.of(TraceBreakpointKind.WRITE))) {
if (copyKinds.equals(TraceBreakpointKindSet.WRITE)) {
return placeWriteBreakAsync(range, condition, commands);
}
if (copyKinds.equals(Set.of(TraceBreakpointKind.READ, TraceBreakpointKind.WRITE))) {
if (copyKinds.equals(TraceBreakpointKindSet.ACCESS)) {
return placeAccessBreakAsync(range, condition, commands);
}
Msg.error(this, "Invalid kinds in combination: " + kinds);
@ -986,20 +1310,22 @@ public class TraceRmiTarget extends AbstractTarget {
return AsyncUtils.nil();
}
return delBreak.method
.invokeAsync(Map.of(delBreak.params.get("specification").name(), spec))
.invokeAsync(Map.of(delBreak.params.get("specification").name(), spec.getObject()))
.toCompletableFuture()
.thenApply(__ -> null);
}
// TODO: Would this make sense for any debugger? To delete individual locations?
protected CompletableFuture<Void> deleteBreakpointLocAsync(TraceObjectBreakpointLocation loc) {
MatchedMethod delBreak =
matches.getBest("delBreakLoc", ActionName.DELETE, DelBreakMatcher.ALL);
if (delBreak == null) {
return AsyncUtils.nil();
Msg.debug(this, "Falling back to delete spec");
return deleteBreakpointSpecAsync(loc.getSpecification());
}
RemoteParameter paramLocation = delBreak.params.get("location");
if (paramLocation != null) {
return delBreak.method.invokeAsync(Map.of(paramLocation.name(), loc))
return delBreak.method.invokeAsync(Map.of(paramLocation.name(), loc.getObject()))
.toCompletableFuture()
.thenApply(__ -> null);
}
@ -1027,7 +1353,7 @@ public class TraceRmiTarget extends AbstractTarget {
}
return delBreak.method
.invokeAsync(Map.ofEntries(
Map.entry(delBreak.params.get("specification").name(), spec),
Map.entry(delBreak.params.get("specification").name(), spec.getObject()),
Map.entry(delBreak.params.get("enabled").name(), enabled)))
.toCompletableFuture()
.thenApply(__ -> null);
@ -1038,18 +1364,19 @@ public class TraceRmiTarget extends AbstractTarget {
MatchedMethod delBreak =
matches.getBest("toggleBreakLoc", ActionName.TOGGLE, ToggleBreakMatcher.ALL);
if (delBreak == null) {
return AsyncUtils.nil();
Msg.debug(this, "Falling back to toggle spec");
return toggleBreakpointSpecAsync(loc.getSpecification(), enabled);
}
RemoteParameter paramLocation = delBreak.params.get("location");
if (paramLocation != null) {
return delBreak.method
.invokeAsync(Map.ofEntries(
Map.entry(paramLocation.name(), loc),
Map.entry(paramLocation.name(), loc.getObject()),
Map.entry(delBreak.params.get("enabled").name(), enabled)))
.toCompletableFuture()
.thenApply(__ -> null);
}
return deleteBreakpointSpecAsync(loc.getSpecification());
return toggleBreakpointSpecAsync(loc.getSpecification(), enabled);
}
@Override
@ -1065,6 +1392,23 @@ public class TraceRmiTarget extends AbstractTarget {
return AsyncUtils.nil();
}
@Override
public CompletableFuture<Void> forceTerminateAsync() {
Map<String, ActionEntry> kills = collectKillActions(null);
for (ActionEntry kill : kills.values()) {
if (kill.requiresPrompt()) {
continue;
}
return kill.invokeAsync(false).handle((v, e) -> {
connection.forceCloseTrace(trace);
return null;
});
}
Msg.warn(this, "Cannot find way to gracefully kill. Forcing close regardless.");
connection.forceCloseTrace(trace);
return AsyncUtils.nil();
}
@Override
public CompletableFuture<Void> disconnectAsync() {
return CompletableFuture.runAsync(() -> {

View file

@ -0,0 +1,35 @@
/* ###
* 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.DefaultTraceRmiAcceptor;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
/**
* The same as the {@link TraceRmiService}, but grants access to the internal types (without
* casting) to implementors of {@link TraceRmiLaunchOpinion}.
*/
public interface InternalTraceRmiService extends TraceRmiService {
@Override
DefaultTraceRmiAcceptor acceptOne(SocketAddress address) throws IOException;
@Override
TraceRmiHandler connect(SocketAddress address) throws IOException;
}

View file

@ -0,0 +1,87 @@
/* ###
* 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.debug.spi.tracermi;
import java.util.Collection;
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
import ghidra.app.services.InternalTraceRmiService;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.framework.options.Options;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.util.classfinder.ExtensionPoint;
/**
* A factory of launch offers
*
* <p>
* Each factory is instantiated only once for the entire application, even when multiple tools are
* open. Thus, {@link #init(PluginTool)} and {@link #dispose(PluginTool)} will be invoked for each
* tool.
*/
public interface TraceRmiLaunchOpinion extends ExtensionPoint {
/**
* Register any options
*
* @param tool the tool
*/
default void registerOptions(Options options) {
}
/**
* Check if a change in the given option requires a refresh of offers
*
* @param optionName the name of the option that changed
* @return true to refresh, false otherwise
*/
default boolean requiresRefresh(String optionName) {
return false;
}
/**
* Generate or retrieve a collection of offers based on the current program.
*
* <p>
* Take care trying to "validate" a particular mechanism. For example, it is <em>not</em>
* appropriate to check that GDB exists, nor to execute it to derive its version.
*
* <ol>
* <li>It's possible the user has dependencies installed in non-standard locations. I.e., the
* user needs a chance to configure things <em>before</em> the UI decides whether or not to
* display them.</li>
* <li>The menus are meant to display <em>all</em> possibilities installed in Ghidra, even if
* some dependencies are missing on the local system. Discovery of the feature is most
* important. Knowing a feature exists may motivate a user to obtain the required dependencies
* and try it out.</li>
* <li>An offer is only promoted to the quick-launch menu upon <em>successful</em> connection.
* I.e., the entries there are already validated; they've worked at least once before.</li>
* </ol>
*
* @param plugin the Trace RMI launcher service plugin. <b>NOTE:</b> to get access to the Trace
* RMI (connection) service, use the {@link InternalTraceRmiService}, so that the
* offers can register the connection's resources. See
* {@link TraceRmiHandler#registerResources(Collection)}. Resource registration is
* required for the Disconnect button to completely terminate the back end.
* @param program the current program. While this is not <em>always</em> used by the launcher,
* it is implied that the user expects the debugger to do something with the current
* program, even if it's just informing the back-end debugger of the target image.
* @return the offers. The order is ignored, since items are displayed alphabetically.
*/
public Collection<TraceRmiLaunchOffer> getOffers(TraceRmiLauncherServicePlugin plugin,
Program program);
}

View file

@ -17,23 +17,14 @@ import socket
import traceback
def send_all(s, data):
sent = 0
while sent < len(data):
l = s.send(data[sent:])
if l == 0:
raise Exception("Socket closed")
sent += l
def send_length(s, value):
send_all(s, value.to_bytes(4, 'big'))
s.sendall(value.to_bytes(4, 'big'))
def send_delimited(s, msg):
data = msg.SerializeToString()
send_length(s, len(data))
send_all(s, data)
s.sendall(data)
def recv_all(s, size):
@ -44,7 +35,7 @@ def recv_all(s, size):
return buf
buf += part
return buf
#return s.recv(size, socket.MSG_WAITALL)
# return s.recv(size, socket.MSG_WAITALL)
def recv_length(s):

View file

@ -0,0 +1,188 @@
/* ###
* 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.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import ghidra.app.services.DebuggerTargetService;
import ghidra.async.AsyncPairingQueue;
import ghidra.async.AsyncUtils;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.tracermi.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
public class TestTraceRmiConnection implements TraceRmiConnection {
protected final TestRemoteMethodRegistry registry = new TestRemoteMethodRegistry();
protected final CompletableFuture<Trace> firstTrace = new CompletableFuture<>();
protected final Map<Trace, Long> snapshots = new HashMap<>();
protected final CompletableFuture<Void> closed = new CompletableFuture<>();
protected final Map<Trace, TraceRmiTarget> targets = new HashMap<>();
public static class TestRemoteMethodRegistry extends DefaultRemoteMethodRegistry {
@Override
public void add(RemoteMethod method) {
super.add(method);
}
}
public record TestRemoteMethod(String name, ActionName action, String description,
Map<String, RemoteParameter> parameters, SchemaName retType,
AsyncPairingQueue<Map<String, Object>> argQueue, AsyncPairingQueue<Object> retQueue)
implements RemoteMethod {
public TestRemoteMethod(String name, ActionName action, String description,
Map<String, RemoteParameter> parameters, SchemaName retType) {
this(name, action, description, parameters, retType, new AsyncPairingQueue<>(),
new AsyncPairingQueue<>());
}
public TestRemoteMethod(String name, ActionName action, String description,
SchemaName retType, RemoteParameter... parameters) {
this(name, action, description, Stream.of(parameters)
.collect(Collectors.toMap(RemoteParameter::name, p -> p)),
retType);
}
@Override
public RemoteAsyncResult invokeAsync(Map<String, Object> arguments) {
argQueue.give().complete(arguments);
DefaultRemoteAsyncResult result = new DefaultRemoteAsyncResult();
retQueue.take().handle(AsyncUtils.copyTo(result));
return result;
}
public Map<String, Object> expect() throws InterruptedException, ExecutionException {
return argQueue.take().get();
}
public void result(Object ret) {
retQueue.give().complete(ret);
}
}
public record TestRemoteParameter(String name, SchemaName type, boolean required,
Object defaultValue, String display, String description) implements RemoteParameter {
@Override
public Object getDefaultValue() {
return defaultValue;
}
}
@Override
public SocketAddress getRemoteAddress() {
return new InetSocketAddress("localhost", 0);
}
@Override
public TestRemoteMethodRegistry getMethods() {
return registry;
}
public void injectTrace(Trace trace) {
firstTrace.complete(trace);
}
public TraceRmiTarget publishTarget(PluginTool tool, Trace trace) {
injectTrace(trace);
TraceRmiTarget target = new TraceRmiTarget(tool, this, trace);
synchronized (targets) {
targets.put(trace, target);
}
DebuggerTargetService targetService = tool.getService(DebuggerTargetService.class);
targetService.publishTarget(target);
return target;
}
@Override
public Trace waitForTrace(long timeoutMillis) throws TimeoutException {
try {
return firstTrace.get(timeoutMillis, TimeUnit.MILLISECONDS);
}
catch (InterruptedException | ExecutionException e) {
throw new AssertionError(e);
}
}
public void setLastSnapshot(Trace trace, long snap) {
synchronized (snapshots) {
snapshots.put(trace, snap);
}
}
@Override
public long getLastSnapshot(Trace trace) {
synchronized (snapshots) {
Long snap = snapshots.get(trace);
return snap == null ? 0 : snap;
}
}
@Override
public void forceCloseTrace(Trace trace) {
TraceRmiTarget target;
synchronized (targets) {
target = targets.remove(trace);
}
DebuggerTargetService targetService =
target.getTool().getService(DebuggerTargetService.class);
targetService.withdrawTarget(target);
}
@Override
public boolean isTarget(Trace trace) {
synchronized (this.targets) {
return targets.containsKey(trace);
}
}
@Override
public void close() throws IOException {
Set<TraceRmiTarget> targets;
synchronized (this.targets) {
targets = new HashSet<>(this.targets.values());
this.targets.clear();
}
for (TraceRmiTarget target : targets) {
DebuggerTargetService targetService =
target.getTool().getService(DebuggerTargetService.class);
targetService.withdrawTarget(target);
}
closed.complete(null);
}
@Override
public boolean isClosed() {
return closed.isDone();
}
@Override
public void waitClosed() {
try {
closed.get();
}
catch (InterruptedException | ExecutionException e) {
throw new AssertionError(e);
}
}
}