mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-21 19:42:14 +00:00
GP-3818: Create TraceRMI launcher framework. Launch script for gdb.
This commit is contained in:
parent
4561e8335d
commit
eea90f49c9
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
58
Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/local-gdb.sh
Executable file
58
Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/local-gdb.sh
Executable 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"
|
@ -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
|
||||
""")
|
||||
|
0
Ghidra/Debug/Debugger-api/Module.manifest
Normal file
0
Ghidra/Debug/Debugger-api/Module.manifest
Normal 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')
|
||||
}
|
2
Ghidra/Debug/Debugger-api/certification.manifest
Normal file
2
Ghidra/Debug/Debugger-api/certification.manifest
Normal file
@ -0,0 +1,2 @@
|
||||
##VERSION: 2.0
|
||||
Module.manifest||GHIDRA||||END|
|
@ -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 {
|
||||
|
||||
/**
|
@ -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 {
|
@ -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 {
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
/**
|
@ -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 {
|
||||
/**
|
@ -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;
|
||||
|
@ -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;
|
@ -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 {
|
||||
|
||||
/**
|
@ -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 {
|
||||
/**
|
@ -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();
|
||||
}
|
@ -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();
|
@ -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 {
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
@ -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
|
@ -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;
|
||||
|
@ -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;
|
@ -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);
|
@ -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.*;
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
@ -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
|
@ -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;
|
||||
|
||||
/**
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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.*;
|
||||
|
@ -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.*;
|
||||
|
@ -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
|
@ -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
|
||||
*
|
@ -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);
|
||||
}
|
@ -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;
|
@ -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
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
@ -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() {
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
@ -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
|
@ -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.*;
|
||||
|
@ -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.
|
||||
|
@ -27,6 +27,7 @@ eclipse.project.name = 'Debug Debugger-rmi-trace'
|
||||
dependencies {
|
||||
api project(':Pty')
|
||||
api project(':Debugger')
|
||||
api project(':Debugger-api')
|
||||
}
|
||||
|
||||
task generateProtoPy {
|
||||
|
@ -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|
|
||||
|
@ -0,0 +1 @@
|
||||
TraceRmiLaunchOpinion
|
@ -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')"));
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
@ -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);
|
||||
}
|
||||
|
@ -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.*;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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')
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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.*;
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user