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

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

View File

@ -24,28 +24,27 @@ import agent.frida.manager.FridaThread;
import agent.frida.manager.evt.FridaStateChangedEvent;
import agent.frida.manager.evt.FridaThreadSelectedEvent;
import agent.frida.manager.impl.FridaManagerImpl;
import ghidra.app.plugin.core.debug.service.workflow.DebuggerWorkflowServicePlugin;
import ghidra.app.services.DebuggerBot;
import ghidra.app.services.DebuggerBotInfo;
import ghidra.app.services.DebuggerWorkflowFrontEndService;
import ghidra.dbg.*;
import ghidra.dbg.error.DebuggerMemoryAccessException;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.util.DebuggerCallbackReorderer;
import ghidra.debug.api.workflow.DebuggerBot;
import ghidra.debug.api.workflow.DebuggerBotInfo;
import ghidra.framework.options.annotation.HelpInfo;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
import ghidra.util.datastruct.PrivatelyQueuedListener;
@DebuggerBotInfo( //
description = "Link debugger to Frida", //
details = "Listens for debuggers to add state to Frida.", //
help = @HelpInfo(anchor = "link_frida"), //
enabledByDefault = true //
)
@DebuggerBotInfo(
description = "Link debugger to Frida",
details = "Listens for debuggers to add state to Frida.",
help = @HelpInfo(anchor = "link_frida"),
enabledByDefault = true)
public class FridaDebuggerBot implements DebuggerBot {
private DebuggerWorkflowServicePlugin plugin;
private DebuggerWorkflowFrontEndService service;
private FridaObjectListener listener = new FridaObjectListener();
private List<DebuggerObjectModel> models = new ArrayList<>();
@ -53,18 +52,18 @@ public class FridaDebuggerBot implements DebuggerBot {
private FridaManagerImpl manager;
@Override
public void enable(DebuggerWorkflowServicePlugin wp) {
this.plugin = wp;
public void enable(DebuggerWorkflowFrontEndService service) {
this.service = service;
}
@Override
public boolean isEnabled() {
return plugin != null;
return service != null;
}
@Override
public void disable() {
plugin = null;
service = null;
}
@Override
@ -73,8 +72,9 @@ public class FridaDebuggerBot implements DebuggerBot {
if (model instanceof FridaModelImpl) {
primary = (FridaModelImpl) model;
manager = primary.getManager();
} else {
model.addModelListener(getListener(), true);
}
else {
model.addModelListener(getListener(), true);
}
}
@ -84,15 +84,16 @@ public class FridaDebuggerBot implements DebuggerBot {
if (model instanceof FridaModelImpl) {
primary = null;
manager = null;
} else {
model.removeModelListener(getListener());
}
else {
model.removeModelListener(getListener());
}
}
public DebuggerModelListener getListener() {
return listener.queue.in;
}
private Object findMatchingObject(TargetObject object) {
if (object instanceof TargetProcess) {
String id = Long.toHexString(((TargetProcess) object).getPid());
@ -109,7 +110,7 @@ public class FridaDebuggerBot implements DebuggerBot {
}
return null;
}
class FridaObjectListener extends AnnotatedDebuggerAttributeListener {
protected final DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this);
protected final PrivatelyQueuedListener<DebuggerModelListener> queue =
@ -158,7 +159,7 @@ public class FridaDebuggerBot implements DebuggerBot {
if (localObject != null) {
FridaThreadInfo info = new FridaThreadInfo((FridaThread) localObject);
manager.processEvent(new FridaThreadSelectedEvent(info));
}
}
}
}
}

View File

@ -0,0 +1,58 @@
#!/usr/bin/bash
## ###
# 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.
##
#@title gdb
#@desc <html><body width="300px">
#@desc <h3>Launch with <tt>gdb</tt></h3>
#@desc <p>This will launch the target on the local machine using <tt>gdb</tt>. GDB must already
#@desc be installed on your system, and it must embed the Python 3 interpreter. You will also
#@desc need <tt>protobuf</tt> and <tt>psutil</tt> installed for Python 3.
#@desc </body></html>
#@menu-group local
#@icon icon.debugger
#@help TraceRmiLauncherServicePlugin#gdb
#@enum StartCmd:str run start starti
#@env OPT_GDB_PATH:str="gdb" "Path to gdb" "The path to gdb. Omit the full path to resolve using the system PATH."
#@env OPT_START_CMD:StartCmd="start" "Run command" "The gdb command to actually run the target."
#@arg :str "Image" "The target binary executable image"
#@args "Arguments" "Command-line arguments to pass to the target"
#@tty TTY_TARGET
if [ -d ${GHIDRA_HOME}/ghidra/.git ]
then
export PYTHONPATH=$GHIDRA_HOME/ghidra/Ghidra/Debug/Debugger-agent-gdb/build/pypkg/src:$GHIDRA_HOME/ghidra/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH
elif [ -d ${GHIDRA_HOME}/.git ]
then
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-agent-gdb/build/pypkg/src:$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH
else
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-agent-gdb/pypkg/src:$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH
fi
target_image="$1"
shift
target_args="$@"
"$OPT_GDB_PATH" \
-ex "set pagination off" \
-ex "python import ghidragdb" \
-ex "file \"$target_image\"" \
-ex "set args $target_args" \
-ex "set inferior-tty $TTY_TARGET" \
-ex "ghidra trace connect \"$GHIDRA_TRACE_RMI_ADDR\"" \
-ex "ghidra trace start" \
-ex "ghidra trace sync-enable" \
-ex "$OPT_START_CMD" \
-ex "set pagination on"

View File

@ -24,7 +24,7 @@ class GhidraHookPrefix(gdb.Command):
"""Commands for exporting data to a Ghidra trace"""
def __init__(self):
super().__init__('ghidra-hook', gdb.COMMAND_NONE, prefix=True)
super().__init__('hooks-ghidra', gdb.COMMAND_NONE, prefix=True)
GhidraHookPrefix()
@ -386,7 +386,7 @@ def on_before_prompt():
# This will be called by a catchpoint
class GhidraTraceEventMemoryCommand(gdb.Command):
def __init__(self):
super().__init__('ghidra-hook event-memory', gdb.COMMAND_NONE)
super().__init__('hooks-ghidra event-memory', gdb.COMMAND_NONE)
def invoke(self, argument, from_tty):
self.dont_repeat()
@ -401,10 +401,10 @@ def cmd_hook(name):
class _ActiveCommand(gdb.Command):
def __init__(self):
# It seems we can't hook commands using the Python API....
super().__init__(f"ghidra-hook def-{name}", gdb.COMMAND_USER)
super().__init__(f"hooks-ghidra def-{name}", gdb.COMMAND_USER)
gdb.execute(f"""
define {name}
ghidra-hook def-{name}
hooks-ghidra def-{name}
end
""")
@ -474,7 +474,7 @@ def install_hooks():
catch syscall group:memory
commands
silent
ghidra-hook event-memory
hooks-ghidra event-memory
cont
end
""")

View File

@ -13,22 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.utils;
import java.awt.Component;
import java.awt.KeyboardFocusManager;
apply from: "${rootProject.projectDir}/gradle/javaProject.gradle"
apply from: "${rootProject.projectDir}/gradle/jacocoProject.gradle"
apply from: "${rootProject.projectDir}/gradle/javaTestProject.gradle"
apply from: "${rootProject.projectDir}/gradle/distributableGhidraModule.gradle"
import javax.swing.SwingUtilities;
apply plugin: 'eclipse'
eclipse.project.name = 'Debug Debugger-api'
public interface FocusUtils {
static boolean isFocusIn(Component c) {
Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
if (c == owner) {
return true;
}
if (owner == null) {
return false;
}
return SwingUtilities.isDescendingFrom(owner, c);
}
dependencies {
api project(':SoftwareModeling')
api project(':Framework-TraceModeling')
api project(':Emulation')
}

View File

@ -0,0 +1,2 @@
##VERSION: 2.0
Module.manifest||GHIDRA||||END|

View File

@ -21,12 +21,11 @@ import javax.swing.Icon;
import docking.ActionContext;
import docking.action.DockingActionIf;
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin;
import ghidra.dbg.DebuggerConsoleLogger;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.util.HTMLUtilities;
@ServiceInfo(defaultProvider = DebuggerConsolePlugin.class)
@ServiceInfo(defaultProviderName = "ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin")
public interface DebuggerConsoleService extends DebuggerConsoleLogger {
/**

View File

@ -17,18 +17,19 @@ package ghidra.app.services;
import java.util.concurrent.CompletableFuture;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin;
import ghidra.debug.api.control.ControlMode;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.pcode.utils.Utils;
import ghidra.program.model.address.Address;
import ghidra.program.model.lang.*;
import ghidra.program.model.lang.Register;
import ghidra.program.model.lang.RegisterValue;
import ghidra.program.model.mem.LiveMemoryHandler;
import ghidra.trace.model.Trace;
import ghidra.trace.model.program.TraceProgramView;
@ServiceInfo(
defaultProvider = DebuggerControlServicePlugin.class,
defaultProviderName = "ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin",
description = "Centralized service for modifying machine states")
public interface DebuggerControlService {
interface StateEditor {

View File

@ -18,7 +18,8 @@ package ghidra.app.services;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import ghidra.app.plugin.core.debug.service.emulation.*;
import ghidra.debug.api.emulation.DebuggerPcodeEmulatorFactory;
import ghidra.debug.api.emulation.DebuggerPcodeMachine;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.trace.model.Trace;
import ghidra.trace.model.guest.TracePlatform;
@ -36,7 +37,8 @@ import ghidra.util.task.TaskMonitor;
* user. Scripts may interact with these managed emulators, or they may instantiate their own
* unmanaged emulators, without using this service.
*/
@ServiceInfo(defaultProvider = DebuggerEmulationServicePlugin.class)
@ServiceInfo(
defaultProviderName = "ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin")
public interface DebuggerEmulationService {
interface EmulationResult extends RunResult {

View File

@ -15,16 +15,14 @@
*/
package ghidra.app.services;
import ghidra.app.plugin.core.debug.gui.interpreters.DebuggerInterpreterConnection;
import ghidra.app.plugin.core.debug.gui.interpreters.DebuggerInterpreterPlugin;
import ghidra.dbg.target.TargetConsole;
import ghidra.dbg.target.TargetInterpreter;
import ghidra.debug.api.interpreter.DebuggerInterpreterConnection;
import ghidra.framework.plugintool.ServiceInfo;
@ServiceInfo( //
defaultProvider = DebuggerInterpreterPlugin.class, //
description = "Service for managing debugger interpreter panels" //
)
@ServiceInfo(
defaultProviderName = "ghidra.app.plugin.core.debug.gui.interpreters.DebuggerInterpreterPlugin",
description = "Service for managing debugger interpreter panels")
public interface DebuggerInterpreterService {
DebuggerInterpreterConnection showConsole(TargetConsole console);

View File

@ -15,10 +15,9 @@
*/
package ghidra.app.services;
import ghidra.app.plugin.core.debug.gui.action.LocationTrackingSpec;
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin;
import ghidra.app.plugin.core.debug.gui.listing.MultiBlendedListingBackgroundColorModel;
import ghidra.app.util.viewer.listingpanel.ListingPanel;
import ghidra.debug.api.action.LocationTrackingSpec;
import ghidra.debug.api.listing.MultiBlendedListingBackgroundColorModel;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.program.model.address.Address;
import ghidra.program.util.ProgramSelection;
@ -27,7 +26,7 @@ import ghidra.program.util.ProgramSelection;
* A service providing access to the main listing panel
*/
@ServiceInfo(
defaultProvider = DebuggerListingPlugin.class,
defaultProviderName = "ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin",
description = "Replacement CodeViewerService for Debugger")
public interface DebuggerListingService extends CodeViewerService {

View File

@ -20,19 +20,21 @@ import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import ghidra.app.plugin.core.debug.service.breakpoint.DebuggerLogicalBreakpointServicePlugin;
import ghidra.app.services.LogicalBreakpoint.State;
import ghidra.debug.api.breakpoint.LogicalBreakpoint;
import ghidra.debug.api.breakpoint.LogicalBreakpointsChangeListener;
import ghidra.debug.api.breakpoint.LogicalBreakpoint.State;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.util.*;
import ghidra.program.util.CodeUnitLocation;
import ghidra.program.util.ProgramLocation;
import ghidra.trace.model.Trace;
import ghidra.trace.model.breakpoint.TraceBreakpoint;
import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.trace.model.program.TraceProgramView;
@ServiceInfo( //
defaultProvider = DebuggerLogicalBreakpointServicePlugin.class,
@ServiceInfo(
defaultProviderName = "ghidra.app.plugin.core.debug.service.breakpoint.DebuggerLogicalBreakpointServicePlugin",
description = "Aggregate breakpoints for programs and traces")
public interface DebuggerLogicalBreakpointService {
/**

View File

@ -21,13 +21,11 @@ import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import ghidra.app.plugin.core.debug.mapping.DebuggerMappingOpinion;
import ghidra.app.plugin.core.debug.mapping.DebuggerTargetTraceMapper;
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceProxyPlugin;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.*;
import ghidra.debug.api.action.ActionSource;
import ghidra.debug.api.model.*;
import ghidra.framework.plugintool.PluginEvent;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.program.model.listing.Program;
@ -36,7 +34,7 @@ import ghidra.trace.model.thread.TraceThread;
import ghidra.util.datastruct.CollectionChangeListener;
@ServiceInfo(
defaultProvider = DebuggerModelServiceProxyPlugin.class,
defaultProviderName = "ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceProxyPlugin",
description = "Service for managing debug sessions and connections")
public interface DebuggerModelService {
/**

View File

@ -15,7 +15,7 @@
*/
package ghidra.app.services;
import ghidra.app.plugin.core.debug.mapping.DebuggerPlatformMapper;
import ghidra.debug.api.platform.DebuggerPlatformMapper;
import ghidra.trace.model.Trace;
import ghidra.trace.model.target.TraceObject;

View File

@ -18,9 +18,10 @@ package ghidra.app.services;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import ghidra.app.services.ModuleMapProposal.ModuleMapEntry;
import ghidra.app.services.RegionMapProposal.RegionMapEntry;
import ghidra.app.services.SectionMapProposal.SectionMapEntry;
import ghidra.debug.api.modules.*;
import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry;
import ghidra.debug.api.modules.RegionMapProposal.RegionMapEntry;
import ghidra.debug.api.modules.SectionMapProposal.SectionMapEntry;
import ghidra.framework.model.DomainFile;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program;

View File

@ -18,9 +18,8 @@ package ghidra.app.services;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin;
import ghidra.async.AsyncReference;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.model.DomainFile;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.program.model.listing.Program;
@ -35,7 +34,8 @@ import ghidra.util.TriConsumer;
/**
* The interface for managing open traces and navigating among them and their contents
*/
@ServiceInfo(defaultProvider = DebuggerTraceManagerServicePlugin.class)
@ServiceInfo(
defaultProviderName = "ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin")
public interface DebuggerTraceManagerService {
/**

View File

@ -17,15 +17,14 @@ package ghidra.app.services;
import java.util.Collection;
import ghidra.app.plugin.core.debug.gui.watch.DebuggerWatchesPlugin;
import ghidra.app.plugin.core.debug.gui.watch.WatchRow;
import ghidra.debug.api.watch.WatchRow;
import ghidra.framework.plugintool.ServiceInfo;
/**
* A service interface for controlling the Watches window
*/
@ServiceInfo(
defaultProvider = DebuggerWatchesPlugin.class,
defaultProviderName = "ghidra.app.plugin.core.debug.gui.watch.DebuggerWatchesPlugin",
description = "Service for managing watches")
public interface DebuggerWatchesService {
/**

View File

@ -0,0 +1,33 @@
/* ###
* 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.util.Collection;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.ServiceInfo;
@ServiceInfo(
defaultProviderName = "ghidra.app.plugin.core.debug.service.workflow.DebuggerWorkflowServicePlugin",
description = "Service for managing automatic debugger actions and analysis")
public interface DebuggerWorkflowFrontEndService extends DebuggerWorkflowService {
/**
* Get all the tools with the corresponding {@link DebuggerWorkflowToolService}
*
* @return the tools proxying this service
*/
Collection<PluginTool> getProxyingPluginTools();
}

View File

@ -17,14 +17,12 @@ package ghidra.app.services;
import java.util.Set;
import ghidra.app.plugin.core.debug.service.workflow.DebuggerWorkflowServiceProxyPlugin;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.debug.api.workflow.DebuggerBot;
import ghidra.framework.plugintool.PluginTool;
@ServiceInfo( //
defaultProvider = DebuggerWorkflowServiceProxyPlugin.class, //
description = "Service for managing automatic debugger actions and analysis" //
)
public interface DebuggerWorkflowService {
PluginTool getTool();
Set<DebuggerBot> getAllBots();
Set<DebuggerBot> getEnabledBots();

View File

@ -0,0 +1,25 @@
/* ###
* 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 ghidra.framework.plugintool.ServiceInfo;
@ServiceInfo(
defaultProviderName = "ghidra.app.plugin.core.debug.service.workflow.DebuggerWorkflowServiceProxyPlugin",
description = "Service for managing automatic debugger actions and analysis")
public interface DebuggerWorkflowToolService extends DebuggerWorkflowService {
}

View File

@ -0,0 +1,46 @@
/* ###
* 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.util.Collection;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOpinion;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.program.model.listing.Program;
/**
* The service for launching Trace RMI targets in the GUI.
*/
@ServiceInfo(
description = "Manages and presents launchers for Trace RMI Targets",
defaultProviderName = "ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin")
public interface TraceRmiLauncherService {
/**
* Get all of the installed opinions
*
* @return the opinions
*/
Collection<TraceRmiLaunchOpinion> getOpinions();
/**
* Get all offers for the given program
*
* @param program the program
* @return the offers
*/
Collection<TraceRmiLaunchOffer> getOffers(Program program);
}

View File

@ -0,0 +1,108 @@
/* ###
* 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 java.util.Collection;
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
import ghidra.debug.api.tracermi.TraceRmiConnection;
import ghidra.framework.plugintool.ServiceInfo;
/**
* A service (both in the Ghidra framework sense, and in the network sense) for connecting Trace
* RMI-based back-end debuggers.
*
* <p>
* This service connects to back-end debuggers, and/or allows back-end debuggers to connect to it.
* Either way, Ghidra becomes the front-end, acting as the Trace RMI server, and the back-end
* debugger acts as the Trace RMI client. The Ghidra front-end may also send control commands to the
* back-end, e.g., to step, resume, or suspend the target.
*/
@ServiceInfo(
description = "A service for accepting and creating Trace RMI connections",
defaultProviderName = "ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin")
public interface TraceRmiService {
/**
* Get the address (and port) of the Trace RMI TCP server
*
* @return
*/
SocketAddress getServerAddress();
/**
* Set the address (and port) of the Trace RMI TCP server
*
* @param serverAddress may be null to bind to ephemeral port
*/
void setServerAddress(SocketAddress serverAddress);
/**
* Start the Trace RMI TCP server
*
* @throws IOException
*/
void startServer() throws IOException;
/**
* Stop the Trace RMI TCP server
*/
void stopServer();
/**
* Check if the service is listening for inbound connections (other than those expected by
* {@link #acceptOne(SocketAddress)}).
*
* @return true if listening, false otherwise
*/
boolean isServerStarted();
/**
* Assuming a back-end debugger is listening, connect to it.
*
* @param address the address (and port) of the back-end system
* @return the connection
* @throws IOException if the connection failed
*/
TraceRmiConnection connect(SocketAddress address) throws IOException;
/**
* Prepare to accept a single connection by listening on the given address.
*
* <p>
* This essentially starts a server (separate from the one creased by {@link #startServer()})
* that will accept a single connection. The server is started by this method. The caller can
* then invoke (on the same thread) whatever back-end system or agent that is expected to
* connect back to this service. Assuming that system runs in the background, this thread can
* then invoke {@link TraceRmiAcceptor#accept()} to actually accept that connection. Once
* accepted, the service is terminated, and the server socket is closed. The client socket
* remains open.
*
* @param address the socket address to bind, or null for ephemeral
* @return the acceptor, which can be used to retrieve the ephemeral address and accept the
* actual connection
* @throws IOException on error
*/
TraceRmiAcceptor acceptOne(SocketAddress address) throws IOException;
/**
* Get all of the active connections
*
* @return the connections
*/
Collection<TraceRmiConnection> getAllConnections();
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.action;
/**
* Possible sources that drive actions or method invocations

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.gui.action;
package ghidra.debug.api.action;
import ghidra.program.model.address.Address;

View File

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.action;
package ghidra.debug.api.action;
import java.util.concurrent.CompletableFuture;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.Address;
import ghidra.program.util.ProgramLocation;

View File

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.action;
package ghidra.debug.api.action;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.AutoConfigState.ConfigFieldCodec;
import ghidra.trace.model.TraceAddressSnapRange;
@ -73,6 +73,9 @@ public interface LocationTrackingSpec {
return false;
}
if (!space.getAddressSpace().isMemorySpace()) {
if (current.getThread() == null) {
return false;
}
TraceMemorySpace memSpace = current.getTrace()
.getMemoryManager()
.getMemoryRegisterSpace(current.getThread(), current.getFrame(), false);

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.gui.action;
package ghidra.debug.api.action;
import java.util.*;

View File

@ -13,14 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.breakpoint;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import generic.theme.GIcon;
import ghidra.app.services.DebuggerLogicalBreakpointService;
import ghidra.framework.model.DomainObject;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Bookmark;
@ -29,6 +30,7 @@ import ghidra.program.util.ProgramLocation;
import ghidra.trace.model.Trace;
import ghidra.trace.model.breakpoint.TraceBreakpoint;
import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import resources.MultiIcon;
/**
* A logical breakpoint
@ -55,8 +57,31 @@ import ghidra.trace.model.breakpoint.TraceBreakpointKind;
* {@link LogicalBreakpointsChangeListener#breakpointRemoved(LogicalBreakpoint)}.
*/
public interface LogicalBreakpoint {
String BREAKPOINT_ENABLED_BOOKMARK_TYPE = "BreakpointEnabled";
String BREAKPOINT_DISABLED_BOOKMARK_TYPE = "BreakpointDisabled";
String ENABLED_BOOKMARK_TYPE = "BreakpointEnabled";
String DISABLED_BOOKMARK_TYPE = "BreakpointDisabled";
String NAME_MARKER_ENABLED = "Enabled Breakpoint";
String NAME_MARKER_DISABLED = "Disabled Breakpoint";
String NAME_MARKER_MIXED = "Mixed Breakpoint";
String NAME_MARKER_INEFF_EN = "Ineffective Enabled Breakpoint";
String NAME_MARKER_INEFF_DIS = "Ineffective Disabled Breakpoint";
String NAME_MARKER_INEFF_MIX = "Ineffective Mixed Breakpoint";
String NAME_MARKER_INCON_EN = "Inconsistent Enabled Breakpoint";
String NAME_MARKER_INCON_DIS = "Inconsistent Disabled Breakpoint";
String NAME_MARKER_INCON_MIX = "Inconsistent Mixed Breakpoint";
Icon ICON_OVERLAY_INCONSISTENT = new GIcon("icon.debugger.breakpoint.overlay.inconsistent");
Icon ICON_MARKER_ENABLED = new GIcon("icon.debugger.breakpoint.marker.enabled");
Icon ICON_MARKER_DISABLED = new GIcon("icon.debugger.breakpoint.marker.disabled");
Icon ICON_MARKER_MIXED = new GIcon("icon.debugger.breakpoint.marker.mixed");
Icon ICON_MARKER_INEFF_EN = new GIcon("icon.debugger.breakpoint.marker.ineffective.enabled");
Icon ICON_MARKER_INEFF_DIS = new GIcon("icon.debugger.breakpoint.marker.ineffective.disabled");
Icon ICON_MARKER_INEFF_MIX = new GIcon("icon.debugger.breakpoint.marker.ineffective.mixed");
Icon ICON_MARKER_INCON_EN = new MultiIcon(ICON_MARKER_ENABLED, ICON_OVERLAY_INCONSISTENT);
Icon ICON_MARKER_INCON_DIS = new MultiIcon(ICON_MARKER_DISABLED, ICON_OVERLAY_INCONSISTENT);
Icon ICON_MARKER_INCON_MIX = new MultiIcon(ICON_MARKER_MIXED, ICON_OVERLAY_INCONSISTENT);
/**
* The state of a logical breakpoint's program bookmark
@ -407,44 +432,44 @@ public interface LogicalBreakpoint {
/**
* The breakpoint is enabled, and all locations and its bookmark agree
*/
ENABLED(Mode.ENABLED, Consistency.NORMAL, DebuggerResources.NAME_BREAKPOINT_MARKER_ENABLED, DebuggerResources.ICON_BREAKPOINT_MARKER_ENABLED),
ENABLED(Mode.ENABLED, Consistency.NORMAL, NAME_MARKER_ENABLED, ICON_MARKER_ENABLED),
/**
* The breakpoint is disabled, and all locations and its bookmark agree
*/
DISABLED(Mode.DISABLED, Consistency.NORMAL, DebuggerResources.NAME_BREAKPOINT_MARKER_DISABLED, DebuggerResources.ICON_BREAKPOINT_MARKER_DISABLED),
DISABLED(Mode.DISABLED, Consistency.NORMAL, NAME_MARKER_DISABLED, ICON_MARKER_DISABLED),
/**
* There are multiple logical breakpoints at this address, and they are all saved and
* effective, but some are enabled, and some are disabled.
*/
MIXED(Mode.MIXED, Consistency.NORMAL, DebuggerResources.NAME_BREAKPOINT_MARKER_MIXED, DebuggerResources.ICON_BREAKPOINT_MARKER_MIXED),
MIXED(Mode.MIXED, Consistency.NORMAL, NAME_MARKER_MIXED, ICON_MARKER_MIXED),
/**
* The breakpoint is saved as enabled, but one or more trace locations are absent.
*/
INEFFECTIVE_ENABLED(Mode.ENABLED, Consistency.INEFFECTIVE, DebuggerResources.NAME_BREAKPOINT_MARKER_INEFF_EN, DebuggerResources.ICON_BREAKPOINT_MARKER_INEFF_EN),
INEFFECTIVE_ENABLED(Mode.ENABLED, Consistency.INEFFECTIVE, NAME_MARKER_INEFF_EN, ICON_MARKER_INEFF_EN),
/**
* The breakpoint is saved as disabled, and one or more trace locations are absent.
*/
INEFFECTIVE_DISABLED(Mode.DISABLED, Consistency.INEFFECTIVE, DebuggerResources.NAME_BREAKPOINT_MARKER_INEFF_DIS, DebuggerResources.ICON_BREAKPOINT_MARKER_INEFF_DIS),
INEFFECTIVE_DISABLED(Mode.DISABLED, Consistency.INEFFECTIVE, NAME_MARKER_INEFF_DIS, ICON_MARKER_INEFF_DIS),
/**
* There are multiple logical breakpoints at this address, and they are all saved, but at
* least one is ineffective; furthermore, some are enabled, and some are disabled.
*/
INEFFECTIVE_MIXED(Mode.MIXED, Consistency.INEFFECTIVE, DebuggerResources.NAME_BREAKPOINT_MARKER_INEFF_MIX, DebuggerResources.ICON_BREAKPOINT_MARKER_INEFF_MIX),
INEFFECTIVE_MIXED(Mode.MIXED, Consistency.INEFFECTIVE, NAME_MARKER_INEFF_MIX, ICON_MARKER_INEFF_MIX),
/**
* The breakpoint is enabled, and all locations agree, but the bookmark is absent or
* disagrees.
*/
INCONSISTENT_ENABLED(Mode.ENABLED, Consistency.INCONSISTENT, DebuggerResources.NAME_BREAKPOINT_MARKER_INCON_EN, DebuggerResources.ICON_BREAKPOINT_MARKER_INCON_EN),
INCONSISTENT_ENABLED(Mode.ENABLED, Consistency.INCONSISTENT, NAME_MARKER_INCON_EN, ICON_MARKER_INCON_EN),
/**
* The breakpoint is disabled, and all locations agree, but the bookmark is absent or
* disagrees.
*/
INCONSISTENT_DISABLED(Mode.DISABLED, Consistency.INCONSISTENT, DebuggerResources.NAME_BREAKPOINT_MARKER_INCON_DIS, DebuggerResources.ICON_BREAKPOINT_MARKER_INCON_DIS),
INCONSISTENT_DISABLED(Mode.DISABLED, Consistency.INCONSISTENT, NAME_MARKER_INCON_DIS, ICON_MARKER_INCON_DIS),
/**
* The breakpoint is terribly inconsistent: its locations disagree, and the bookmark may be
* absent.
*/
INCONSISTENT_MIXED(Mode.MIXED, Consistency.INCONSISTENT, DebuggerResources.NAME_BREAKPOINT_MARKER_INCON_MIX, DebuggerResources.ICON_BREAKPOINT_MARKER_INCON_MIX);
INCONSISTENT_MIXED(Mode.MIXED, Consistency.INCONSISTENT, NAME_MARKER_INCON_MIX, ICON_MARKER_INCON_MIX);
public final Mode mode;
public final Consistency consistency;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.breakpoint;
import java.util.Collection;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.control;
import java.nio.ByteBuffer;
import java.util.List;
@ -24,9 +24,11 @@ import javax.swing.Icon;
import db.Transaction;
import generic.theme.GIcon;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.services.*;
import ghidra.app.services.DebuggerTraceManagerService.ActivationCause;
import ghidra.async.AsyncUtils;
import ghidra.debug.api.model.TraceRecorder;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;

View File

@ -13,11 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.emulation;
package ghidra.debug.api.emulation;
import ghidra.app.plugin.core.debug.service.emulation.data.DefaultPcodeDebuggerAccess;
import ghidra.app.plugin.core.debug.service.emulation.data.PcodeDebuggerAccess;
import ghidra.app.services.TraceRecorder;
import ghidra.debug.api.model.TraceRecorder;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.guest.TracePlatform;
import ghidra.util.classfinder.ExtensionPoint;
@ -48,10 +46,8 @@ public interface DebuggerPcodeEmulatorFactory extends ExtensionPoint {
* @param recorder if applicable, the recorder for the trace's live target
* @return the emulator
*/
default DebuggerPcodeMachine<?> create(PluginTool tool, TracePlatform platform, long snap,
TraceRecorder recorder) {
return create(new DefaultPcodeDebuggerAccess(tool, recorder, platform, snap));
}
DebuggerPcodeMachine<?> create(PluginTool tool, TracePlatform platform, long snap,
TraceRecorder recorder);
/**
* Create the emulator

View File

@ -13,10 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.emulation;
package ghidra.debug.api.emulation;
import ghidra.pcode.exec.debug.auxiliary.AuxDebuggerEmulatorPartsFactory;
import ghidra.pcode.exec.debug.auxiliary.AuxDebuggerPcodeEmulator;
import ghidra.pcode.exec.trace.TracePcodeMachine;
/**

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.emulation.data;
package ghidra.debug.api.emulation;
import ghidra.pcode.emu.PcodeThread;
import ghidra.pcode.exec.trace.data.PcodeTraceAccess;

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.emulation.data;
package ghidra.debug.api.emulation;
import ghidra.pcode.exec.trace.data.PcodeTraceDataAccess;

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.emulation.data;
package ghidra.debug.api.emulation;
import java.util.concurrent.CompletableFuture;

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.emulation.data;
package ghidra.debug.api.emulation;
import java.util.concurrent.CompletableFuture;

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.gui.interpreters;
package ghidra.debug.api.interpreter;
import ghidra.app.plugin.core.interpreter.InterpreterConnection;
import ghidra.app.plugin.core.interpreter.InterpreterConsole;

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.gui.listing;
package ghidra.debug.api.listing;
import java.awt.Color;
import java.math.BigInteger;

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.mapping;
package ghidra.debug.api.model;
import java.util.Collection;
import java.util.List;

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.mapping;
package ghidra.debug.api.model;
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.mapping;
package ghidra.debug.api.model;
import ghidra.program.model.address.*;

View File

@ -13,15 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.model.launch;
package ghidra.debug.api.model;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.services.TraceRecorder;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.TargetLauncher;
@ -170,9 +168,7 @@ public interface DebuggerProgramLaunchOffer {
*
* @return the icon
*/
default Icon getIcon() {
return DebuggerResources.ICON_DEBUGGER;
}
Icon getIcon();
/**
* Get the text display on the parent menu for this offer

View File

@ -13,13 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.mapping;
package ghidra.debug.api.model;
import java.math.BigInteger;
import java.util.*;
import java.util.Map.Entry;
import ghidra.app.plugin.core.debug.register.RegisterTypeInfo;
import ghidra.dbg.target.TargetRegister;
import ghidra.dbg.util.ConversionUtils;
import ghidra.program.model.lang.Register;
@ -159,17 +158,6 @@ public interface DebuggerRegisterMapper {
*/
Register targetToTrace(TargetRegister tReg);
/**
* Get suggested type information for a given trace register
*
* <P>
* TODO: The recorder should apply this type when recording register values
*
* @param lReg the name of the trace register
* @return the default type information
*/
RegisterTypeInfo getDefaultTypeInfo(Register lReg);
/**
* Get the (base) registers on target that can be mapped
*

View File

@ -13,10 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.mapping;
package ghidra.debug.api.model;
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServicePlugin;
import ghidra.app.services.TraceRecorder;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.lang.CompilerSpec;
import ghidra.program.model.lang.Language;
import ghidra.trace.model.Trace;
@ -27,5 +26,5 @@ public interface DebuggerTargetTraceMapper {
CompilerSpec getTraceCompilerSpec();
TraceRecorder startRecording(DebuggerModelServicePlugin service, Trace trace);
TraceRecorder startRecording(PluginTool tool, Trace trace);
}

View File

@ -13,14 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.model;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper;
import ghidra.app.plugin.core.debug.mapping.DebuggerRegisterMapper;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.model;
/**
* A listener for state changes in a recorded target, or in the recorder itself

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.modules;
import java.util.Set;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.modules;
import ghidra.program.model.address.AddressRange;
import ghidra.program.model.listing.Program;

View File

@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.modules;
import java.util.*;
import java.util.stream.Collectors;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.trace.model.modules.TraceStaticMappingManager;

View File

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.modules;
import ghidra.app.services.ModuleMapProposal.ModuleMapEntry;
import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry;
import ghidra.program.model.address.AddressRange;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.modules.TraceModule;

View File

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.modules;
import ghidra.app.services.RegionMapProposal.RegionMapEntry;
import ghidra.debug.api.modules.RegionMapProposal.RegionMapEntry;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.trace.model.memory.TraceMemoryRegion;

View File

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.modules;
import ghidra.app.services.SectionMapProposal.SectionMapEntry;
import ghidra.debug.api.modules.SectionMapProposal.SectionMapEntry;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.trace.model.modules.TraceModule;

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.mapping;
package ghidra.debug.api.platform;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSetView;

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.mapping;
package ghidra.debug.api.platform;
public class DisassemblyResult {
public static final DisassemblyResult SUCCESS = new DisassemblyResult(true, null);

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;
package ghidra.debug.api.tracemgr;
import java.io.IOException;
import java.util.*;
@ -21,8 +21,8 @@ import java.util.*;
import org.jdom.Element;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.app.services.TraceRecorder;
import ghidra.dbg.target.TargetObject;
import ghidra.debug.api.model.TraceRecorder;
import ghidra.framework.data.DefaultProjectData;
import ghidra.framework.model.*;
import ghidra.framework.options.SaveState;

View File

@ -13,12 +13,12 @@
* 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.debug.api.tracermi;
import java.util.concurrent.*;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Future;
import ghidra.trace.model.target.TraceObject;
import ghidra.util.Swing;
/**
* The future result of invoking a {@link RemoteMethod}.
@ -40,31 +40,6 @@ import ghidra.util.Swing;
* a non-swing thread, e.g., a task thread or script thread, to wait for results, or chain
* callbacks.
*/
public class RemoteAsyncResult extends CompletableFuture<Object> {
final ValueDecoder decoder;
public interface RemoteAsyncResult extends CompletionStage<Object>, Future<Object> {
public RemoteAsyncResult() {
this.decoder = ValueDecoder.DEFAULT;
}
public RemoteAsyncResult(OpenTrace open) {
this.decoder = open;
}
@Override
public Object get() throws InterruptedException, ExecutionException {
if (Swing.isSwingThread()) {
throw new AssertionError("Refusing indefinite wait on Swing thread");
}
return super.get();
}
@Override
public Object get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (Swing.isSwingThread() && unit.toSeconds(timeout) > 1) {
throw new AssertionError("Refusing a timeout > 1 second on Swing thread");
}
return super.get(timeout, unit);
}
}

View File

@ -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.debug.api.tracermi;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@ -316,15 +316,4 @@ public interface RemoteMethod {
throw new TraceRmiError(e);
}
}
record RecordRemoteMethod(TraceRmiHandler handler, String name, Action action,
String description, Map<String, RemoteParameter> parameters, SchemaName retType)
implements RemoteMethod {
@Override
public RemoteAsyncResult invokeAsync(Map<String, Object> arguments) {
Trace trace = validate(arguments);
OpenTrace open = handler.getOpenTrace(trace);
return handler.invoke(open, name, arguments);
}
}
}

View File

@ -13,14 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.register;
package ghidra.debug.api.tracermi;
import ghidra.program.model.data.DataType;
import ghidra.program.model.lang.Register;
import ghidra.program.model.lang.RegisterValue;
import java.util.Map;
import java.util.Set;
public interface DynamicRegisterChangeListener {
void updateRegisterValue(RegisterValue registerValue);
import ghidra.debug.api.tracermi.RemoteMethod.Action;
void updateRegisterDataType(Register register, DataType dataType);
public interface RemoteMethodRegistry {
Map<String, RemoteMethod> all();
RemoteMethod get(String name);
Set<RemoteMethod> getByAction(Action action);
}

View File

@ -13,10 +13,14 @@
* 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.debug.api.tracermi;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
public record RemoteParameter(String name, SchemaName type, boolean required,
ValueSupplier defaultValue, String display, String description) {
public interface RemoteParameter {
SchemaName type();
boolean required();
Object getDefaultValue();
}

View File

@ -0,0 +1,51 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.debug.api.tracermi;
import java.io.IOException;
import java.net.SocketAddress;
import java.net.SocketException;
/**
* An acceptor to receive a single Trace RMI connection from a back-end
*/
public interface TraceRmiAcceptor {
/**
* Accept a single connection
*
* <p>
* This acceptor is no longer valid after the connection is accepted.
*
* @return the connection, if successful
* @throws IOException if there was an error
*/
TraceRmiConnection accept() throws IOException;
/**
* Get the address (and port) where the acceptor is listening
*
* @return the socket address
*/
SocketAddress getAddress();
/**
* Set the timeout
*
* @param millis the number of milliseconds after which an {@link #accept()} will time out.
* @throws SocketException if there's a protocol error
*/
void setTimeout(int millis) throws SocketException;
}

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.debug.api.tracermi;
import java.io.IOException;
import java.net.SocketAddress;
import java.util.concurrent.TimeoutException;
import ghidra.trace.model.Trace;
public interface TraceRmiConnection extends AutoCloseable {
SocketAddress getRemoteAddress();
RemoteMethodRegistry getMethods();
Trace waitForTrace(long timeoutMillis) throws TimeoutException;
long getLastSnapshot(Trace trace);
@Override
void close() throws IOException;
boolean isClosed();
void waitClosed();
}

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.debug.api.tracermi;
public class TraceRmiError extends RuntimeException {
public TraceRmiError() {

View File

@ -0,0 +1,288 @@
/* ###
* 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.api.tracermi;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.util.HelpLocation;
import ghidra.util.task.TaskMonitor;
/**
* An offer to launch a program with a given mechanism
*
* <p>
* Typically each offer is configured with the program it's going to launch, and knows how to work a
* specific connector and platform to obtain a target executing the program's image. The mechanisms
* may vary wildly from platform to platform.
*/
public interface TraceRmiLaunchOffer {
/**
* A terminal with some back-end element attached to it
*/
interface TerminalSession extends AutoCloseable {
@Override
void close() throws IOException;
/**
* Terminate the session without closing the terminal
*/
void terminate() throws IOException;
}
/**
* The result of launching a program
*
* <p>
* The launch may not always be completely successful. Instead of tearing things down, partial
* launches are left in place, in case the user wishes to repair/complete the steps manually. If
* the result includes a connection, then at least that was successful. If not, then the caller
* can choose how to treat the terminal sessions. If the cause of failure was an exception, it
* is included. If the launch succeeded, but module mapping failed, the result will include a
* trace and the exception. If an error occurred in the shell script, it may not be communicated
* here, but instead displayed only in the terminal.
*
* @param program the program associated with the launch attempt
* @param sessions any terminal sessions created while launching the back-end. If there are more
* than one, they are distinguished by launcher-defined keys. If there are no
* sessions, then there was likely a catastrophic error in the launcher.
* @param connection if the target connected back to Ghidra, that connection
* @param trace if the connection started a trace, the (first) trace it created
* @param exception optional error, if failed
*/
public record LaunchResult(Program program, Map<String, TerminalSession> sessions,
TraceRmiConnection connection, Trace trace, Throwable exception)
implements AutoCloseable {
@Override
public void close() throws Exception {
for (TerminalSession s : sessions.values()) {
s.close();
}
if (connection != null) {
connection.close();
}
}
}
/**
* When programmatically customizing launch configuration, describes callback timing relative to
* prompting the user.
*/
public enum RelPrompt {
/**
* The user is not prompted for parameters. This will be the only callback.
*/
NONE,
/**
* The user will be prompted. This callback can pre-populate suggested parameters. Another
* callback will be issued if the user does not cancel.
*/
BEFORE,
/**
* The user has confirmed the parameters. This callback can validate or override the users
* parameters. Overriding the user is discouraged. This is the final callback.
*/
AFTER;
}
public enum PromptMode {
/**
* The user is always prompted for parameters.
*/
ALWAYS,
/**
* The user is never prompted for parameters.
*/
NEVER,
/**
* The user is prompted after an error.
*/
ON_ERROR;
}
/**
* Callbacks for custom configuration when launching a program
*/
public interface LaunchConfigurator {
LaunchConfigurator NOP = new LaunchConfigurator() {};
/**
* Determine whether the user should be prompted to confirm launch parameters
*
* @return the prompt mode
*/
default PromptMode getPromptMode() {
return PromptMode.NEVER;
}
/**
* Re-write the launcher arguments, if desired
*
* @param launcher the launcher that will create the target
* @param arguments the arguments suggested by the offer or saved settings
* @param relPrompt describes the timing of this callback relative to prompting the user
* @return the adjusted arguments
*/
default Map<String, ?> configureLauncher(TraceRmiLaunchOffer offer,
Map<String, ?> arguments, RelPrompt relPrompt) {
return arguments;
}
}
/**
* Launch the program using the offered mechanism
*
* @param monitor a monitor for progress and cancellation
* @param configurator the configuration callback
* @return the launch result
*/
LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator);
/**
* Launch the program using the offered mechanism
*
* @param monitor a monitor for progress and cancellation
* @return the launch result
*/
default LaunchResult launchProgram(TaskMonitor monitor) {
return launchProgram(monitor, LaunchConfigurator.NOP);
}
/**
* A name so that this offer can be recognized later
*
* <p>
* The name is saved to configuration files, so that user preferences and priorities can be
* memorized. The opinion will generate each offer fresh each time, so it's important that the
* "same offer" have the same configuration name. Note that the name <em>cannot</em> depend on
* the program name, but can depend on the model factory and program language and/or compiler
* spec. This name cannot contain semicolons ({@ code ;}).
*
* @return the configuration name
*/
String getConfigName();
/**
* Get the icon displayed in the UI for this offer
*
* <p>
* Please take care when overriding this that the icon still clearly indicates the target will
* be executed. Changing it, e.g., to the same icon as "Step" would be an unwelcome prank. A
* more reasonable choice would be the standard {@code "icon.debugger"} plus an overlay, or the
* branding of the underlying technology, e.g., QEMU or GDB.
*
* @return the icon
*/
Icon getIcon();
/**
* Get the menu path subordinate to "Debugger.Debug [imagePath]" for this offer.
*
* <p>
* By default, this is just the title, i.e., the same as in the quick-launch drop-down menu. A
* package that introduces a large number of offers should override this method to organize
* them. A general rule of thumb is "no more than seven." Except at the level immediately under
* "Debug [imagePath]," no more than seven items should be presented to the user. In some cases,
* it may be more appropriate to group things using {@link #getMenuGroup() menu groups} rather
* than sub menus.
*
* <p>
* Organization is very much a matter of taste, but consider that you are cooperating with other
* packages to populate the launcher menu. The top level is especially contentious, but sub
* menus, if named appropriately, are presumed to belong to a single package.
*
* @return the path
*/
default List<String> getMenuPath() {
return List.of(getTitle());
}
/**
* Get the text displayed in the quick-launch drop-down menu.
*
* <p>
* No two offers should ever have the same title, even if they appear in different sub-menus.
* Otherwise, the user cannot distinguish the offers in the quick-launch drop-down menu.
*
* @return the menu title
*/
public String getTitle();
/**
* Get an HTML description of the connector
*
* @return the description
*/
public String getDescription();
/**
* Get the menu group for the offer
*
* <p>
* Especially for entries immediately under to "Debugger.Debug [imagePath]", specifies the menu
* group. A package that introduces a large number of offers should instead consider
* {@link #getMenuPath() sub menus}.
*
* @return the menu group
*/
default String getMenuGroup() {
return "";
}
/**
* Controls the position in the menu (within its group) of the entry
*
* <p>
* The menus will always be presented in the same order, barring any changes to the plugins or
* launcher properties. Groups are alphabetized and visually separated. Then sub groups are
* alphabetized, but <em>not</em> visually separated. Finally, offers are alphabetized by their
* final path element, usually the title.
*
* <p>
* The order of entries in the quick-launch drop-down menu is always most-recently to
* least-recently used. An entry that has never been used does not appear in the quick launch
* menu.
*/
default String getMenuOrder() {
return "";
}
/**
* Get the location for additional help about this specific offer
*
* <p>
* The default is just the entry on Trace RMI launchers in general.
*
* @return the location
*/
default HelpLocation getHelpLocation() {
return new HelpLocation("TraceRmiPlugin", "launch");
}
/**
* Get the parameter descriptions for the launcher
*
* @return the parameters
*/
Map<String, ParameterDescription<?>> getParameters();
}

View File

@ -0,0 +1,78 @@
/* ###
* 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.api.tracermi;
import java.util.Collection;
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 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.
* @param tool the current tool for context and services
* @return the offers. The order is not important since items are displayed alphabetically.
*/
public Collection<TraceRmiLaunchOffer> getOffers(Program program, PluginTool tool);
}

View File

@ -0,0 +1,233 @@
/* ###
* 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.api.watch;
import ghidra.docking.settings.Settings;
import ghidra.program.model.address.*;
import ghidra.program.model.data.DataType;
import ghidra.program.model.symbol.Symbol;
/**
* A row in the Watches table
*/
public interface WatchRow {
/**
* Get the Sleigh expression
*
* @return the expression
*/
String getExpression();
/**
* Set the Sleigh expression
*
* @param expression the expression
*/
void setExpression(String expression);
/**
* Get the data type for interpreting the value
*
* @return the data type
*/
DataType getDataType();
/**
* Set the data type for interpreting the value
*
* @param dataType the data type
*/
void setDataType(DataType dataType);
/**
* Get the settings on the data type
*
* <p>
* The returned settings may be modified, after which {@link #settingsChanged()} must be called.
* There is no {@code setSettings} method.
*
* @return the settings
*/
Settings getSettings();
/**
* Notify the row that the settings were changed
*
* @see #getSettings()
*/
void settingsChanged();
/**
* Get the address of the value, if it exists at one (memory or register)
*
* @return the address, or null
*/
Address getAddress();
/**
* Get the address range of the value, if it exists at an address (memory or register)
*
* @return the range, or null
*/
AddressRange getRange();
/**
* Get the complete set of all addresses read to evaluate the expression
*
* @return the address set, or null
*/
AddressSetView getReads();
/**
* Get the nearest symbol before the value's address, if applicable
*
* @return the symbol, or null
*/
Symbol getSymbol();
/**
* Get the raw value
*
* @return the value, or null
*/
byte[] getValue();
/**
* Get the raw value displayed as a string
*
* <p>
* For values in memory, this is a list of hex bytes. For others, it is a hex integer subject to
* the platform's endian.
*
* @return the value, or null
*/
String getRawValueString();
/**
* Get the number of bytes in the value
*
* @return the length, or 0 if evaluation failed
*/
int getValueLength();
/**
* Patch memory or register values such that the expression evaluates to the given raw value
*
* <p>
* This is only supported when {@link #isRawValueEditable} returns true. The given value must be
* a list of hex bytes (as returned by {@link #getRawValueString()}), or a hex integer subject
* to the platform's endian. Either is accepted, regardless of whether the value resides in
* memory.
*
* @see #getAddress()
* @param value the raw value as returned by {@link #getRawValueString()}
*/
void setRawValueString(String value);
/**
* Check if {@link #setRawValueString(String)} is supported
*
* <p>
* Setting the value may not be supported for many reasons: 1) The expression is not valid, 2)
* The expression could not be evaluated, 3) The value has no address or register. Reason 3 is
* somewhat strict, but reasonable, lest we have to implement a solver.
*
* @return whether or not the value can be modified
*/
boolean isRawValueEditable();
/**
* Get the value as returned by the data type
*
* @return the data-type defined value
*/
Object getValueObject();
/**
* Get the value as represented by the data type
*
* @return the value's data-type-defined representation
*/
String getValueString();
/**
* Patch memory or register values such that the expression evaluates to the given value
*
* <p>
* This is only supported when {@link #isValueEditable()} returns true. The given value must be
* encodable by the data type.
*
* @param value the desired value, as returned by {@link #getValueString()}
*/
void setValueString(String value);
/**
* Check if {@link #setValueString(String)} is supported
*
* <p>
* In addition to those reasons given in {@link #isRawValueEditable()}, setting the value may
* not be supported because: 1) No data type is set, or 2) The selected data type does not
* support encoding.
*
* @return whether or not the data-type interpreted value can be modified
*/
boolean isValueEditable();
/**
* If the watch could not be evaluated, get the cause
*
* @return the error
*/
Throwable getError();
/**
* If the watch could not be evaluated, get a message explaining why
*
* <p>
* This is essentially the message given by {@link #getError()}. If the exception does not
* provide a message, this will at least give the name of the exception class.
*
* @return the error message, or an empty string
*/
String getErrorMessage();
/**
* Check if the value given is actually known to be the value
*
* <p>
* If the value itself or any value encountered during the evaluation of the expression is
* stale, then the final value is considered stale, i.e., not known.
*
* @return true all memory and registers involved in the evaluation are known, false otherwise.
*/
boolean isKnown();
/**
* Check if the value has changed
*
* <p>
* "Changed" technically deals in navigation. In the case of a step, resume-and-break, patch,
* etc. This will detect the changes as expected. When manually navigating, this compares the
* two most recent times visited. Only the value itself is compared, without consideration for
* any intermediate values encountered during evaluation. Consider an array whose elements are
* all currently 0. An expression that dereferences an index in that array will be considered
* unchanged, even if the index did change.
*
* @return true if the value changed, false otherwise.
*/
boolean isChanged();
}

View File

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.workflow;
import ghidra.app.plugin.core.debug.service.workflow.DebuggerWorkflowServicePlugin;
import ghidra.app.services.DebuggerModelService;
import ghidra.app.services.DebuggerWorkflowFrontEndService;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.framework.options.AutoOptions;
import ghidra.framework.plugintool.PluginTool;
@ -153,15 +154,15 @@ public interface DebuggerBot extends ExtensionPoint {
* If {@link #isEnabled()} is already equal to the given -enabled- value, this method has no
* effect.
*
* @param plugin the front-end plugin, required if -enabled- is set
* @param service the front-end service, required if -enabled- is set
* @param enabled true to enable, false to disable
*/
default void setEnabled(DebuggerWorkflowServicePlugin plugin, boolean enabled) {
default void setEnabled(DebuggerWorkflowFrontEndService service, boolean enabled) {
if (isEnabled() == enabled) {
return;
}
if (enabled) {
enable(plugin);
enable(service);
}
else {
disable();
@ -171,9 +172,9 @@ public interface DebuggerBot extends ExtensionPoint {
/**
* Enable and initialize the bot
*
* @param plugin the front-end plugin
* @param service the front-end service
*/
void enable(DebuggerWorkflowServicePlugin plugin);
void enable(DebuggerWorkflowFrontEndService service);
/**
* Disable and dispose the bot

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.workflow;
import java.lang.annotation.*;

View File

@ -278,3 +278,10 @@ I wouldn't expect the user to manage multiple transaction objects.
The recommendation is that the CLI can have at most one active transaction.
For the user to open a second transaction may be considered an error.
Take care as you're coding (and likely re-using command logic) that you don't accidentally take or otherwise conflict with the CLI's transaction manager when processing an event.
# Regarding launcher shell scripts:
Need to document all the @metadata stuff
In particular, "Image" is a special parameter that will get the program executable by default.

View File

@ -27,6 +27,7 @@ eclipse.project.name = 'Debug Debugger-rmi-trace'
dependencies {
api project(':Pty')
api project(':Debugger')
api project(':Debugger-api')
}
task generateProtoPy {

View File

@ -1,6 +1,7 @@
##VERSION: 2.0
DEVNOTES.txt||GHIDRA||||END|
Module.manifest||GHIDRA||||END|
data/ExtensionPoint.manifest||GHIDRA||||END|
src/main/py/LICENSE||GHIDRA||||END|
src/main/py/README.md||GHIDRA||||END|
src/main/py/pyproject.toml||GHIDRA||||END|

View File

@ -0,0 +1 @@
TraceRmiLaunchOpinion

View File

@ -14,10 +14,8 @@
* limitations under the License.
*/
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.Objects;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin;
import ghidra.app.script.GhidraScript;
import ghidra.app.services.TraceRmiService;
@ -36,10 +34,9 @@ public class ConnectTraceRmiScript extends GhidraScript {
@Override
protected void run() throws Exception {
TraceRmiService service = getService();
TraceRmiHandler handler = service.connect(
new InetSocketAddress(askString("Trace RMI", "hostname", "localhost"), askInt("Trace RMI", "port")));
service.connect(new InetSocketAddress(askString("Trace RMI", "hostname", "localhost"),
askInt("Trace RMI", "port")));
println("Connected");
handler.start();
// if (askYesNo("Execute?", "Execute 'echo test'?")) {
// handler.getMethods().get("execute").invoke(Map.of("cmd", "script print('test')"));

View File

@ -16,9 +16,11 @@
import java.util.Map;
import java.util.Objects;
import ghidra.app.plugin.core.debug.service.rmi.trace.*;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin;
import ghidra.app.script.GhidraScript;
import ghidra.app.services.TraceRmiService;
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
import ghidra.debug.api.tracermi.TraceRmiConnection;
public class ListenTraceRmiScript extends GhidraScript {
@ -37,12 +39,11 @@ public class ListenTraceRmiScript extends GhidraScript {
TraceRmiAcceptor acceptor = service.acceptOne(null);
println("Listening at " + acceptor.getAddress());
TraceRmiHandler handler = acceptor.accept();
println("Connection from " + handler.getRemoteAddress());
handler.start();
TraceRmiConnection connection = acceptor.accept();
println("Connection from " + connection.getRemoteAddress());
while (askYesNo("Execute?", "Execute 'echo test'?")) {
handler.getMethods().get("execute").invoke(Map.of("cmd", "echo test"));
connection.getMethods().get("execute").invoke(Map.of("cmd", "echo test"));
}
}
}

View File

@ -0,0 +1,42 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.connection;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.services.TraceRmiService;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
@PluginInfo(
shortDescription = "GUI elements to manage Trace RMI connections",
description = """
Provides a panel for managing Trace RMI connections. The panel also allows users to
control the Trace RMI server and/or create manual connections.
""",
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.RELEASED,
servicesRequired = {
TraceRmiService.class,
})
public class TraceRmiConnectionManagerPlugin extends Plugin {
public TraceRmiConnectionManagerPlugin(PluginTool tool) {
super(tool);
}
// TODO: Add the actual provider. This will probably replace DebuggerTargetsPlugin.
}

View File

@ -0,0 +1,553 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.Charset;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.*;
import javax.swing.Icon;
import org.jdom.Element;
import org.jdom.JDOMException;
import db.Transaction;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
import ghidra.app.plugin.core.terminal.TerminalListener;
import ghidra.app.services.*;
import ghidra.async.AsyncUtils;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.util.ShellUtils;
import ghidra.debug.api.modules.*;
import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry;
import ghidra.debug.api.tracermi.*;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.*;
import ghidra.program.util.ProgramLocation;
import ghidra.pty.*;
import ghidra.trace.model.Trace;
import ghidra.trace.model.TraceLocation;
import ghidra.trace.model.modules.TraceModule;
import ghidra.util.MessageType;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
import ghidra.util.xml.XmlUtilities;
public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer {
public static final String PREFIX_DBGLAUNCH = "DBGLAUNCH_";
public static final String PARAM_DISPLAY_IMAGE = "Image";
protected record PtyTerminalSession(Terminal terminal, Pty pty, PtySession session,
Thread waiter) implements TerminalSession {
@Override
public void close() throws IOException {
terminate();
terminal.close();
}
@Override
public void terminate() throws IOException {
terminal.terminated();
session.destroyForcibly();
pty.close();
waiter.interrupt();
}
}
protected record NullPtyTerminalSession(Terminal terminal, Pty pty, String name)
implements TerminalSession {
@Override
public void close() throws IOException {
terminate();
terminal.close();
}
@Override
public void terminate() throws IOException {
terminal.terminated();
pty.close();
}
}
static class TerminateSessionTask extends Task {
private final TerminalSession session;
public TerminateSessionTask(TerminalSession session) {
super("Terminate Session", false, false, false);
this.session = session;
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
try {
session.close();
}
catch (IOException e) {
Msg.error(this, "Could not terminate: " + e, e);
}
}
}
protected final Program program;
protected final PluginTool tool;
protected final TerminalService terminalService;
public AbstractTraceRmiLaunchOffer(Program program, PluginTool tool) {
this.program = Objects.requireNonNull(program);
this.tool = Objects.requireNonNull(tool);
this.terminalService = Objects.requireNonNull(tool.getService(TerminalService.class));
}
protected int getTimeoutMillis() {
return 10000;
}
@Override
public Icon getIcon() {
return DebuggerResources.ICON_DEBUGGER;
}
protected Address getMappingProbeAddress() {
AddressIterator eepi = program.getSymbolTable().getExternalEntryPointIterator();
if (eepi.hasNext()) {
return eepi.next();
}
InstructionIterator ii = program.getListing().getInstructions(true);
if (ii.hasNext()) {
return ii.next().getAddress();
}
AddressSetView es = program.getMemory().getExecuteSet();
if (!es.isEmpty()) {
return es.getMinAddress();
}
if (!program.getMemory().isEmpty()) {
return program.getMinAddress();
}
return null; // I guess we won't wait for a mapping, then
}
protected CompletableFuture<Void> listenForMapping(
DebuggerStaticMappingService mappingService, TraceRmiConnection connection,
Trace trace) {
Address probeAddress = getMappingProbeAddress();
if (probeAddress == null) {
return AsyncUtils.NIL; // No need to wait on mapping of nothing
}
ProgramLocation probe = new ProgramLocation(program, probeAddress);
var result = new CompletableFuture<Void>() {
DebuggerStaticMappingChangeListener listener = (affectedTraces, affectedPrograms) -> {
if (!affectedPrograms.contains(program) &&
!affectedTraces.contains(trace)) {
return;
}
check();
};
protected void check() {
long snap = connection.getLastSnapshot(trace);
TraceLocation result = mappingService.getOpenMappedLocation(trace, probe, snap);
if (result == null) {
return;
}
complete(null);
mappingService.removeChangeListener(listener);
}
};
mappingService.addChangeListener(result.listener);
result.check();
result.exceptionally(ex -> {
mappingService.removeChangeListener(result.listener);
return null;
});
return result;
}
protected Collection<ModuleMapEntry> invokeMapper(TaskMonitor monitor,
DebuggerStaticMappingService mappingService, Trace trace) throws CancelledException {
Map<TraceModule, ModuleMapProposal> map = mappingService
.proposeModuleMaps(trace.getModuleManager().getAllModules(), List.of(program));
Collection<ModuleMapEntry> proposal = MapProposal.flatten(map.values());
mappingService.addModuleMappings(proposal, monitor, true);
return proposal;
}
private void saveLauncherArgs(Map<String, ?> args,
Map<String, ParameterDescription<?>> params) {
SaveState state = new SaveState();
for (ParameterDescription<?> param : params.values()) {
Object val = args.get(param.name);
if (val != null) {
ConfigStateField.putState(state, param.type.asSubclass(Object.class),
"param_" + param.name, val);
state.putLong("last", System.currentTimeMillis());
}
}
if (program != null) {
ProgramUserData userData = program.getProgramUserData();
try (Transaction tx = userData.openTransaction()) {
Element element = state.saveToXml();
userData.setStringProperty(PREFIX_DBGLAUNCH + getConfigName(),
XmlUtilities.toString(element));
}
}
}
/**
* Generate the default launcher arguments
*
* <p>
* It is not sufficient to simply take the defaults specified in the parameters. This must
* populate the arguments necessary to launch the requested program.
*
* @param params the parameters
* @return the default arguments
*/
@SuppressWarnings("unchecked")
protected Map<String, ?> generateDefaultLauncherArgs(
Map<String, ParameterDescription<?>> params) {
if (program == null) {
return Map.of();
}
Map<String, Object> map = new LinkedHashMap<String, Object>();
ParameterDescription<String> paramImage = null;
for (Entry<String, ParameterDescription<?>> entry : params.entrySet()) {
ParameterDescription<?> param = entry.getValue();
map.put(entry.getKey(), param.defaultValue);
if (PARAM_DISPLAY_IMAGE.equals(param.display)) {
if (param.type != String.class) {
Msg.warn(this, "'Image' parameter has unexpected type: " + paramImage.type);
}
paramImage = (ParameterDescription<String>) param;
}
}
if (paramImage != null) {
File imageFile = TraceRmiLauncherServicePlugin.getProgramPath(program);
if (imageFile != null) {
paramImage.set(map, imageFile.getAbsolutePath());
}
}
return map;
}
/**
* Prompt the user for arguments, showing those last used or defaults
*
* @param lastExc
*
* @param params the parameters of the model's launcher
* @param lastExc if re-prompting, an error to display
* @return the arguments given by the user, or null if cancelled
*/
protected Map<String, ?> promptLauncherArgs(LaunchConfigurator configurator,
Throwable lastExc) {
Map<String, ParameterDescription<?>> params = getParameters();
DebuggerMethodInvocationDialog dialog =
new DebuggerMethodInvocationDialog(tool, getTitle(), "Launch", getIcon());
dialog.setDescription(getDescription());
// NB. Do not invoke read/writeConfigState
Map<String, ?> args;
boolean reset = false;
do {
args =
configurator.configureLauncher(this, loadLastLauncherArgs(true), RelPrompt.BEFORE);
for (ParameterDescription<?> param : params.values()) {
Object val = args.get(param.name);
if (val != null) {
dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class),
val);
}
}
if (lastExc != null) {
dialog.setStatusText(lastExc.toString(), MessageType.ERROR);
}
else {
dialog.setStatusText("");
}
args = dialog.promptArguments(params);
if (args == null) {
// Cancelled
return null;
}
reset = dialog.isResetRequested();
if (reset) {
args = generateDefaultLauncherArgs(params);
}
saveLauncherArgs(args, params);
}
while (reset);
return args;
}
/**
* Load the arguments last used for this offer, or give the defaults
*
* <p>
* If there are no saved "last used" arguments, then this will return the defaults. If there are
* saved arguments, but they cannot be loaded, then this will behave differently depending on
* whether the user will be confirming the arguments. If there will be no prompt/confirmation,
* then this method must throw an exception in order to avoid launching with defaults, when the
* user may be expecting a customized launch. If there will be a prompt, then this may safely
* return the defaults, since the user will be given a chance to correct them.
*
* @param params the parameters of the model's launcher
* @param forPrompt true if the user will be confirming the arguments
* @return the loaded arguments, or defaults
*/
protected Map<String, ?> loadLastLauncherArgs(boolean forPrompt) {
/**
* TODO: Supposedly, per-program, per-user config stuff is being generalized for analyzers.
* Re-examine this if/when that gets merged
*/
if (program != null) {
Map<String, ParameterDescription<?>> params = getParameters();
ProgramUserData userData = program.getProgramUserData();
String property =
userData.getStringProperty(PREFIX_DBGLAUNCH + getConfigName(), null);
if (property != null) {
try {
Element element = XmlUtilities.fromString(property);
SaveState state = new SaveState(element);
List<String> names = List.of(state.getNames());
Map<String, Object> args = new LinkedHashMap<>();
for (ParameterDescription<?> param : params.values()) {
String key = "param_" + param.name;
if (names.contains(key)) {
Object configState = ConfigStateField.getState(state, param.type, key);
if (configState != null) {
args.put(param.name, configState);
}
}
}
if (!args.isEmpty()) {
return args;
}
}
catch (JDOMException | IOException e) {
if (!forPrompt) {
throw new RuntimeException(
"Saved launcher args are corrupt, or launcher parameters changed. Not launching.",
e);
}
Msg.error(this,
"Saved launcher args are corrupt, or launcher parameters changed. Defaulting.",
e);
}
}
Map<String, ?> args = generateDefaultLauncherArgs(params);
saveLauncherArgs(args, params);
return args;
}
return new LinkedHashMap<>();
}
/**
* Obtain the launcher args
*
* <p>
* This should either call {@link #promptLauncherArgs(Map))} or
* {@link #loadLastLauncherArgs(Map, boolean))}. Note if choosing the latter, the user will not
* be prompted to confirm.
*
* @param params the parameters of the model's launcher
* @param configurator the rules for configuring the launcher
* @param lastExc if retrying, the last exception to display as an error message
* @return the chosen arguments, or null if the user cancels at the prompt
*/
public Map<String, ?> getLauncherArgs(boolean prompt, LaunchConfigurator configurator,
Throwable lastExc) {
return prompt
? configurator.configureLauncher(this, promptLauncherArgs(configurator, lastExc),
RelPrompt.AFTER)
: configurator.configureLauncher(this, loadLastLauncherArgs(false), RelPrompt.NONE);
}
public Map<String, ?> getLauncherArgs(boolean prompt) {
return getLauncherArgs(prompt, LaunchConfigurator.NOP, null);
}
protected PtyFactory getPtyFactory() {
return PtyFactory.local();
}
protected PtyTerminalSession runInTerminal(List<String> commandLine, Map<String, String> env,
Collection<TerminalSession> subordinates)
throws IOException {
PtyFactory factory = getPtyFactory();
Pty pty = factory.openpty();
PtyParent parent = pty.getParent();
Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream());
terminal.setSubTitle(ShellUtils.generateLine(commandLine));
TerminalListener resizeListener = new TerminalListener() {
@Override
public void resized(short cols, short rows) {
parent.setWindowSize(cols, rows);
}
};
terminal.addTerminalListener(resizeListener);
env.put("TERM", "xterm-256color");
PtySession session = pty.getChild().session(commandLine.toArray(String[]::new), env);
Thread waiter = new Thread(() -> {
try {
session.waitExited();
terminal.terminated();
pty.close();
for (TerminalSession ss : subordinates) {
ss.terminate();
}
}
catch (InterruptedException | IOException e) {
Msg.error(this, e);
}
}, "Waiter: " + getConfigName());
waiter.start();
PtyTerminalSession terminalSession =
new PtyTerminalSession(terminal, pty, session, waiter);
terminal.setTerminateAction(() -> {
tool.execute(new TerminateSessionTask(terminalSession));
});
return terminalSession;
}
protected NullPtyTerminalSession nullPtyTerminal() throws IOException {
PtyFactory factory = getPtyFactory();
Pty pty = factory.openpty();
PtyParent parent = pty.getParent();
Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream());
TerminalListener resizeListener = new TerminalListener() {
@Override
public void resized(short cols, short rows) {
parent.setWindowSize(cols, rows);
}
};
terminal.addTerminalListener(resizeListener);
String name = pty.getChild().nullSession();
terminal.setSubTitle(name);
NullPtyTerminalSession terminalSession = new NullPtyTerminalSession(terminal, pty, name);
terminal.setTerminateAction(() -> {
tool.execute(new TerminateSessionTask(terminalSession));
});
return terminalSession;
}
protected abstract void launchBackEnd(TaskMonitor monitor,
Map<String, TerminalSession> sessions, Map<String, ?> args, SocketAddress address)
throws Exception;
@Override
public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) {
TraceRmiService service = tool.getService(TraceRmiService.class);
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
DebuggerTraceManagerService traceManager =
tool.getService(DebuggerTraceManagerService.class);
final PromptMode mode = configurator.getPromptMode();
boolean prompt = mode == PromptMode.ALWAYS;
TraceRmiAcceptor acceptor = null;
Map<String, TerminalSession> sessions = new LinkedHashMap<>();
TraceRmiConnection connection = null;
Trace trace = null;
Throwable lastExc = null;
monitor.setMaximum(5);
while (true) {
monitor.setMessage("Gathering arguments");
Map<String, ?> args = getLauncherArgs(prompt, configurator, lastExc);
if (args == null) {
if (lastExc == null) {
lastExc = new CancelledException();
}
return new LaunchResult(program, sessions, connection, trace, lastExc);
}
acceptor = null;
sessions.clear();
connection = null;
trace = null;
lastExc = null;
try {
monitor.setMessage("Listening for connection");
acceptor = service.acceptOne(new InetSocketAddress("127.0.0.1", 0));
monitor.setMessage("Launching back-end");
launchBackEnd(monitor, sessions, args, acceptor.getAddress());
monitor.setMessage("Waiting for connection");
acceptor.setTimeout(getTimeoutMillis());
connection = acceptor.accept();
monitor.setMessage("Waiting for trace");
trace = connection.waitForTrace(getTimeoutMillis());
traceManager.openTrace(trace);
traceManager.activateTrace(trace);
monitor.setMessage("Waiting for module mapping");
try {
listenForMapping(mappingService, connection, trace).get(getTimeoutMillis(),
TimeUnit.MILLISECONDS);
}
catch (TimeoutException e) {
monitor.setMessage(
"Timed out waiting for module mapping. Invoking the mapper.");
Collection<ModuleMapEntry> mapped;
try {
mapped = invokeMapper(monitor, mappingService, trace);
}
catch (CancelledException ce) {
throw new CancellationException(e.getMessage());
}
if (mapped.isEmpty()) {
monitor.setMessage(
"Could not formulate a mapping with the target program. " +
"Continuing without one.");
Msg.showWarn(this, null, "Launch " + program,
"The resulting target process has no mapping to the static image " +
program + ". Intervention is required before static and dynamic " +
"addresses can be translated. Check the target's module list.");
}
}
}
catch (Exception e) {
lastExc = e;
prompt = mode != PromptMode.NEVER;
if (prompt) {
continue;
}
return new LaunchResult(program, sessions, connection, trace, lastExc);
}
return new LaunchResult(program, sessions, connection, trace, null);
}
}
}

View File

@ -0,0 +1,198 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;
import javax.swing.*;
import org.jdom.Element;
import org.jdom.JDOMException;
import docking.ActionContext;
import docking.PopupMenuHandler;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.menu.*;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.framework.options.SaveState;
import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.ProgramUserData;
import ghidra.util.*;
import ghidra.util.xml.XmlUtilities;
public class LaunchAction extends MultiActionDockingAction {
public static final String NAME = "Launch";
public static final Icon ICON = DebuggerResources.ICON_DEBUGGER;
public static final String GROUP = DebuggerResources.GROUP_GENERAL;
public static final String HELP_ANCHOR = "launch_tracermi";
private final TraceRmiLauncherServicePlugin plugin;
private MenuActionDockingToolbarButton button;
public LaunchAction(TraceRmiLauncherServicePlugin plugin) {
super(NAME, plugin.getName());
this.plugin = plugin;
setToolBarData(new ToolBarData(ICON, GROUP, "A"));
setHelpLocation(new HelpLocation(plugin.getName(), HELP_ANCHOR));
}
protected String[] prependConfigAndLaunch(List<String> menuPath) {
Program program = plugin.currentProgram;
return Stream.concat(
Stream.of("Configure and Launch " + program.getName() + " using..."),
menuPath.stream()).toArray(String[]::new);
}
record ConfigLast(String configName, long last) {
}
ConfigLast checkSavedConfig(ProgramUserData userData, String propName) {
if (!propName.startsWith(AbstractTraceRmiLaunchOffer.PREFIX_DBGLAUNCH)) {
return null;
}
String configName =
propName.substring(AbstractTraceRmiLaunchOffer.PREFIX_DBGLAUNCH.length());
String propVal = Objects.requireNonNull(
userData.getStringProperty(propName, null));
Element element;
try {
element = XmlUtilities.fromString(propVal);
}
catch (JDOMException | IOException e) {
Msg.error(this, "Could not load launcher config for " + configName + ": " + e, e);
return null;
}
SaveState state = new SaveState(element);
if (!state.hasValue("last")) {
return null;
}
return new ConfigLast(configName, state.getLong("last", 0));
}
ConfigLast findMostRecentConfig() {
Program program = plugin.currentProgram;
ConfigLast best = null;
ProgramUserData userData = program.getProgramUserData();
for (String propName : userData.getStringPropertyNames()) {
ConfigLast candidate = checkSavedConfig(userData, propName);
if (candidate == null) {
continue;
}
else if (best == null) {
best = candidate;
}
else if (candidate.last > best.last) {
best = candidate;
}
}
return best;
}
@Override
public List<DockingActionIf> getActionList(ActionContext context) {
Program program = plugin.currentProgram;
Collection<TraceRmiLaunchOffer> offers = plugin.getOffers(program);
List<DockingActionIf> actions = new ArrayList<>();
ProgramUserData userData = program.getProgramUserData();
Map<String, Long> saved = new HashMap<>();
for (String propName : userData.getStringPropertyNames()) {
ConfigLast check = checkSavedConfig(userData, propName);
if (check == null) {
continue;
}
saved.put(check.configName, check.last);
}
for (TraceRmiLaunchOffer offer : offers) {
actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName())
.popupMenuPath(prependConfigAndLaunch(offer.getMenuPath()))
.popupMenuGroup(offer.getMenuGroup(), offer.getMenuOrder())
.popupMenuIcon(offer.getIcon())
.helpLocation(offer.getHelpLocation())
.enabledWhen(ctx -> true)
.onAction(ctx -> plugin.configureAndLaunch(offer))
.build());
Long last = saved.get(offer.getConfigName());
if (last == null) {
continue;
}
actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName())
.popupMenuPath("Re-launch " + program.getName() + " using " + offer.getTitle())
.popupMenuGroup("0", "%016x".formatted(Long.MAX_VALUE - last))
.popupMenuIcon(offer.getIcon())
.helpLocation(offer.getHelpLocation())
.enabledWhen(ctx -> true)
.onAction(ctx -> plugin.relaunch(offer))
.build());
}
return actions;
}
class MenuActionDockingToolbarButton extends MultipleActionDockingToolbarButton {
public MenuActionDockingToolbarButton(MultiActionDockingActionIf action) {
super(action);
}
@Override
protected JPopupMenu doCreateMenu() {
ActionContext context = getActionContext();
List<DockingActionIf> actionList = getActionList(context);
MenuHandler handler =
new PopupMenuHandler(plugin.getTool().getWindowManager(), context);
MenuManager manager =
new MenuManager("Launch", (char) 0, GROUP, true, handler, null);
for (DockingActionIf action : actionList) {
manager.addAction(action);
}
return manager.getPopupMenu();
}
@Override
protected JPopupMenu showPopup() {
// Make accessible to this file
return super.showPopup();
}
}
@Override
public JButton doCreateButton() {
return button = new MenuActionDockingToolbarButton(this);
}
@Override
public void actionPerformed(ActionContext context) {
// See comment on super method about use of runLater
ConfigLast last = findMostRecentConfig();
if (last == null) {
Swing.runLater(() -> button.showPopup());
return;
}
for (TraceRmiLaunchOffer offer : plugin.getOffers(plugin.currentProgram)) {
if (offer.getConfigName().equals(last.configName)) {
plugin.relaunch(offer);
return;
}
}
Swing.runLater(() -> button.showPopup());
}
}

View File

@ -0,0 +1,107 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.swing.JButton;
import javax.swing.JPanel;
import org.apache.commons.text.StringEscapeUtils;
import docking.DockingWindowManager;
import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooserMode;
import docking.widgets.pathmanager.*;
import utility.function.Callback;
public class ScriptPathsPropertyEditor extends AbstractTypedPropertyEditor<String> {
@Override
protected String fromText(String text) {
return text;
}
@Override
public String getJavaInitializationString() {
return "\"" + StringEscapeUtils.escapeJava(getValue()) + "\"";
}
@Override
public Component getCustomEditor() {
return new ScriptPathsEditor();
}
protected class ScriptPathsEditor extends JPanel {
public ScriptPathsEditor() {
super(new BorderLayout());
JButton button = new JButton("Edit Paths");
button.addActionListener(this::showDialog);
add(button);
}
protected void showDialog(ActionEvent evt) {
DockingWindowManager.showDialog(this, new ScriptPathsDialog());
}
}
protected class ScriptPathsDialog extends AbstractPathsDialog {
protected ScriptPathsDialog() {
super("Debugger Launch Script Paths");
}
@Override
protected String[] loadPaths() {
return getValue().lines().filter(d -> !d.isBlank()).toArray(String[]::new);
}
@Override
protected void savePaths(String[] paths) {
setValue(Stream.of(paths).collect(Collectors.joining("\n")));
}
@Override
protected PathnameTablePanel newPathnameTablePanel() {
PathnameTablePanel tablePanel = new ScriptPathsPanel(this::reset);
tablePanel.setFileChooserProperties(getTitle(), "DebuggerLaunchScriptDirectory",
GhidraFileChooserMode.DIRECTORIES_ONLY, true, null);
return tablePanel;
}
}
protected class ScriptPathsPanel extends PathnameTablePanel {
public ScriptPathsPanel(Callback resetCallback) {
// disable edits, top/bottom irrelevant, unordered
super(null, resetCallback, false, false, false);
}
@Override
protected int promptConfirmReset() {
String confirmation = """
<html><body width="200px">
Are you sure you would like to reload the Debugger's launcher script paths?
This will reset any changes you've made so far.
</html>""";
String header = "Reset Script Paths?";
return OptionDialog.showYesNoDialog(this, header, confirmation);
}
}
}

View File

@ -0,0 +1,244 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;
import docking.action.DockingActionIf;
import docking.action.builder.ActionBuilder;
import ghidra.app.events.ProgramActivatedPluginEvent;
import ghidra.app.events.ProgramClosedPluginEvent;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.DebugProgramAction;
import ghidra.app.services.*;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchConfigurator;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.PromptMode;
import ghidra.debug.api.tracermi.TraceRmiLaunchOpinion;
import ghidra.framework.options.OptionsChangeListener;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program;
import ghidra.util.Msg;
import ghidra.util.bean.opteditor.OptionsVetoException;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
@PluginInfo(
shortDescription = "GUI elements to launch targets using Trace RMI",
description = """
Provides menus and toolbar actions to launch Trace RMI targets.
""",
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.UNSTABLE,
eventsConsumed = {
ProgramActivatedPluginEvent.class,
ProgramClosedPluginEvent.class,
},
servicesRequired = {
TraceRmiService.class,
TerminalService.class,
},
servicesProvided = {
TraceRmiLauncherService.class,
})
public class TraceRmiLauncherServicePlugin extends Plugin
implements TraceRmiLauncherService, OptionsChangeListener {
protected static final String OPTION_NAME_SCRIPT_PATHS = "Script Paths";
private final static LaunchConfigurator PROMPT = new LaunchConfigurator() {
@Override
public PromptMode getPromptMode() {
return PromptMode.ALWAYS;
}
};
private static abstract class AbstractLaunchTask extends Task {
final TraceRmiLaunchOffer offer;
public AbstractLaunchTask(TraceRmiLaunchOffer offer) {
super(offer.getTitle(), true, true, true);
this.offer = offer;
}
}
private static class ReLaunchTask extends AbstractLaunchTask {
public ReLaunchTask(TraceRmiLaunchOffer offer) {
super(offer);
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
offer.launchProgram(monitor);
}
}
private static class ConfigureAndLaunchTask extends AbstractLaunchTask {
public ConfigureAndLaunchTask(TraceRmiLaunchOffer offer) {
super(offer);
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
offer.launchProgram(monitor, PROMPT);
}
}
public static File getProgramPath(Program program) {
if (program == null) {
return null;
}
String path = program.getExecutablePath();
if (path == null) {
return null;
}
File file = new File(path);
try {
if (!file.canExecute()) {
return null;
}
return file.getCanonicalFile();
}
catch (SecurityException | IOException e) {
Msg.error(TraceRmiLauncherServicePlugin.class, "Cannot examine file " + path, e);
return null;
}
}
protected final ToolOptions options;
protected Program currentProgram;
protected LaunchAction launchAction;
protected List<DockingActionIf> currentLaunchers = new ArrayList<>();
public TraceRmiLauncherServicePlugin(PluginTool tool) {
super(tool);
this.options = tool.getOptions(DebuggerPluginPackage.NAME);
this.options.addOptionsChangeListener(this);
createActions();
}
@Override
protected void init() {
super.init();
for (TraceRmiLaunchOpinion opinion : ClassSearcher
.getInstances(TraceRmiLaunchOpinion.class)) {
opinion.registerOptions(options);
}
}
protected void createActions() {
launchAction = new LaunchAction(this);
tool.addAction(launchAction);
}
@Override
public void optionsChanged(ToolOptions options, String optionName, Object oldValue,
Object newValue) throws OptionsVetoException {
for (TraceRmiLaunchOpinion opinion : ClassSearcher
.getInstances(TraceRmiLaunchOpinion.class)) {
if (opinion.requiresRefresh(optionName)) {
updateLauncherMenu();
return;
}
}
}
@Override
public Collection<TraceRmiLaunchOpinion> getOpinions() {
return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class);
}
@Override
public Collection<TraceRmiLaunchOffer> getOffers(Program program) {
if (program == null) {
return List.of();
}
return ClassSearcher.getInstances(TraceRmiLaunchOpinion.class)
.stream()
.flatMap(op -> op.getOffers(program, getTool()).stream())
.toList();
}
protected void relaunch(TraceRmiLaunchOffer offer) {
tool.execute(new ReLaunchTask(offer));
}
protected void configureAndLaunch(TraceRmiLaunchOffer offer) {
tool.execute(new ConfigureAndLaunchTask(offer));
}
protected String[] constructLaunchMenuPrefix() {
return new String[] {
DebuggerPluginPackage.NAME,
"Configure and Launch " + currentProgram.getName() + " using..." };
}
protected String[] prependConfigAndLaunch(List<String> menuPath) {
return Stream.concat(
Stream.of(constructLaunchMenuPrefix()),
menuPath.stream()).toArray(String[]::new);
}
private void updateLauncherMenu() {
Collection<TraceRmiLaunchOffer> offers = currentProgram == null
? List.of()
: getOffers(currentProgram);
synchronized (currentLaunchers) {
for (DockingActionIf launcher : currentLaunchers) {
tool.removeAction(launcher);
}
currentLaunchers.clear();
if (!offers.isEmpty()) {
tool.setMenuGroup(constructLaunchMenuPrefix(), DebugProgramAction.GROUP, "zz");
}
for (TraceRmiLaunchOffer offer : offers) {
currentLaunchers.add(new ActionBuilder(offer.getConfigName(), getName())
.menuPath(prependConfigAndLaunch(offer.getMenuPath()))
.menuGroup(offer.getMenuGroup(), offer.getMenuOrder())
.menuIcon(offer.getIcon())
.helpLocation(offer.getHelpLocation())
.enabledWhen(ctx -> true)
.onAction(ctx -> configureAndLaunch(offer))
.buildAndInstall(tool));
}
}
}
@Override
public void processEvent(PluginEvent event) {
super.processEvent(event);
if (event instanceof ProgramActivatedPluginEvent evt) {
currentProgram = evt.getActiveProgram();
updateLauncherMenu();
}
if (event instanceof ProgramClosedPluginEvent evt) {
if (currentProgram == evt.getProgram()) {
currentProgram = null;
updateLauncherMenu();
}
}
}
}

View File

@ -0,0 +1,633 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import java.io.*;
import java.math.BigInteger;
import java.net.*;
import java.util.*;
import java.util.Map.Entry;
import javax.swing.Icon;
import generic.theme.GIcon;
import generic.theme.Gui;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.util.ShellUtils;
import ghidra.framework.Application;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.task.TaskMonitor;
/**
* A launcher implemented by a simple UNIX shell script.
*
* <p>
* The script must start with an attributes header in a comment block. Some attributes are required.
* Others are optional:
* <ul>
* <li>{@code @menu-path}: <b>(Required)</b></li>
* </ul>
*/
public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer {
public static final String SHEBANG = "#!";
public static final String AT_TITLE = "@title";
public static final String AT_DESC = "@desc";
public static final String AT_MENU_PATH = "@menu-path";
public static final String AT_MENU_GROUP = "@menu-group";
public static final String AT_MENU_ORDER = "@menu-order";
public static final String AT_ICON = "@icon";
public static final String AT_HELP = "@help";
public static final String AT_ENUM = "@enum";
public static final String AT_ENV = "@env";
public static final String AT_ARG = "@arg";
public static final String AT_ARGS = "@args";
public static final String AT_TTY = "@tty";
public static final String PREFIX_ENV = "env:";
public static final String PREFIX_ARG = "arg:";
public static final String KEY_ARGS = "args";
public static final String MSGPAT_INVALID_HELP_SYNTAX =
"%s: Invalid %s syntax. Use Topic#anchor";
public static final String MSGPAT_INVALID_ENUM_SYNTAX =
"%s: Invalid %s syntax. Use NAME:type Choice1 [ChoiceN...]";
public static final String MSGPAT_INVALID_ENV_SYNTAX =
"%s: Invalid %s syntax. Use NAME:type=default \"Display\" \"Tool Tip\"";
public static final String MSGPAT_INVALID_ARG_SYNTAX =
"%s: Invalid %s syntax. Use :type \"Display\" \"Tool Tip\"";
public static final String MSGPAT_INVALID_ARGS_SYNTAX =
"%s: Invalid %s syntax. Use \"Display\" \"Tool Tip\"";
protected record Location(String fileName, int lineNo) {
@Override
public String toString() {
return "%s:%d".formatted(fileName, lineNo);
}
}
protected interface OptType<T> {
static OptType<?> parse(Location loc, String typeName,
Map<String, UserType<?>> userEnums) {
OptType<?> type = switch (typeName) {
case "str" -> BaseType.STRING;
case "int" -> BaseType.INT;
case "bool" -> BaseType.BOOL;
default -> userEnums.get(typeName);
};
if (type == null) {
Msg.error(AttributesParser.class, "%s: Invalid type %s".formatted(loc, typeName));
return null;
}
return type;
}
default TypeAndDefault<T> withCastDefault(Object defaultValue) {
return new TypeAndDefault<>(this, cls().cast(defaultValue));
}
Class<T> cls();
T decode(Location loc, String str);
ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description);
}
protected interface BaseType<T> extends OptType<T> {
static BaseType<?> parse(Location loc, String typeName) {
BaseType<?> type = switch (typeName) {
case "str" -> BaseType.STRING;
case "int" -> BaseType.INT;
case "bool" -> BaseType.BOOL;
default -> null;
};
if (type == null) {
Msg.error(AttributesParser.class,
"%s: Invalid base type %s".formatted(loc, typeName));
return null;
}
return type;
}
public static final BaseType<String> STRING = new BaseType<>() {
@Override
public Class<String> cls() {
return String.class;
}
@Override
public String decode(Location loc, String str) {
return str;
}
};
public static final BaseType<BigInteger> INT = new BaseType<>() {
@Override
public Class<BigInteger> cls() {
return BigInteger.class;
}
@Override
public BigInteger decode(Location loc, String str) {
try {
if (str.startsWith("0x")) {
return new BigInteger(str.substring(2), 16);
}
return new BigInteger(str);
}
catch (NumberFormatException e) {
Msg.error(AttributesParser.class,
("%s: Invalid int for %s: %s. You may prefix with 0x for hexadecimal. " +
"Otherwise, decimal is used.").formatted(loc, AT_ENV, str));
return null;
}
}
};
public static final BaseType<Boolean> BOOL = new BaseType<>() {
@Override
public Class<Boolean> cls() {
return Boolean.class;
}
@Override
public Boolean decode(Location loc, String str) {
Boolean result = switch (str) {
case "true" -> true;
case "false" -> false;
default -> null;
};
if (result == null) {
Msg.error(AttributesParser.class,
"%s: Invalid bool for %s: %s. Only true or false (in lower case) is allowed."
.formatted(loc, AT_ENV, str));
return null;
}
return result;
}
};
default UserType<T> withCastChoices(List<?> choices) {
return new UserType<>(this, choices.stream().map(cls()::cast).toList());
}
@Override
default ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description) {
return ParameterDescription.create(cls(), name, false, defaultValue, display,
description);
}
}
protected record UserType<T> (BaseType<T> base, List<T> choices) implements OptType<T> {
@Override
public Class<T> cls() {
return base.cls();
}
@Override
public T decode(Location loc, String str) {
return base.decode(loc, str);
}
@Override
public ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description) {
return ParameterDescription.choices(cls(), name, choices, defaultValue, display,
description);
}
}
protected record TypeAndDefault<T> (OptType<T> type, T defaultValue) {
public static TypeAndDefault<?> parse(Location loc, String typeName, String defaultString,
Map<String, UserType<?>> userEnums) {
OptType<?> tac = OptType.parse(loc, typeName, userEnums);
if (tac == null) {
return null;
}
Object value = tac.decode(loc, defaultString);
if (value == null) {
return null;
}
return tac.withCastDefault(value);
}
public ParameterDescription<T> createParameter(String name, String display,
String description) {
return type.createParameter(name, defaultValue, display, description);
}
}
protected static class AttributesParser {
protected int argc = 0;
protected String title;
protected StringBuilder description;
protected List<String> menuPath;
protected String menuGroup;
protected String menuOrder;
protected String iconId;
protected HelpLocation helpLocation;
protected final Map<String, UserType<?>> userTypes = new HashMap<>();
protected final Map<String, ParameterDescription<?>> parameters = new LinkedHashMap<>();
protected final Set<String> extraTtys = new LinkedHashSet<>();
/**
* Process a line in the metadata comment block
*
* @param line the line, excluding any comment delimiters
*/
public void parseLine(Location loc, String line) {
String afterHash = line.stripLeading().substring(1);
if (afterHash.isBlank()) {
return;
}
String[] parts = afterHash.split("\\s+", 2);
if (!parts[0].startsWith("@")) {
return;
}
if (parts.length < 2) {
Msg.error(this, "%s: Too few tokens: %s".formatted(loc, line));
return;
}
switch (parts[0].trim()) {
case AT_TITLE -> parseTitle(loc, parts[1]);
case AT_DESC -> parseDesc(loc, parts[1]);
case AT_MENU_PATH -> parseMenuPath(loc, parts[1]);
case AT_MENU_GROUP -> parseMenuGroup(loc, parts[1]);
case AT_MENU_ORDER -> parseMenuOrder(loc, parts[1]);
case AT_ICON -> parseIcon(loc, parts[1]);
case AT_HELP -> parseHelp(loc, parts[1]);
case AT_ENUM -> parseEnum(loc, parts[1]);
case AT_ENV -> parseEnv(loc, parts[1]);
case AT_ARG -> parseArg(loc, parts[1], ++argc);
case AT_ARGS -> parseArgs(loc, parts[1]);
case AT_TTY -> parseTty(loc, parts[1]);
default -> parseUnrecognized(loc, line);
}
}
protected void parseTitle(Location loc, String str) {
if (title != null) {
Msg.warn(this, "%s: Duplicate @title".formatted(loc));
}
title = str;
}
protected void parseDesc(Location loc, String str) {
if (description == null) {
description = new StringBuilder();
}
description.append(str);
description.append("\n");
}
protected void parseMenuPath(Location loc, String str) {
if (menuPath != null) {
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_PATH));
}
menuPath = List.of(str.trim().split("\\."));
if (menuPath.isEmpty()) {
Msg.error(this,
"%s: Empty %s. Ignoring.".formatted(loc, AT_MENU_PATH));
}
}
protected void parseMenuGroup(Location loc, String str) {
if (menuGroup != null) {
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_GROUP));
}
menuGroup = str;
}
protected void parseMenuOrder(Location loc, String str) {
if (menuOrder != null) {
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_ORDER));
}
menuOrder = str;
}
protected void parseIcon(Location loc, String str) {
if (iconId != null) {
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_ICON));
}
iconId = str.trim();
if (!Gui.hasIcon(iconId)) {
Msg.error(this,
"%s: Icon id %s not registered in the theme".formatted(loc, iconId));
}
}
protected void parseHelp(Location loc, String str) {
if (helpLocation != null) {
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_HELP));
}
String[] parts = str.trim().split("#", 2);
if (parts.length != 2) {
Msg.error(this, MSGPAT_INVALID_HELP_SYNTAX.formatted(loc, AT_HELP));
return;
}
helpLocation = new HelpLocation(parts[0].trim(), parts[1].trim());
}
protected void parseEnum(Location loc, String str) {
List<String> parts = ShellUtils.parseArgs(str);
if (parts.size() < 2) {
Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM));
return;
}
String[] nameParts = parts.get(0).split(":", 2);
if (nameParts.length != 2) {
Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM));
return;
}
String name = nameParts[0].trim();
BaseType<?> baseType = BaseType.parse(loc, nameParts[1]);
if (baseType == null) {
return;
}
List<?> choices = parts.stream().skip(1).map(s -> baseType.decode(loc, s)).toList();
if (choices.contains(null)) {
return;
}
UserType<?> userType = baseType.withCastChoices(choices);
if (userTypes.put(name, userType) != null) {
Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENUM, name));
}
}
protected void parseEnv(Location loc, String str) {
List<String> parts = ShellUtils.parseArgs(str);
if (parts.size() != 3) {
Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
return;
}
String[] nameParts = parts.get(0).split(":", 2);
if (nameParts.length != 2) {
Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
return;
}
String trimmed = nameParts[0].trim();
String name = PREFIX_ENV + trimmed;
String[] tadParts = nameParts[1].split("=", 2);
if (tadParts.length != 2) {
Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
return;
}
TypeAndDefault<?> tad =
TypeAndDefault.parse(loc, tadParts[0].trim(), tadParts[1].trim(), userTypes);
ParameterDescription<?> param = tad.createParameter(name, parts.get(1), parts.get(2));
if (parameters.put(name, param) != null) {
Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENV, trimmed));
}
}
protected void parseArg(Location loc, String str, int argNum) {
List<String> parts = ShellUtils.parseArgs(str);
if (parts.size() != 3) {
Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG));
return;
}
String colonType = parts.get(0).trim();
if (!colonType.startsWith(":")) {
Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG));
return;
}
OptType<?> type = OptType.parse(loc, colonType.substring(1), userTypes);
if (type == null) {
return;
}
String name = PREFIX_ARG + argNum;
parameters.put(name, ParameterDescription.create(type.cls(), name, true, null,
parts.get(1), parts.get(2)));
}
protected void parseArgs(Location loc, String str) {
List<String> parts = ShellUtils.parseArgs(str);
if (parts.size() != 2) {
Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS));
return;
}
ParameterDescription<String> parameter = ParameterDescription.create(String.class,
"args", false, "", parts.get(0), parts.get(1));
if (parameters.put(KEY_ARGS, parameter) != null) {
Msg.warn(this, "%s: Duplicate %s. Replaced".formatted(loc, AT_ARGS));
}
}
protected void parseTty(Location loc, String str) {
if (!extraTtys.add(str)) {
Msg.warn(this, "%s: Duplicate %s. Ignored".formatted(loc, AT_TTY));
}
}
protected void parseUnrecognized(Location loc, String line) {
Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, line));
}
protected void validate(String fileName) {
if (title == null) {
Msg.error(this, "%s is required. Using script file name.".formatted(AT_TITLE));
title = fileName;
}
if (menuPath == null) {
menuPath = List.of(title);
}
if (menuGroup == null) {
menuGroup = "";
}
if (menuOrder == null) {
menuOrder = "";
}
if (iconId == null) {
iconId = "icon.debugger";
}
}
public String getDescription() {
return description == null ? null : description.toString();
}
}
/**
* Create a launch offer from the given shell script.
*
* @param program the current program, usually the target image. In general, this should be used
* for at least two purposes. 1) To populate the default command line. 2) To ensure
* the target image is mapped in the resulting target trace.
* @throws FileNotFoundException
*/
public static UnixShellScriptTraceRmiLaunchOffer create(Program program, PluginTool tool,
File script) throws FileNotFoundException {
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(new FileInputStream(script)))) {
AttributesParser attrs = new AttributesParser();
String line;
for (int lineNo = 1; (line = reader.readLine()) != null; lineNo++) {
if (line.startsWith(SHEBANG) && lineNo == 1) {
}
else if (line.isBlank()) {
continue;
}
else if (line.stripLeading().startsWith("#")) {
attrs.parseLine(new Location(script.getName(), lineNo), line);
}
else {
break;
}
}
attrs.validate(script.getName());
return new UnixShellScriptTraceRmiLaunchOffer(program, tool, script,
"UNIX_SHELL:" + script.getName(), attrs.title, attrs.getDescription(),
attrs.menuPath, attrs.menuGroup, attrs.menuOrder, new GIcon(attrs.iconId),
attrs.helpLocation, attrs.parameters, attrs.extraTtys);
}
catch (FileNotFoundException e) {
// Avoid capture by IOException
throw e;
}
catch (IOException e) {
throw new AssertionError(e);
}
}
protected final File script;
protected final String configName;
protected final String title;
protected final String description;
protected final List<String> menuPath;
protected final String menuGroup;
protected final String menuOrder;
protected final Icon icon;
protected final HelpLocation helpLocation;
protected final Map<String, ParameterDescription<?>> parameters;
protected final List<String> extraTtys;
public UnixShellScriptTraceRmiLaunchOffer(Program program, PluginTool tool, File script,
String configName, String title, String description, List<String> menuPath,
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
Map<String, ParameterDescription<?>> parameters, Collection<String> extraTtys) {
super(program, tool);
this.script = script;
this.configName = configName;
this.title = title;
this.description = description;
this.menuPath = List.copyOf(menuPath);
this.menuGroup = menuGroup;
this.menuOrder = menuOrder;
this.icon = icon;
this.helpLocation = helpLocation;
this.parameters = Collections.unmodifiableMap(new LinkedHashMap<>(parameters));
this.extraTtys = List.copyOf(extraTtys);
}
@Override
public String getConfigName() {
return configName;
}
@Override
public String getTitle() {
return title;
}
@Override
public String getDescription() {
return description;
}
@Override
public List<String> getMenuPath() {
return menuPath;
}
@Override
public String getMenuGroup() {
return menuGroup;
}
@Override
public String getMenuOrder() {
return menuOrder;
}
@Override
public Icon getIcon() {
return icon;
}
@Override
public HelpLocation getHelpLocation() {
return helpLocation;
}
@Override
public Map<String, ParameterDescription<?>> getParameters() {
return parameters;
}
protected static String addrToString(InetAddress address) {
if (address.isAnyLocalAddress()) {
return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost.
}
return address.getHostAddress();
}
protected static String sockToString(SocketAddress address) {
if (address instanceof InetSocketAddress tcp) {
return addrToString(tcp.getAddress()) + ":" + tcp.getPort();
}
throw new AssertionError("Unhandled address type " + address);
}
@Override
protected void launchBackEnd(TaskMonitor monitor, Map<String, TerminalSession> sessions,
Map<String, ?> args, SocketAddress address) throws Exception {
List<String> commandLine = new ArrayList<>();
Map<String, String> env = new HashMap<>(System.getenv());
commandLine.add(script.getAbsolutePath());
env.put("GHIDRA_HOME", Application.getInstallationDirectory().getAbsolutePath());
env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address));
ParameterDescription<?> paramDesc;
for (int i = 1; (paramDesc = parameters.get("arg:" + i)) != null; i++) {
commandLine.add(Objects.toString(paramDesc.get(args)));
}
paramDesc = parameters.get("args");
if (paramDesc != null) {
commandLine.addAll(ShellUtils.parseArgs((String) paramDesc.get(args)));
}
for (Entry<String, ParameterDescription<?>> ent : parameters.entrySet()) {
String key = ent.getKey();
if (key.startsWith(PREFIX_ENV)) {
String varName = key.substring(PREFIX_ENV.length());
env.put(varName, Objects.toString(ent.getValue().get(args)));
}
}
for (String tty : extraTtys) {
NullPtyTerminalSession ns = nullPtyTerminal();
env.put(tty, ns.name());
sessions.put(ns.name(), ns);
}
sessions.put("Shell", runInTerminal(commandLine, env, sessions.values()));
}
}

View File

@ -0,0 +1,81 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import generic.jar.ResourceFile;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOpinion;
import ghidra.framework.Application;
import ghidra.framework.options.OptionType;
import ghidra.framework.options.Options;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginUtils;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
public class UnixShellScriptTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion {
@Override
public void registerOptions(Options options) {
String pluginName = PluginUtils.getPluginNameFromClass(TraceRmiLauncherServicePlugin.class);
options.registerOption(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS,
OptionType.STRING_TYPE, "", new HelpLocation(pluginName, "options"),
"Paths to search for user-created debugger launchers", new ScriptPathsPropertyEditor());
}
@Override
public boolean requiresRefresh(String optionName) {
return TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS.equals(optionName);
}
protected Stream<ResourceFile> getModuleScriptPaths() {
return Application.findModuleSubDirectories("data/debugger-launchers").stream();
}
protected Stream<ResourceFile> getUserScriptPaths(PluginTool tool) {
Options options = tool.getOptions(DebuggerPluginPackage.NAME);
String scriptPaths =
options.getString(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS, "");
return scriptPaths.lines().filter(d -> !d.isBlank()).map(ResourceFile::new);
}
protected Stream<ResourceFile> getScriptPaths(PluginTool tool) {
return Stream.concat(getModuleScriptPaths(), getUserScriptPaths(tool));
}
@Override
public Collection<TraceRmiLaunchOffer> getOffers(Program program, PluginTool tool) {
return getScriptPaths(tool)
.flatMap(rf -> Stream.of(rf.listFiles(crf -> crf.getName().endsWith(".sh"))))
.flatMap(sf -> {
try {
return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(program, tool,
sf.getFile(false)));
}
catch (Exception e) {
Msg.error(this, "Could not offer " + sf + ":" + e.getMessage(), e);
return Stream.of();
}
})
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,51 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
import java.util.concurrent.*;
import ghidra.debug.api.tracermi.RemoteAsyncResult;
import ghidra.util.Swing;
public class DefaultRemoteAsyncResult extends CompletableFuture<Object>
implements RemoteAsyncResult {
final ValueDecoder decoder;
public DefaultRemoteAsyncResult() {
this.decoder = ValueDecoder.DEFAULT;
}
public DefaultRemoteAsyncResult(OpenTrace open) {
this.decoder = open;
}
@Override
public Object get() throws InterruptedException, ExecutionException {
if (Swing.isSwingThread()) {
throw new AssertionError("Refusing indefinite wait on Swing thread");
}
return super.get();
}
@Override
public Object get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (Swing.isSwingThread() && unit.toSeconds(timeout) > 1) {
throw new AssertionError("Refusing a timeout > 1 second on Swing thread");
}
return super.get(timeout, unit);
}
}

View File

@ -17,9 +17,11 @@ package ghidra.app.plugin.core.debug.service.rmi.trace;
import java.util.*;
import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod.Action;
import ghidra.debug.api.tracermi.RemoteMethod;
import ghidra.debug.api.tracermi.RemoteMethodRegistry;
import ghidra.debug.api.tracermi.RemoteMethod.Action;
public class RemoteMethodRegistry {
public class DefaultRemoteMethodRegistry implements RemoteMethodRegistry {
private final Map<String, RemoteMethod> map = new HashMap<>();
private final Map<Action, Set<RemoteMethod>> byAction = new HashMap<>();
@ -30,18 +32,21 @@ public class RemoteMethodRegistry {
}
}
@Override
public Map<String, RemoteMethod> all() {
synchronized (map) {
return Map.copyOf(map);
}
}
@Override
public RemoteMethod get(String name) {
synchronized (map) {
return map.get(name);
}
}
@Override
public Set<RemoteMethod> getByAction(Action action) {
synchronized (map) {
return byAction.getOrDefault(action, Set.of());

View File

@ -19,9 +19,11 @@ import java.io.IOException;
import java.net.ServerSocket;
import java.net.SocketAddress;
public class TraceRmiAcceptor extends TraceRmiServer {
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
public TraceRmiAcceptor(TraceRmiPlugin plugin, SocketAddress address) {
public class DefaultTraceRmiAcceptor extends TraceRmiServer implements TraceRmiAcceptor {
public DefaultTraceRmiAcceptor(TraceRmiPlugin plugin, SocketAddress address) {
super(plugin, address);
}

View File

@ -16,6 +16,7 @@
package ghidra.app.plugin.core.debug.service.rmi.trace;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler.*;
import ghidra.debug.api.tracermi.TraceRmiError;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.Register;
import ghidra.rmi.trace.TraceRmi.*;

View File

@ -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.plugin.core.debug.service.rmi.trace;
import java.util.Map;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.debug.api.tracermi.RemoteMethod;
import ghidra.debug.api.tracermi.RemoteParameter;
import ghidra.debug.api.tracermi.RemoteMethod.Action;
import ghidra.trace.model.Trace;
public record RecordRemoteMethod(TraceRmiHandler handler, String name, Action action,
String description, Map<String, RemoteParameter> parameters, SchemaName retType)
implements RemoteMethod {
@Override
public DefaultRemoteAsyncResult invokeAsync(Map<String, Object> arguments) {
Trace trace = validate(arguments);
OpenTrace open = handler.getOpenTrace(trace);
return handler.invoke(open, name, arguments);
}
}

View File

@ -0,0 +1,49 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.debug.api.tracermi.RemoteParameter;
import ghidra.program.model.address.AddressOverflowException;
import ghidra.trace.model.Trace;
public record RecordRemoteParameter(TraceRmiHandler handler, String name, SchemaName type,
boolean required, ValueSupplier defaultValue, String display, String description)
implements RemoteParameter {
public Object getDefaultValue(Trace trace) {
OpenTrace open = handler.getOpenTrace(trace);
if (open == null) {
throw new IllegalArgumentException("Trace is not from this connection");
}
try {
return defaultValue().get(open);
}
catch (AddressOverflowException e) {
throw new AssertionError(e);
}
}
@Override
public Object getDefaultValue() {
try {
return defaultValue().get(ValueDecoder.DEFAULT);
}
catch (AddressOverflowException e) {
throw new AssertionError(e);
}
}
}

View File

@ -25,8 +25,7 @@ import java.nio.file.Paths;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.*;
import java.util.stream.*;
import org.apache.commons.lang3.ArrayUtils;
@ -35,16 +34,16 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import com.google.protobuf.ByteString;
import db.Transaction;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod.Action;
import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod.RecordRemoteMethod;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.dbg.target.schema.XmlSchemaContext;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.debug.api.tracermi.*;
import ghidra.debug.api.tracermi.RemoteMethod.Action;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.AutoService.Wiring;
@ -69,7 +68,7 @@ import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.task.TaskMonitor;
public class TraceRmiHandler {
public class TraceRmiHandler implements TraceRmiConnection {
public static final String VERSION = "10.4";
protected static class VersionMismatchError extends TraceRmiError {
@ -139,6 +138,7 @@ public class TraceRmiHandler {
protected static class OpenTraceMap {
private final Map<DoId, OpenTrace> byId = new HashMap<>();
private final Map<Trace, OpenTrace> byTrace = new HashMap<>();
private final CompletableFuture<OpenTrace> first = new CompletableFuture<>();
public synchronized boolean isEmpty() {
return byId.isEmpty();
@ -168,6 +168,11 @@ public class TraceRmiHandler {
public synchronized void put(OpenTrace openTrace) {
byId.put(openTrace.doId, openTrace);
byTrace.put(openTrace.trace, openTrace);
first.complete(openTrace);
}
public CompletableFuture<OpenTrace> getFirstAsync() {
return first;
}
}
@ -181,9 +186,9 @@ public class TraceRmiHandler {
private final OpenTraceMap openTraces = new OpenTraceMap();
private final Map<Tid, OpenTx> openTxes = new HashMap<>();
private final RemoteMethodRegistry methodRegistry = new RemoteMethodRegistry();
private final DefaultRemoteMethodRegistry methodRegistry = new DefaultRemoteMethodRegistry();
// The remote must service requests and reply in the order received.
private final Deque<RemoteAsyncResult> xReqQueue = new ArrayDeque<>();
private final Deque<DefaultRemoteAsyncResult> xReqQueue = new ArrayDeque<>();
@AutoServiceConsumed
private DebuggerTraceManagerService traceManager;
@ -201,6 +206,7 @@ public class TraceRmiHandler {
*/
public TraceRmiHandler(TraceRmiPlugin plugin, Socket socket) throws IOException {
this.plugin = plugin;
plugin.addHandler(this);
this.socket = socket;
this.in = socket.getInputStream();
this.out = socket.getOutputStream();
@ -211,17 +217,18 @@ public class TraceRmiHandler {
}
protected void flushXReqQueue(Throwable exc) {
List<RemoteAsyncResult> copy;
List<DefaultRemoteAsyncResult> copy;
synchronized (xReqQueue) {
copy = List.copyOf(xReqQueue);
xReqQueue.clear();
}
for (RemoteAsyncResult result : copy) {
for (DefaultRemoteAsyncResult result : copy) {
result.completeExceptionally(exc);
}
}
public void dispose() throws IOException {
plugin.removeHandler(this);
flushXReqQueue(new TraceRmiError("Socket closed"));
socket.close();
while (!openTxes.isEmpty()) {
@ -248,12 +255,24 @@ public class TraceRmiHandler {
closed.complete(null);
}
@Override
public void close() throws IOException {
dispose();
}
@Override
public boolean isClosed() {
return socket.isClosed() && closed.isDone();
}
public void waitClosed() throws InterruptedException, ExecutionException {
closed.get();
@Override
public void waitClosed() {
try {
closed.get();
}
catch (InterruptedException | ExecutionException e) {
throw new TraceRmiError(e);
}
}
protected DomainFolder getOrCreateNewTracesFolder()
@ -442,8 +461,9 @@ public class TraceRmiHandler {
req.getRequestActivate().getObject().getPath().getPath());
case REQUEST_END_TX -> "endTx(%d)".formatted(
req.getRequestEndTx().getTxid().getId());
case REQUEST_START_TX -> "startTx(%d)".formatted(
req.getRequestStartTx().getTxid().getId());
case REQUEST_START_TX -> "startTx(%d,%s)".formatted(
req.getRequestStartTx().getTxid().getId(),
req.getRequestStartTx().getDescription());
default -> null;
};
}
@ -728,7 +748,12 @@ public class TraceRmiHandler {
OpenTrace open = requireOpenTrace(req.getOid());
TraceObject object = open.getObject(req.getObject(), true);
DebuggerCoordinates coords = traceManager.getCurrent();
coords = coords.object(object);
if (coords.getTrace() == object.getTrace()) {
coords = coords.object(object);
}
else {
coords = DebuggerCoordinates.NOWHERE.object(object);
}
if (open.lastSnapshot != null) {
coords = coords.snap(open.lastSnapshot.getKey());
}
@ -738,7 +763,7 @@ public class TraceRmiHandler {
}
else {
Trace currentTrace = traceManager.getCurrentTrace();
if (currentTrace == null || currentTrace == open.trace) {
if (currentTrace == null || openTraces.getByTrace(currentTrace) != null) {
traceManager.activate(coords);
}
}
@ -939,8 +964,7 @@ public class TraceRmiHandler {
RemoteMethod rm = new RecordRemoteMethod(this, m.getName(), new Action(m.getAction()),
m.getDescription(), m.getParametersList()
.stream()
.collect(Collectors.toMap(MethodParameter::getName,
TraceRmiHandler::makeParameter)),
.collect(Collectors.toMap(MethodParameter::getName, this::makeParameter)),
new SchemaName(m.getReturnType().getName()));
methodRegistry.add(rm);
}
@ -978,8 +1002,8 @@ public class TraceRmiHandler {
return ReplyPutRegisterValue.getDefaultInstance();
}
protected static RemoteParameter makeParameter(MethodParameter mp) {
return new RemoteParameter(mp.getName(), new SchemaName(mp.getType().getName()),
protected RecordRemoteParameter makeParameter(MethodParameter mp) {
return new RecordRemoteParameter(this, mp.getName(), new SchemaName(mp.getType().getName()),
mp.getRequired(), ot -> ot.toValue(mp.getDefaultValue()), mp.getDisplay(),
mp.getDescription());
}
@ -1084,7 +1108,7 @@ public class TraceRmiHandler {
protected RootMessage.Builder handleXInvokeMethod(XReplyInvokeMethod xrep) {
String error = xrep.getError();
RemoteAsyncResult result;
DefaultRemoteAsyncResult result;
synchronized (xReqQueue) {
result = xReqQueue.poll();
}
@ -1102,11 +1126,13 @@ public class TraceRmiHandler {
return null;
}
@Override
public SocketAddress getRemoteAddress() {
return socket.getRemoteSocketAddress();
}
public RemoteMethodRegistry getMethods() {
@Override
public DefaultRemoteMethodRegistry getMethods() {
return methodRegistry;
}
@ -1121,20 +1147,20 @@ public class TraceRmiHandler {
return open;
}
protected RemoteAsyncResult invoke(OpenTrace open, String methodName,
protected DefaultRemoteAsyncResult invoke(OpenTrace open, String methodName,
Map<String, Object> arguments) {
RootMessage.Builder req = RootMessage.newBuilder();
XRequestInvokeMethod.Builder invoke = XRequestInvokeMethod.newBuilder()
.setName(methodName)
.addAllArguments(
arguments.entrySet().stream().map(TraceRmiHandler::makeArgument).toList());
RemoteAsyncResult result;
DefaultRemoteAsyncResult result;
if (open != null) {
result = new RemoteAsyncResult(open);
result = new DefaultRemoteAsyncResult(open);
invoke.setOid(open.doId.toDomObjId());
}
else {
result = new RemoteAsyncResult();
result = new DefaultRemoteAsyncResult();
}
req.setXrequestInvokeMethod(invoke);
synchronized (xReqQueue) {
@ -1151,6 +1177,7 @@ public class TraceRmiHandler {
}
}
@Override
@Internal
public long getLastSnapshot(Trace trace) {
TraceSnapshot lastSnapshot = openTraces.getByTrace(trace).lastSnapshot;
@ -1159,4 +1186,14 @@ public class TraceRmiHandler {
}
return lastSnapshot.getKey();
}
@Override
public Trace waitForTrace(long timeoutMillis) throws TimeoutException {
try {
return openTraces.getFirstAsync().get(timeoutMillis, TimeUnit.MILLISECONDS).trace;
}
catch (InterruptedException | ExecutionException e) {
throw new TraceRmiError(e);
}
}
}

View File

@ -17,12 +17,14 @@ package ghidra.app.plugin.core.debug.service.rmi.trace;
import java.io.IOException;
import java.net.*;
import java.util.*;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
import ghidra.app.services.TraceRmiService;
import ghidra.debug.api.tracermi.TraceRmiConnection;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.util.task.ConsoleTaskMonitor;
@ -31,11 +33,8 @@ import ghidra.util.task.TaskMonitor;
@PluginInfo(
shortDescription = "Connect to back-end debuggers via Trace RMI",
description = """
Provides an alternative for connecting to back-end debuggers. The DebuggerModel has
become a bit onerous to implement. Despite its apparent flexibility, the recorder at
the front-end imposes many restrictions, and getting it to work turns into a lot of
guess work and frustration. Trace RMI should offer a more direct means of recording a
trace from a back-end.
Provides a means for connecting to back-end debuggers.
NOTE this is an alternative to the DebuggerModel and is meant to replace it.
""",
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
@ -54,6 +53,8 @@ public class TraceRmiPlugin extends Plugin implements TraceRmiService {
private SocketAddress serverAddress = new InetSocketAddress("0.0.0.0", DEFAULT_PORT);
private TraceRmiServer server;
private final Set<TraceRmiHandler> handlers = new LinkedHashSet<>();
public TraceRmiPlugin(PluginTool tool) {
super(tool);
}
@ -107,13 +108,28 @@ public class TraceRmiPlugin extends Plugin implements TraceRmiService {
public TraceRmiHandler connect(SocketAddress address) throws IOException {
Socket socket = new Socket();
socket.connect(address);
return new TraceRmiHandler(this, socket);
TraceRmiHandler handler = new TraceRmiHandler(this, socket);
handler.start();
return handler;
}
@Override
public TraceRmiAcceptor acceptOne(SocketAddress address) throws IOException {
TraceRmiAcceptor acceptor = new TraceRmiAcceptor(this, address);
public DefaultTraceRmiAcceptor acceptOne(SocketAddress address) throws IOException {
DefaultTraceRmiAcceptor acceptor = new DefaultTraceRmiAcceptor(this, address);
acceptor.start();
return acceptor;
}
void addHandler(TraceRmiHandler handler) {
handlers.add(handler);
}
void removeHandler(TraceRmiHandler handler) {
handlers.remove(handler);
}
@Override
public Collection<TraceRmiConnection> getAllConnections() {
return List.copyOf(handlers);
}
}

View File

@ -1,51 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
import java.io.IOException;
import java.net.SocketAddress;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiAcceptor;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
public interface TraceRmiService {
SocketAddress getServerAddress();
/**
* Set the server address and port
*
* @param serverAddress may be null to bind to ephemeral port
*/
void setServerAddress(SocketAddress serverAddress);
void startServer() throws IOException;
void stopServer();
boolean isServerStarted();
TraceRmiHandler connect(SocketAddress address) throws IOException;
/**
* Accept a single connection by listening on the given address
*
* @param address the socket address to bind, or null for ephemeral
* @return the acceptor, which can be used to retrieve the ephemeral address and accept the
* actual connection
* @throws IOException on error
*/
TraceRmiAcceptor acceptOne(SocketAddress address) throws IOException;
}

View File

@ -23,6 +23,7 @@ apply plugin: 'eclipse'
eclipse.project.name = 'Debug Debugger'
dependencies {
api project(':Debugger-api')
api project(':Framework-AsyncComm')
api project(':Framework-Debugging')
api project(':Framework-TraceModeling')

View File

@ -25,9 +25,9 @@
import java.util.Set;
import java.util.concurrent.TimeUnit;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer.LaunchResult;
import ghidra.app.script.GhidraScript;
import ghidra.app.services.LogicalBreakpoint;
import ghidra.debug.api.breakpoint.LogicalBreakpoint;
import ghidra.debug.api.model.DebuggerProgramLaunchOffer.LaunchResult;
import ghidra.debug.flatapi.FlatDebuggerAPI;
import ghidra.program.model.address.Address;
import ghidra.trace.model.Trace;

View File

@ -15,7 +15,6 @@
*/
import java.util.Set;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.script.GhidraScript;
import ghidra.app.services.*;
import ghidra.dbg.DebuggerObjectModel;
@ -23,6 +22,8 @@ import ghidra.dbg.DebuggerObjectModel.RefreshBehavior;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetRegisterBank;
import ghidra.dbg.util.PathMatcher;
import ghidra.debug.api.model.TraceRecorder;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
public class RefreshRegistersScript extends GhidraScript {
@Override

View File

@ -18,11 +18,11 @@ package ghidra.app.plugin.core.debug.disassemble;
import docking.ActionContext;
import docking.action.*;
import ghidra.app.context.ListingActionContext;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin.Reqs;
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingActionContext;
import ghidra.app.plugin.core.debug.mapping.DebuggerPlatformMapper;
import ghidra.app.plugin.core.debug.mapping.DisassemblyResult;
import ghidra.debug.api.platform.DebuggerPlatformMapper;
import ghidra.debug.api.platform.DisassemblyResult;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.cmd.TypedBackgroundCommand;
import ghidra.program.model.address.*;
import ghidra.program.util.ProgramSelection;

View File

@ -25,9 +25,9 @@ import generic.jar.ResourceFile;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingActionContext;
import ghidra.app.plugin.core.debug.mapping.DebuggerPlatformMapper;
import ghidra.app.services.DebuggerPlatformService;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.debug.api.platform.DebuggerPlatformMapper;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.AutoService.Wiring;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;

View File

@ -15,7 +15,7 @@
*/
package ghidra.app.plugin.core.debug.event;
import ghidra.app.plugin.core.debug.mapping.DebuggerPlatformMapper;
import ghidra.debug.api.platform.DebuggerPlatformMapper;
import ghidra.framework.plugintool.PluginEvent;
import ghidra.trace.model.Trace;

View File

@ -15,8 +15,8 @@
*/
package ghidra.app.plugin.core.debug.event;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.services.DebuggerTraceManagerService.ActivationCause;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginEvent;
public class TraceActivatedPluginEvent extends PluginEvent {

View File

@ -15,7 +15,7 @@
*/
package ghidra.app.plugin.core.debug.event;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginEvent;
public class TraceInactiveCoordinatesPluginEvent extends PluginEvent {

View File

@ -15,7 +15,7 @@
*/
package ghidra.app.plugin.core.debug.event;
import ghidra.app.services.TraceRecorder;
import ghidra.debug.api.model.TraceRecorder;
import ghidra.framework.plugintool.PluginEvent;
public class TraceRecorderAdvancedPluginEvent extends PluginEvent {

View File

@ -21,9 +21,9 @@ import javax.swing.JLabel;
import org.apache.commons.collections4.ComparatorUtils;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.async.AsyncDebouncer;
import ghidra.async.AsyncTimer;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.program.model.address.Address;
import ghidra.trace.model.Trace;
import ghidra.trace.model.Trace.*;

View File

@ -49,9 +49,9 @@ import ghidra.app.plugin.core.debug.gui.target.DebuggerTargetsPlugin;
import ghidra.app.plugin.core.debug.gui.thread.DebuggerThreadsPlugin;
import ghidra.app.plugin.core.debug.gui.time.DebuggerTimePlugin;
import ghidra.app.plugin.core.debug.gui.watch.DebuggerWatchesPlugin;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer;
import ghidra.app.services.DebuggerTraceManagerService.BooleanChangeAdapter;
import ghidra.async.AsyncUtils;
import ghidra.debug.api.model.DebuggerProgramLaunchOffer;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.util.PluginUtils;
import ghidra.program.database.ProgramContentHandler;
@ -275,37 +275,6 @@ public interface DebuggerResources {
Color COLOR_VALUE_CHANGED_SEL =
new GColor("color.debugger.plugin.resources.value.changed.selected");
String NAME_BREAKPOINT_MARKER_ENABLED = "Enabled Breakpoint";
String NAME_BREAKPOINT_MARKER_DISABLED = "Disabled Breakpoint";
String NAME_BREAKPOINT_MARKER_MIXED = "Mixed Breakpoint";
String NAME_BREAKPOINT_MARKER_INEFF_EN = "Ineffective Enabled Breakpoint";
String NAME_BREAKPOINT_MARKER_INEFF_DIS = "Ineffective Disabled Breakpoint";
String NAME_BREAKPOINT_MARKER_INEFF_MIX = "Ineffective Mixed Breakpoint";
String NAME_BREAKPOINT_MARKER_INCON_EN = "Inconsistent Enabled Breakpoint";
String NAME_BREAKPOINT_MARKER_INCON_DIS = "Inconsistent Disabled Breakpoint";
String NAME_BREAKPOINT_MARKER_INCON_MIX = "Inconsistent Mixed Breakpoint";
Icon ICON_BREAKPOINT_OVERLAY_INCONSISTENT =
new GIcon("icon.debugger.breakpoint.overlay.inconsistent");
Icon ICON_BREAKPOINT_MARKER_ENABLED = new GIcon("icon.debugger.breakpoint.marker.enabled");
Icon ICON_BREAKPOINT_MARKER_DISABLED = new GIcon("icon.debugger.breakpoint.marker.disabled");
Icon ICON_BREAKPOINT_MARKER_MIXED =
new GIcon("icon.debugger.breakpoint.marker.mixed");
Icon ICON_BREAKPOINT_MARKER_INEFF_EN =
new GIcon("icon.debugger.breakpoint.marker.ineffective.enabled");
Icon ICON_BREAKPOINT_MARKER_INEFF_DIS =
new GIcon("icon.debugger.breakpoint.marker.ineffective.disabled");
Icon ICON_BREAKPOINT_MARKER_INEFF_MIX =
new GIcon("icon.debugger.breakpoint.marker.ineffective.mixed");
Icon ICON_BREAKPOINT_MARKER_INCON_EN =
new MultiIcon(ICON_BREAKPOINT_MARKER_ENABLED, ICON_BREAKPOINT_OVERLAY_INCONSISTENT);
Icon ICON_BREAKPOINT_MARKER_INCON_DIS =
new MultiIcon(ICON_BREAKPOINT_MARKER_DISABLED, ICON_BREAKPOINT_OVERLAY_INCONSISTENT);
Icon ICON_BREAKPOINT_MARKER_INCON_MIX =
new MultiIcon(ICON_BREAKPOINT_MARKER_MIXED, ICON_BREAKPOINT_OVERLAY_INCONSISTENT);
Icon ICON_UNIQUE_REF_READ = new GIcon("icon.debugger.unique.ref.read"); // TODO
Icon ICON_UNIQUE_REF_WRITE = new GIcon("icon.debugger.unique.ref.write"); // TODO
Icon ICON_UNIQUE_REF_RW = new MultiIcon(ICON_UNIQUE_REF_READ, ICON_UNIQUE_REF_WRITE); // TODO
@ -1147,19 +1116,6 @@ public interface DebuggerResources {
}
}
abstract class AbstractToggleBreakpointAction extends DockingAction {
public static final String NAME = "Toggle Breakpoint";
// TODO: A "toggle breakpoint" icon
public static final Icon ICON = ICON_BREAKPOINT_MARKER_MIXED;
public static final String HELP_ANCHOR = "toggle_breakpoint";
public AbstractToggleBreakpointAction(Plugin owner) {
super(NAME, owner.getName());
setDescription("Set, enable, or disable a breakpoint");
setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR));
}
}
abstract class AbstractSetBreakpointAction extends DockingAction {
public static final String NAME = "Set Breakpoint";
public static final Icon ICON = ICON_SET_BREAKPOINT;

Some files were not shown because too many files have changed in this diff Show More