mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +02:00
GP-3857: Port most Debugger components to TraceRmi.
This commit is contained in:
parent
7e4d2bcfaa
commit
fd4380c07a
222 changed files with 7241 additions and 3752 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 → Terminals →
|
||||
...</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()) + " → <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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(() -> {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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):
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue