GP-3836: Add Trace RMI 'Connections' pane.

This commit is contained in:
Dan 2023-12-01 09:10:12 -05:00
parent 5fd01c739d
commit bf8f7c8f78
82 changed files with 3836 additions and 270 deletions

View file

@ -17,6 +17,8 @@ package ghidra.app.plugin.core.debug.gui.tracermi.connection;
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.TraceInactiveCoordinatesPluginEvent;
import ghidra.app.services.TraceRmiService;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
@ -29,14 +31,30 @@ import ghidra.framework.plugintool.util.PluginStatus;
""",
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.RELEASED,
status = PluginStatus.STABLE,
eventsConsumed = {
TraceActivatedPluginEvent.class,
TraceInactiveCoordinatesPluginEvent.class,
},
servicesRequired = {
TraceRmiService.class,
})
public class TraceRmiConnectionManagerPlugin extends Plugin {
private final TraceRmiConnectionManagerProvider provider;
public TraceRmiConnectionManagerPlugin(PluginTool tool) {
super(tool);
this.provider = new TraceRmiConnectionManagerProvider(this);
}
// TODO: Add the actual provider. This will probably replace DebuggerTargetsPlugin.
@Override
public void processEvent(PluginEvent event) {
super.processEvent(event);
if (event instanceof TraceActivatedPluginEvent evt) {
provider.coordinates(evt.getActiveCoordinates());
}
if (event instanceof TraceInactiveCoordinatesPluginEvent evt) {
provider.coordinates(evt.getCoordinates());
}
}
}

View file

@ -0,0 +1,532 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection;
import java.awt.AWTEvent;
import java.awt.BorderLayout;
import java.awt.event.*;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import javax.swing.*;
import javax.swing.tree.TreeSelectionModel;
import docking.ActionContext;
import docking.WindowPosition;
import docking.action.DockingAction;
import docking.action.builder.ActionBuilder;
import docking.widgets.tree.*;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
import ghidra.app.plugin.core.debug.gui.tracermi.connection.tree.*;
import ghidra.app.services.*;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
import ghidra.debug.api.control.ControlMode;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
import ghidra.debug.api.tracermi.TraceRmiConnection;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.AutoService.Wiring;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginUtils;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
public class TraceRmiConnectionManagerProvider extends ComponentProviderAdapter {
public static final String TITLE = "Connections";
public static final HelpLocation HELP =
new HelpLocation(PluginUtils.getPluginNameFromClass(TraceRmiConnectionManagerPlugin.class),
DebuggerResources.HELP_ANCHOR_PLUGIN);
private static final String GROUP_SERVER = "2. Server";
private static final String GROUP_CONNECT = "1. Connect";
private static final String GROUP_MAINTENANCE = "3. Maintenance";
private static final ParameterDescription<String> PARAM_ADDRESS =
ParameterDescription.create(String.class, "address", true, "localhost",
"Host/Address", "Address or hostname for interface(s) to listen on");
private static final ParameterDescription<Integer> PARAM_PORT =
ParameterDescription.create(Integer.class, "port", true, 0,
"Port", "TCP port number, 0 for ephemeral");
private static final TargetParameterMap PARAMETERS = TargetParameterMap.ofEntries(
Map.entry(PARAM_ADDRESS.name, PARAM_ADDRESS),
Map.entry(PARAM_PORT.name, PARAM_PORT));
interface StartServerAction {
String NAME = "Start Server";
String DESCRIPTION = "Start a TCP server for incoming connections (indefinitely)";
String GROUP = GROUP_SERVER;
String HELP_ANCHOR = "start_server";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.menuPath(NAME)
.menuGroup(GROUP)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface StopServerAction {
String NAME = "Stop Server";
String DESCRIPTION = "Close the TCP server";
String GROUP = GROUP_SERVER;
String HELP_ANCHOR = "stop_server";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.menuPath(NAME)
.menuGroup(GROUP)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface ConnectAcceptAction {
String NAME = "Connect by Accept";
String DESCRIPTION = "Accept a single inbound TCP connection";
String GROUP = GROUP_CONNECT;
Icon ICON = DebuggerResources.ICON_CONNECT_ACCEPT;
String HELP_ANCHOR = "connect_accept";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface ConnectOutboundAction {
String NAME = "Connect Outbound";
String DESCRIPTION = "Connect to a listening agent/plugin by TCP";
String GROUP = GROUP_CONNECT;
Icon ICON = DebuggerResources.ICON_CONNECT_OUTBOUND;
String HELP_ANCHOR = "connect_outbound";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface CloseConnectionAction {
String NAME = "Close";
String DESCRIPTION = "Close a connection or server";
String GROUP = GROUP_MAINTENANCE;
String HELP_ANCHOR = "close";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.menuPath(NAME)
.popupMenuPath(NAME)
.menuGroup(GROUP)
.popupMenuGroup(GROUP)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface CloseAllAction {
String NAME = "Close All";
String DESCRIPTION = "Close all connections and the server";
String GROUP = GROUP_MAINTENANCE;
String HELP_ANCHOR = "close_all";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.menuPath(NAME)
.menuGroup(GROUP)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
class InjectableGTree extends GTree {
public InjectableGTree(GTreeNode root) {
super(root);
}
/**
* This allows the test framework to use reflection to access this method.
*/
@Override
protected void processEvent(AWTEvent e) {
super.processEvent(e);
}
}
private final TraceRmiConnectionManagerPlugin plugin;
// @AutoServiceConsumed via method
TraceRmiService traceRmiService;
// @AutoServiceConsumed via method
DebuggerTargetService targetService;
@AutoServiceConsumed
DebuggerConsoleService consoleService;
@AutoServiceConsumed
DebuggerTraceManagerService traceManagerService;
@AutoServiceConsumed
DebuggerControlService controlService;
@SuppressWarnings("unused")
private final Wiring autoServiceWiring;
private JPanel mainPanel;
protected GTree tree;
protected TraceRmiServiceNode rootNode = new TraceRmiServiceNode(this);
DockingAction actionStartServer;
DockingAction actionStopServer;
DockingAction actionConnectAccept;
DockingAction actionConnectOutbound;
DockingAction actionCloseConnection;
DockingAction actionCloseAll;
TraceRmiManagerActionContext myActionContext;
public TraceRmiConnectionManagerProvider(TraceRmiConnectionManagerPlugin plugin) {
super(plugin.getTool(), TITLE, plugin.getName());
this.plugin = plugin;
this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
setTitle(TITLE);
setIcon(DebuggerResources.ICON_PROVIDER_TARGETS);
setHelpLocation(HELP);
setWindowMenuGroup(DebuggerPluginPackage.NAME);
buildMainPanel();
setDefaultWindowPosition(WindowPosition.LEFT);
setVisible(true);
createActions();
}
private void buildMainPanel() {
mainPanel = new JPanel(new BorderLayout());
tree = new InjectableGTree(rootNode);
tree.setRootVisible(false);
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
mainPanel.add(tree);
tree.getGTSelectionModel().addGTreeSelectionListener(evt -> {
setContext();
});
tree.addGTModelListener((AnyChangeTreeModelListener) e -> {
setContext();
});
// TODO: Double-click or ENTER (activate) should open and/or activate trace/snap
tree.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) {
activateSelectedNode();
e.consume();
}
}
});
tree.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
activateSelectedNode();
e.consume();
}
}
});
}
private void activateSelectedNode() {
List<GTreeNode> selList = tree.getSelectedNodes();
if (selList.isEmpty()) {
return;
}
assert selList.size() == 1;
GTreeNode sel = selList.get(0);
nodeActivated((TraceRmiManagerNode) sel);
}
private void nodeActivated(TraceRmiManagerNode node) {
if (node instanceof TraceRmiTargetNode tNode) {
if (traceManagerService == null) {
return;
}
Target target = tNode.getTarget();
traceManagerService.activateTarget(target);
if (controlService == null) {
return;
}
if (!controlService.getCurrentMode(target.getTrace()).isTarget()) {
controlService.setCurrentMode(target.getTrace(), ControlMode.RO_TARGET);
}
}
}
private void createActions() {
actionStartServer = StartServerAction.builder(plugin)
.enabledWhen(this::isActionStartServerEnabled)
.onAction(this::doActionStartServerActivated)
.buildAndInstallLocal(this);
actionStopServer = StopServerAction.builder(plugin)
.enabledWhen(this::isActionStopServerEnabled)
.onAction(this::doActionStopServerActivated)
.buildAndInstallLocal(this);
actionConnectAccept = ConnectAcceptAction.builder(plugin)
.enabledWhen(this::isActionConnectAcceptEnabled)
.onAction(this::doActionConnectAcceptActivated)
.buildAndInstallLocal(this);
actionConnectOutbound = ConnectOutboundAction.builder(plugin)
.enabledWhen(this::isActionConnectOutboundEnabled)
.onAction(this::doActionConnectOutboundActivated)
.buildAndInstallLocal(this);
actionCloseConnection = CloseConnectionAction.builder(plugin)
.withContext(TraceRmiManagerActionContext.class)
.enabledWhen(this::isActionCloseConnectionEnabled)
.onAction(this::doActionCloseConnectionActivated)
.buildAndInstallLocal(this);
actionCloseAll = CloseAllAction.builder(plugin)
.enabledWhen(this::isActionCloseAllEnabled)
.onAction(this::doActionCloseAllActivated)
.buildAndInstallLocal(this);
}
@Override
public ActionContext getActionContext(MouseEvent event) {
if (myActionContext == null) {
return super.getActionContext(event);
}
return myActionContext;
}
@Override
public JComponent getComponent() {
return mainPanel;
}
private void setContext() {
myActionContext = new TraceRmiManagerActionContext(this, tree.getSelectionPath(), tree);
contextChanged();
}
private boolean isActionStartServerEnabled(ActionContext __) {
return traceRmiService != null && !traceRmiService.isServerStarted();
}
private InetSocketAddress promptSocketAddress(String title, String okText) {
DebuggerMethodInvocationDialog dialog = new DebuggerMethodInvocationDialog(tool,
title, okText, DebuggerResources.ICON_CONNECTION);
Map<String, ?> arguments;
do {
dialog.forgetMemorizedArguments();
arguments = dialog.promptArguments(PARAMETERS);
}
while (dialog.isResetRequested());
if (arguments == null) {
return null;
}
String address = PARAM_ADDRESS.get(arguments);
int port = PARAM_PORT.get(arguments);
return new InetSocketAddress(address, port);
}
private void doActionStartServerActivated(ActionContext __) {
InetSocketAddress sockaddr = promptSocketAddress("Start Trace RMI Server", "Start");
if (sockaddr == null) {
return;
}
try {
traceRmiService.setServerAddress(sockaddr);
traceRmiService.startServer();
if (consoleService != null) {
consoleService.log(DebuggerResources.ICON_CONNECTION,
"TraceRmi Server listening at " + traceRmiService.getServerAddress());
}
}
catch (Exception e) {
Msg.error(this, "Could not start TraceRmi server: " + e);
}
}
private boolean isActionStopServerEnabled(ActionContext __) {
return traceRmiService != null && traceRmiService.isServerStarted();
}
private void doActionStopServerActivated(ActionContext __) {
traceRmiService.stopServer();
if (consoleService != null) {
consoleService.log(DebuggerResources.ICON_DISCONNECT, "TraceRmi Server stopped");
}
}
private boolean isActionConnectAcceptEnabled(ActionContext __) {
return traceRmiService != null;
}
private void doActionConnectAcceptActivated(ActionContext __) {
InetSocketAddress sockaddr = promptSocketAddress("Accept Trace RMI Connection", "Listen");
if (sockaddr == null) {
return;
}
CompletableFuture.runAsync(() -> {
// TODO: Progress entry
try {
TraceRmiAcceptor acceptor = traceRmiService.acceptOne(sockaddr);
acceptor.accept();
}
catch (CancelledException e) {
// Nothing. User should already know.
}
catch (Throwable e) {
Msg.showError(this, null, "Accept",
"Could not accept Trace RMI Connection on " + sockaddr + ": " + e);
}
});
}
private boolean isActionConnectOutboundEnabled(ActionContext __) {
return traceRmiService != null;
}
private void doActionConnectOutboundActivated(ActionContext __) {
InetSocketAddress sockaddr = promptSocketAddress("Connect to Trace RMI", "Connect");
if (sockaddr == null) {
return;
}
CompletableFuture.runAsync(() -> {
// TODO: Progress entry?
try {
traceRmiService.connect(sockaddr);
}
catch (Throwable e) {
Msg.showError(this, null, "Connect",
"Could connect to Trace RMI at " + sockaddr + ": " + e.getMessage());
}
});
}
private boolean isActionCloseConnectionEnabled(TraceRmiManagerActionContext context) {
TraceRmiManagerNode node = context.getSelectedNode();
if (node instanceof TraceRmiConnectionNode) {
return true;
}
if (node instanceof TraceRmiAcceptorNode) {
return true;
}
return false;
}
private void doActionCloseConnectionActivated(TraceRmiManagerActionContext context) {
TraceRmiManagerNode node = context.getSelectedNode();
if (node instanceof TraceRmiConnectionNode cxNode) {
try {
cxNode.getConnection().close();
}
catch (IOException e) {
Msg.showError(this, null, "Close Connection",
"Could not close Trace RMI connection: " + e);
}
}
else if (node instanceof TraceRmiAcceptorNode acNode) {
acNode.getAcceptor().cancel();
}
}
private boolean isActionCloseAllEnabled(ActionContext __) {
return traceRmiService != null;
}
private void doActionCloseAllActivated(ActionContext __) {
try {
doActionStopServerActivated(__);
}
catch (Throwable e) {
Msg.error(this, "Could not close server: " + e);
}
for (TraceRmiConnection connection : traceRmiService.getAllConnections()) {
try {
connection.close();
}
catch (Throwable e) {
Msg.error(this, "Could not close " + connection + ": " + e);
}
}
for (TraceRmiAcceptor acceptor : traceRmiService.getAllAcceptors()) {
try {
acceptor.cancel();
}
catch (Throwable e) {
Msg.error(this, "Could not cancel " + acceptor + ": " + e);
}
}
}
@AutoServiceConsumed
private void setTraceRmiService(TraceRmiService traceRmiService) {
if (this.traceRmiService != null) {
this.traceRmiService.removeTraceServiceListener(rootNode);
}
this.traceRmiService = traceRmiService;
if (this.traceRmiService != null) {
this.traceRmiService.addTraceServiceListener(rootNode);
}
}
@AutoServiceConsumed
private void setTargetService(DebuggerTargetService targetService) {
if (this.targetService != null) {
this.targetService.removeTargetPublicationListener(rootNode);
}
this.targetService = targetService;
if (this.targetService != null) {
this.targetService.addTargetPublicationListener(rootNode);
}
}
public TraceRmiService getTraceRmiService() {
return traceRmiService;
}
/**
* Coordinates, whether active or inactive, for a trace changed
*
* @param coordinates the coordinates
*/
public void coordinates(DebuggerCoordinates coordinates) {
if (rootNode == null) {
return;
}
rootNode.coordinates(coordinates);
}
}

View file

@ -0,0 +1,39 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection;
import javax.swing.tree.TreePath;
import docking.DefaultActionContext;
import docking.widgets.tree.GTree;
import ghidra.app.plugin.core.debug.gui.tracermi.connection.tree.TraceRmiManagerNode;
public class TraceRmiManagerActionContext extends DefaultActionContext {
private final TreePath path;
public TraceRmiManagerActionContext(TraceRmiConnectionManagerProvider provider,
TreePath path, GTree tree) {
super(provider, path, tree);
this.path = path;
}
public TraceRmiManagerNode getSelectedNode() {
if (path == null) {
return null;
}
return (TraceRmiManagerNode) path.getLastPathComponent();
}
}

View file

@ -0,0 +1,34 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
import docking.widgets.tree.GTreeNode;
import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
public abstract class AbstractTraceRmiManagerNode extends GTreeNode implements TraceRmiManagerNode {
protected final TraceRmiConnectionManagerProvider provider;
protected final String name;
public AbstractTraceRmiManagerNode(TraceRmiConnectionManagerProvider provider, String name) {
this.provider = provider;
this.name = name;
}
@Override
public String getName() {
return name;
}
}

View file

@ -0,0 +1,54 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
public class TraceRmiAcceptorNode extends AbstractTraceRmiManagerNode {
private static final Icon ICON = DebuggerResources.ICON_CONNECT_ACCEPT;
private final TraceRmiAcceptor acceptor;
public TraceRmiAcceptorNode(TraceRmiConnectionManagerProvider provider,
TraceRmiAcceptor acceptor) {
super(provider, "ACCEPTING: " + acceptor.getAddress());
this.acceptor = acceptor;
}
@Override
public Icon getIcon(boolean expanded) {
return ICON;
}
@Override
public String getToolTip() {
return "Trace RMI Acceptor listening at " + acceptor.getAddress();
}
@Override
public boolean isLeaf() {
return true;
}
public TraceRmiAcceptor getAcceptor() {
return acceptor;
}
}

View file

@ -0,0 +1,97 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
import java.util.HashMap;
import java.util.Map;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracermi.TraceRmiConnection;
public class TraceRmiConnectionNode extends AbstractTraceRmiManagerNode {
private static final Icon ICON = DebuggerResources.ICON_CONNECTION;
private final TraceRmiConnection connection;
private final Map<Target, TraceRmiTargetNode> targetNodes = new HashMap<>();
public TraceRmiConnectionNode(TraceRmiConnectionManagerProvider provider,
TraceRmiConnection connection) {
// TODO: Can the connector identify/describe itself for this display?
super(provider, "Connected: " + connection.getRemoteAddress());
this.connection = connection;
}
@Override
public String getDisplayText() {
return connection.getDescription() + " at " + connection.getRemoteAddress();
}
@Override
public Icon getIcon(boolean expanded) {
return ICON;
}
@Override
public String getToolTip() {
return "Trace RMI Connection to " + connection.getDescription() + " at " +
connection.getRemoteAddress();
}
@Override
public boolean isLeaf() {
return false;
}
private TraceRmiTargetNode newTargetNode(Target target) {
return new TraceRmiTargetNode(provider, this, target);
}
private TraceRmiTargetNode addTargetNode(Target target) {
TraceRmiTargetNode node;
synchronized (targetNodes) {
node = targetNodes.computeIfAbsent(target, this::newTargetNode);
}
addNode(node);
return node;
}
private void removeTargetNode(Target target) {
TraceRmiTargetNode node;
synchronized (targetNodes) {
node = targetNodes.remove(target);
}
if (node == null) {
return;
}
removeNode(node);
}
public TraceRmiTargetNode targetPublished(Target target) {
return addTargetNode(target);
}
public void targetWithdrawn(Target target) {
removeTargetNode(target);
}
public TraceRmiConnection getConnection() {
return connection;
}
}

View file

@ -0,0 +1,20 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
public interface TraceRmiManagerNode {
}

View file

@ -0,0 +1,57 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
import ghidra.app.services.TraceRmiService;
public class TraceRmiServerNode extends AbstractTraceRmiManagerNode {
private static final Icon ICON = DebuggerResources.ICON_THREAD; // TODO: Different name?
public TraceRmiServerNode(TraceRmiConnectionManagerProvider provider) {
super(provider, "Server");
}
@Override
public Icon getIcon(boolean expanded) {
return ICON;
}
@Override
public String getDisplayText() {
TraceRmiService service = provider.getTraceRmiService();
if (service == null) {
return "<SERVICE MISSING>";
}
if (!service.isServerStarted()) {
return "Server: CLOSED";
}
return "Server: LISTENING " + service.getServerAddress();
}
@Override
public String getToolTip() {
return getDisplayText();
}
@Override
public boolean isLeaf() {
return true;
}
}

View file

@ -0,0 +1,205 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
import java.net.SocketAddress;
import java.util.*;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.TargetPublicationListener;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.debug.api.tracermi.*;
import ghidra.util.Msg;
public class TraceRmiServiceNode extends AbstractTraceRmiManagerNode
implements TraceRmiServiceListener, TargetPublicationListener {
private static final String DESCRIPTION = "The TraceRmi service";
final TraceRmiServerNode serverNode;
final Map<TraceRmiConnection, TraceRmiConnectionNode> connectionNodes = new HashMap<>();
final Map<TraceRmiAcceptor, TraceRmiAcceptorNode> acceptorNodes = new HashMap<>();
// weak because each connection node keeps the strong map
final Map<Target, TraceRmiTargetNode> targetNodes = new WeakHashMap<>();
public TraceRmiServiceNode(TraceRmiConnectionManagerProvider provider) {
super(provider, "<root>");
this.serverNode = new TraceRmiServerNode(provider);
addNode(serverNode);
}
@Override
public Icon getIcon(boolean expanded) {
return null;
}
@Override
public String getToolTip() {
return DESCRIPTION;
}
@Override
public boolean isLeaf() {
return false;
}
private TraceRmiConnectionNode newConnectionNode(TraceRmiConnection connection) {
return new TraceRmiConnectionNode(provider, connection);
}
private void addConnectionNode(TraceRmiConnection connection) {
TraceRmiConnectionNode node;
synchronized (connectionNodes) {
node = connectionNodes.computeIfAbsent(connection, this::newConnectionNode);
}
addNode(node);
}
private void removeConnectionNode(TraceRmiConnection connection) {
TraceRmiConnectionNode node;
synchronized (connectionNodes) {
node = connectionNodes.remove(connection);
}
if (node == null) {
return;
}
removeNode(node);
}
private TraceRmiAcceptorNode newAcceptorNode(TraceRmiAcceptor acceptor) {
return new TraceRmiAcceptorNode(provider, acceptor);
}
private void addAcceptorNode(TraceRmiAcceptor acceptor) {
TraceRmiAcceptorNode node;
synchronized (acceptorNodes) {
node = acceptorNodes.computeIfAbsent(acceptor, this::newAcceptorNode);
}
addNode(node);
}
private void removeAcceptorNode(TraceRmiAcceptor acceptor) {
TraceRmiAcceptorNode node;
synchronized (acceptorNodes) {
node = acceptorNodes.remove(acceptor);
}
if (node == null) {
return;
}
removeNode(node);
}
@Override
public void serverStarted(SocketAddress address) {
serverNode.fireNodeChanged();
provider.contextChanged();
}
@Override
public void serverStopped() {
serverNode.fireNodeChanged();
provider.contextChanged();
}
@Override
public void connected(TraceRmiConnection connection, ConnectMode mode,
TraceRmiAcceptor acceptor) {
addConnectionNode(connection);
removeAcceptorNode(acceptor);
provider.contextChanged();
}
@Override
public void disconnected(TraceRmiConnection connection) {
removeConnectionNode(connection);
provider.contextChanged();
}
@Override
public void waitingAccept(TraceRmiAcceptor acceptor) {
addAcceptorNode(acceptor);
provider.contextChanged();
}
@Override
public void acceptCancelled(TraceRmiAcceptor acceptor) {
removeAcceptorNode(acceptor);
provider.contextChanged();
}
@Override
public void acceptFailed(TraceRmiAcceptor acceptor, Exception e) {
removeAcceptorNode(acceptor);
provider.contextChanged();
}
@Override
public void targetPublished(TraceRmiConnection connection, Target target) {
TraceRmiConnectionNode cxNode;
synchronized (connectionNodes) {
cxNode = connectionNodes.get(connection);
}
if (cxNode == null) {
Msg.warn(this,
"Target published on a connection I don't have! " + connection + " " + target);
return;
}
TraceRmiTargetNode tNode = cxNode.targetPublished(target);
if (tNode == null) {
return;
}
synchronized (targetNodes) {
targetNodes.put(target, tNode);
}
provider.contextChanged();
}
@Override
public void targetPublished(Target target) {
// Dont care. Using targetPublished(connection, target) instead
}
@Override
public void targetWithdrawn(Target target) {
TraceRmiTargetNode node;
synchronized (targetNodes) {
node = targetNodes.remove(target);
}
if (node == null) {
return;
}
node.getConnectionNode().targetWithdrawn(target);
provider.contextChanged();
}
public void coordinates(DebuggerCoordinates coordinates) {
Target target = coordinates.getTarget();
if (target == null) {
return;
}
TraceRmiTargetNode node;
synchronized (targetNodes) {
node = targetNodes.get(target);
}
if (node == null) {
return;
}
node.fireNodeChanged();
}
}

View file

@ -0,0 +1,64 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
import ghidra.debug.api.target.Target;
public class TraceRmiTargetNode extends AbstractTraceRmiManagerNode {
private static final Icon ICON = DebuggerResources.ICON_RECORD;
private final TraceRmiConnectionNode connectionNode;
private final Target target;
public TraceRmiTargetNode(TraceRmiConnectionManagerProvider provider,
TraceRmiConnectionNode connectionNode, Target target) {
super(provider, target.getTrace().getName());
this.connectionNode = connectionNode;
this.target = target;
}
@Override
public Icon getIcon(boolean expanded) {
return ICON;
}
@Override
public String getDisplayText() {
return target.getTrace().getName() + " (snap=" + target.getSnap() + ")";
}
@Override
public String getToolTip() {
return "Target: " + target.getTrace().getName();
}
@Override
public boolean isLeaf() {
return true;
}
public TraceRmiConnectionNode getConnectionNode() {
return connectionNode;
}
public Target getTarget() {
return target;
}
}

View file

@ -33,8 +33,8 @@ 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.debug.service.tracermi.DefaultTraceRmiAcceptor;
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler;
import ghidra.app.plugin.core.terminal.TerminalListener;
import ghidra.app.services.*;
import ghidra.app.services.DebuggerTraceManagerService.ActivationCause;

View file

@ -1,47 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.SocketAddress;
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
public class DefaultTraceRmiAcceptor extends TraceRmiServer implements TraceRmiAcceptor {
public DefaultTraceRmiAcceptor(TraceRmiPlugin plugin, SocketAddress address) {
super(plugin, address);
}
@Override
public void start() throws IOException {
socket = new ServerSocket();
bind();
}
@Override
protected void bind() throws IOException {
socket.bind(address, 1);
}
@Override
public TraceRmiHandler accept() throws IOException {
TraceRmiHandler handler = super.accept();
close();
return handler;
}
}

View file

@ -13,38 +13,43 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import java.io.IOException;
import java.net.*;
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
import ghidra.debug.api.tracermi.TraceRmiServiceListener.ConnectMode;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
public class TraceRmiServer {
public abstract class AbstractTraceRmiListener {
protected final TraceRmiPlugin plugin;
protected final SocketAddress address;
protected ServerSocket socket;
public TraceRmiServer(TraceRmiPlugin plugin, SocketAddress address) {
public AbstractTraceRmiListener(TraceRmiPlugin plugin, SocketAddress address) {
this.plugin = plugin;
this.address = address;
}
protected void bind() throws IOException {
socket.bind(address);
}
protected abstract void bind() throws IOException;
public void start() throws IOException {
socket = new ServerSocket();
bind();
new Thread(this::serviceLoop, "trace-rmi server " + socket.getLocalSocketAddress()).start();
startServiceLoop();
}
protected abstract void startServiceLoop();
public void setTimeout(int millis) throws SocketException {
socket.setSoTimeout(millis);
}
protected abstract ConnectMode getConnectMode();
/**
* Accept a connection and handle its requests.
*
@ -54,36 +59,17 @@ public class TraceRmiServer {
*
* @return the handler
* @throws IOException on error
* @throws CancelledException if the accept is cancelled
*/
@SuppressWarnings("resource")
protected TraceRmiHandler accept() throws IOException {
protected TraceRmiHandler doAccept(TraceRmiAcceptor acceptor) throws IOException {
Socket client = socket.accept();
TraceRmiHandler handler = new TraceRmiHandler(plugin, client);
handler.start();
plugin.listeners.invoke().connected(handler, getConnectMode(), acceptor);
return handler;
}
protected void serviceLoop() {
try {
accept();
}
catch (IOException e) {
if (socket.isClosed()) {
return;
}
Msg.error("Error accepting TraceRmi client", e);
return;
}
finally {
try {
socket.close();
}
catch (IOException e) {
Msg.error("Error closing TraceRmi service", e);
}
}
}
public void close() {
try {
socket.close();

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import java.util.concurrent.*;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import java.util.*;

View file

@ -0,0 +1,77 @@
/* ###
* 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.tracermi;
import java.io.IOException;
import java.net.SocketAddress;
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
import ghidra.debug.api.tracermi.TraceRmiServiceListener.ConnectMode;
import ghidra.util.exception.CancelledException;
public class DefaultTraceRmiAcceptor extends AbstractTraceRmiListener implements TraceRmiAcceptor {
private boolean cancelled = false;
public DefaultTraceRmiAcceptor(TraceRmiPlugin plugin, SocketAddress address) {
super(plugin, address);
}
@Override
protected void startServiceLoop() {
// Don't. Instead, client calls accept()
}
@Override
protected void bind() throws IOException {
socket.bind(address, 1);
plugin.addAcceptor(this);
}
@Override
protected ConnectMode getConnectMode() {
return ConnectMode.ACCEPT_ONE;
}
@Override
public TraceRmiHandler accept() throws IOException, CancelledException {
try {
TraceRmiHandler handler = doAccept(this);
close();
return handler;
}
catch (Exception e) {
close();
if (cancelled) {
throw new CancelledException();
}
plugin.listeners.invoke().acceptFailed(this, e);
throw e;
}
}
@Override
public void close() {
plugin.removeAcceptor(this);
super.close();
}
@Override
public void cancel() {
cancelled = true;
close();
plugin.listeners.invoke().acceptCancelled(this);
}
}

View file

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler.*;
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler.*;
import ghidra.debug.api.tracermi.TraceRmiError;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.Register;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import java.util.Map;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.debug.api.tracermi.RemoteParameter;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import java.io.*;
import java.math.BigInteger;
@ -41,9 +41,12 @@ 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.progress.CloseableTaskMonitor;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.debug.api.tracermi.*;
import ghidra.framework.Application;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.AutoService.Wiring;
@ -65,7 +68,6 @@ import ghidra.trace.model.time.TraceSnapshot;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.task.TaskMonitor;
public class TraceRmiHandler implements TraceRmiConnection {
public static final String VERSION = "10.4";
@ -180,7 +182,14 @@ public class TraceRmiHandler implements TraceRmiConnection {
byTrace.put(openTrace.trace, openTrace);
first.complete(openTrace);
plugin.publishTarget(openTrace.target);
plugin.publishTarget(TraceRmiHandler.this, openTrace.target);
}
public synchronized List<Target> getTargets() {
return byId.values()
.stream()
.map(ot -> ot.target)
.collect(Collectors.toUnmodifiableList());
}
public CompletableFuture<OpenTrace> getFirstAsync() {
@ -192,7 +201,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
private final Socket socket;
private final InputStream in;
private final OutputStream out;
private final CompletableFuture<Void> negotiate = new CompletableFuture<>();
private final CompletableFuture<String> negotiate = new CompletableFuture<>();
private final CompletableFuture<Void> closed = new CompletableFuture<>();
private final Set<TerminalSession> terminals = new LinkedHashSet<>();
@ -276,8 +285,8 @@ public class TraceRmiHandler implements TraceRmiConnection {
DoId nextKey = openTraces.idSet().iterator().next();
OpenTrace open = openTraces.removeById(nextKey);
if (traceManager == null || traceManager.isSaveTracesByDefault()) {
try {
open.trace.save("Save on Disconnect", plugin.getTaskMonitor());
try (CloseableTaskMonitor monitor = plugin.createMonitor()) {
open.trace.save("Save on Disconnect", monitor);
}
catch (IOException e) {
Msg.error(this, "Could not save " + open.trace);
@ -289,6 +298,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
open.trace.release(this);
}
closed.complete(null);
plugin.listeners.invoke().disconnected(this);
}
@Override
@ -344,18 +354,19 @@ public class TraceRmiHandler implements TraceRmiConnection {
protected DomainFile createDeconflictedFile(DomainFolder parent, DomainObject object)
throws InvalidNameException, CancelledException, IOException {
String name = object.getName();
TaskMonitor monitor = plugin.getTaskMonitor();
for (int nextId = 1; nextId < 100; nextId++) {
try {
return parent.createFile(name, object, monitor);
}
catch (DuplicateFileException e) {
name = object.getName() + "." + nextId;
try (CloseableTaskMonitor monitor = plugin.createMonitor()) {
for (int nextId = 1; nextId < 100; nextId++) {
try {
return parent.createFile(name, object, monitor);
}
catch (DuplicateFileException e) {
name = object.getName() + "." + nextId;
}
}
name = object.getName() + "." + System.currentTimeMillis();
// Don't catch it this last time
return parent.createFile(name, object, monitor);
}
name = object.getName() + "." + System.currentTimeMillis();
// Don't catch it this last time
return parent.createFile(name, object, monitor);
}
public void start() {
@ -911,16 +922,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
OpenTrace open = requireOpenTrace(req.getOid());
long snap = req.getSnap().getSnap();
/**
* TODO: Is this composition of laziness upon laziness efficient enough?
*
* <p>
* Can experiment with ordering of address-set-view "expression" to optimize early
* termination.
*
* <p>
* Want addresses satisfying {@code known | (readOnly & everKnown)}
*/
// Want addresses satisfying {@code known | (readOnly & everKnown)}
TraceMemoryManager memoryManager = open.trace.getMemoryManager();
AddressSetView readOnly =
memoryManager.getRegionsAddressSetWith(snap, r -> !r.isWrite());
@ -939,8 +941,9 @@ public class TraceRmiHandler implements TraceRmiConnection {
dis.setInitialContext(DebuggerDisassemblerPlugin.deriveAlternativeDefaultContext(
host.getLanguage(), host.getLanguage().getLanguageID(), start));
TaskMonitor monitor = plugin.getTaskMonitor();
dis.applyToTyped(open.trace.getFixedProgramView(snap), monitor);
try (CloseableTaskMonitor monitor = plugin.createMonitor()) {
dis.applyToTyped(open.trace.getFixedProgramView(snap), monitor);
}
return ReplyDisassemble.newBuilder()
.setLength(dis.getDisassembledAddressSet().getNumAddresses())
@ -1027,8 +1030,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
new SchemaName(m.getReturnType().getName()));
methodRegistry.add(rm);
}
negotiate.complete(null);
return ReplyNegotiate.getDefaultInstance();
negotiate.complete(req.getDescription());
return ReplyNegotiate.newBuilder()
.setDescription(Application.getName() + " " + Application.getApplicationVersion())
.build();
}
protected ReplyPutBytes handlePutBytes(RequestPutBytes req) {
@ -1110,7 +1115,9 @@ public class TraceRmiHandler implements TraceRmiConnection {
protected ReplySaveTrace handleSaveTrace(RequestSaveTrace req)
throws CancelledException, IOException {
OpenTrace open = requireOpenTrace(req.getOid());
open.trace.save("TraceRMI", plugin.getTaskMonitor());
try (CloseableTaskMonitor monitor = plugin.createMonitor()) {
open.trace.save("TraceRMI", monitor);
}
return ReplySaveTrace.getDefaultInstance();
}
@ -1271,9 +1278,25 @@ public class TraceRmiHandler implements TraceRmiConnection {
return openTraces.getByTrace(trace) != null;
}
@Override
public Collection<Target> getTargets() {
return openTraces.getTargets();
}
public void registerTerminals(Collection<TerminalSession> terminals) {
synchronized (this.terminals) {
this.terminals.addAll(terminals);
}
}
@Override
public String getDescription() {
// NOTE: Negotiation happens during construction, so unless this is called internally,
// or there's some error, we should always have a read description.
String description = negotiate.getNow("(Negotiating...)");
if (description.isBlank()) {
return "Trace RMI";
}
return description;
}
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import java.io.IOException;
import java.net.*;
@ -24,14 +24,16 @@ 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.*;
import ghidra.debug.api.tracermi.TraceRmiConnection;
import ghidra.debug.api.progress.CloseableTaskMonitor;
import ghidra.debug.api.tracermi.*;
import ghidra.debug.api.tracermi.TraceRmiServiceListener.ConnectMode;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.AutoService.Wiring;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.util.Swing;
import ghidra.util.datastruct.ListenerSet;
import ghidra.util.task.ConsoleTaskMonitor;
import ghidra.util.task.TaskMonitor;
@PluginInfo(
shortDescription = "Connect to back-end debuggers via Trace RMI",
@ -56,26 +58,41 @@ import ghidra.util.task.TaskMonitor;
public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
private static final int DEFAULT_PORT = 15432;
static class FallbackTaskMonitor extends ConsoleTaskMonitor implements CloseableTaskMonitor {
@Override
public void close() {
// Nothing
}
}
@AutoServiceConsumed
private DebuggerTargetService targetService;
@AutoServiceConsumed
private ProgressService progressService;
@SuppressWarnings("unused")
private final Wiring autoServiceWiring;
private final TaskMonitor monitor = new ConsoleTaskMonitor();
private SocketAddress serverAddress = new InetSocketAddress("0.0.0.0", DEFAULT_PORT);
private TraceRmiServer server;
private final Set<TraceRmiHandler> handlers = new LinkedHashSet<>();
private final Set<DefaultTraceRmiAcceptor> acceptors = new LinkedHashSet<>();
final ListenerSet<TraceRmiServiceListener> listeners =
new ListenerSet<>(TraceRmiServiceListener.class, true);
private final CloseableTaskMonitor fallbackMonitor = new FallbackTaskMonitor();
public TraceRmiPlugin(PluginTool tool) {
super(tool);
autoServiceWiring = AutoService.wireServicesProvidedAndConsumed(this);
}
public TaskMonitor getTaskMonitor() {
// TODO: Create one in the Debug Console?
return monitor;
protected CloseableTaskMonitor createMonitor() {
if (progressService == null) {
return fallbackMonitor;
}
return progressService.publishTask();
}
@Override
@ -102,14 +119,16 @@ public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
}
server = new TraceRmiServer(this, serverAddress);
server.start();
listeners.invoke().serverStarted(server.getAddress());
}
@Override
public void stopServer() {
if (server != null) {
server.close();
server = null;
listeners.invoke().serverStopped();
}
server = null;
}
@Override
@ -124,6 +143,7 @@ public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
socket.connect(address);
TraceRmiHandler handler = new TraceRmiHandler(this, socket);
handler.start();
listeners.invoke().connected(handler, ConnectMode.CONNECT, null);
return handler;
}
@ -131,25 +151,52 @@ public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
public DefaultTraceRmiAcceptor acceptOne(SocketAddress address) throws IOException {
DefaultTraceRmiAcceptor acceptor = new DefaultTraceRmiAcceptor(this, address);
acceptor.start();
listeners.invoke().waitingAccept(acceptor);
return acceptor;
}
void addHandler(TraceRmiHandler handler) {
handlers.add(handler);
synchronized (handlers) {
handlers.add(handler);
}
}
void removeHandler(TraceRmiHandler handler) {
handlers.remove(handler);
synchronized (handlers) {
handlers.remove(handler);
}
}
@Override
public Collection<TraceRmiConnection> getAllConnections() {
return List.copyOf(handlers);
synchronized (handlers) {
return List.copyOf(handlers);
}
}
void publishTarget(TraceRmiTarget target) {
void addAcceptor(DefaultTraceRmiAcceptor acceptor) {
synchronized (acceptors) {
acceptors.add(acceptor);
}
}
void removeAcceptor(DefaultTraceRmiAcceptor acceptor) {
synchronized (acceptors) {
acceptors.remove(acceptor);
}
}
@Override
public Collection<TraceRmiAcceptor> getAllAcceptors() {
synchronized (acceptors) {
return List.copyOf(acceptors);
}
}
void publishTarget(TraceRmiHandler handler, TraceRmiTarget target) {
Swing.runIfSwingOrRunLater(() -> {
targetService.publishTarget(target);
listeners.invoke().targetPublished(handler, target);
});
}
@ -158,4 +205,14 @@ public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
targetService.withdrawTarget(target);
});
}
@Override
public void addTraceServiceListener(TraceRmiServiceListener listener) {
listeners.add(listener);
}
@Override
public void removeTraceServiceListener(TraceRmiServiceListener listener) {
listeners.remove(listener);
}
}

View file

@ -0,0 +1,65 @@
/* ###
* 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.tracermi;
import java.io.IOException;
import java.net.SocketAddress;
import ghidra.debug.api.tracermi.TraceRmiServiceListener.ConnectMode;
import ghidra.util.Msg;
public class TraceRmiServer extends AbstractTraceRmiListener {
public TraceRmiServer(TraceRmiPlugin plugin, SocketAddress address) {
super(plugin, address);
}
@Override
protected void bind() throws IOException {
socket.bind(address);
}
@Override
protected void startServiceLoop() {
new Thread(this::serviceLoop, "trace-rmi server " + socket.getLocalSocketAddress()).start();
}
@Override
protected ConnectMode getConnectMode() {
return ConnectMode.SERVER;
}
@SuppressWarnings("resource")
protected void serviceLoop() {
try {
doAccept(null);
}
catch (IOException e) {
if (socket.isClosed()) {
return;
}
Msg.error("Error accepting TraceRmi client", e);
return;
}
finally {
try {
socket.close();
}
catch (IOException e) {
Msg.error("Error closing TraceRmi service", e);
}
}
}
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import java.io.IOException;
import java.util.*;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import org.apache.commons.lang3.ArrayUtils;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import ghidra.program.model.address.AddressOverflowException;

View file

@ -18,8 +18,8 @@ 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.app.plugin.core.debug.service.tracermi.DefaultTraceRmiAcceptor;
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler;
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
/**

View file

@ -18,7 +18,7 @@ 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.plugin.core.debug.service.tracermi.TraceRmiHandler;
import ghidra.app.services.InternalTraceRmiService;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.framework.options.Options;

View file

@ -427,9 +427,11 @@ message Method {
message RequestNegotiate {
string version = 1;
repeated Method methods = 2;
string description = 3;
}
message ReplyNegotiate {
string description = 1;
}
message XRequestInvokeMethod {

View file

@ -720,7 +720,7 @@ class Client(object):
return Client._read_obj_desc(msg.child_desc), sch.OBJECT
raise ValueError("Could not read value: {}".format(msg))
def __init__(self, s, method_registry: MethodRegistry):
def __init__(self, s, description: str, method_registry: MethodRegistry):
self._traces = {}
self._next_trace_id = 1
self.tlock = Lock()
@ -732,7 +732,7 @@ class Client(object):
self.slock = Lock()
self.receiver.start()
self._method_registry = method_registry
self._negotiate()
self.description = self._negotiate(description)
def close(self):
self.s.close()
@ -1083,15 +1083,16 @@ class Client(object):
return reply.length
return self._batch_or_now(root, 'reply_disassemble', _handle)
def _negotiate(self):
def _negotiate(self, description: str):
root = bufs.RootMessage()
root.request_negotiate.version = VERSION
root.request_negotiate.description = description
self._write_methods(root.request_negotiate.methods,
self._method_registry._methods.values())
def _handle(reply):
pass
self._now(root, 'reply_negotiate', _handle)
return reply.description
return self._now(root, 'reply_negotiate', _handle)
def _handle_invoke_method(self, request):
if request.HasField('oid'):

View file

@ -0,0 +1,416 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection;
import static org.junit.Assert.*;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Map;
import java.util.concurrent.*;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.Before;
import org.junit.Test;
import generic.Unique;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.plugin.core.debug.gui.objects.components.InvocationDialogHelper;
import ghidra.app.plugin.core.debug.gui.tracermi.connection.tree.*;
import ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin;
import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiClient;
import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiClient.Tx;
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin;
import ghidra.app.services.DebuggerControlService;
import ghidra.app.services.TraceRmiService;
import ghidra.dbg.target.schema.SchemaContext;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.dbg.target.schema.XmlSchemaContext;
import ghidra.debug.api.control.ControlMode;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
import ghidra.debug.api.tracermi.TraceRmiConnection;
import ghidra.util.exception.CancelledException;
public class TraceRmiConnectionManagerProviderTest extends AbstractGhidraHeadedDebuggerTest {
TraceRmiConnectionManagerProvider provider;
TraceRmiService traceRmiService;
DebuggerControlService controlService;
@Before
public void setUpConnectionManager() throws Exception {
controlService = addPlugin(tool, DebuggerControlServicePlugin.class);
traceRmiService = addPlugin(tool, TraceRmiPlugin.class);
addPlugin(tool, TraceRmiConnectionManagerPlugin.class);
provider = waitForComponentProvider(TraceRmiConnectionManagerProvider.class);
}
@Test
public void testActionAccept() throws Exception {
performEnabledAction(provider, provider.actionConnectAccept, false);
InvocationDialogHelper helper = InvocationDialogHelper.waitFor();
helper.dismissWithArguments(Map.ofEntries(
Map.entry("address", "localhost"),
Map.entry("port", 0)));
waitForPass(() -> Unique.assertOne(traceRmiService.getAllAcceptors()));
}
@Test
public void testActionConnect() throws Exception {
try (ServerSocketChannel server = ServerSocketChannel.open()) {
server.bind(new InetSocketAddress("localhost", 0), 1);
if (!(server.getLocalAddress() instanceof InetSocketAddress sockaddr)) {
throw new AssertionError();
}
performEnabledAction(provider, provider.actionConnectOutbound, false);
InvocationDialogHelper helper = InvocationDialogHelper.waitFor();
helper.dismissWithArguments(Map.ofEntries(
Map.entry("address", sockaddr.getHostString()),
Map.entry("port", sockaddr.getPort())));
try (SocketChannel channel = server.accept()) {
TestTraceRmiClient client = new TestTraceRmiClient(channel);
client.sendNegotiate("Test client");
client.recvNegotiate();
waitForPass(() -> Unique.assertOne(traceRmiService.getAllConnections()));
}
}
}
@Test
public void testActionStartServer() throws Exception {
performEnabledAction(provider, provider.actionStartServer, false);
InvocationDialogHelper helper = InvocationDialogHelper.waitFor();
helper.dismissWithArguments(Map.ofEntries(
Map.entry("address", "localhost"),
Map.entry("port", 0)));
waitForPass(() -> assertTrue(traceRmiService.isServerStarted()));
waitForPass(() -> assertFalse(provider.actionStartServer.isEnabled()));
traceRmiService.stopServer();
waitForPass(() -> assertTrue(provider.actionStartServer.isEnabled()));
}
@Test
public void testActionStopServer() throws Exception {
waitForPass(() -> assertFalse(provider.actionStopServer.isEnabled()));
traceRmiService.startServer();
waitForSwing();
performEnabledAction(provider, provider.actionStopServer, true);
assertFalse(traceRmiService.isServerStarted());
waitForPass(() -> assertFalse(provider.actionStopServer.isEnabled()));
}
@Test
public void testActionCloseOnAcceptor() throws Exception {
TraceRmiAcceptor acceptor =
traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
TraceRmiAcceptorNode node =
TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode).get(acceptor);
assertNotNull(node);
provider.tree.setSelectedNode(node);
// Tree uses a task queue for selection requests
waitForPass(() -> assertEquals(node, Unique.assertOne(provider.tree.getSelectedNodes())));
performEnabledAction(provider, provider.actionCloseConnection, true);
try {
acceptor.accept();
fail();
}
catch (CancelledException e) {
// pass
}
}
@Test
public void testActionCloseOnConnection() throws Exception {
try (Cx cx = Cx.connect(traceRmiService, "Test client")) {
TraceRmiConnectionNode node =
TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
.get(cx.connection);
assertNotNull(node);
provider.tree.setSelectedNode(node);
// Tree uses a task queue for selection requests
waitForPass(
() -> assertEquals(node, Unique.assertOne(provider.tree.getSelectedNodes())));
performEnabledAction(provider, provider.actionCloseConnection, true);
waitForPass(() -> assertTrue(cx.connection.isClosed()));
}
}
@Test
public void testActionCloseAll() throws Exception {
traceRmiService.startServer();
TraceRmiAcceptor acceptor =
traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
try (Cx cx = Cx.connect(traceRmiService, "Test client")) {
performEnabledAction(provider, provider.actionCloseAll, true);
waitForPass(() -> assertFalse(traceRmiService.isServerStarted()));
waitForPass(() -> assertTrue(cx.connection.isClosed()));
try {
acceptor.accept();
fail();
}
catch (CancelledException e) {
// pass
}
}
}
@Test
public void testServerNode() throws Exception {
TraceRmiServerNode node = TraceRmiConnectionTreeHelper.getServerNode(provider.rootNode);
assertEquals("Server: CLOSED", node.getDisplayText());
traceRmiService.startServer();
waitForPass(() -> assertEquals("Server: LISTENING " + traceRmiService.getServerAddress(),
node.getDisplayText()));
traceRmiService.stopServer();
waitForPass(() -> assertEquals("Server: CLOSED", node.getDisplayText()));
}
@Test
public void testAcceptHasNode() throws Exception {
TraceRmiAcceptor acceptor =
traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
TraceRmiAcceptorNode node =
TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode).get(acceptor);
assertNotNull(node);
assertEquals("ACCEPTING: " + acceptor.getAddress(), node.getDisplayText());
}
@Test
public void testAcceptThenCancelNoNode() throws Exception {
TraceRmiAcceptor acceptor =
traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
assertNotNull(
TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode).get(acceptor));
acceptor.cancel();
waitForPass(() -> traceRmiService.getAllAcceptors().isEmpty());
assertNull(
TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode).get(acceptor));
}
record Cx(SocketChannel channel, TestTraceRmiClient client,
TraceRmiConnection connection)
implements AutoCloseable {
public static Cx complete(TraceRmiAcceptor acceptor, String description)
throws IOException, CancelledException {
SocketChannel channel = null;
TraceRmiConnection connection = null;
try {
channel = SocketChannel.open(acceptor.getAddress());
TestTraceRmiClient client = new TestTraceRmiClient(channel);
client.sendNegotiate(description);
connection = acceptor.accept();
client.recvNegotiate();
return new Cx(channel, client, connection);
}
catch (Throwable t) {
if (channel != null) {
channel.close();
}
if (connection != null) {
connection.close();
}
throw t;
}
}
public static Cx toServer(TraceRmiService service, String description) throws IOException {
SocketChannel channel = null;
try {
channel = SocketChannel.open(service.getServerAddress());
TestTraceRmiClient client = new TestTraceRmiClient(channel);
client.sendNegotiate(description);
client.recvNegotiate();
return new Cx(channel, client,
waitForPass(() -> Unique.assertOne(service.getAllConnections())));
}
catch (Throwable t) {
if (channel != null) {
channel.close();
}
throw t;
}
}
public static Cx connect(TraceRmiService service, String description)
throws IOException, InterruptedException, ExecutionException, TimeoutException {
SocketChannel channel = null;
CompletableFuture<TraceRmiConnection> future = null;
try (ServerSocketChannel server = ServerSocketChannel.open()) {
server.bind(new InetSocketAddress("localhost", 0), 1);
future = CompletableFuture.supplyAsync(() -> {
try {
return service.connect(server.getLocalAddress());
}
catch (IOException e) {
return ExceptionUtils.rethrow(e);
}
});
channel = server.accept();
TestTraceRmiClient client = new TestTraceRmiClient(channel);
client.sendNegotiate(description);
client.recvNegotiate();
return new Cx(channel, client, future.get(1, TimeUnit.SECONDS));
}
catch (Throwable t) {
if (channel != null) {
channel.close();
}
throw t;
}
}
@Override
public void close() throws Exception {
connection.close();
channel.close();
}
}
@Test
public void testAcceptThenSuccessNodes() throws Exception {
TraceRmiAcceptor acceptor =
traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
assertNotNull(
TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode).get(acceptor));
try (Cx cx = Cx.complete(acceptor, "Test client")) {
waitForPass(() -> traceRmiService.getAllAcceptors().isEmpty());
waitForPass(() -> assertNull(
TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode)
.get(acceptor)));
waitForPass(() -> assertEquals(cx.connection,
Unique.assertOne(traceRmiService.getAllConnections())));
TraceRmiConnectionNode node =
TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
.get(cx.connection);
assertNotNull(node);
assertEquals("Test client at " + cx.connection.getRemoteAddress(),
node.getDisplayText());
}
}
@Test
public void testServerConnectNode() throws Exception {
traceRmiService.startServer();
try (Cx cx = Cx.toServer(traceRmiService, "Test client")) {
waitForPass(() -> traceRmiService.getAllAcceptors().isEmpty());
TraceRmiConnectionNode node = waitForValue(
() -> TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
.get(cx.connection));
assertEquals("Test client at " + cx.connection.getRemoteAddress(),
node.getDisplayText());
}
}
@Test
public void testConnectThenSuccessNodes() throws Exception {
try (Cx cx = Cx.connect(traceRmiService, "Test client")) {
waitForPass(() -> assertEquals(cx.connection,
Unique.assertOne(traceRmiService.getAllConnections())));
TraceRmiConnectionNode node =
TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
.get(cx.connection);
assertNotNull(node);
assertEquals("Test client at " + cx.connection.getRemoteAddress(),
node.getDisplayText());
}
}
@Test
public void testFrontEndCloseNoNodes() throws Exception {
TraceRmiAcceptor acceptor =
traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
try (Cx cx = Cx.complete(acceptor, "Test client")) {
assertNotNull(TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
.get(cx.connection));
cx.connection.close();
waitForPass(() -> assertTrue(traceRmiService.getAllConnections().isEmpty()));
waitForPass(() -> assertNull(
TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
.get(cx.connection)));
}
}
@Test
public void testBackEndCloseNoNodes() throws Exception {
TraceRmiAcceptor acceptor =
traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
try (Cx cx = Cx.complete(acceptor, "Test client")) {
assertNotNull(TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
.get(cx.connection));
cx.channel.close();
waitForPass(() -> assertTrue(traceRmiService.getAllConnections().isEmpty()));
waitForPass(() -> assertNull(
TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
.get(cx.connection)));
}
}
@Test
public void testActivateTargetNode() throws Exception {
SchemaContext ctx = XmlSchemaContext.deserialize("""
<context>
<schema name="Root" elementResync="NEVER" attributeResync="NEVER" />
</context>
""");
try (Cx cx = Cx.connect(traceRmiService, "Test client")) {
cx.client.createTrace(1, "bash");
try (Tx tx = cx.client.new Tx(1, 1, "Create snapshots")) {
cx.client.snapshot(1, 0, "First snapshot");
cx.client.createRootObject(1, ctx.getSchema(new SchemaName("Root")));
cx.client.snapshot(1, 1, "Stepped");
}
cx.client.activate(1, "");
Target target = waitForValue(() -> traceManager.getCurrent().getTarget());
TraceRmiTargetNode node =
TraceRmiConnectionTreeHelper.getTargetNodeMap(provider.rootNode).get(target);
assertEquals("bash (snap=1)", node.getDisplayText());
provider.tree.setSelectedNode(node);
// Tree uses a task queue for selection requests
waitForPass(
() -> assertEquals(node, Unique.assertOne(provider.tree.getSelectedNodes())));
traceManager.activateSnap(0);
waitForPass(() -> {
assertEquals(0, traceManager.getCurrentSnap());
assertEquals(ControlMode.RO_TRACE,
controlService.getCurrentMode(target.getTrace()));
});
triggerEnter(provider.tree);
waitForPass(() -> {
assertEquals(1, traceManager.getCurrentSnap());
assertEquals(ControlMode.RO_TARGET,
controlService.getCurrentMode(target.getTrace()));
});
}
}
}

View file

@ -0,0 +1,43 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
import java.util.Map;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
import ghidra.debug.api.tracermi.TraceRmiConnection;
public class TraceRmiConnectionTreeHelper {
public static Map<TraceRmiAcceptor, TraceRmiAcceptorNode> getAcceptorNodeMap(
TraceRmiServiceNode serviceNode) {
return serviceNode.acceptorNodes;
}
public static Map<TraceRmiConnection, TraceRmiConnectionNode> getConnectionNodeMap(
TraceRmiServiceNode serviceNode) {
return serviceNode.connectionNodes;
}
public static Map<Target, TraceRmiTargetNode> getTargetNodeMap(
TraceRmiServiceNode serviceNode) {
return serviceNode.targetNodes;
}
public static TraceRmiServerNode getServerNode(TraceRmiServiceNode serviceNode) {
return serviceNode.serverNode;
}
}

View file

@ -0,0 +1,69 @@
/* ###
* 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.tracermi;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
public class ProtobufSocket<T extends AbstractMessage> {
public interface Decoder<T> {
T decode(ByteBuffer buf) throws InvalidProtocolBufferException;
}
private final ByteBuffer lenSend = ByteBuffer.allocate(4);
private final ByteBuffer lenRecv = ByteBuffer.allocate(4);
private final SocketChannel channel;
private final Decoder<T> decoder;
public ProtobufSocket(SocketChannel channel, Decoder<T> decoder) {
this.channel = channel;
this.decoder = decoder;
}
public void send(T msg) throws IOException {
synchronized (lenSend) {
lenSend.clear();
lenSend.putInt(msg.getSerializedSize());
lenSend.flip();
channel.write(lenSend);
for (ByteBuffer buf : msg.toByteString().asReadOnlyByteBufferList()) {
channel.write(buf);
}
}
}
public T recv() throws IOException {
synchronized (lenRecv) {
lenRecv.clear();
while (lenRecv.hasRemaining()) {
channel.read(lenRecv);
}
lenRecv.flip();
int len = lenRecv.getInt();
// This is just for testing, so littering on the heap is okay.
ByteBuffer buf = ByteBuffer.allocate(len);
while (buf.hasRemaining()) {
channel.read(buf);
}
buf.flip();
return decoder.decode(buf);
}
}
}

View file

@ -0,0 +1,162 @@
/* ###
* 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.tracermi;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.target.schema.XmlSchemaContext;
import ghidra.framework.Application;
import ghidra.rmi.trace.TraceRmi.*;
import ghidra.rmi.trace.TraceRmi.Compiler;
public class TestTraceRmiClient {
final ProtobufSocket<RootMessage> socket;
public TestTraceRmiClient(SocketChannel channel) {
this.socket = new ProtobufSocket<>(channel, RootMessage::parseFrom);
}
public void sendNegotiate(String description) throws IOException {
socket.send(RootMessage.newBuilder()
.setRequestNegotiate(RequestNegotiate.newBuilder()
.setVersion(TraceRmiHandler.VERSION)
.setDescription(description))
.build());
}
public void recvNegotiate() throws IOException {
assertEquals(RootMessage.newBuilder()
.setReplyNegotiate(ReplyNegotiate.newBuilder()
.setDescription(
Application.getName() + " " +
Application.getApplicationVersion()))
.build(),
socket.recv());
}
public void createTrace(int id, String name) throws IOException {
socket.send(RootMessage.newBuilder()
.setRequestCreateTrace(RequestCreateTrace.newBuilder()
.setOid(DomObjId.newBuilder()
.setId(id))
.setLanguage(Language.newBuilder()
.setId("Toy:BE:64:default"))
.setCompiler(Compiler.newBuilder()
.setId("default"))
.setPath(FilePath.newBuilder()
.setPath("test/" + name)))
.build());
assertEquals(RootMessage.newBuilder()
.setReplyCreateTrace(ReplyCreateTrace.newBuilder())
.build(),
socket.recv());
}
public void startTx(int traceId, int txId, String description) throws IOException {
socket.send(RootMessage.newBuilder()
.setRequestStartTx(RequestStartTx.newBuilder()
.setOid(DomObjId.newBuilder()
.setId(traceId))
.setTxid(TxId.newBuilder().setId(txId))
.setDescription(description))
.build());
assertEquals(RootMessage.newBuilder()
.setReplyStartTx(ReplyStartTx.newBuilder())
.build(),
socket.recv());
}
public void endTx(int traceId, int txId) throws IOException {
socket.send(RootMessage.newBuilder()
.setRequestEndTx(RequestEndTx.newBuilder()
.setOid(DomObjId.newBuilder()
.setId(traceId))
.setTxid(TxId.newBuilder().setId(txId))
.setAbort(false))
.build());
assertEquals(RootMessage.newBuilder()
.setReplyEndTx(ReplyEndTx.newBuilder())
.build(),
socket.recv());
}
public class Tx implements AutoCloseable {
private final int traceId;
private final int txId;
public Tx(int traceId, int txId, String description) throws IOException {
this.traceId = traceId;
this.txId = txId;
startTx(traceId, txId, description);
}
@Override
public void close() throws Exception {
endTx(traceId, txId);
}
}
public void snapshot(int traceId, long snap, String description) throws IOException {
socket.send(RootMessage.newBuilder()
.setRequestSnapshot(RequestSnapshot.newBuilder()
.setOid(DomObjId.newBuilder()
.setId(traceId))
.setSnap(Snap.newBuilder()
.setSnap(snap))
.setDescription(description))
.build());
assertEquals(RootMessage.newBuilder()
.setReplySnapshot(ReplySnapshot.newBuilder())
.build(),
socket.recv());
}
public void createRootObject(int traceId, TargetObjectSchema schema) throws IOException {
String xmlCtx = XmlSchemaContext.serialize(schema.getContext());
socket.send(RootMessage.newBuilder()
.setRequestCreateRootObject(RequestCreateRootObject.newBuilder()
.setOid(DomObjId.newBuilder()
.setId(traceId))
.setSchemaContext(xmlCtx)
.setRootSchema(schema.getName().toString()))
.build());
assertEquals(RootMessage.newBuilder()
.setReplyCreateObject(ReplyCreateObject.newBuilder()
.setObject(ObjSpec.newBuilder()
.setId(0)))
.build(),
socket.recv());
}
public void activate(int traceId, String path) throws IOException {
socket.send(RootMessage.newBuilder()
.setRequestActivate(RequestActivate.newBuilder()
.setOid(DomObjId.newBuilder()
.setId(traceId))
.setObject(ObjSpec.newBuilder()
.setPath(ObjPath.newBuilder()
.setPath(path))))
.build());
assertEquals(RootMessage.newBuilder()
.setReplyActivate(ReplyActivate.newBuilder())
.build(),
socket.recv());
}
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
package ghidra.app.plugin.core.debug.service.tracermi;
import java.io.IOException;
import java.net.InetSocketAddress;
@ -28,6 +28,7 @@ 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.target.Target;
import ghidra.debug.api.tracermi.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
@ -89,6 +90,11 @@ public class TestTraceRmiConnection implements TraceRmiConnection {
}
}
@Override
public String getDescription() {
return "Test Trace RMI connnection";
}
@Override
public SocketAddress getRemoteAddress() {
return new InetSocketAddress("localhost", 0);
@ -185,4 +191,9 @@ public class TestTraceRmiConnection implements TraceRmiConnection {
throw new AssertionError(e);
}
}
@Override
public Collection<Target> getTargets() {
return List.copyOf(targets.values());
}
}