GP-3857: Port most Debugger components to TraceRmi.

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

View file

@ -46,7 +46,10 @@ shift
target_args="$@"
"$OPT_GDB_PATH" \
-q \
-ex "set pagination off" \
-ex "set confirm off" \
-ex "show version" \
-ex "python import ghidragdb" \
-ex "file \"$target_image\"" \
-ex "set args $target_args" \
@ -55,4 +58,5 @@ target_args="$@"
-ex "ghidra trace start" \
-ex "ghidra trace sync-enable" \
-ex "$OPT_START_CMD" \
-ex "set confirm on" \
-ex "set pagination on"

View file

@ -239,11 +239,18 @@ class DefaultRegisterMapper(object):
.format(name, value, value.type))
return RegVal(self.map_name(inf, name), av)
def convert_value_back(self, value, size=None):
if size is not None:
value = value[-size:].rjust(size, b'\0')
if self.byte_order == 'little':
value = bytes(reversed(value))
return value
def map_name_back(self, inf, name):
return name
def map_value_back(self, inf, name, value):
return RegVal(self.map_name_back(inf, name), value)
return RegVal(self.map_name_back(inf, name), self.convert_value_back(value))
class Intel_x86_64_RegisterMapper(DefaultRegisterMapper):
@ -268,6 +275,7 @@ class Intel_x86_64_RegisterMapper(DefaultRegisterMapper):
def map_name_back(self, inf, name):
if name == 'rflags':
return 'eflags'
return name
DEFAULT_BE_REGISTER_MAPPER = DefaultRegisterMapper('big')

View file

@ -50,6 +50,8 @@ STACK_PATTERN = THREAD_PATTERN + '.Stack'
FRAME_KEY_PATTERN = '[{level}]'
FRAME_PATTERN = STACK_PATTERN + FRAME_KEY_PATTERN
REGS_PATTERN = FRAME_PATTERN + '.Registers'
REG_KEY_PATTERN = '[{regname}]'
REG_PATTERN = REGS_PATTERN + REG_KEY_PATTERN
MEMORY_PATTERN = INFERIOR_PATTERN + '.Memory'
REGION_KEY_PATTERN = '[{start:08x}]'
REGION_PATTERN = MEMORY_PATTERN + REGION_KEY_PATTERN
@ -564,15 +566,26 @@ def putreg(frame, reg_descs):
space = REGS_PATTERN.format(infnum=inf.num, tnum=gdb.selected_thread().num,
level=frame.level())
STATE.trace.create_overlay_space('register', space)
robj = STATE.trace.create_object(space)
robj.insert()
cobj = STATE.trace.create_object(space)
cobj.insert()
mapper = STATE.trace.register_mapper
keys = []
values = []
for desc in reg_descs:
v = frame.read_register(desc)
values.append(mapper.map_value(inf, desc.name, v))
rv = mapper.map_value(inf, desc.name, v)
values.append(rv)
# TODO: Key by gdb's name or mapped name? I think gdb's.
rpath = REG_PATTERN.format(infnum=inf.num, tnum=gdb.selected_thread(
).num, level=frame.level(), regname=desc.name)
keys.append(REG_KEY_PATTERN.format(regname=desc.name))
robj = STATE.trace.create_object(rpath)
robj.set_value('_value', rv.value)
robj.insert()
cobj.retain_values(keys)
# TODO: Memorize registers that failed for this arch, and omit later.
return {'missing': STATE.trace.put_registers(space, values)}
missing = STATE.trace.put_registers(space, values)
return {'missing': missing}
@cmd('ghidra trace putreg', '-ghidra-trace-putreg', gdb.COMMAND_DATA, True)
@ -585,7 +598,8 @@ def ghidra_trace_putreg(group='all', *, is_mi, **kwargs):
STATE.require_tx()
frame = gdb.selected_frame()
return putreg(frame, frame.architecture().registers(group))
with STATE.client.batch() as b:
return putreg(frame, frame.architecture().registers(group))
@cmd('ghidra trace delreg', '-ghidra-trace-delreg', gdb.COMMAND_DATA, True)
@ -977,6 +991,17 @@ def compute_inf_state(inf):
return 'STOPPED'
def put_inferior_state(inf):
ipath = INFERIOR_PATTERN.format(infnum=inf.num)
infobj = STATE.trace.proxy_object_path(ipath)
istate = compute_inf_state(inf)
infobj.set_value('_state', istate)
for t in inf.threads():
tpath = THREAD_PATTERN.format(infnum=inf.num, tnum=t.num)
tobj = STATE.trace.proxy_object_path(tpath)
tobj.set_value('_state', convert_state(t))
def put_inferiors():
# TODO: Attributes like _exit_code, _state?
# _state would be derived from threads
@ -1034,6 +1059,7 @@ def put_single_breakpoint(b, ibobj, inf, ikeys):
mapper = STATE.trace.memory_mapper
bpath = BREAKPOINT_PATTERN.format(breaknum=b.number)
brkobj = STATE.trace.create_object(bpath)
brkobj.set_value('_enabled', b.enabled)
if b.type == gdb.BP_BREAKPOINT:
brkobj.set_value('_expression', b.location)
brkobj.set_value('_kinds', 'SW_EXECUTE')
@ -1073,6 +1099,7 @@ def put_single_breakpoint(b, ibobj, inf, ikeys):
if inf.num not in l.thread_groups:
continue
locobj = STATE.trace.create_object(bpath + k)
locobj.set_value('_enabled', l.enabled)
ik = INF_BREAK_KEY_PATTERN.format(breaknum=b.number, locnum=i+1)
ikeys.append(ik)
if b.location is not None: # Implies execution break

View file

@ -31,12 +31,13 @@ GhidraHookPrefix()
class HookState(object):
__slots__ = ('installed', 'mem_catchpoint', 'batch')
__slots__ = ('installed', 'mem_catchpoint', 'batch', 'skip_continue')
def __init__(self):
self.installed = False
self.mem_catchpoint = None
self.batch = None
self.skip_continue = False
def ensure_batch(self):
if self.batch is None:
@ -48,6 +49,11 @@ class HookState(object):
commands.STATE.client.end_batch()
self.batch = None
def check_skip_continue(self):
skip = self.skip_continue
self.skip_continue = False
return skip
class InferiorState(object):
__slots__ = ('first', 'regions', 'modules', 'threads', 'breaks', 'visited')
@ -70,6 +76,8 @@ class InferiorState(object):
if first:
commands.put_inferiors()
commands.put_environment()
else:
commands.put_inferior_state(gdb.selected_inferior())
if self.threads:
commands.put_threads()
self.threads = False
@ -81,7 +89,8 @@ class InferiorState(object):
frame = gdb.selected_frame()
hashable_frame = (thread, frame.level())
if first or hashable_frame not in self.visited:
commands.putreg(frame, frame.architecture().registers())
commands.putreg(
frame, frame.architecture().registers('general'))
commands.putmem("$pc", "1", from_tty=False)
commands.putmem("$sp", "1", from_tty=False)
self.visited.add(hashable_frame)
@ -224,7 +233,6 @@ def on_memory_changed(event):
def on_register_changed(event):
gdb.write("Register changed: {}".format(dir(event)))
inf = gdb.selected_inferior()
if inf.num not in INF_STATES:
return
@ -240,6 +248,8 @@ def on_register_changed(event):
def on_cont(event):
if (HOOK_STATE.check_skip_continue()):
return
inf = gdb.selected_inferior()
if inf.num not in INF_STATES:
return
@ -254,6 +264,7 @@ def on_cont(event):
def on_stop(event):
if hasattr(event, 'breakpoints') and HOOK_STATE.mem_catchpoint in event.breakpoints:
HOOK_STATE.skip_continue = True
return
inf = gdb.selected_inferior()
if inf.num not in INF_STATES:
@ -337,6 +348,8 @@ def on_breakpoint_created(b):
def on_breakpoint_modified(b):
if b == HOOK_STATE.mem_catchpoint:
return
inf = gdb.selected_inferior()
notify_others_breaks(inf)
if inf.num not in INF_STATES:
@ -438,6 +451,16 @@ def hook_frame():
on_frame_selected()
@cmd_hook('hookpost-up')
def hook_frame_up():
on_frame_selected()
@cmd_hook('hookpost-down')
def hook_frame_down():
on_frame_selected()
# TODO: Checks and workarounds for events missing in gdb 8
def install_hooks():
if HOOK_STATE.installed:
@ -451,6 +474,8 @@ def install_hooks():
gdb.events.new_thread.connect(on_new_thread)
hook_thread.hook()
hook_frame.hook()
hook_frame_up.hook()
hook_frame_down.hook()
# Respond to user-driven state changes: (Not target-driven)
gdb.events.memory_changed.connect(on_memory_changed)
@ -508,6 +533,8 @@ def remove_hooks():
gdb.events.new_thread.disconnect(on_new_thread)
hook_thread.unhook()
hook_frame.unhook()
hook_frame_up.unhook()
hook_frame_down.unhook()
gdb.events.memory_changed.disconnect(on_memory_changed)
gdb.events.register_changed.disconnect(on_register_changed)

View file

@ -14,6 +14,7 @@
# limitations under the License.
##
from concurrent.futures import Future, Executor
from contextlib import contextmanager
import re
from ghidratrace import sch
@ -24,13 +25,30 @@ import gdb
from . import commands, hooks, util
@contextmanager
def no_pagination():
before = gdb.parameter('pagination')
gdb.set_parameter('pagination', False)
yield
gdb.set_parameter('pagination', before)
@contextmanager
def no_confirm():
before = gdb.parameter('confirm')
gdb.set_parameter('confirm', False)
yield
gdb.set_parameter('confirm', before)
class GdbExecutor(Executor):
def submit(self, fn, *args, **kwargs):
fut = Future()
def _exec():
try:
result = fn(*args, **kwargs)
with no_pagination():
result = fn(*args, **kwargs)
hooks.HOOK_STATE.end_batch()
fut.set_result(result)
except Exception as e:
@ -186,7 +204,9 @@ def find_frame_by_regs_obj(object):
# Because there's no method to get a register by name....
def find_reg_by_name(f, name):
for reg in f.architecture().registers():
if reg.name == name:
# TODO: gdb appears to be case sensitive, but until we encounter a
# situation where case matters, we'll be insensitive
if reg.name.lower() == name.lower():
return reg
raise KeyError(f"No such register: {name}")
@ -453,7 +473,8 @@ def launch_run(inferior: sch.Schema('Inferior'),
def kill(inferior: sch.Schema('Inferior')):
"""Kill execution of the inferior."""
switch_inferior(find_inf_by_obj(inferior))
gdb.execute('kill')
with no_confirm():
gdb.execute('kill')
@REGISTRY.method
@ -463,8 +484,11 @@ def resume(inferior: sch.Schema('Inferior')):
gdb.execute('continue')
# Technically, inferior is not required, but it hints that the affected object
# is the current inferior. This in turn queues the UI to enable or disable the
# button appropriately
@REGISTRY.method
def interrupt():
def interrupt(inferior: sch.Schema('Inferior')):
"""Interrupt the execution of the debugged program."""
gdb.execute('interrupt')
@ -490,7 +514,7 @@ def step_out(thread: sch.Schema('Thread')):
gdb.execute('finish')
@REGISTRY.method(action='step_ext')
@REGISTRY.method(action='step_ext', name='Advance')
def step_advance(thread: sch.Schema('Thread'), address: Address):
"""Continue execution up to the given address (advance)."""
t = find_thread_by_obj(thread)
@ -499,7 +523,7 @@ def step_advance(thread: sch.Schema('Thread'), address: Address):
gdb.execute(f'advance *0x{offset:x}')
@REGISTRY.method(action='step_ext')
@REGISTRY.method(action='step_ext', name='Return')
def step_return(thread: sch.Schema('Thread'), value: int=None):
"""Skip the remainder of the current function (return)."""
find_thread_by_obj(thread).switch()
@ -641,13 +665,13 @@ def write_mem(inferior: sch.Schema('Inferior'), address: Address, data: bytes):
@REGISTRY.method
def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes):
def write_reg(frame: sch.Schema('StackFrame'), name: str, value: bytes):
"""Write a register."""
f = find_frame_by_obj(frame)
f.select()
inf = gdb.selected_inferior()
mname, mval = frame.trace.register_mapper.map_value_back(inf, name, value)
reg = find_reg_by_name(f, mname)
size = int(gdb.parse_and_eval(f'sizeof(${mname})'))
size = int(gdb.parse_and_eval(f'sizeof(${reg.name})'))
arr = '{' + ','.join(str(b) for b in mval) + '}'
gdb.execute(f'set ((unsigned char[{size}])${mname}) = {arr}')
gdb.execute(f'set ((unsigned char[{size}])${reg.name}) = {arr}')

View file

@ -18,7 +18,6 @@ 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;
@ -29,13 +28,6 @@ import ghidra.program.model.listing.Program;
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
*

View file

@ -87,4 +87,14 @@ public interface LocationTracker {
* @return true if re-computation and "goto" is warranted
*/
boolean affectedByStackChange(TraceStack stack, DebuggerCoordinates coordinates);
/**
* Indicates whether the user should expect instructions at the tracked location.
*
* <p>
* Essentially, is this tracking the program counter?
*
* @return true to disassemble, false not to
*/
boolean shouldDisassemble();
}

View file

@ -15,6 +15,9 @@
*/
package ghidra.debug.api.target;
import java.util.HashMap;
import java.util.Map;
/**
* A name for a commonly-recognized target action.
*
@ -31,15 +34,43 @@ package ghidra.debug.api.target;
* effort to match its methods to these stock actions where applicable, but ultimately, it is up to
* the UI to decide what is presented where.
*/
public record ActionName(String name) {
public static final ActionName REFRESH = new ActionName("refresh");
public record ActionName(String name, boolean builtIn) {
private static final Map<String, ActionName> NAMES = new HashMap<>();
public static ActionName name(String name) {
synchronized (NAMES) {
return NAMES.computeIfAbsent(name, n -> new ActionName(n, false));
}
}
private static ActionName builtIn(String name) {
synchronized (NAMES) {
ActionName action = new ActionName(name, true);
if (NAMES.put(name, action) != null) {
throw new AssertionError();
}
return action;
}
}
private static ActionName extended(String name) {
synchronized (NAMES) {
ActionName action = new ActionName(name, false);
if (NAMES.put(name, action) != null) {
throw new AssertionError();
}
return action;
}
}
public static final ActionName REFRESH = builtIn("refresh");
/**
* Activate a given object and optionally a time
*
* <p>
* Forms: (focus:Object), (focus:Object, snap:LONG), (focus:Object, time:STR)
*/
public static final ActionName ACTIVATE = new ActionName("activate");
public static final ActionName ACTIVATE = builtIn("activate");
/**
* A weaker form of activate.
*
@ -48,9 +79,9 @@ public record ActionName(String name) {
* used to communicate selection (i.e., highlight) of the object. Whereas, double-clicking or
* pressing enter would more likely invoke 'activate.'
*/
public static final ActionName FOCUS = new ActionName("focus");
public static final ActionName TOGGLE = new ActionName("toggle");
public static final ActionName DELETE = new ActionName("delete");
public static final ActionName FOCUS = builtIn("focus");
public static final ActionName TOGGLE = builtIn("toggle");
public static final ActionName DELETE = builtIn("delete");
/**
* Execute a CLI command
@ -58,7 +89,7 @@ public record ActionName(String name) {
* <p>
* Forms: (cmd:STRING):STRING; Optional arguments: capture:BOOL
*/
public static final ActionName EXECUTE = new ActionName("execute");
public static final ActionName EXECUTE = builtIn("execute");
/**
* Connect the back-end to a (usually remote) target
@ -66,23 +97,23 @@ public record ActionName(String name) {
* <p>
* Forms: (spec:STRING)
*/
public static final ActionName CONNECT = new ActionName("connect");
public static final ActionName CONNECT = extended("connect");
/**
* Forms: (target:Attachable), (pid:INT), (spec:STRING)
*/
public static final ActionName ATTACH = new ActionName("attach");
public static final ActionName DETACH = new ActionName("detach");
public static final ActionName ATTACH = extended("attach");
public static final ActionName DETACH = extended("detach");
/**
* Forms: (command_line:STRING), (file:STRING,args:STRING), (file:STRING,args:STRING_ARRAY),
* (ANY*)
*/
public static final ActionName LAUNCH = new ActionName("launch");
public static final ActionName KILL = new ActionName("kill");
public static final ActionName LAUNCH = extended("launch");
public static final ActionName KILL = builtIn("kill");
public static final ActionName RESUME = new ActionName("resume");
public static final ActionName INTERRUPT = new ActionName("interrupt");
public static final ActionName RESUME = builtIn("resume");
public static final ActionName INTERRUPT = builtIn("interrupt");
/**
* All of these will show in the "step" portion of the control toolbar, if present. The
@ -93,25 +124,25 @@ public record ActionName(String name) {
* context. (Multiple will appear, but may confuse the user.) You can have as many extended step
* actions as you like. They will be ordered lexicographically by name.
*/
public static final ActionName STEP_INTO = new ActionName("step_into");
public static final ActionName STEP_OVER = new ActionName("step_over");
public static final ActionName STEP_OUT = new ActionName("step_out");
public static final ActionName STEP_INTO = builtIn("step_into");
public static final ActionName STEP_OVER = builtIn("step_over");
public static final ActionName STEP_OUT = builtIn("step_out");
/**
* Skip is not typically available, except in emulators. If the back-end debugger does not have
* a command for this action out-of-the-box, we do not recommend trying to implement it
* yourself. The purpose of these actions just to expose/map each command to the UI, not to
* invent new features for the back-end debugger.
*/
public static final ActionName STEP_SKIP = new ActionName("step_skip");
public static final ActionName STEP_SKIP = builtIn("step_skip");
/**
* Step back is not typically available, except in emulators and timeless (or time-travel)
* debuggers.
*/
public static final ActionName STEP_BACK = new ActionName("step_back");
public static final ActionName STEP_BACK = builtIn("step_back");
/**
* The action for steps that don't fit one of the common stepping actions.
*/
public static final ActionName STEP_EXT = new ActionName("step_ext");
public static final ActionName STEP_EXT = extended("step_ext");
/**
* Forms: (addr:ADDRESS), R/W(rng:RANGE), (expr:STRING)
@ -123,25 +154,25 @@ public record ActionName(String name) {
* The client may pass either null or "" for condition and/or commands to indicate omissions of
* those arguments.
*/
public static final ActionName BREAK_SW_EXECUTE = new ActionName("break_sw_execute");
public static final ActionName BREAK_HW_EXECUTE = new ActionName("break_hw_execute");
public static final ActionName BREAK_READ = new ActionName("break_read");
public static final ActionName BREAK_WRITE = new ActionName("break_write");
public static final ActionName BREAK_ACCESS = new ActionName("break_access");
public static final ActionName BREAK_EXT = new ActionName("break_ext");
public static final ActionName BREAK_SW_EXECUTE = builtIn("break_sw_execute");
public static final ActionName BREAK_HW_EXECUTE = builtIn("break_hw_execute");
public static final ActionName BREAK_READ = builtIn("break_read");
public static final ActionName BREAK_WRITE = builtIn("break_write");
public static final ActionName BREAK_ACCESS = builtIn("break_access");
public static final ActionName BREAK_EXT = extended("break_ext");
/**
* Forms: (rng:RANGE)
*/
public static final ActionName READ_MEM = new ActionName("read_mem");
public static final ActionName READ_MEM = builtIn("read_mem");
/**
* Forms: (addr:ADDRESS,data:BYTES)
*/
public static final ActionName WRITE_MEM = new ActionName("write_mem");
public static final ActionName WRITE_MEM = builtIn("write_mem");
// NOTE: no read_reg. Use refresh(RegContainer), refresh(RegGroup), refresh(Register)
/**
* Forms: (frame:Frame,name:STRING,value:BYTES), (register:Register,value:BYTES)
*/
public static final ActionName WRITE_REG = new ActionName("write_reg");
public static final ActionName WRITE_REG = builtIn("write_reg");
}

View file

@ -19,14 +19,11 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.util.function.Function;
import docking.ActionContext;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.Register;
import ghidra.program.model.lang.RegisterValue;
@ -38,39 +35,81 @@ import ghidra.trace.model.memory.TraceMemoryState;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.target.TraceObjectKeyPath;
import ghidra.trace.model.thread.TraceThread;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* The interface between the front-end UI and the back-end connector.
*
* <p>
* Anything the UI might command a target to do must be defined as a method here. Each
* implementation can then sort out, using context from the UI as appropriate, how best to effect
* the command using the protocol and resources available on the back-end.
*/
public interface Target {
long TIMEOUT_MILLIS = 10000;
/**
* A description of a UI action provided by this target.
*
* <p>
* In most cases, this will generate a menu entry or a toolbar button, but in some cases, it's
* just invoked implicitly. Often, the two suppliers are implemented using lambda functions, and
* those functions will keep whatever some means of querying UI and/or target context in their
* closures.
*
* @param display the text to display on UI actions associated with this entry
* @param name the name of a common debugger command this action implements
* @param details text providing more details, usually displayed in a tool tip
* @param requiresPrompt true if invoking the action requires further user interaction
* @param enabled a supplier to determine whether an associated action in the UI is enabled.
* @param action a function for invoking this action asynchronously
*/
record ActionEntry(String display, ActionName name, String details, boolean requiresPrompt,
BooleanSupplier enabled, Supplier<CompletableFuture<?>> action) {
BooleanSupplier enabled, Function<Boolean, CompletableFuture<?>> action) {
/**
* Check if this action is currently enabled
*
* @return true if enabled
*/
public boolean isEnabled() {
return enabled.getAsBoolean();
}
/**
* Invoke the action asynchronously, prompting if desired
*
* <p>
* Note this will impose a timeout of {@value Target#TIMEOUT_MILLIS} milliseconds.
*
* @param prompt whether or not to prompt the user for arguments
* @return the future result, often {@link Void}
*/
public CompletableFuture<?> invokeAsync(boolean prompt) {
return action.get().orTimeout(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
public CompletableFuture<?> invokeAsyncLogged(boolean prompt, PluginTool tool) {
return invokeAsync(prompt).exceptionally(ex -> {
if (tool != null) {
tool.setStatusInfo(display + " failed: " + ex, true);
}
Msg.error(this, display + " failed: " + ex, ex);
return ExceptionUtils.rethrow(ex);
});
return action.apply(prompt).orTimeout(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
/**
* Invoke the action synchronously
*
* <p>
* To avoid blocking the Swing thread on a remote socket, this method cannot be called on
* the Swing thread.
*
* @param prompt whether or not to prompt the user for arguments
*/
public void run(boolean prompt) {
get(prompt);
}
/**
* Invoke the action synchronously, getting its result
*
* @param prompt whether or not to prompt the user for arguments
* @return the resulting value, if applicable
*/
public Object get(boolean prompt) {
if (Swing.isSwingThread()) {
throw new AssertionError("Refusing to block the Swing thread. Use a Task.");
@ -82,30 +121,107 @@ public interface Target {
throw new RuntimeException(e);
}
}
/**
* Check if this action's name is built in
*
* @return true if built in.
*/
public boolean builtIn() {
return name != null && name.builtIn();
}
}
/**
* Check if the target is still valid
*
* @return true if valid
*/
boolean isValid();
/**
* Get the trace into which this target is recorded
*
* @return the trace
*/
Trace getTrace();
/**
* Get the current snapshot key for the target
*
* <p>
* For most targets, this is the most recently created snapshot.
*
* @return the snapshot
*/
// TODO: Should this be TraceSchedule getTime()?
long getSnap();
/**
* Collect all actions that implement the given common debugger command
*
* @param name the action name
* @param context applicable context from the UI
* @return the collected actions
*/
Map<String, ActionEntry> collectActions(ActionName name, ActionContext context);
/**
* Get the trace thread that contains the given object
*
* @param path the path of the object
* @return the thread, or null
*/
TraceThread getThreadForSuccessor(TraceObjectKeyPath path);
/**
* Get the execution state of the given thread
*
* @param thread the thread
* @return the state
*/
TargetExecutionState getThreadExecutionState(TraceThread thread);
/**
* Get the trace stack frame that contains the given object
*
* @param path the path of the object
* @return the stack frame, or null
*/
TraceStackFrame getStackFrameForSuccessor(TraceObjectKeyPath path);
/**
* Check if the target supports synchronizing focus
*
* @return true if supported
*/
boolean isSupportsFocus();
/**
* Get the object that currently has focus on the back end's UI
*
* @return the focused object's path, or null
*/
TraceObjectKeyPath getFocus();
/**
* @see #activate(DebuggerCoordinates, DebuggerCoordinates)
*/
CompletableFuture<Void> activateAsync(DebuggerCoordinates prev, DebuggerCoordinates coords);
/**
* Request that the back end's focus be set to the same as the front end's (Ghidra's) GUI.
*
* @param prev the GUI's immediately previous coordinates
* @param coords the GUI's current coordinates
*/
void activate(DebuggerCoordinates prev, DebuggerCoordinates coords);
/**
* @see #invalidateMemoryCaches()
*/
CompletableFuture<Void> invalidateMemoryCachesAsync();
/**
* Invalidate any caches on the target's back end or on the client side of the connection.
*
@ -118,11 +234,6 @@ public interface Target {
* <b>NOTE:</b> This method exists for invalidating model-based target caches. It may be
* deprecated and removed, unless it turns out we need this for Trace RMI, too.
*/
CompletableFuture<Void> invalidateMemoryCachesAsync();
/**
* See {@link #invalidateMemoryCachesAsync()}
*/
void invalidateMemoryCaches();
/**
@ -135,11 +246,11 @@ public interface Target {
*
* <p>
* The target may read more than the requested memory, usually because it will read all pages
* containing any portion of the requested set.
*
* <p>
* This task is relatively error tolerant. If a range cannot be captured -- a common occurrence
* -- the error is logged without throwing an exception.
* containing any portion of the requested set. The target should attempt to read at least the
* given memory. To the extent it is successful, it must cause the values to be recorded into
* the trace <em>before</em> this method returns. Only if the request is <em>entirely</em>
* unsuccessful should this method throw an exception. Otherwise, the failed portions, if any,
* should be logged without throwing an exception.
*
* @param set the addresses to capture
* @param monitor a monitor for displaying task steps
@ -147,30 +258,97 @@ public interface Target {
*/
void readMemory(AddressSetView set, TaskMonitor monitor) throws CancelledException;
/**
* @see #readMemory(AddressSetView, TaskMonitor)
*/
CompletableFuture<Void> writeMemoryAsync(Address address, byte[] data);
/**
* Write data to the target's memory
*
* <p>
* The target should attempt to write the memory. To the extent it is successful, it must cause
* the effects to be recorded into the trace <em>before</em> this method returns. Only if the
* request is <em>entirely</em> unsuccessful should this method throw an exception. Otherwise,
* the failed portions, if any, should be logged without throwing an exception.
*
* @param address the starting address
* @param data the bytes to write
*/
void writeMemory(Address address, byte[] data);
/**
* @see #readRegisters(TracePlatform, TraceThread, int, Set)
*/
CompletableFuture<Void> readRegistersAsync(TracePlatform platform, TraceThread thread,
int frame, Set<Register> registers);
/**
* Read and capture the named target registers for the given platform, thread, and frame.
*
* <p>
* Target target should read the registers and, to the extent it is successful, cause the values
* to be recorded into the trace <em>before</em> this method returns. Only if the request is
* <em>entirely</em> unsuccessful should this method throw an exception. Otherwise, the failed
* registers, if any, should be logged without throwing an exception.
*
* @param platform the platform defining the registers
* @param thread the thread whose context contains the register values
* @param frame the frame, if applicable, for saved register values. 0 for current values.
* @param registers the registers to read
*/
void readRegisters(TracePlatform platform, TraceThread thread, int frame,
Set<Register> registers);
/**
* @see #readRegistersAsync(TracePlatform, TraceThread, int, AddressSetView)
*/
CompletableFuture<Void> readRegistersAsync(TracePlatform platform, TraceThread thread,
int frame, AddressSetView guestSet);
/**
* Read and capture the target registers in the given address set.
*
* <p>
* Aside from how registers are named, this works equivalently to
* {@link #readRegisters(TracePlatform, TraceThread, int, Set)}.
*/
void readRegisters(TracePlatform platform, TraceThread thread, int frame,
AddressSetView guestSet);
/**
* @see #writeRegister(TracePlatform, TraceThread, int, RegisterValue)
*/
CompletableFuture<Void> writeRegisterAsync(TracePlatform platform, TraceThread thread,
int frame, RegisterValue value);
/**
* Write a value to a target register for the given platform, thread, and frame
*
* <p>
* The target should attempt to write the register. If successful, it must cause the effects to
* be recorded into the trace <em>before</em> this method returns. If the request is
* unsuccessful, this method throw an exception.
*
* @param address the starting address
* @param data the bytes to write
*/
void writeRegister(TracePlatform platform, TraceThread thread, int frame, RegisterValue value);
/**
* @see #writeRegister(TracePlatform, TraceThread, int, Address, byte[])
*/
CompletableFuture<Void> writeRegisterAsync(TracePlatform platform, TraceThread thread,
int frame, Address address, byte[] data);
/**
* Write a value to a target register by its address
*
* <p>
* Aside from how the register is named, this works equivalently to
* {@link #writeRegister(TracePlatform, TraceThread, int, RegisterValue)}. The address is the
* one defined by Ghidra.
*/
void writeRegister(TracePlatform platform, TraceThread thread, int frame, Address address,
byte[] data);
@ -179,7 +357,7 @@ public interface Target {
*
* @param platform the platform whose language defines the registers
* @param thread if a register, the thread whose registers to examine
* @param frameLevel the frame, usually 0.
* @param frame the frame level, usually 0.
* @param address the address of the variable
* @param size the size of the variable. Ignored for memory
* @return true if the variable can be mapped to the target
@ -211,11 +389,35 @@ public interface Target {
void writeVariable(TracePlatform platform, TraceThread thread, int frame, Address address,
byte[] data);
/**
* Get the kinds of breakpoints supported by the target.
*
* @return the set of kinds
*/
Set<TraceBreakpointKind> getSupportedBreakpointKinds();
/**
* @see #placeBreakpoint(AddressRange, Set, String, String)
*/
CompletableFuture<Void> placeBreakpointAsync(AddressRange range,
Set<TraceBreakpointKind> kinds, String condition, String commands);
/**
* Place a new breakpoint of the given kind(s) over the given range
*
* <p>
* If successful, this method must cause the breakpoint to be recorded into the trace.
* Otherwise, it should throw an exception.
*
* @param range the range. NOTE: The target is only required to support length-1 execution
* breakpoints.
* @param kinds the kind(s) of the breakpoint.
* @param condition optionally, a condition for the breakpoint, expressed in the back-end's
* language. NOTE: May be silently ignored by the implementation, if not supported.
* @param commands optionally, a command to execute upon hitting the breakpoint, expressed in
* the back-end's language. NOTE: May be silently ignored by the implementation, if
* not supported.
*/
void placeBreakpoint(AddressRange range, Set<TraceBreakpointKind> kinds, String condition,
String commands);
@ -227,14 +429,61 @@ public interface Target {
*/
boolean isBreakpointValid(TraceBreakpoint breakpoint);
/**
* @see #deleteBreakpoint(TraceBreakpoint)
*/
CompletableFuture<Void> deleteBreakpointAsync(TraceBreakpoint breakpoint);
/**
* Delete the given breakpoint from the target
*
* <p>
* If successful, this method must cause the breakpoint removal to be recorded in the trace.
* Otherwise, it should throw an exception.
*
* @param breakpoint the breakpoint to delete
*/
void deleteBreakpoint(TraceBreakpoint breakpoint);
/**
* @see #toggleBreakpoint(TraceBreakpoint, boolean)
*/
CompletableFuture<Void> toggleBreakpointAsync(TraceBreakpoint breakpoint, boolean enabled);
/**
* Toggle the given breakpoint on the target
*
* <p>
* If successful, this method must cause the breakpoint toggle to be recorded in the trace. If
* the state is already as desired, this method may have no effect. If unsuccessful, this method
* should throw an exception.
*
* @param breakpoint the breakpoint to toggle
* @param enabled true to enable, false to disable
*/
void toggleBreakpoint(TraceBreakpoint breakpoint, boolean enabled);
/**
* @see #forceTerminate()
*/
CompletableFuture<Void> forceTerminateAsync();
/**
* Forcefully terminate the target
*
* <p>
* This will first attempt to kill the target gracefully. In addition, and whether or not the
* target is successfully terminated, the target will be dissociated from its trace, and the
* target will be invalidated. To attempt only a graceful termination, check
* {@link #collectActions(ActionName, ActionContext)} with {@link ActionName#KILL}.
*/
void forceTerminate();
/**
* @see #disconnect()
*/
CompletableFuture<Void> disconnectAsync();
/**
* Terminate the target and its connection
*
@ -244,13 +493,6 @@ public interface Target {
* the debugger is configured to remain attached to both. Whether this is expected or acceptable
* behavior has not been decided.
*
* @see #disconnect()
*/
CompletableFuture<Void> disconnectAsync();
/**
* Terminate the target and its connection
*
* <p>
* <b>NOTE:</b> This method cannot be invoked on the Swing thread, because it may block on I/O.
*

View file

@ -15,8 +15,21 @@
*/
package ghidra.debug.api.target;
/**
* A listener for changes to the set of published targets
*/
public interface TargetPublicationListener {
/**
* The given target was published
*
* @param target the published target
*/
void targetPublished(Target target);
/**
* The given target was withdrawn, usually because it's no longer valid
*
* @param target the withdrawn target
*/
void targetWithdrawn(Target target);
}

View file

@ -658,13 +658,12 @@ public class DebuggerCoordinates {
projData = new DefaultProjectData(projLoc, false, false);
}
catch (NotOwnerException e) {
Msg.showError(DebuggerCoordinates.class, tool.getToolFrame(), "Trace Open Failed",
Msg.error(DebuggerCoordinates.class,
"Not project owner: " + projLoc + "(" + pathname + ")");
return null;
}
catch (IOException | LockException e) {
Msg.showError(DebuggerCoordinates.class, tool.getToolFrame(), "Trace Open Failed",
"Project error: " + e.getMessage());
Msg.error(DebuggerCoordinates.class, "Project error: " + e.getMessage());
return null;
}
}
@ -676,8 +675,7 @@ public class DebuggerCoordinates {
if (version != DomainFile.DEFAULT_VERSION) {
message += " version " + version;
}
String title = df == null ? "Trace Not Found" : "Wrong File Type";
Msg.showError(DebuggerCoordinates.class, tool.getToolFrame(), title, message);
Msg.error(DebuggerCoordinates.class, message);
return null;
}
return df;

View file

@ -13,21 +13,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.debug.api.tracermi;
import java.util.Collection;
import java.io.IOException;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.ServiceInfo;
/**
* A terminal with some back-end element attached to it
*/
public interface TerminalSession extends AutoCloseable {
@Override
void close() throws IOException;
@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
* Terminate the session without closing the terminal
*/
Collection<PluginTool> getProxyingPluginTools();
void terminate() throws IOException;
/**
* Check whether the terminal session is terminated or still active
*
* @return true for terminated, false for active
*/
boolean isTerminated();
/**
* Provide a human-readable description of the session
*
* @return the description
*/
String description();
}

View file

@ -17,23 +17,124 @@ package ghidra.debug.api.tracermi;
import java.io.IOException;
import java.net.SocketAddress;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeoutException;
import ghidra.trace.model.Trace;
/**
* A connection to a TraceRmi back end
*
* <p>
* TraceRmi is a two-way request-reply channel, usually over TCP. The back end, i.e., the trace-rmi
* plugin hosted in the target platform's actual debugger, is granted a fixed set of
* methods/messages for creating and populating a {@link Trace}. Each such trace is designated as a
* target. The back end provides a set of methods for the front-end to use to control the connection
* and its targets. For a given connection, the methods are fixed, but each back end may provide a
* different set of methods to best describe/model its command set. The same methods are applicable
* to all of the back end's target. While uncommon, one back end may create several targets. E.g.,
* if a target creates a child process, and the back-end debugger is configured to remain attached
* to both parent and child, then it should create and publish a second target.
*/
public interface TraceRmiConnection extends AutoCloseable {
/**
* Get the address of the back end debugger
*
* @return the address, usually IP of the host and port for the trace-rmi plugin.
*/
SocketAddress getRemoteAddress();
/**
* Get the methods provided by the back end
*
* @return the method registry
*/
RemoteMethodRegistry getMethods();
/**
* Wait for the first trace created by the back end.
*
* <p>
* Typically, a connection handles only a single target. A shell script handles launching the
* back-end debugger, creating its first target, and connecting back to the front end via
* TraceRmi. If a secondary target does appear, it usually happens only after the initial target
* has run. Thus, this method is useful for waiting on and getting and handle to that initial
* target.
*
* @param timeoutMillis the number of milliseconds to wait for the target
* @return the trace
* @throws TimeoutException if no trace is created after the given timeout. This usually
* indicates there was an error launching the initial target, e.g., the target's
* binary was not found on the target's host.
*/
Trace waitForTrace(long timeoutMillis) throws TimeoutException;
/**
* Get the last snapshot created by the back end for the given trace.
*
* <p>
* Back ends that support timeless or time-travel debugging have not been integrated yet, but in
* those cases, we anticipate this method returning the current snapshot (however the back end
* defines that with respect to its own definition of time), whether or not it is the last
* snapshot it created. If the back end has not created a snapshot yet, 0 is returned.
*
* @param trace
* @return the snapshot number
* @throws NoSuchElementException if the given trace is not a target for this connection
*/
long getLastSnapshot(Trace trace);
/**
* Forcefully remove the given trace from the connection.
*
* <p>
* This removes the back end's access to the given trace and removes this connection from the
* trace's list of consumers (thus, freeing it if this was the only remaining consumer.) For all
* intents and purposes, the given trace is no longer a target for this connection.
*
* <p>
* <b>NOTE:</b> This method should only be used if gracefully killing the target has failed. In
* some cases, it may be better to terminate the entire connection (See {@link #close()}) or to
* terminate the back end debugger. The back end gets no notification that its trace was
* forcefully removed. However, subsequent requests involving that trace will result in errors.
*
* @param trace the trace to remove
*/
void forceCloseTrace(Trace trace);
/**
* Close the TraceRmi connection.
*
* <p>
* {@inheritDoc}
*
* <p>
* Upon closing, all the connection's targets (there's usually only one) will be withdrawn and
* invalidated.
*/
@Override
void close() throws IOException;
/**
* Check if the connection has been closed
*
* @return true if closed, false if still open/valid
*/
boolean isClosed();
/**
* Wait for the connection to become closed.
*
* <p>
* This is usually just for clean-up purposes during automated testing.
*/
void waitClosed();
/**
* Check if the given trace represents one of this connection's targets.
*
* @param trace the trace
* @return true if the trace is a target, false otherwise.
*/
boolean isTarget(Trace trace);
}

View file

@ -15,7 +15,6 @@
*/
package ghidra.debug.api.tracermi;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@ -37,19 +36,6 @@ import ghidra.util.task.TaskMonitor;
*/
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
*

View file

@ -1,221 +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.debug.api.workflow;
import ghidra.app.services.DebuggerWorkflowFrontEndService;
import ghidra.framework.options.AutoOptions;
import ghidra.framework.plugintool.PluginTool;
import ghidra.lifecycle.Internal;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.classfinder.ExtensionPoint;
/**
* A bot (or analyzer) that aids the user in the debugging workflow
*
* <p>
* These are a sort of miniature front-end plugin (TODO: consider tool-only bots) with a number of
* conveniences allowing the specification of automatic actions taken under given circumstances,
* e.g., "Open the interpreter for new debugger connections." Such actions may include analysis of
* open traces, e.g., "Disassemble memory at the Program Counter."
*
* <p>
* Bots which react to target state changes should take care to act quickly in most, if not all,
* circumstances. Otherwise, the UI could become sluggish. It is vitally important that the UI not
* become sluggish when the user is stepping a target. Bots should also be wary of prompts. If too
* many bots are prompting the user for input, they may collectively become a source of extreme
* annoyance. In most cases, the bot should use its best judgment and just perform the action, so
* long as it's not potentially destructive. That way, the user can undo the action and/or disable
* the bot. For cases where the bot, in its best judgment, cannot make a decision, it's probably
* best to simply log an informational message and do nothing. There are exceptions, just consider
* them carefully, and be mindful of prompting the user unexpectedly or incessantly.
*/
public interface DebuggerBot extends ExtensionPoint {
/**
* Log a missing-info-annotation error
*
* @param cls the bot's class missing the annotation
* @param methodName the name of the method requesting the info
*/
@Internal
static void noAnnot(Class<?> cls, String methodName) {
Msg.error(DebuggerBot.class, "Debugger bot " + cls + " must apply @" +
DebuggerBotInfo.class.getSimpleName() + " or override getDescription()");
}
/**
* Utility for obtaining and bot's info annotation
*
* <p>
* If the annotation is not present, an error is logged for the developer's sake.
*
* @param cls the bot's class
* @param methodName the name of the method requesting the info, for error-reporting purposes
* @return the annotation, or {@code null}
*/
@Internal
static DebuggerBotInfo getInfo(Class<?> cls, String methodName) {
DebuggerBotInfo info = cls.getAnnotation(DebuggerBotInfo.class);
if (info == null) {
noAnnot(cls, methodName);
}
return info;
}
/**
* Get a description of the bot
*
* @see DebuggerBotInfo#description()
* @return the description
*/
default String getDescription() {
DebuggerBotInfo info = getInfo(getClass(), "getDescription");
if (info == null) {
return "<NO DESCRIPTION>";
}
return info.description();
}
/**
* Get a detailed description of the bot
*
* @see DebuggerBotInfo#details()
* @return the details
*/
default String getDetails() {
DebuggerBotInfo info = getInfo(getClass(), "getDetails");
if (info == null) {
return "";
}
return info.details();
}
/**
* Get the help location for information about the bot
*
* @see DebuggerBotInfo#help()
* @return the help location
*/
default HelpLocation getHelpLocation() {
DebuggerBotInfo info = getInfo(getClass(), "getHelpLocation");
if (info == null) {
return null;
}
return AutoOptions.getHelpLocation("DebuggerBots", info.help());
}
/**
* Check whether this bot is enabled by default
*
* <p>
* Assuming the user has never configured this bot before, determine whether it should be
* enabled.
*
* @return true if enabled by default, false otherwise
*/
default boolean isEnabledByDefault() {
DebuggerBotInfo info = getInfo(getClass(), "isEnabledByDefault");
if (info == null) {
return false;
}
return info.enabledByDefault();
}
/**
* Check if this bot is enabled
*
* @return true if enabled, false otherwise
*/
boolean isEnabled();
/**
* Enable or disable the bot
*
* <p>
* If {@link #isEnabled()} is already equal to the given -enabled- value, this method has no
* effect.
*
* @param service the front-end service, required if -enabled- is set
* @param enabled true to enable, false to disable
*/
default void setEnabled(DebuggerWorkflowFrontEndService service, boolean enabled) {
if (isEnabled() == enabled) {
return;
}
if (enabled) {
enable(service);
}
else {
disable();
}
}
/**
* Enable and initialize the bot
*
* @param service the front-end service
*/
void enable(DebuggerWorkflowFrontEndService service);
/**
* Disable and dispose the bot
*
* <p>
* Note the bot must be prepared to be enabled again. In other words, it will not be
* re-instantiated. It should return to the same state after construction but before being
* enabled the first time.
*/
void disable();
/**
* A program has been opened in a tool
*
* @param tool the tool which opened the program
* @param program the program that was opened
*/
default void programOpened(PluginTool tool, Program program) {
}
/**
* A program has been closed in a tool
*
* @param tool the tool which closed the program
* @param program the program that was closed
*/
default void programClosed(PluginTool tool, Program program) {
}
/**
* A trace has been opened in a tool
*
* @param tool the tool which opened the trace
* @param trace the trace that was opened
*/
default void traceOpened(PluginTool tool, Trace trace) {
}
/**
* A trace has been closed in a tool
*
* @param tool the tool which closed the trace
* @param trace the trace that was closed
*/
default void traceClosed(PluginTool tool, Trace trace) {
}
}

View file

@ -1,76 +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.debug.api.workflow;
import java.lang.annotation.*;
import ghidra.framework.options.annotation.HelpInfo;
/**
* Required information annotation on {@link DebuggerBot}s
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DebuggerBotInfo {
/**
* A quick one-line description of the actor
*
* This is used as the option name to enable and disable the actor, to please, keep it short.
* Use {@link #details()} or {@link #help()} to provide more details.
*
* @return the description
*/
String description();
/**
* A longer description of this actor
*
* A one-to-three-sentence detailed description of the actor. Again, it should be relatively
* short, as it used as the tool-tip popup in the plugin's options dialog. On some systems, such
* tips only display for a short time.
*
* @return the detailed description
*/
String details();
/**
* The location for help about this actor
*
* Help is the best place to put lengthy descriptions of the actor and/or describe the caveats
* of using it. Since, in most cases, the actor is simply performing automatic actions, it is
* useful to show the reader how to perform those same actions manually. This way, if/when the
* actor takes an unreasonable action, the user can manually correct it.
*
* @return the link to detailed help about the actor
*/
HelpInfo help() default @HelpInfo(topic = {});
/**
* Check whether the actor should be enabled by default
*
* For the stock plugin, a collection of actors should be enabled by default that make the
* debugger most accessible, erring toward ease of use, rather than toward correctness. Advanced
* users can always disable unwanted actors, tweak the options (TODO: Allow actors to present
* additional options in the tool config), and/or write their own actors and scripts.
*
* For extensions, consider the user's expectations upon installing your extension. For example,
* if the extension consists of just an actor and some supporting classes, it should probably be
* enabled by default.
*
* @return true to enable by default, false to leave disabled by default
*/
boolean enabledByDefault() default false;
}

View file

@ -280,8 +280,35 @@ 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.
# Regarding the schema and method signatures
An interface like Togglable requires that a TOGGLE action takes the given schema as a parameter
(e.g., a BreakpointLocation)
# Regarding registers
The register container has to exist, even if its left empty in favor of the register space.
1. The space is named after the container
2. The UI uses the container as an anchor in its searches
To allow register writes, each writable register must exist as an object it the register container.
1. I might like to relax this....
2. The UI it to validate the register is editable, even though the RemoteMethod may accept
frame/thread,name,val.
# Regarding reading and writing memory
The process parameter, if accepted, is technically redundant.
Because all address spaces among all targets must be unique, the address space encodes the process (or other target object).
If taken, the back end must validate that the address space belongs to the given process.
Otherwise, the back end must figure out the applicable target based on the space name.

View file

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

View file

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

View file

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

View file

@ -27,8 +27,8 @@ import generic.theme.GIcon;
import generic.theme.Gui;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.util.ShellUtils;
import ghidra.debug.api.tracermi.TerminalSession;
import ghidra.framework.Application;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
@ -471,8 +471,8 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
* the target image is mapped in the resulting target trace.
* @throws FileNotFoundException
*/
public static UnixShellScriptTraceRmiLaunchOffer create(Program program, PluginTool tool,
File script) throws FileNotFoundException {
public static UnixShellScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin,
Program program, File script) throws FileNotFoundException {
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(new FileInputStream(script)))) {
AttributesParser attrs = new AttributesParser();
@ -491,7 +491,7 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
}
}
attrs.validate(script.getName());
return new UnixShellScriptTraceRmiLaunchOffer(program, tool, script,
return new UnixShellScriptTraceRmiLaunchOffer(plugin, program, script,
"UNIX_SHELL:" + script.getName(), attrs.title, attrs.getDescription(),
attrs.menuPath, attrs.menuGroup, attrs.menuOrder, new GIcon(attrs.iconId),
attrs.helpLocation, attrs.parameters, attrs.extraTtys);
@ -517,11 +517,11 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
protected final Map<String, ParameterDescription<?>> parameters;
protected final List<String> extraTtys;
public UnixShellScriptTraceRmiLaunchOffer(Program program, PluginTool tool, File script,
String configName, String title, String description, List<String> menuPath,
public UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program,
File script, String configName, String title, String description, List<String> menuPath,
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
Map<String, ParameterDescription<?>> parameters, Collection<String> extraTtys) {
super(program, tool);
super(plugin, program);
this.script = script;
this.configName = configName;
this.title = title;

View file

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

View file

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

View file

@ -23,8 +23,7 @@ import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
import ghidra.app.services.DebuggerTargetService;
import ghidra.app.services.TraceRmiService;
import ghidra.app.services.*;
import ghidra.debug.api.tracermi.TraceRmiConnection;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.AutoService.Wiring;
@ -52,8 +51,9 @@ import ghidra.util.task.TaskMonitor;
},
servicesProvided = {
TraceRmiService.class,
InternalTraceRmiService.class,
})
public class TraceRmiPlugin extends Plugin implements TraceRmiService {
public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
private static final int DEFAULT_PORT = 15432;
@AutoServiceConsumed

View file

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

View file

@ -0,0 +1,35 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
import java.io.IOException;
import java.net.SocketAddress;
import ghidra.app.plugin.core.debug.service.rmi.trace.DefaultTraceRmiAcceptor;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
/**
* The same as the {@link TraceRmiService}, but grants access to the internal types (without
* casting) to implementors of {@link TraceRmiLaunchOpinion}.
*/
public interface InternalTraceRmiService extends TraceRmiService {
@Override
DefaultTraceRmiAcceptor acceptOne(SocketAddress address) throws IOException;
@Override
TraceRmiHandler connect(SocketAddress address) throws IOException;
}

View file

@ -13,10 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.debug.api.tracermi;
package ghidra.debug.spi.tracermi;
import java.util.Collection;
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin;
import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
import ghidra.app.services.InternalTraceRmiService;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.framework.options.Options;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
@ -68,11 +72,16 @@ public interface TraceRmiLaunchOpinion extends ExtensionPoint {
* I.e., the entries there are already validated; they've worked at least once before.</li>
* </ol>
*
* @param plugin the Trace RMI launcher service plugin. <b>NOTE:</b> to get access to the Trace
* RMI (connection) service, use the {@link InternalTraceRmiService}, so that the
* offers can register the connection's resources. See
* {@link TraceRmiHandler#registerResources(Collection)}. Resource registration is
* required for the Disconnect button to completely terminate the back end.
* @param program the current program. While this is not <em>always</em> used by the launcher,
* it is implied that the user expects the debugger to do something with the current
* program, even if it's just informing the back-end debugger of the target image.
* @param tool the current tool for context and services
* @return the offers. The order is not important since items are displayed alphabetically.
* @return the offers. The order is ignored, since items are displayed alphabetically.
*/
public Collection<TraceRmiLaunchOffer> getOffers(Program program, PluginTool tool);
public Collection<TraceRmiLaunchOffer> getOffers(TraceRmiLauncherServicePlugin plugin,
Program program);
}

View file

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

View file

@ -0,0 +1,188 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.service.rmi.trace;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import ghidra.app.services.DebuggerTargetService;
import ghidra.async.AsyncPairingQueue;
import ghidra.async.AsyncUtils;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.tracermi.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
public class TestTraceRmiConnection implements TraceRmiConnection {
protected final TestRemoteMethodRegistry registry = new TestRemoteMethodRegistry();
protected final CompletableFuture<Trace> firstTrace = new CompletableFuture<>();
protected final Map<Trace, Long> snapshots = new HashMap<>();
protected final CompletableFuture<Void> closed = new CompletableFuture<>();
protected final Map<Trace, TraceRmiTarget> targets = new HashMap<>();
public static class TestRemoteMethodRegistry extends DefaultRemoteMethodRegistry {
@Override
public void add(RemoteMethod method) {
super.add(method);
}
}
public record TestRemoteMethod(String name, ActionName action, String description,
Map<String, RemoteParameter> parameters, SchemaName retType,
AsyncPairingQueue<Map<String, Object>> argQueue, AsyncPairingQueue<Object> retQueue)
implements RemoteMethod {
public TestRemoteMethod(String name, ActionName action, String description,
Map<String, RemoteParameter> parameters, SchemaName retType) {
this(name, action, description, parameters, retType, new AsyncPairingQueue<>(),
new AsyncPairingQueue<>());
}
public TestRemoteMethod(String name, ActionName action, String description,
SchemaName retType, RemoteParameter... parameters) {
this(name, action, description, Stream.of(parameters)
.collect(Collectors.toMap(RemoteParameter::name, p -> p)),
retType);
}
@Override
public RemoteAsyncResult invokeAsync(Map<String, Object> arguments) {
argQueue.give().complete(arguments);
DefaultRemoteAsyncResult result = new DefaultRemoteAsyncResult();
retQueue.take().handle(AsyncUtils.copyTo(result));
return result;
}
public Map<String, Object> expect() throws InterruptedException, ExecutionException {
return argQueue.take().get();
}
public void result(Object ret) {
retQueue.give().complete(ret);
}
}
public record TestRemoteParameter(String name, SchemaName type, boolean required,
Object defaultValue, String display, String description) implements RemoteParameter {
@Override
public Object getDefaultValue() {
return defaultValue;
}
}
@Override
public SocketAddress getRemoteAddress() {
return new InetSocketAddress("localhost", 0);
}
@Override
public TestRemoteMethodRegistry getMethods() {
return registry;
}
public void injectTrace(Trace trace) {
firstTrace.complete(trace);
}
public TraceRmiTarget publishTarget(PluginTool tool, Trace trace) {
injectTrace(trace);
TraceRmiTarget target = new TraceRmiTarget(tool, this, trace);
synchronized (targets) {
targets.put(trace, target);
}
DebuggerTargetService targetService = tool.getService(DebuggerTargetService.class);
targetService.publishTarget(target);
return target;
}
@Override
public Trace waitForTrace(long timeoutMillis) throws TimeoutException {
try {
return firstTrace.get(timeoutMillis, TimeUnit.MILLISECONDS);
}
catch (InterruptedException | ExecutionException e) {
throw new AssertionError(e);
}
}
public void setLastSnapshot(Trace trace, long snap) {
synchronized (snapshots) {
snapshots.put(trace, snap);
}
}
@Override
public long getLastSnapshot(Trace trace) {
synchronized (snapshots) {
Long snap = snapshots.get(trace);
return snap == null ? 0 : snap;
}
}
@Override
public void forceCloseTrace(Trace trace) {
TraceRmiTarget target;
synchronized (targets) {
target = targets.remove(trace);
}
DebuggerTargetService targetService =
target.getTool().getService(DebuggerTargetService.class);
targetService.withdrawTarget(target);
}
@Override
public boolean isTarget(Trace trace) {
synchronized (this.targets) {
return targets.containsKey(trace);
}
}
@Override
public void close() throws IOException {
Set<TraceRmiTarget> targets;
synchronized (this.targets) {
targets = new HashSet<>(this.targets.values());
this.targets.clear();
}
for (TraceRmiTarget target : targets) {
DebuggerTargetService targetService =
target.getTool().getService(DebuggerTargetService.class);
targetService.withdrawTarget(target);
}
closed.complete(null);
}
@Override
public boolean isClosed() {
return closed.isDone();
}
@Override
public void waitClosed() {
try {
closed.get();
}
catch (InterruptedException | ExecutionException e) {
throw new AssertionError(e);
}
}
}

View file

@ -1,3 +1,4 @@
AutoMapSpec
AutoReadMemorySpec
DebuggerBot
DebuggerMappingOpinion

View file

@ -0,0 +1,60 @@
/* ###
* 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.
*/
import java.util.*;
import ghidra.app.script.GhidraScript;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetThread;
import ghidra.debug.flatapi.FlatDebuggerAPI;
public class MonitorModelEventsScript extends GhidraScript implements FlatDebuggerAPI {
static DebuggerModelListener listener = new DebuggerModelListener() {
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
System.err.println("attributesChanged(%s, removed=%s, added=%s)"
.formatted(object.getJoinedPath("."), removed, added));
}
@Override
public void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
System.err.println("elementsChanged(%s, removed=%s, added=%s)"
.formatted(object.getJoinedPath("."), removed, added));
}
@Override
public void event(TargetObject object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
System.err.println(
"event(%s, thread=%s, type=%s, desc=%s)".formatted(object.getJoinedPath("."),
eventThread == null ? "<null>" : eventThread.getJoinedPath("."), type,
description));
}
@Override
public void invalidateCacheRequested(TargetObject object) {
System.err.println("invalidateCache(%s)".formatted(object.getJoinedPath(".")));
}
};
@Override
protected void run() throws Exception {
getModelService().getCurrentModel().addModelListener(listener);
}
}

View file

@ -13,10 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.workflow;
package ghidra.app.plugin.core.debug.disassemble;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInjectInfo.CompilerInfo;
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInjectInfo.CompilerInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.lang.Language;

View file

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

View file

@ -296,7 +296,7 @@ public interface DebuggerResources {
boolean DEFAULT_COLOR_INEFF_DIS_BREAKPOINT_COLORING_BACKGROUND = false;
String OPTION_NAME_LOG_BUFFER_LIMIT = "Log Buffer Size";
int DEFAULT_LOG_BUFFER_LIMIT = 100;
int DEFAULT_LOG_BUFFER_LIMIT = 20;
// TODO: Re-assign/name groups
String GROUP_GENERAL = "Dbg1. General";
@ -878,7 +878,8 @@ public interface DebuggerResources {
static <T> MultiStateActionBuilder<T> builder(Plugin owner) {
String ownerName = owner.getName();
return new MultiStateActionBuilder<T>(NAME, ownerName).description(DESCRIPTION)
return new MultiStateActionBuilder<T>(NAME, ownerName)
.description(DESCRIPTION)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,124 @@
/* ###
* 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.action;
import java.util.*;
import javax.swing.Icon;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.framework.cmd.BackgroundCommand;
import ghidra.framework.model.DomainObject;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.AutoConfigState.ConfigFieldCodec;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
import ghidra.trace.util.TraceChangeType;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.classfinder.ExtensionPoint;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* An interface for specifying how to automatically map dynamic memory to static memory.
*/
public interface AutoMapSpec extends ExtensionPoint {
class Private {
private final Map<String, AutoMapSpec> specsByName = new TreeMap<>();
private final ChangeListener classListener = this::classesChanged;
private Private() {
ClassSearcher.addChangeListener(classListener);
}
private synchronized void classesChanged(ChangeEvent evt) {
MiscellaneousUtils.collectUniqueInstances(AutoMapSpec.class, specsByName,
AutoMapSpec::getConfigName);
}
}
Private PRIVATE = new Private();
public static class AutoMapSpecConfigFieldCodec implements ConfigFieldCodec<AutoMapSpec> {
@Override
public AutoMapSpec read(SaveState state, String name,
AutoMapSpec current) {
String specName = state.getString(name, null);
return fromConfigName(specName);
}
@Override
public void write(SaveState state, String name, AutoMapSpec value) {
state.putString(name, value.getConfigName());
}
}
static AutoMapSpec fromConfigName(String name) {
synchronized (PRIVATE) {
return PRIVATE.specsByName.get(name);
}
}
static Map<String, AutoMapSpec> allSpecs() {
synchronized (PRIVATE) {
return new TreeMap<>(PRIVATE.specsByName);
}
}
String getConfigName();
String getMenuName();
default Icon getMenuIcon() {
return DebuggerResources.ICON_CONFIG;
}
Collection<TraceChangeType<?, ?>> getChangeTypes();
default String getTaskTitle() {
return getMenuName();
}
default void runTask(PluginTool tool, Trace trace) {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
ProgramManager programManager = tool.getService(ProgramManager.class);
if (mappingService == null || programManager == null) {
return;
}
BackgroundCommand cmd = new BackgroundCommand(getTaskTitle(), true, true, false) {
@Override
public boolean applyTo(DomainObject obj, TaskMonitor monitor) {
try {
performMapping(mappingService, trace, programManager, monitor);
return true;
}
catch (CancelledException e) {
return false;
}
}
};
tool.executeBackgroundCommand(cmd, trace);
}
void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException;
}

View file

@ -15,7 +15,8 @@
*/
package ghidra.app.plugin.core.debug.gui.action;
import java.util.*;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import javax.swing.Icon;
@ -31,9 +32,12 @@ import ghidra.program.model.address.AddressSetView;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.classfinder.ExtensionPoint;
/**
* An interface for specifying how to automatically read target memory.
*/
public interface AutoReadMemorySpec extends ExtensionPoint {
class Private {
private final Map<String, AutoReadMemorySpec> specsByName = new HashMap<>();
private final Map<String, AutoReadMemorySpec> specsByName = new TreeMap<>();
private final ChangeListener classListener = this::classesChanged;
private Private() {
@ -86,13 +90,14 @@ public interface AutoReadMemorySpec extends ExtensionPoint {
*
* <p>
* Note, the implementation should perform all the error handling. The returned future is for
* follow-up purposes only, and should always complete normally.
* follow-up purposes only, and should always complete normally. It should complete with true if
* any memory was actually loaded. Otherwise, it should complete with false.
*
* @param tool the tool containing the provider
* @param coordinates the provider's current coordinates
* @param visible the provider's visible addresses
* @return a future that completes when the memory has been read
*/
CompletableFuture<?> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
CompletableFuture<Boolean> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
AddressSetView visible);
}

View file

@ -13,17 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.workflow;
package ghidra.app.plugin.core.debug.gui.action;
import java.util.*;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.debug.api.modules.MapProposal;
import ghidra.debug.api.modules.ModuleMapProposal;
import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry;
import ghidra.debug.api.workflow.DebuggerBotInfo;
import ghidra.framework.options.annotation.HelpInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.trace.model.Trace.TraceMemoryRegionChangeType;
@ -32,30 +30,33 @@ import ghidra.trace.util.TraceChangeType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@DebuggerBotInfo(
description = "Map modules to open programs",
details = "Monitors open traces and programs, attempting to map modules by \"best\" match.",
help = @HelpInfo(anchor = "map_modules"),
enabledByDefault = true)
public class MapModulesDebuggerBot extends AbstractMapDebuggerBot {
public class ByModuleAutoMapSpec implements AutoMapSpec {
public static final String CONFIG_NAME = "1_MAP_BY_MODULE";
@Override
protected Collection<TraceChangeType<?, ?>> getChangeTypes() {
public String getConfigName() {
return CONFIG_NAME;
}
@Override
public String getMenuName() {
return "Auto-Map by Module";
}
@Override
public Collection<TraceChangeType<?, ?>> getChangeTypes() {
return List.of(TraceModuleChangeType.ADDED, TraceModuleChangeType.CHANGED,
TraceMemoryRegionChangeType.ADDED, TraceMemoryRegionChangeType.CHANGED);
}
@Override
protected void doAnalysis(PluginTool tool, Trace trace, Set<Program> programs,
TaskMonitor monitor) throws CancelledException {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
if (mappingService != null) {
Map<?, ModuleMapProposal> maps = mappingService
.proposeModuleMaps(trace.getModuleManager().getAllModules(), programs);
Collection<ModuleMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addModuleMappings(entries, monitor, false);
}
public void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException {
List<Program> programs = Arrays.asList(programManager.getAllOpenPrograms());
Map<?, ModuleMapProposal> maps = mappingService
.proposeModuleMaps(trace.getModuleManager().getAllModules(), programs);
Collection<ModuleMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addModuleMappings(entries, monitor, false);
}
}

View file

@ -13,17 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.workflow;
package ghidra.app.plugin.core.debug.gui.action;
import java.util.*;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.debug.api.modules.MapProposal;
import ghidra.debug.api.modules.RegionMapProposal;
import ghidra.debug.api.modules.RegionMapProposal.RegionMapEntry;
import ghidra.debug.api.workflow.DebuggerBotInfo;
import ghidra.framework.options.annotation.HelpInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.trace.model.Trace.TraceMemoryRegionChangeType;
@ -31,29 +29,32 @@ import ghidra.trace.util.TraceChangeType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@DebuggerBotInfo(
description = "Map regions to open programs",
details = "Monitors open traces and programs, attempting to map regions by \"best\" match.",
help = @HelpInfo(anchor = "map_regions"),
enabledByDefault = false)
public class MapRegionsDebuggerBot extends AbstractMapDebuggerBot {
public class ByRegionAutoMapSpec implements AutoMapSpec {
public static final String CONFIG_NAME = "1_MAP_BY_REGION";
@Override
protected Collection<TraceChangeType<?, ?>> getChangeTypes() {
public String getConfigName() {
return CONFIG_NAME;
}
@Override
public String getMenuName() {
return "Auto-Map by Region";
}
@Override
public Collection<TraceChangeType<?, ?>> getChangeTypes() {
return List.of(TraceMemoryRegionChangeType.ADDED);
}
@Override
protected void doAnalysis(PluginTool tool, Trace trace, Set<Program> programs,
TaskMonitor monitor) throws CancelledException {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
if (mappingService != null) {
Map<?, RegionMapProposal> maps = mappingService
.proposeRegionMaps(trace.getMemoryManager().getAllRegions(), programs);
Collection<RegionMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addRegionMappings(entries, monitor, false);
}
public void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException {
List<Program> programs = Arrays.asList(programManager.getAllOpenPrograms());
Map<?, RegionMapProposal> maps = mappingService
.proposeRegionMaps(trace.getMemoryManager().getAllRegions(), programs);
Collection<RegionMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addRegionMappings(entries, monitor, false);
}
}

View file

@ -13,17 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.workflow;
package ghidra.app.plugin.core.debug.gui.action;
import java.util.*;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.debug.api.modules.MapProposal;
import ghidra.debug.api.modules.SectionMapProposal;
import ghidra.debug.api.modules.SectionMapProposal.SectionMapEntry;
import ghidra.debug.api.workflow.DebuggerBotInfo;
import ghidra.framework.options.annotation.HelpInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Trace;
import ghidra.trace.model.Trace.TraceSectionChangeType;
@ -31,29 +29,32 @@ import ghidra.trace.util.TraceChangeType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@DebuggerBotInfo(
description = "Map sections to open programs",
details = "Monitors open traces and programs, attempting to map sections by \"best\" match.",
help = @HelpInfo(anchor = "map_sections"),
enabledByDefault = false)
public class MapSectionsDebuggerBot extends AbstractMapDebuggerBot {
public class BySectionAutoMapSpec implements AutoMapSpec {
public static final String CONFIG_NAME = "1_MAP_BY_SECTION";
@Override
protected Collection<TraceChangeType<?, ?>> getChangeTypes() {
public String getConfigName() {
return CONFIG_NAME;
}
@Override
public String getMenuName() {
return "Auto-Map by Section";
}
@Override
public Collection<TraceChangeType<?, ?>> getChangeTypes() {
return List.of(TraceSectionChangeType.ADDED);
}
@Override
protected void doAnalysis(PluginTool tool, Trace trace, Set<Program> programs,
TaskMonitor monitor) throws CancelledException {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
if (mappingService != null) {
Map<?, SectionMapProposal> maps = mappingService
.proposeSectionMaps(trace.getModuleManager().getAllModules(), programs);
Collection<SectionMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addSectionMappings(entries, monitor, false);
}
public void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException {
List<Program> programs = Arrays.asList(programManager.getAllOpenPrograms());
Map<?, SectionMapProposal> maps = mappingService
.proposeSectionMaps(trace.getModuleManager().getAllModules(), programs);
Collection<SectionMapEntry> entries = MapProposal.flatten(maps.values());
entries = MapProposal.removeOverlapping(entries);
mappingService.addSectionMappings(entries, monitor, false);
}
}

View file

@ -36,8 +36,7 @@ import ghidra.framework.model.DomainObject;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoConfigStateField;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.address.*;
import ghidra.trace.model.*;
import ghidra.trace.model.Trace.TraceMemoryStateChangeType;
import ghidra.trace.model.Trace.TraceSnapshotChangeType;
@ -79,6 +78,7 @@ public abstract class DebuggerReadsMemoryTrait {
target.invalidateMemoryCaches();
try {
target.readMemory(sel, monitor);
memoryWasRead(sel);
}
catch (CancelledException e) {
return false;
@ -219,7 +219,12 @@ public abstract class DebuggerReadsMemoryTrait {
if (!isConsistent()) {
return;
}
autoSpec.readMemory(tool, current, visible).exceptionally(ex -> {
AddressSet visible = new AddressSet(this.visible);
autoSpec.readMemory(tool, current, visible).thenAccept(b -> {
if (b) {
memoryWasRead(visible);
}
}).exceptionally(ex -> {
Msg.error(this, "Could not auto-read memory: " + ex);
return null;
});
@ -286,4 +291,8 @@ public abstract class DebuggerReadsMemoryTrait {
protected abstract AddressSetView getSelection();
protected abstract void repaintPanel();
protected void memoryWasRead(AddressSetView read) {
// Extension point
}
}

View file

@ -26,7 +26,6 @@ import ghidra.app.plugin.core.debug.service.emulation.ProgramEmulationUtils;
import ghidra.app.plugin.core.debug.service.model.record.RecorderUtils;
import ghidra.app.plugin.core.debug.utils.AbstractMappedMemoryBytesVisitor;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.async.AsyncUtils;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.*;
@ -37,7 +36,7 @@ import ghidra.trace.model.memory.TraceMemoryManager;
import ghidra.trace.model.memory.TraceMemoryState;
public class LoadEmulatorAutoReadMemorySpec implements AutoReadMemorySpec {
public static final String CONFIG_NAME = "LOAD_EMULATOR";
public static final String CONFIG_NAME = "2_LOAD_EMULATOR";
@Override
public String getConfigName() {
@ -55,18 +54,18 @@ public class LoadEmulatorAutoReadMemorySpec implements AutoReadMemorySpec {
}
@Override
public CompletableFuture<?> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
public CompletableFuture<Boolean> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
AddressSetView visible) {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
if (mappingService == null) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
Trace trace = coordinates.getTrace();
if (trace == null || coordinates.isAlive() ||
!ProgramEmulationUtils.isEmulatedProgram(trace)) {
// Never interfere with a live target
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
TraceMemoryManager mm = trace.getMemoryManager();
AddressSet toRead = new AddressSet(RecorderUtils.INSTANCE.quantize(12, visible));
@ -80,7 +79,7 @@ public class LoadEmulatorAutoReadMemorySpec implements AutoReadMemorySpec {
}
if (toRead.isEmpty()) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
long snap = coordinates.getSnap();
@ -94,7 +93,7 @@ public class LoadEmulatorAutoReadMemorySpec implements AutoReadMemorySpec {
mm.putBytes(snap, hostAddr, buf);
}
}.visit(trace, snap, toRead);
return AsyncUtils.nil();
return CompletableFuture.completedFuture(true);
}
catch (MemoryAccessException e) {
throw new AssertionError(e);

View file

@ -0,0 +1,56 @@
/* ###
* 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.action;
import java.util.Collection;
import java.util.List;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
import ghidra.trace.util.TraceChangeType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class NoneAutoMapSpec implements AutoMapSpec {
public static final String CONFIG_NAME = "0_MAP_NONE";
@Override
public String getConfigName() {
return CONFIG_NAME;
}
@Override
public String getMenuName() {
return "Do Not Auto-Map";
}
@Override
public Collection<TraceChangeType<?, ?>> getChangeTypes() {
return List.of();
}
@Override
public void runTask(PluginTool tool, Trace trace) {
// Don't bother launching a task that does nothing
}
@Override
public void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException {
}
}

View file

@ -20,13 +20,12 @@ import java.util.concurrent.CompletableFuture;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.AutoReadMemoryAction;
import ghidra.async.AsyncUtils;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.AddressSetView;
public class NoneAutoReadMemorySpec implements AutoReadMemorySpec {
public static final String CONFIG_NAME = "READ_NONE";
public static final String CONFIG_NAME = "0_READ_NONE";
@Override
public String getConfigName() {
@ -44,8 +43,8 @@ public class NoneAutoReadMemorySpec implements AutoReadMemorySpec {
}
@Override
public CompletableFuture<Void> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
public CompletableFuture<Boolean> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
AddressSetView visible) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
}

View file

@ -90,4 +90,9 @@ public enum NoneLocationTrackingSpec implements LocationTrackingSpec, LocationTr
public boolean affectedByStackChange(TraceStack stack, DebuggerCoordinates coordinates) {
return false;
}
@Override
public boolean shouldDisassemble() {
return false;
}
}

View file

@ -0,0 +1,64 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.action;
import java.util.Collection;
import java.util.List;
import ghidra.app.services.DebuggerStaticMappingService;
import ghidra.app.services.ProgramManager;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.modules.TraceConflictedMappingException;
import ghidra.trace.util.TraceChangeType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class OneToOneAutoMapSpec implements AutoMapSpec {
public static final String CONFIG_NAME = "2_MAP_ONE_TO_ONE";
@Override
public String getConfigName() {
return CONFIG_NAME;
}
@Override
public String getMenuName() {
return "Auto-Map Identically (1-to-1)";
}
@Override
public Collection<TraceChangeType<?, ?>> getChangeTypes() {
return List.of();
}
@Override
public void performMapping(DebuggerStaticMappingService mappingService, Trace trace,
ProgramManager programManager, TaskMonitor monitor) throws CancelledException {
Program program = programManager.getCurrentProgram();
if (program == null) {
return;
}
try {
mappingService.addIdentityMapping(trace, program,
Lifespan.nowOn(trace.getProgramView().getSnap()), false);
}
catch (TraceConflictedMappingException e) {
// aww well
}
}
}

View file

@ -61,4 +61,9 @@ public enum PCByRegisterLocationTrackingSpec implements RegisterLocationTracking
public AddressSpace computeDefaultAddressSpace(DebuggerCoordinates coordinates) {
return coordinates.getPlatform().getLanguage().getDefaultSpace();
}
@Override
public boolean shouldDisassemble() {
return true;
}
}

View file

@ -125,4 +125,9 @@ public enum PCByStackLocationTrackingSpec implements LocationTrackingSpec, Locat
DebuggerCoordinates coordinates) {
return false;
}
@Override
public boolean shouldDisassemble() {
return true;
}
}

View file

@ -100,4 +100,9 @@ public enum PCLocationTrackingSpec implements LocationTrackingSpec, LocationTrac
DebuggerCoordinates coordinates) {
return BY_REG.affectedByBytesChange(space, range, coordinates);
}
@Override
public boolean shouldDisassemble() {
return true;
}
}

View file

@ -61,4 +61,9 @@ public enum SPLocationTrackingSpec implements RegisterLocationTrackingSpec {
public AddressSpace computeDefaultAddressSpace(DebuggerCoordinates coordinates) {
return coordinates.getTrace().getBaseLanguage().getDefaultDataSpace();
}
@Override
public boolean shouldDisassemble() {
return false;
}
}

View file

@ -20,7 +20,6 @@ import java.util.concurrent.CompletableFuture;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.AutoReadMemoryAction;
import ghidra.async.AsyncUtils;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
@ -31,7 +30,7 @@ import ghidra.trace.model.memory.TraceMemoryState;
import ghidra.util.task.TaskMonitor;
public class VisibleAutoReadMemorySpec implements AutoReadMemorySpec {
public static final String CONFIG_NAME = "READ_VISIBLE";
public static final String CONFIG_NAME = "1_READ_VISIBLE";
@Override
public String getConfigName() {
@ -49,10 +48,10 @@ public class VisibleAutoReadMemorySpec implements AutoReadMemorySpec {
}
@Override
public CompletableFuture<?> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
public CompletableFuture<Boolean> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
AddressSetView visible) {
if (!coordinates.isAliveAndReadsPresent()) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
Target target = coordinates.getTarget();
TraceMemoryManager mm = coordinates.getTrace().getMemoryManager();
@ -61,9 +60,9 @@ public class VisibleAutoReadMemorySpec implements AutoReadMemorySpec {
AddressSet toRead = visible.subtract(alreadyKnown);
if (toRead.isEmpty()) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
return target.readMemoryAsync(toRead, TaskMonitor.DUMMY);
return target.readMemoryAsync(toRead, TaskMonitor.DUMMY).thenApply(__ -> true);
}
}

View file

@ -21,7 +21,6 @@ import java.util.concurrent.CompletableFuture;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.AutoReadMemoryAction;
import ghidra.async.AsyncUtils;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.PluginTool;
@ -32,7 +31,7 @@ import ghidra.trace.model.memory.*;
import ghidra.util.task.TaskMonitor;
public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec {
public static final String CONFIG_NAME = "READ_VIS_RO_ONCE";
public static final String CONFIG_NAME = "1_READ_VIS_RO_ONCE";
@Override
public String getConfigName() {
@ -50,10 +49,10 @@ public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec {
}
@Override
public CompletableFuture<?> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
public CompletableFuture<Boolean> readMemory(PluginTool tool, DebuggerCoordinates coordinates,
AddressSetView visible) {
if (!coordinates.isAliveAndReadsPresent()) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
Target target = coordinates.getTarget();
TraceMemoryManager mm = coordinates.getTrace().getMemoryManager();
@ -62,7 +61,7 @@ public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec {
AddressSet toRead = visible.subtract(alreadyKnown);
if (toRead.isEmpty()) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
AddressSet everKnown = new AddressSet();
@ -85,9 +84,9 @@ public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec {
toRead.delete(everKnown.intersect(readOnly));
if (toRead.isEmpty()) {
return AsyncUtils.nil();
return CompletableFuture.completedFuture(false);
}
return target.readMemoryAsync(toRead, TaskMonitor.DUMMY);
return target.readMemoryAsync(toRead, TaskMonitor.DUMMY).thenApply(__ -> true);
}
}

View file

@ -165,6 +165,11 @@ public class WatchLocationTrackingSpec implements LocationTrackingSpec {
public boolean affectedByStackChange(TraceStack stack, DebuggerCoordinates coordinates) {
return false;
}
@Override
public boolean shouldDisassemble() {
return false;
}
}
@Override

View file

@ -620,7 +620,8 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter
private boolean isVisible(TraceBreakpoint location) {
long snap = traceManager.getCurrentFor(trace).getSnap();
return location.getLifespan().contains(snap);
Lifespan span = location.getLifespan();
return span != null && span.contains(snap);
}
private void locationAdded(TraceBreakpoint location) {

View file

@ -86,15 +86,15 @@ public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleServ
provider = new DebuggerConsoleProvider(this);
rootLogger = (Logger) LogManager.getRootLogger();
appender.start();
rootLogger.addAppender(appender);
//appender.start();
//rootLogger.addAppender(appender);
}
@Override
protected void dispose() {
if (rootLogger != null) {
rootLogger.removeAppender(appender);
appender.stop();
//rootLogger.removeAppender(appender);
//appender.stop();
provider.dispose();
tool.removeComponentProvider(provider);

View file

@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.app.plugin.core.debug.gui.control;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
@ServiceInfo(
defaultProviderName = "ghidra.app.plugin.core.debug.service.workflow.DebuggerWorkflowServiceProxyPlugin",
description = "Service for managing automatic debugger actions and analysis")
public interface DebuggerWorkflowToolService extends DebuggerWorkflowService {
interface ControlAction {
String GROUP = DebuggerResources.GROUP_CONTROL;
static String intSubGroup(int subGroup) {
return String.format("%02d", subGroup);
}
}

View file

@ -0,0 +1,64 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.control;
import java.util.stream.Collectors;
import docking.ActionContext;
import docking.action.ToolBarData;
import docking.menu.ActionState;
import docking.menu.MultiStateDockingAction;
import docking.widgets.EventTrigger;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.debug.api.control.ControlMode;
import ghidra.util.HelpLocation;
class ControlModeAction extends MultiStateDockingAction<ControlMode> {
public static final String NAME = "Control Mode";
public static final String DESCRIPTION = "Choose what to control and edit in dynamic views";
public static final String GROUP = DebuggerResources.GROUP_CONTROL;
public static final String HELP_ANCHOR = "control_mode";
private final DebuggerControlPlugin plugin;
public ControlModeAction(DebuggerControlPlugin plugin) {
super(NAME, plugin.getName());
this.plugin = plugin;
setDescription(DESCRIPTION);
setToolBarData(new ToolBarData(DebuggerResources.ICON_BLANK, GROUP, ""));
setHelpLocation(new HelpLocation(getOwner(), HELP_ANCHOR));
setActionStates(ControlMode.ALL.stream()
.map(m -> new ActionState<>(m.name, m.icon, m))
.collect(Collectors.toList()));
setEnabled(false);
}
@Override
public boolean isEnabledForContext(ActionContext context) {
return plugin.current.getTrace() != null;
}
@Override
protected boolean isStateEnabled(ActionState<ControlMode> state) {
return state.getUserData().isSelectable(plugin.current);
}
@Override
public void actionStateChanged(ActionState<ControlMode> newActionState,
EventTrigger trigger) {
plugin.activateControlMode(newActionState, trigger);
}
}

View file

@ -15,28 +15,22 @@
*/
package ghidra.app.plugin.core.debug.gui.control;
import java.awt.event.KeyEvent;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import docking.ActionContext;
import docking.DockingContextListener;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.action.DockingAction;
import docking.action.DockingActionIf;
import docking.menu.ActionState;
import docking.menu.MultiStateDockingAction;
import docking.widgets.EventTrigger;
import ghidra.app.context.ProgramLocationActionContext;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
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.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.service.model.TraceRecorderTarget;
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
import ghidra.app.services.*;
import ghidra.app.services.DebuggerControlService.ControlModeChangeListener;
import ghidra.app.services.DebuggerEmulationService.CachedEmulator;
@ -52,12 +46,15 @@ import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.trace.model.*;
import ghidra.trace.model.Trace.TraceObjectChangeType;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.time.schedule.Scheduler;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.util.*;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
@ -79,316 +76,6 @@ import ghidra.util.task.TaskMonitor;
public class DebuggerControlPlugin extends AbstractDebuggerPlugin
implements DockingContextListener {
static String intSubGroup(int subGroup) {
return String.format("%02d", subGroup);
}
interface ControlAction {
String GROUP = DebuggerResources.GROUP_CONTROL;
}
protected class ControlModeAction extends MultiStateDockingAction<ControlMode> {
public static final String NAME = "Control Mode";
public static final String DESCRIPTION = "Choose what to control and edit in dynamic views";
public static final String GROUP = DebuggerResources.GROUP_CONTROL;
public static final String HELP_ANCHOR = "control_mode";
public ControlModeAction() {
super(NAME, DebuggerControlPlugin.this.getName());
setDescription(DESCRIPTION);
setToolBarData(new ToolBarData(DebuggerResources.ICON_BLANK, GROUP, ""));
setHelpLocation(new HelpLocation(getOwner(), HELP_ANCHOR));
setActionStates(ControlMode.ALL.stream()
.map(m -> new ActionState<>(m.name, m.icon, m))
.collect(Collectors.toList()));
setEnabled(false);
}
@Override
public boolean isEnabledForContext(ActionContext context) {
return current.getTrace() != null;
}
@Override
protected boolean isStateEnabled(ActionState<ControlMode> state) {
return state.getUserData().isSelectable(current);
}
@Override
public void actionStateChanged(ActionState<ControlMode> newActionState,
EventTrigger trigger) {
activateControlMode(newActionState, trigger);
}
}
interface ResumeAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_RESUME;
int SUB_GROUP = 0;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0);
}
interface TargetResumeAction extends ResumeAction {
String HELP_ANCHOR = "target_resume";
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(name, ownerName)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface EmulateResumeAction extends ResumeAction {
String NAME = "Resume Emulator";
String DESCRIPTION = "Resume, i.e., go or continue execution of the integrated emulator";
String HELP_ANCHOR = "emu_resume";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface InterruptAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_INTERRUPT;
int SUB_GROUP = 1;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_I, KeyEvent.CTRL_DOWN_MASK);
}
interface TargetInterruptAction extends InterruptAction {
String HELP_ANCHOR = "target_interrupt";
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(name, ownerName)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface EmulateInterruptAction extends InterruptAction {
String NAME = "Interrupt Emulator";
String DESCRIPTION = "Interrupt, i.e., suspend, the integrated emulator";
String HELP_ANCHOR = "emu_interrupt";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface TargetKillAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_KILL;
String HELP_ANCHOR = "target_kill";
int SUB_GROUP = 2;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_K,
KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK);
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(name, ownerName)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface DisconnectAction extends ControlAction {
String NAME = "Disconnect";
String DESCRIPTION = "Close the connection to the debugging agent";
Icon ICON = DebuggerResources.ICON_DISCONNECT;
String HELP_ANCHOR = "target_disconnect";
int SUB_GROUP = 3;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_K,
KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK);
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface EmulateStepBackAction extends ControlAction {
String NAME = "Step Emulator Back";
String DESCRIPTION = "Step the integrated emulator a single instruction backward";
Icon ICON = DebuggerResources.ICON_STEP_BACK;
String HELP_ANCHOR = "emu_step_back";
int SUB_GROUP = 4;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0);
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface StepIntoAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_STEP_INTO;
int SUB_GROUP = 5;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0);
}
interface TargetStepIntoAction extends StepIntoAction {
String HELP_ANCHOR = "target_step_into";
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(name, ownerName)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface EmulateStepIntoAction extends StepIntoAction {
String NAME = "Step Emulator Into";
String DESCRIPTION =
"Step the integrated emulator a single instruction, descending into calls";
String HELP_ANCHOR = "emu_step_into";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface TargetStepOverAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_STEP_OVER;
String HELP_ANCHOR = "target_step_over";
int SUB_GROUP = 6;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0);
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(name, ownerName)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface EmulateSkipOverAction extends ControlAction {
String NAME = "Skip Emulator";
String DESCRIPTION =
"Skip the integrated emulator a single instruction, ignoring its effects";
Icon ICON = DebuggerResources.ICON_SKIP_OVER;
String HELP_ANCHOR = "emu_skip_over";
int SUB_GROUP = 7;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F10, KeyEvent.CTRL_DOWN_MASK);
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface TargetStepOutAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_STEP_FINISH;
String HELP_ANCHOR = "target_step_out";
int SUB_GROUP = 8;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F12, 0);
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(name, ownerName)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface TargetStepExtAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_STEP_LAST;
String HELP_ANCHOR = "target_step_ext";
int SUB_GROUP = 9;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, KeyEvent.CTRL_DOWN_MASK);
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(name, ownerName)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP) + name)
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface TraceSnapBackwardAction extends ControlAction {
String NAME = "Trace Snapshot Backward";
String DESCRIPTION = "Navigate the trace recording backward one snapshot";
Icon ICON = DebuggerResources.ICON_SNAP_BACKWARD;
String HELP_ANCHOR = "trace_snap_backward";
int SUB_GROUP = 10;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0);
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
interface TraceSnapForwardAction extends ControlAction {
String NAME = "Trace Snapshot Forward";
String DESCRIPTION = "Navigate the trace recording forward one snapshot";
Icon ICON = DebuggerResources.ICON_SNAP_FORWARD;
String HELP_ANCHOR = "trace_snap_backward";
int SUB_GROUP = 11;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0);
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
private final TraceDomainObjectListener listenerForObjects = new TraceDomainObjectListener() {
{
listenFor(TraceObjectChangeType.VALUE_CREATED, this::valueChanged);
@ -427,8 +114,16 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
protected MultiStateDockingAction<ControlMode> actionControlMode;
DockingAction actionTargetResume;
DockingAction actionTargetInterrupt;
DockingAction actionTargetKill;
DockingAction actionTargetStepInto;
DockingAction actionTargetStepOver;
DockingAction actionTargetStepOut;
DockingAction actionTargetDisconnect;
final Set<DockingAction> actionsTarget = new HashSet<>();
Set<DockingAction> actionsTarget;
final Set<DockingAction> actionsTargetStepExt = new HashSet<>();
final Set<DockingAction> actionsTargetAll = new HashSet<>();
DockingAction actionEmulateResume;
DockingAction actionEmulateInterrupt;
@ -442,7 +137,6 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
Set<DockingAction> actionsTrace;
Set<Set<DockingAction>> actionSets;
Collection<? extends DockingActionIf> curActionSet;
ActionContext context;
@ -463,7 +157,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
switch (mode) {
case RO_TARGET:
case RW_TARGET:
return actionsTarget;
return actionsTargetAll;
case RO_TRACE:
case RW_TRACE:
return actionsTrace;
@ -475,7 +169,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
}
protected Set<DockingAction> getActionSet() {
return getActionSet(computeCurrentEditingMode());
return getActionSet(computeCurrentControlMode());
}
protected void updateActionsEnabled(ControlMode mode) {
@ -485,24 +179,70 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
}
protected void updateActionsEnabled() {
updateActionsEnabled(computeCurrentEditingMode());
updateActionsEnabled(computeCurrentControlMode());
}
protected static boolean isSameContext(ActionContext ctx1, ActionContext ctx2) {
if (ctx1 instanceof ProgramLocationActionContext locCtx1) {
if (!(ctx2 instanceof ProgramLocationActionContext locCtx2)) {
return false;
}
Program prog1 = locCtx1.getProgram();
Program prog2 = locCtx2.getProgram();
if (prog1 != prog2) {
return false;
}
Address addr1 = locCtx1.getAddress();
Address addr2 = locCtx2.getAddress();
if (!Objects.equals(addr1, addr2)) {
return false;
}
return true;
}
if (ctx1 instanceof DebuggerObjectActionContext objCtx1) {
if (!(ctx2 instanceof DebuggerObjectActionContext objCtx2)) {
return false;
}
return Objects.equals(objCtx1.getObjectValues(), objCtx2.getObjectValues());
}
return true; // Treat all unknowns as same.
}
@Override
public void contextChanged(ActionContext context) {
boolean same = isSameContext(this.context, context);
this.context = context;
updateActionsEnabled();
if (same) {
return;
}
updateTargetStepExtActions();
updateActions();
}
protected void createActions() {
actionControlMode = new ControlModeAction();
actionControlMode = new ControlModeAction(this);
tool.addAction(actionControlMode);
actionTargetResume = TargetResumeAction.builder(this)
.build();
actionTargetInterrupt = TargetInterruptAction.builder(this)
.build();
actionTargetKill = TargetKillAction.builder(this)
.build();
actionTargetStepInto = TargetStepIntoAction.builder(this)
.build();
actionTargetStepOver = TargetStepOverAction.builder(this)
.build();
actionTargetStepOut = TargetStepOutAction.builder(this)
.build();
actionTargetDisconnect = DisconnectAction.builder(this)
.enabledWhen(this::isActionTargetDisconnectEnabled)
.onAction(this::activatedTargetDisconnect)
.build();
updateTargetActions();
actionsTarget = Set.of(actionTargetResume, actionTargetInterrupt, actionTargetKill,
actionTargetStepInto, actionTargetStepOver, actionTargetStepOut,
actionTargetDisconnect);
updateTargetStepExtActions();
actionEmulateResume = EmulateResumeAction.builder(this)
.enabledWhen(this::isActionEmulateResumeEnabled)
@ -537,51 +277,37 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
.build();
actionsTrace = Set.of(actionTraceSnapBackward, actionTraceSnapForward);
actionSets = Set.of(actionsTarget, actionsEmulate, actionsTrace);
actionSets = Set.of(actionsTargetAll, actionsEmulate, actionsTrace);
updateActions();
}
protected interface TargetActionBuilderFactory
extends BiFunction<String, DebuggerControlPlugin, ActionBuilder> {
protected void runTask(String title, ActionEntry entry) {
tool.execute(new TargetActionTask(title, entry));
}
protected DockingAction buildTargetAction(TargetActionBuilderFactory factory,
ActionEntry entry) {
return factory.apply(entry.display(), this)
.description(entry.details())
.enabledWhen(ctx -> entry.isEnabled())
.onAction(ctx -> runTask(entry))
.build();
}
protected void runTask(ActionEntry entry) {
tool.execute(new TargetActionTask(entry));
}
protected void addTargetActionsForName(Target target, ActionName name,
TargetActionBuilderFactory factory) {
for (ActionEntry entry : target.collectActions(name, context).values()) {
protected void addTargetStepExtActions(Target target) {
for (ActionEntry entry : target.collectActions(ActionName.STEP_EXT, context).values()) {
if (entry.requiresPrompt()) {
continue;
}
actionsTarget.add(buildTargetAction(factory, entry));
actionsTargetStepExt.add(TargetStepExtAction.builder(entry.display(), this)
.description(entry.details())
.enabledWhen(ctx -> entry.isEnabled())
.onAction(ctx -> runTask(entry.display(), entry))
.build());
}
}
/**
* This is for testing purposes only. Fetch an action from "targetActions" whose name matches
* that given.
*
* <p>
* Since the tests are still assuming {@link TraceRecorderTarget}s, actions can be reliably
* retrieved by name.
* This is for testing purposes only. Fetch an action from {@link #actionsTargetStepExt} whose
* name matches that given.
*
* @param name the action name
* @return the action, or null
*/
/* testing */ DockingAction getTargetAction(String name) {
for (DockingAction action : actionsTarget) {
/* testing */ DockingAction getTargetStepExtAction(String name) {
for (DockingAction action : actionsTargetStepExt) {
if (name.equals(action.getName())) {
return action;
}
@ -589,23 +315,19 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
return null;
}
protected void updateTargetActions() {
hideActions(actionsTarget);
actionsTarget.clear();
actionsTarget.add(actionTargetDisconnect);
protected void updateTargetStepExtActions() {
hideActions(actionsTargetStepExt);
actionsTargetStepExt.clear();
actionsTargetAll.clear();
actionsTargetAll.addAll(actionsTarget);
Target target = current.getTarget();
if (target == null || !target.isValid()) {
return;
}
addTargetActionsForName(target, ActionName.RESUME, TargetResumeAction::builder);
addTargetActionsForName(target, ActionName.INTERRUPT, TargetInterruptAction::builder);
addTargetActionsForName(target, ActionName.KILL, TargetKillAction::builder);
addTargetActionsForName(target, ActionName.STEP_INTO, TargetStepIntoAction::builder);
addTargetActionsForName(target, ActionName.STEP_OVER, TargetStepOverAction::builder);
addTargetActionsForName(target, ActionName.STEP_OUT, TargetStepOutAction::builder);
addTargetActionsForName(target, ActionName.STEP_EXT, TargetStepExtAction::builder);
addTargetStepExtActions(target);
actionsTargetAll.addAll(actionsTargetStepExt);
}
protected void activateControlMode(ActionState<ControlMode> state, EventTrigger trigger) {
@ -792,7 +514,9 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
}
protected void coordinatesActivated(DebuggerCoordinates coords) {
boolean sameTrace = true;
if (current.getTrace() != coords.getTrace()) {
sameTrace = false;
if (current.getTrace() != null) {
current.getTrace().removeListener(listenerForObjects);
}
@ -801,11 +525,13 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
}
}
current = coords;
updateTargetActions();
if (!sameTrace) {
updateTargetStepExtActions();
}
updateActions();
}
private ControlMode computeCurrentEditingMode() {
private ControlMode computeCurrentControlMode() {
if (controlService == null) {
return ControlMode.DEFAULT;
}
@ -819,9 +545,6 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
if (tool == null) {
return;
}
if (curActionSet == actions) {
curActionSet = null;
}
for (DockingActionIf action : actions) {
tool.removeAction(action);
}
@ -831,17 +554,16 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
if (tool == null) {
return;
}
if (curActionSet == actions) {
return;
}
Set<DockingActionIf> already = tool.getDockingActionsByOwnerName(name);
for (DockingActionIf action : actions) {
tool.addAction(action);
if (!already.contains(action)) {
tool.addAction(action);
}
}
curActionSet = actions;
}
private void updateActions() {
ControlMode mode = computeCurrentEditingMode();
ControlMode mode = computeCurrentControlMode();
actionControlMode.setCurrentActionStateByUserData(mode);
Set<DockingAction> actions = getActionSet(mode);
@ -860,6 +582,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
if (current.getTrace() == trace) {
trace.removeListener(listenerForObjects);
current = DebuggerCoordinates.NOWHERE;
updateTargetStepExtActions();
}
updateActions();
}

View file

@ -23,8 +23,10 @@ import docking.Tool;
import docking.action.*;
import docking.actions.PopupActionProvider;
import ghidra.app.context.ProgramActionContext;
import ghidra.app.context.ProgramLocationActionContext;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
import ghidra.app.services.*;
import ghidra.debug.api.control.ControlMode;
import ghidra.debug.api.target.Target;
@ -62,7 +64,7 @@ public class DebuggerMethodActionsPlugin extends Plugin implements PopupActionPr
@Override
public void actionPerformed(ActionContext context) {
tool.execute(new TargetActionTask(entry));
tool.execute(new TargetActionTask(entry.display(), entry));
}
}
@ -95,11 +97,28 @@ public class DebuggerMethodActionsPlugin extends Plugin implements PopupActionPr
return mode.isTarget();
}
/**
* While it may be possible to retrieve sufficient context from the "current state," it's not
* always appropriate to display all of these actions. They should really only appear when an
* address is clearly intended, or when a trace object is clearly intended. We'll have to see
* how/if this works in the type-specific trace object tables, e.g., the Modules panel.
*/
protected boolean isAppropriate(ActionContext context) {
if (context instanceof ProgramLocationActionContext) {
return true;
}
if (context instanceof DebuggerObjectActionContext) {
return true;
}
return false;
}
@Override
public List<DockingActionIf> getPopupActions(Tool tool, ActionContext context) {
if (!isControlTarget()) {
if (!isControlTarget() || !isAppropriate(context)) {
return List.of();
}
Target target = getTarget(context);
if (target == null) {
return List.of();
@ -107,6 +126,9 @@ public class DebuggerMethodActionsPlugin extends Plugin implements PopupActionPr
List<DockingActionIf> result = new ArrayList<>();
for (ActionEntry entry : target.collectActions(null, context).values()) {
if (entry.requiresPrompt() || entry.builtIn()) {
continue;
}
result.add(new InvokeActionEntryAction(entry));
}
return result;

View file

@ -0,0 +1,46 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import docking.action.builder.ActionBuilder;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.framework.plugintool.Plugin;
import ghidra.util.HelpLocation;
interface DisconnectAction extends ControlAction {
String NAME = "Disconnect";
String DESCRIPTION = "Close the connection to the debugging agent";
Icon ICON = DebuggerResources.ICON_DISCONNECT;
String HELP_ANCHOR = "target_disconnect";
int SUB_GROUP = 3;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_K,
KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK);
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,36 @@
/* ###
* 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.control;
import docking.action.builder.ActionBuilder;
import ghidra.framework.plugintool.Plugin;
import ghidra.util.HelpLocation;
interface EmulateInterruptAction extends InterruptAction {
String NAME = "Interrupt Emulator";
String DESCRIPTION = "Interrupt, i.e., suspend, the integrated emulator";
String HELP_ANCHOR = "emu_interrupt";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,36 @@
/* ###
* 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.control;
import docking.action.builder.ActionBuilder;
import ghidra.framework.plugintool.Plugin;
import ghidra.util.HelpLocation;
interface EmulateResumeAction extends ResumeAction {
String NAME = "Resume Emulator";
String DESCRIPTION = "Resume, i.e., go or continue execution of the integrated emulator";
String HELP_ANCHOR = "emu_resume";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,46 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import docking.action.builder.ActionBuilder;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.framework.plugintool.Plugin;
import ghidra.util.HelpLocation;
interface EmulateSkipOverAction extends ControlAction {
String NAME = "Skip Emulator";
String DESCRIPTION =
"Skip the integrated emulator a single instruction, ignoring its effects";
Icon ICON = DebuggerResources.ICON_SKIP_OVER;
String HELP_ANCHOR = "emu_skip_over";
int SUB_GROUP = 7;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F10, KeyEvent.CTRL_DOWN_MASK);
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,45 @@
/* ###
* 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.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import docking.action.builder.ActionBuilder;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.framework.plugintool.Plugin;
import ghidra.util.HelpLocation;
interface EmulateStepBackAction extends ControlAction {
String NAME = "Step Emulator Back";
String DESCRIPTION = "Step the integrated emulator a single instruction backward";
Icon ICON = DebuggerResources.ICON_STEP_BACK;
String HELP_ANCHOR = "emu_step_back";
int SUB_GROUP = 4;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0);
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,37 @@
/* ###
* 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.control;
import docking.action.builder.ActionBuilder;
import ghidra.framework.plugintool.Plugin;
import ghidra.util.HelpLocation;
interface EmulateStepIntoAction extends StepIntoAction {
String NAME = "Step Emulator Into";
String DESCRIPTION =
"Step the integrated emulator a single instruction, descending into calls";
String HELP_ANCHOR = "emu_step_into";
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -13,23 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.services;
package ghidra.app.plugin.core.debug.gui.control;
import java.util.Set;
import java.awt.event.KeyEvent;
import ghidra.debug.api.workflow.DebuggerBot;
import ghidra.framework.plugintool.PluginTool;
import javax.swing.Icon;
import javax.swing.KeyStroke;
public interface DebuggerWorkflowService {
PluginTool getTool();
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
Set<DebuggerBot> getAllBots();
Set<DebuggerBot> getEnabledBots();
Set<DebuggerBot> getDisabledBots();
void enableBots(Set<DebuggerBot> actors);
void disableBots(Set<DebuggerBot> actors);
interface InterruptAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_INTERRUPT;
int SUB_GROUP = 1;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_I, KeyEvent.CTRL_DOWN_MASK);
}

View file

@ -0,0 +1,29 @@
/* ###
* 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.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
interface ResumeAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_RESUME;
int SUB_GROUP = 0;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0);
}

View file

@ -0,0 +1,29 @@
/* ###
* 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.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
interface StepIntoAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_STEP_INTO;
int SUB_GROUP = 5;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0);
}

View file

@ -0,0 +1,70 @@
/* ###
* 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.control;
import docking.ActionContext;
import docking.action.builder.AbstractActionBuilder;
import ghidra.debug.api.target.ActionName;
class TargetActionBuilder
extends AbstractActionBuilder<TargetDockingAction, ActionContext, TargetActionBuilder> {
private final DebuggerControlPlugin plugin;
private ActionName action;
private String defaultDescription;
public TargetActionBuilder(String name, DebuggerControlPlugin plugin) {
super(name, plugin.getName());
this.plugin = plugin;
}
@Override
protected TargetActionBuilder self() {
return this;
}
@Override
protected void validate() {
super.validate();
if (action == null) {
throw new IllegalStateException(
"Can't build a " + TargetDockingAction.class.getSimpleName() +
" without an action name");
}
}
public TargetActionBuilder action(ActionName action) {
this.action = action;
return self();
}
public TargetActionBuilder defaultDescription(String defaultDescription) {
this.defaultDescription = defaultDescription;
return self();
}
@Override
public TargetDockingAction build() {
onAction(ctx -> {
// Make the super.validate() hush
});
validate();
TargetDockingAction result = new TargetDockingAction(name, owner, keyBindingType, plugin,
action, defaultDescription);
decorateAction(result);
return result;
}
}

View file

@ -23,8 +23,8 @@ import ghidra.util.task.TaskMonitor;
class TargetActionTask extends Task {
private ActionEntry entry;
public TargetActionTask(ActionEntry entry) {
super(entry.display(), false, false, false);
public TargetActionTask(String title, ActionEntry entry) {
super(title, false, false, false);
this.entry = entry;
}

View file

@ -0,0 +1,78 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.control;
import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.KeyBindingType;
import ghidra.debug.api.target.ActionName;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.Target.ActionEntry;
class TargetDockingAction extends DockingAction {
private final DebuggerControlPlugin plugin;
private final ActionName action;
private final String defaultDescription;
private ActionEntry entry;
public TargetDockingAction(String name, String owner, KeyBindingType keyBindingType,
DebuggerControlPlugin plugin, ActionName action, String defaultDescription) {
super(name, owner, keyBindingType);
this.plugin = plugin;
this.action = action;
this.defaultDescription = defaultDescription;
}
private ActionEntry findEntry(ActionContext context) {
Target target = plugin.current.getTarget();
if (target == null) {
return null;
}
for (ActionEntry ent : target.collectActions(action, context).values()) {
if (ent.requiresPrompt()) {
continue;
}
return ent;
// TODO: What if multiple match? Do I care to display the extras?
}
return null;
}
protected void updateFromContext(ActionContext context) {
entry = findEntry(context);
if (entry == null) {
setDescription(defaultDescription);
}
else {
setDescription(entry.details());
}
}
@Override
public boolean isEnabledForContext(ActionContext context) {
updateFromContext(context);
return entry != null && entry.isEnabled();
}
@Override
public void actionPerformed(ActionContext context) {
if (entry == null) {
return;
}
plugin.runTask(getName(), entry);
}
}

View file

@ -0,0 +1,36 @@
/* ###
* 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.control;
import ghidra.debug.api.target.ActionName;
import ghidra.util.HelpLocation;
interface TargetInterruptAction extends InterruptAction {
String NAME = "Interrupt";
String DESCRIPTION = "Interrupt the target";
String HELP_ANCHOR = "target_interrupt";
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new TargetActionBuilder(NAME, owner)
.action(ActionName.INTERRUPT)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.defaultDescription(DESCRIPTION)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,46 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.debug.api.target.ActionName;
import ghidra.util.HelpLocation;
interface TargetKillAction extends ControlAction {
String NAME = "Kill";
String DESCRIPTION = "Kill the target";
Icon ICON = DebuggerResources.ICON_KILL;
String HELP_ANCHOR = "target_kill";
int SUB_GROUP = 2;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_K,
KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK);
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new TargetActionBuilder(NAME, owner)
.action(ActionName.KILL)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.defaultDescription(DESCRIPTION)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,36 @@
/* ###
* 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.control;
import ghidra.debug.api.target.ActionName;
import ghidra.util.HelpLocation;
interface TargetResumeAction extends ResumeAction {
String NAME = "Resume";
String DESCRIPTION = "Resume the target";
String HELP_ANCHOR = "target_resume";
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new TargetActionBuilder(NAME, owner)
.action(ActionName.RESUME)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.defaultDescription(DESCRIPTION)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,41 @@
/* ###
* 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.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import docking.action.builder.ActionBuilder;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.util.HelpLocation;
interface TargetStepExtAction extends ControlAction {
Icon ICON = DebuggerResources.ICON_STEP_LAST;
String HELP_ANCHOR = "target_step_ext";
int SUB_GROUP = 9;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, KeyEvent.CTRL_DOWN_MASK);
static ActionBuilder builder(String name, DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(name, ownerName)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP) + name)
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,36 @@
/* ###
* 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.control;
import ghidra.debug.api.target.ActionName;
import ghidra.util.HelpLocation;
interface TargetStepIntoAction extends StepIntoAction {
String NAME = "Step Into";
String DESCRIPTION = "Step the target into";
String HELP_ANCHOR = "target_step_into";
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new TargetActionBuilder(NAME, owner)
.action(ActionName.STEP_INTO)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.defaultDescription(DESCRIPTION)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,45 @@
/* ###
* 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.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.debug.api.target.ActionName;
import ghidra.util.HelpLocation;
interface TargetStepOutAction extends ControlAction {
String NAME = "Step Out";
String DESCRIPTION = "Step the target out";
Icon ICON = DebuggerResources.ICON_STEP_FINISH;
String HELP_ANCHOR = "target_step_out";
int SUB_GROUP = 8;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F12, 0);
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new TargetActionBuilder(NAME, owner)
.action(ActionName.STEP_OUT)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.defaultDescription(DESCRIPTION)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,45 @@
/* ###
* 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.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.debug.api.target.ActionName;
import ghidra.util.HelpLocation;
interface TargetStepOverAction extends ControlAction {
String NAME = "Step Over";
String DESCRIPTION = "Step the target over";
Icon ICON = DebuggerResources.ICON_STEP_OVER;
String HELP_ANCHOR = "target_step_over";
int SUB_GROUP = 6;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0);
static TargetActionBuilder builder(DebuggerControlPlugin owner) {
String ownerName = owner.getName();
return new TargetActionBuilder(NAME, owner)
.action(ActionName.STEP_OVER)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.defaultDescription(DESCRIPTION)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,45 @@
/* ###
* 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.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import docking.action.builder.ActionBuilder;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.framework.plugintool.Plugin;
import ghidra.util.HelpLocation;
interface TraceSnapBackwardAction extends ControlAction {
String NAME = "Trace Snapshot Backward";
String DESCRIPTION = "Navigate the trace recording backward one snapshot";
Icon ICON = DebuggerResources.ICON_SNAP_BACKWARD;
String HELP_ANCHOR = "trace_snap_backward";
int SUB_GROUP = 10;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0);
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -0,0 +1,45 @@
/* ###
* 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.control;
import java.awt.event.KeyEvent;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import docking.action.builder.ActionBuilder;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.framework.plugintool.Plugin;
import ghidra.util.HelpLocation;
interface TraceSnapForwardAction extends ControlAction {
String NAME = "Trace Snapshot Forward";
String DESCRIPTION = "Navigate the trace recording forward one snapshot";
Icon ICON = DebuggerResources.ICON_SNAP_FORWARD;
String HELP_ANCHOR = "trace_snap_backward";
int SUB_GROUP = 11;
KeyStroke KEY_BINDING = KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0);
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.toolBarIcon(ICON)
.toolBarGroup(GROUP, ControlAction.intSubGroup(SUB_GROUP))
.keyBinding(KEY_BINDING)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}

View file

@ -38,6 +38,7 @@ import docking.ActionContext;
import docking.WindowPosition;
import docking.action.DockingAction;
import docking.action.ToggleDockingAction;
import docking.action.builder.ToggleActionBuilder;
import docking.menu.MultiStateDockingAction;
import docking.widgets.EventTrigger;
import docking.widgets.fieldpanel.support.ViewerPosition;
@ -47,6 +48,7 @@ import ghidra.app.nav.ListingPanelContainer;
import ghidra.app.plugin.core.clipboard.CodeBrowserClipboardProvider;
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
import ghidra.app.plugin.core.codebrowser.MarkerServiceBackgroundColorModel;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.plugin.core.debug.gui.DebuggerLocationLabel;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAction;
@ -61,6 +63,8 @@ import ghidra.app.services.*;
import ghidra.app.services.DebuggerListingService.LocationTrackingSpecChangeListener;
import ghidra.app.util.viewer.format.FormatManager;
import ghidra.app.util.viewer.listingpanel.ListingPanel;
import ghidra.async.AsyncDebouncer;
import ghidra.async.AsyncTimer;
import ghidra.debug.api.action.GoToInput;
import ghidra.debug.api.action.LocationTrackingSpec;
import ghidra.debug.api.control.ControlMode;
@ -69,11 +73,11 @@ import ghidra.debug.api.modules.DebuggerStaticMappingChangeListener;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.model.DomainFile;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.AutoConfigState;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoConfigStateField;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
@ -114,6 +118,20 @@ public class DebuggerListingProvider extends CodeViewerProvider {
return true;
}
interface AutoDisassembleAction {
String NAME = "Auto-Disassembly";
String DESCRIPTION = "If the tracking spec follows the PC, disassemble automatically.";
String HELP_ANCHOR = "auto_disassembly";
static ToggleActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ToggleActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.menuPath(NAME)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
protected class MarkerSetChangeListener implements ChangeListener {
@Override
public void stateChanged(ChangeEvent e) {
@ -223,6 +241,14 @@ public class DebuggerListingProvider extends CodeViewerProvider {
@Override
protected void locationTracked() {
doGoToTracked();
if (!autoDisassemble || !trackingTrait.shouldDisassemble()) {
return;
}
disassemblyDebouncer.contact(trackedLocation.getByteAddress());
}
boolean shouldDisassemble() {
return trackedLocation != null && tracker.shouldDisassemble();
}
}
@ -241,6 +267,18 @@ public class DebuggerListingProvider extends CodeViewerProvider {
protected void repaintPanel() {
getListingPanel().getFieldPanel().repaint();
}
@Override
protected void memoryWasRead(AddressSetView read) {
if (!autoDisassemble || !trackingTrait.shouldDisassemble()) {
return;
}
ProgramLocation loc = trackingTrait.getTrackedLocation();
if (!read.contains(loc.getByteAddress())) {
return;
}
disassemblyDebouncer.contact(loc.getByteAddress());
}
}
private final DebuggerListingPlugin plugin;
@ -276,6 +314,7 @@ public class DebuggerListingProvider extends CodeViewerProvider {
protected DockingAction actionSyncSelectionIntoStaticListing;
protected DockingAction actionSyncSelectionFromStaticListing;
protected ToggleDockingAction actionFollowsCurrentThread;
protected ToggleDockingAction actionAutoDisassemble;
protected MultiStateDockingAction<AutoReadMemorySpec> actionAutoReadMemory;
protected DockingAction actionRefreshSelectedMemory;
protected DockingAction actionOpenProgram;
@ -284,12 +323,17 @@ public class DebuggerListingProvider extends CodeViewerProvider {
@AutoConfigStateField
protected boolean followsCurrentThread = true;
// TODO: followsCurrentSnap?
@AutoConfigStateField
protected boolean autoDisassemble = true;
protected final ForListingSyncTrait syncTrait;
protected final ForListingGoToTrait goToTrait;
protected final ForListingTrackingTrait trackingTrait;
protected final ForListingReadsMemoryTrait readsMemTrait;
protected final AsyncDebouncer<Address> disassemblyDebouncer =
new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, 100);
protected final ListenerSet<LocationTrackingSpecChangeListener> trackingSpecChangeListeners =
new ListenerSet<>(LocationTrackingSpecChangeListener.class, true);
@ -322,6 +366,8 @@ public class DebuggerListingProvider extends CodeViewerProvider {
trackingTrait = new ForListingTrackingTrait();
readsMemTrait = new ForListingReadsMemoryTrait();
disassemblyDebouncer.addListener(this::doAutoDisassemble);
ListingPanel listingPanel = getListingPanel();
colorModel = plugin.createListingBackgroundColorModel(listingPanel);
colorModel.addModel(trackingTrait.createListingBackgroundColorModel(listingPanel));
@ -467,6 +513,7 @@ public class DebuggerListingProvider extends CodeViewerProvider {
actionFollowsCurrentThread.setSelected(followsCurrentThread);
updateBorder();
}
actionAutoDisassemble.setSelected(autoDisassemble);
}
@Override
@ -705,6 +752,12 @@ public class DebuggerListingProvider extends CodeViewerProvider {
.buildAndInstallLocal(this);
}
actionAutoDisassemble = AutoDisassembleAction.builder(plugin)
.enabled(true)
.selected(true)
.onAction(ctx -> doSetAutoDisassemble(actionAutoDisassemble.isSelected()))
.buildAndInstallLocal(this);
actionSyncSelectionIntoStaticListing =
syncTrait.installSyncSelectionIntoStaticListingAction();
actionSyncSelectionFromStaticListing =
@ -757,17 +810,16 @@ public class DebuggerListingProvider extends CodeViewerProvider {
* which applies to many things in general. This one is for changes in the listing model's
* "size", i.e., the memory mapping or assigned view has changed. This should be the perfect
* place to ensure the tracked location is centered, if applicable.
*
* <p>
* It seems this method gets called a bit spuriously. A change in bytes, which does not imply a
* change in layout, will also land us here. Thus, we do some simple test here to verify that
* the layout has actually changed. A good proxy is if the number of addresses in the listing
* has changed. To detect that, we have to record what we've seen each change.
*/
@Override
public void stateChanged(ChangeEvent e) {
super.stateChanged(e);
/*
* It seems this method gets called a bit spuriously. A change in bytes, which does not
* imply a change in layout, will also land us here. Thus, we do some simple test here to
* verify that the layout has actually changed. A good proxy is if the number of addresses
* in the listing has changed. To detect that, we have to record what we've seen each
* change.
*/
long newCountAddressesInIndex =
getListingPanel().getAddressIndexMap().getIndexedAddressSet().getNumAddresses();
if (this.countAddressesInIndex == newCountAddressesInIndex) {
@ -776,7 +828,12 @@ public class DebuggerListingProvider extends CodeViewerProvider {
this.countAddressesInIndex = newCountAddressesInIndex;
ProgramLocation trackedLocation = trackingTrait.getTrackedLocation();
if (trackedLocation != null && !isEffectivelyDifferent(getLocation(), trackedLocation)) {
cbGoTo.invoke(() -> getListingPanel().goTo(trackedLocation, true));
cbGoTo.invoke(() -> Swing.runLater(() -> {
boolean goneTo = getListingPanel().goTo(trackedLocation, true);
if (goneTo) {
getListingPanel().center(trackedLocation);
}
}));
}
}
@ -1037,6 +1094,15 @@ public class DebuggerListingProvider extends CodeViewerProvider {
coordinatesActivated(traceManager.getCurrent());
}
public void setAutoDisassemble(boolean auto) {
actionAutoDisassemble.setSelected(true);
doSetAutoDisassemble(auto);
}
protected void doSetAutoDisassemble(boolean auto) {
this.autoDisassemble = auto;
}
protected void updateBorder() {
// TODO: Probably make this accessible from abstract class, instead
ListingPanelContainer decoration = (ListingPanelContainer) getComponent();
@ -1047,6 +1113,10 @@ public class DebuggerListingProvider extends CodeViewerProvider {
return followsCurrentThread;
}
public boolean isAutoDisassemble() {
return autoDisassemble;
}
public void setAutoReadMemorySpec(AutoReadMemorySpec spec) {
readsMemTrait.setAutoSpec(spec);
}
@ -1113,6 +1183,27 @@ public class DebuggerListingProvider extends CodeViewerProvider {
}
}
protected void doAutoDisassemble(Address start) {
TraceProgramView view = current.getView();
if (view == null) {
return;
}
/**
* We'll avoid re-disassembly only if there already exists an instruction <em>at the start
* address</em>. If it's in the middle, then we're off cut and should re-disassemble at the
* new start.
*/
Instruction exists = view.getListing().getInstructionAt(start);
if (exists != null) {
return;
}
AddressSpace space = start.getAddressSpace();
AddressSet set = new AddressSet(space.getMinAddress(), space.getMaxAddress());
TraceDisassembleCommand dis =
new TraceDisassembleCommand(current.getPlatform(), start, set);
dis.run(tool, view);
}
@Override
public void dispose() {
super.dispose();

View file

@ -115,7 +115,7 @@ public abstract class AbstractObjectsTableBasedPanel<U extends TraceObjectInterf
List<ValueRow> sel = getSelectedItems();
if (!sel.isEmpty()) {
myActionContext = new DebuggerObjectActionContext(
sel.stream().map(r -> r.getValue()).collect(Collectors.toList()), provider, this);
sel.stream().map(r -> r.getValue()).collect(Collectors.toList()), provider, table);
}
}

View file

@ -15,10 +15,15 @@
*/
package ghidra.app.plugin.core.debug.gui.model;
import java.util.stream.*;
import org.apache.commons.lang3.ArrayUtils;
import ghidra.dbg.target.TargetObject;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.util.HTMLUtilities;
import ghidra.util.NumericUtilities;
public interface DisplaysObjectValues {
long getSnap();
@ -27,6 +32,38 @@ public interface DisplaysObjectValues {
return "";
}
default String getBoolsDisplay(boolean[] bools) {
return Stream.of(ArrayUtils.toObject(bools))
.map(b -> b ? "T" : "F")
.collect(Collectors.joining(":"));
}
default String getBytesDisplay(byte[] bytes) {
return NumericUtilities.convertBytesToString(bytes, ":");
}
default String getCharsDisplay(char[] chars) {
return new String(chars);
}
default String getShortsDisplay(short[] shorts) {
return Stream.of(ArrayUtils.toObject(shorts))
.map(s -> "%04x".formatted(s))
.collect(Collectors.joining(":"));
}
default String getIntsDisplay(int[] ints) {
return IntStream.of(ints)
.mapToObj(i -> "%08x".formatted(i))
.collect(Collectors.joining(":"));
}
default String getLongsDisplay(long[] longs) {
return LongStream.of(longs)
.mapToObj(l -> "%016x".formatted(l))
.collect(Collectors.joining(":"));
}
default String getPrimitiveValueDisplay(Object value) {
assert !(value instanceof TraceObject);
assert !(value instanceof TraceObjectValue);
@ -34,6 +71,24 @@ public interface DisplaysObjectValues {
if (value == null) {
return getNullDisplay();
}
if (value instanceof boolean[] bools) {
return getBoolsDisplay(bools);
}
if (value instanceof byte[] bytes) {
return getBytesDisplay(bytes);
}
if (value instanceof char[] chars) {
return getCharsDisplay(chars);
}
if (value instanceof short[] shorts) {
return getShortsDisplay(shorts);
}
if (value instanceof int[] ints) {
return getIntsDisplay(ints);
}
if (value instanceof long[] longs) {
return getLongsDisplay(longs);
}
return value.toString();
}

View file

@ -17,8 +17,9 @@ package ghidra.app.plugin.core.debug.gui.model;
import java.awt.Color;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.*;
import org.apache.commons.lang3.ArrayUtils;
import docking.widgets.table.DynamicTableColumn;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
@ -38,6 +39,7 @@ import ghidra.trace.model.Trace;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.util.HTMLUtilities;
import ghidra.util.NumericUtilities;
public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
@ -61,7 +63,36 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
default public String getDisplay() {
T value = getValue();
return value == null ? "" : value.toString();
if (value == null) {
return "";
}
if (value instanceof boolean[] bools) {
return Stream.of(ArrayUtils.toObject(bools))
.map(b -> b ? "T" : "F")
.collect(Collectors.joining(":"));
}
if (value instanceof byte[] bytes) {
return NumericUtilities.convertBytesToString(bytes, ":");
}
if (value instanceof char[] chars) {
return new String(chars);
}
if (value instanceof short[] shorts) {
return Stream.of(ArrayUtils.toObject(shorts))
.map(s -> "%04x".formatted(s))
.collect(Collectors.joining(":"));
}
if (value instanceof int[] ints) {
return IntStream.of(ints)
.mapToObj(i -> "%08x".formatted(i))
.collect(Collectors.joining(":"));
}
if (value instanceof long[] longs) {
return LongStream.of(longs)
.mapToObj(l -> "%016x".formatted(l))
.collect(Collectors.joining(":"));
}
return value.toString();
}
default public String getHtmlDisplay() {

View file

@ -21,6 +21,7 @@ import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
import ghidra.app.services.*;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
@ -81,4 +82,14 @@ public class DebuggerModulesPlugin extends AbstractDebuggerPlugin {
provider.coordinatesActivated(ev.getActiveCoordinates());
}
}
@Override
public void readConfigState(SaveState saveState) {
provider.readConfigState(saveState);
}
@Override
public void writeConfigState(SaveState saveState) {
provider.writeConfigState(saveState);
}
}

View file

@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.gui.modules;
import java.awt.event.MouseEvent;
import java.io.File;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
@ -28,12 +29,20 @@ import org.apache.commons.lang3.ArrayUtils;
import docking.*;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.action.builder.MultiStateActionBuilder;
import docking.menu.ActionState;
import docking.menu.MultiStateDockingAction;
import docking.widgets.EventTrigger;
import docking.widgets.filechooser.GhidraFileChooser;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerBlockChooserDialog;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import ghidra.app.plugin.core.debug.gui.action.AutoMapSpec;
import ghidra.app.plugin.core.debug.gui.action.AutoMapSpec.AutoMapSpecConfigFieldCodec;
import ghidra.app.plugin.core.debug.gui.action.ByModuleAutoMapSpec;
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
import ghidra.app.plugin.core.debug.service.model.TraceRecorderTarget;
import ghidra.app.plugin.core.debug.service.modules.MapModulesBackgroundCommand;
import ghidra.app.plugin.core.debug.service.modules.MapSectionsBackgroundCommand;
import ghidra.app.services.*;
@ -44,7 +53,9 @@ import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.main.AppInfo;
import ghidra.framework.main.DataTreeDialog;
import ghidra.framework.model.*;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoConfigStateField;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginException;
import ghidra.program.model.address.*;
@ -53,11 +64,15 @@ import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
import ghidra.trace.model.*;
import ghidra.trace.model.Trace.TraceMemoryBytesChangeType;
import ghidra.trace.model.modules.*;
import ghidra.trace.util.TraceChangeType;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
public class DebuggerModulesProvider extends ComponentProviderAdapter {
protected static final AutoConfigState.ClassHandler<DebuggerModulesProvider> CONFIG_STATE_HANDLER =
AutoConfigState.wireHandler(DebuggerModulesProvider.class, MethodHandles.lookup());
protected static boolean sameCoordinates(DebuggerCoordinates a, DebuggerCoordinates b) {
if (!Objects.equals(a.getTrace(), b.getTrace())) {
@ -179,6 +194,53 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
}
}
interface AutoMapAction {
String NAME = "Auto-Map Target Memory";
Icon ICON = DebuggerResources.ICON_CONFIG;
String DESCRIPTION = "Automatically map dynamic memory to static counterparts";
String GROUP = DebuggerResources.GROUP_MAPPING;
String HELP_ANCHOR = "auto_map";
static MultiStateActionBuilder<AutoMapSpec> builder(Plugin owner) {
String ownerName = owner.getName();
MultiStateActionBuilder<AutoMapSpec> builder =
new MultiStateActionBuilder<AutoMapSpec>(NAME, ownerName)
.description(DESCRIPTION)
.toolBarGroup(GROUP)
.toolBarIcon(ICON)
.useCheckboxForIcons(true)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
for (AutoMapSpec spec : AutoMapSpec.allSpecs().values()) {
builder.addState(spec.getMenuName(), spec.getMenuIcon(), spec);
}
return builder;
}
}
protected class ForMappingTraceListener extends TraceDomainObjectListener {
public ForMappingTraceListener(AutoMapSpec spec) {
for (TraceChangeType<?, ?> type : spec.getChangeTypes()) {
listenFor(type, this::changed);
}
// TODO: Delete this if/when TraceRecorderTarget is removed
listenFor(TraceMemoryBytesChangeType.CHANGED, this::memoryChanged);
}
private void changed() {
cueAutoMap = true;
}
private void memoryChanged(TraceAddressSnapRange range) {
if (range.getRange().getAddressSpace().isRegisterSpace()) {
return;
}
if (current.getTarget() instanceof TraceRecorderTarget) {
doCuedAutoMap();
}
}
}
protected static Set<TraceModule> getSelectedModules(ActionContext context) {
if (context instanceof DebuggerModuleActionContext ctx) {
return DebuggerLegacyModulesPanel.getSelectedModulesFromContext(ctx);
@ -332,6 +394,14 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
DockingAction actionMapSectionTo;
DockingAction actionMapSectionsTo;
MultiStateDockingAction<AutoMapSpec> actionAutoMap;
private final AutoMapSpec defaultAutoMapSpec =
AutoMapSpec.fromConfigName(ByModuleAutoMapSpec.CONFIG_NAME);
@AutoConfigStateField(codec = AutoMapSpecConfigFieldCodec.class)
AutoMapSpec autoMapSpec = defaultAutoMapSpec;
boolean cueAutoMap;
private ForMappingTraceListener forMappingListener;
DockingAction actionImportMissingModule;
DockingAction actionMapMissingModule;
@ -492,6 +562,11 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
.onAction(this::activatedMapSectionsTo)
.buildAndInstallLocal(this);
actionAutoMap = AutoMapAction.builder(plugin)
.onActionStateChanged(this::changedAutoMapSpec)
.buildAndInstallLocal(this);
actionAutoMap.setCurrentActionStateByUserData(defaultAutoMapSpec);
actionImportMissingModule = ImportMissingModuleAction.builder(plugin)
.withContext(DebuggerMissingModuleActionContext.class)
.onAction(this::activatedImportMissingModule)
@ -535,7 +610,13 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
if (sel == null || sel.isEmpty()) {
return false;
}
return sel.stream().map(TraceSection::getModule).distinct().count() == 1;
try {
return sel.stream().map(TraceSection::getModule).distinct().count() == 1;
}
catch (Exception e) {
Msg.error(this, "Could not check section selection context: " + e);
return false;
}
}
private void activatedMapIdentically(ActionContext ignored) {
@ -544,7 +625,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
}
try {
staticMappingService.addIdentityMapping(current.getTrace(), currentProgram,
Lifespan.nowOn(traceManager.getCurrentSnap()), false);
Lifespan.nowOn(traceManager.getCurrentSnap()), true);
}
catch (TraceConflictedMappingException e) {
Msg.showError(this, null, "Map Identically", e.getMessage());
@ -611,6 +692,19 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
mapSectionTo(sel.iterator().next());
}
private void changedAutoMapSpec(ActionState<AutoMapSpec> newState, EventTrigger trigger) {
doSetAutoMapSpec(newState.getUserData());
}
private void doSetAutoMapSpec(AutoMapSpec autoMapSpec) {
if (this.autoMapSpec == autoMapSpec) {
return;
}
removeOldTraceListener();
this.autoMapSpec = autoMapSpec;
addNewTraceListener();
}
private void activatedImportMissingModule(DebuggerMissingModuleActionContext context) {
if (importerService == null) {
Msg.error(this, "Import service is not present");
@ -877,13 +971,37 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
}
}
protected void addNewTraceListener() {
if (current.getTrace() != null && autoMapSpec != null) {
forMappingListener = new ForMappingTraceListener(autoMapSpec);
current.getTrace().addListener(forMappingListener);
}
}
protected void removeOldTraceListener() {
if (forMappingListener != null) {
if (current.getTrace() != null) {
current.getTrace().removeListener(forMappingListener);
}
forMappingListener = null;
}
}
public void coordinatesActivated(DebuggerCoordinates coordinates) {
if (sameCoordinates(current, coordinates)) {
current = coordinates;
return;
}
boolean changeTrace = current.getTrace() != coordinates.getTrace();
if (changeTrace) {
myActionContext = null;
removeOldTraceListener();
}
current = coordinates;
if (changeTrace) {
addNewTraceListener();
}
if (Trace.isLegacy(coordinates.getTrace())) {
modulesPanel.coordinatesActivated(DebuggerCoordinates.NOWHERE);
@ -913,6 +1031,22 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
}
contextChanged();
if (coordinates.getTarget() instanceof TraceRecorderTarget) {
// HACK while TraceRecorderTarget is still around
cueAutoMap = true;
}
doCuedAutoMap();
}
private void doCuedAutoMap() {
if (cueAutoMap) {
cueAutoMap = false;
Trace trace = current.getTrace();
if (autoMapSpec != null && trace != null) {
autoMapSpec.runTask(tool, trace);
}
}
}
public void setSelectedModules(Set<TraceModule> sel) {
@ -963,4 +1097,21 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
return blockChooserDialog.chooseBlock(getTool(), section,
List.of(programManager.getAllOpenPrograms()));
}
public void setAutoMapSpec(AutoMapSpec spec) {
actionAutoMap.setCurrentActionStateByUserData(spec);
}
public AutoMapSpec getAutoMapSpec() {
return autoMapSpec;
}
public void writeConfigState(SaveState saveState) {
CONFIG_STATE_HANDLER.writeConfigState(this, saveState);
}
public void readConfigState(SaveState saveState) {
CONFIG_STATE_HANDLER.readConfigState(this, saveState);
actionAutoMap.setCurrentActionStateByUserData(autoMapSpec);
}
}

View file

@ -18,10 +18,11 @@ package ghidra.app.plugin.core.debug.gui.thread;
import java.util.List;
import javax.swing.JTable;
import javax.swing.event.ListSelectionEvent;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import docking.widgets.table.threaded.ThreadedTableModelListener;
import docking.widgets.table.TableColumnDescriptor;
import docking.widgets.table.threaded.ThreadedTableModelListener;
import ghidra.app.plugin.core.debug.gui.model.*;
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.*;
import ghidra.app.plugin.core.debug.gui.model.columns.*;
@ -165,6 +166,8 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
@AutoServiceConsumed
protected DebuggerTraceManagerService traceManager;
private final DebuggerThreadsProvider provider;
private final SeekListener seekListener = pos -> {
long snap = Math.round(pos);
if (current.getTrace() == null || snap < 0) {
@ -175,6 +178,7 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
public DebuggerThreadsPanel(DebuggerThreadsProvider provider) {
super(provider.plugin, provider, TraceObjectThread.class);
this.provider = provider;
setLimitToSnap(false); // TODO: Toggle for this?
addSeekListener(seekListener);
@ -239,4 +243,13 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
traceManager.activateObject(item.getValue().getChild());
}
}
@Override
public void valueChanged(ListSelectionEvent e) {
super.valueChanged(e);
if (e.getValueIsAdjusting()) {
return;
}
provider.threadsPanelContextChanged();
}
}

View file

@ -207,6 +207,10 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
super.addLocalAction(action);
}
void threadsPanelContextChanged() {
myActionContext = panel.getActionContext();
}
void legacyThreadsPanelContextChanged() {
myActionContext = legacyPanel.getActionContext();
}

View file

@ -192,10 +192,14 @@ public class DebuggerTraceTabPanel extends HorizontalTabPanel<Trace>
return;
}
if (event instanceof TraceOpenedPluginEvent evt) {
addItem(evt.getTrace());
try (Suppression supp = cbCoordinateActivation.suppress(null)) {
addItem(evt.getTrace());
}
}
else if (event instanceof TraceClosedPluginEvent evt) {
removeItem(evt.getTrace());
try (Suppression supp = cbCoordinateActivation.suppress(null)) {
removeItem(evt.getTrace());
}
}
}
@ -204,6 +208,8 @@ public class DebuggerTraceTabPanel extends HorizontalTabPanel<Trace>
return;
}
Trace newTrace = setTraceTabActionContext(null);
cbCoordinateActivation.invoke(() -> traceManager.activateTrace(newTrace));
cbCoordinateActivation.invoke(() -> {
traceManager.activateTrace(newTrace);
});
}
}

View file

@ -19,6 +19,7 @@ import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
import ghidra.app.services.*;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
@ -32,6 +33,7 @@ import ghidra.framework.plugintool.util.PluginStatus;
status = PluginStatus.RELEASED,
eventsConsumed = {
TraceActivatedPluginEvent.class,
TraceClosedPluginEvent.class,
},
servicesProvided = {
DebuggerWatchesService.class,
@ -64,10 +66,13 @@ public class DebuggerWatchesPlugin extends AbstractDebuggerPlugin {
@Override
public void processEvent(PluginEvent event) {
super.processEvent(event);
if (event instanceof TraceActivatedPluginEvent) {
TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event;
if (event instanceof TraceActivatedPluginEvent ev) {
provider.coordinatesActivated(ev.getActiveCoordinates());
}
else if (event instanceof TraceClosedPluginEvent ev) {
// Activation not good enough. Need to know if "previous" was closed
provider.traceClosed(ev.getTrace());
}
}
@Override

View file

@ -875,14 +875,34 @@ public class DebuggerWatchesProvider extends ComponentProviderAdapter
language = null;
}
asyncWatchExecutor = current.getPlatform() == null ? null
: DebuggerPcodeUtils.buildWatchExecutor(tool, current);
prevValueExecutor = current.getPlatform() == null || previous.getPlatform() == null ? null
: TraceSleighUtils.buildByteExecutor(previous.getPlatform(),
previous.getViewSnap(), previous.getThread(), previous.getFrame());
try {
asyncWatchExecutor = current.getPlatform() == null ? null
: DebuggerPcodeUtils.buildWatchExecutor(tool, current);
}
catch (Exception e) {
Msg.error(this, "Error constructing watch executor: " + e);
asyncWatchExecutor = null;
}
try {
prevValueExecutor =
current.getPlatform() == null || previous.getPlatform() == null ? null
: TraceSleighUtils.buildByteExecutor(previous.getPlatform(),
previous.getViewSnap(), previous.getThread(), previous.getFrame());
}
catch (Exception e) {
Msg.error(this, "Error constructing previous-value executor: " + e);
prevValueExecutor = null;
}
reevaluate();
}
public void traceClosed(Trace trace) {
if (previous.getTrace() == trace) {
previous = DebuggerCoordinates.NOWHERE;
prevValueExecutor = null;
}
}
protected void clearCachedState() {
if (asyncWatchExecutor != null) {
asyncWatchExecutor.getState().clear();

View file

@ -18,8 +18,8 @@ package ghidra.app.plugin.core.debug.mapping;
import java.util.Collection;
import java.util.Set;
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInject;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInject;
import ghidra.debug.api.platform.DebuggerPlatformMapper;
import ghidra.debug.api.platform.DisassemblyResult;
import ghidra.framework.plugintool.PluginTool;

View file

@ -18,8 +18,8 @@ package ghidra.app.plugin.core.debug.mapping.legacy;
import java.util.*;
import java.util.stream.Collectors;
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInject;
import ghidra.app.plugin.core.debug.mapping.*;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInject;
import ghidra.debug.api.platform.DebuggerPlatformMapper;
import ghidra.framework.plugintool.PluginTool;
import ghidra.lifecycle.Transitional;

View file

@ -17,10 +17,8 @@ package ghidra.app.plugin.core.debug.platform.arm;
import java.math.BigInteger;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInject;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInjectInfo;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInjectInfo.CompilerInfo;
import ghidra.app.plugin.core.debug.disassemble.*;
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInjectInfo.CompilerInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.lang.*;

View file

@ -18,8 +18,8 @@ package ghidra.app.plugin.core.debug.platform.dbgeng;
import java.util.Collection;
import java.util.Set;
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInject;
import ghidra.app.plugin.core.debug.mapping.*;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInject;
import ghidra.debug.api.platform.DebuggerPlatformMapper;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.lang.*;

View file

@ -21,10 +21,8 @@ import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInject;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInjectInfo;
import ghidra.app.plugin.core.debug.workflow.DisassemblyInjectInfo.CompilerInfo;
import ghidra.app.plugin.core.debug.disassemble.*;
import ghidra.app.plugin.core.debug.disassemble.DisassemblyInjectInfo.CompilerInfo;
import ghidra.app.services.DebuggerTargetService;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.bin.MemBufferByteProvider;

View file

@ -228,7 +228,8 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin
}
private void breakpointAdded(TraceBreakpoint tb) {
if (!tb.getLifespan().contains(info.snap)) {
Lifespan span = tb.getLifespan();
if (span == null || !span.contains(info.snap)) {
return;
}
try {
@ -600,6 +601,9 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin
return;
}
Address traceAddr = tb.getMinAddress();
if (traceAddr == null) {
return; // Will update via breakpointChanged when address is set
}
ProgramLocation progLoc = computeStaticLocation(tb);
LogicalBreakpointInternal lb;
if (progLoc != null) {
@ -1112,7 +1116,11 @@ public class DebuggerLogicalBreakpointServicePlugin extends Plugin
public LogicalBreakpoint getBreakpoint(TraceBreakpoint bpt) {
Trace trace = bpt.getTrace();
synchronized (lock) {
for (LogicalBreakpoint lb : getBreakpointsAt(trace, bpt.getMinAddress())) {
Address address = bpt.getMinAddress();
if (address == null) {
return null;
}
for (LogicalBreakpoint lb : getBreakpointsAt(trace, address)) {
if (lb.getTraceBreakpoints(trace).contains(bpt)) {
return lb;
}

View file

@ -20,9 +20,9 @@ import java.util.concurrent.CompletableFuture;
import db.Transaction;
import ghidra.async.AsyncUtils;
import ghidra.dbg.target.TargetBreakpointSpec;
import ghidra.dbg.target.TargetBreakpointSpecContainer;
import ghidra.dbg.target.*;
import ghidra.dbg.util.PathMatcher;
import ghidra.dbg.util.PathPattern;
import ghidra.program.model.address.*;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
@ -88,11 +88,21 @@ public record PlaceEmuBreakpointActionItem(Trace trace, long snap, Address addre
if (specMatcher == null) {
throw new IllegalArgumentException("Cannot find path to breakpoint specifications");
}
List<String> relPath = specMatcher.applyKeys(name).getSingletonPath();
if (relPath == null) {
List<String> specRelPath = specMatcher.applyKeys(name).getSingletonPath();
if (specRelPath == null) {
throw new IllegalArgumentException("Too many wildcards to breakpoint specification");
}
return container.getCanonicalPath().extend(relPath).toString();
PathMatcher locMatcher = container.getTargetSchema()
.getSuccessorSchema(specRelPath)
.searchFor(TargetBreakpointLocation.class, true);
if (locMatcher == null) {
throw new IllegalArgumentException("Cannot find path to breakpoint locations");
}
List<String> locRelPath = locMatcher.applyIntKeys(0).getSingletonPath();
if (locRelPath == null) {
throw new IllegalArgumentException("Too many wildcards to breakpoint location");
}
return container.getCanonicalPath().extend(specRelPath).extend(locRelPath).toString();
}
@Override

View file

@ -24,6 +24,7 @@ import java.util.function.Predicate;
import docking.ActionContext;
import ghidra.app.context.ProgramLocationActionContext;
import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext;
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
import ghidra.app.plugin.core.debug.service.target.AbstractTarget;
import ghidra.app.services.DebuggerConsoleService;
import ghidra.app.services.DebuggerTraceManagerService;
@ -176,6 +177,14 @@ public class TraceRecorderTarget extends AbstractTarget {
}
private record MethodWithArgs(TargetMethod method, Map<String, Object> arguments) {
public boolean requiresPrompt() {
for (ParameterDescription<?> param : method.getParameters().values()) {
if (param.required && !arguments.containsKey(param.name)) {
return true;
}
}
return false;
}
}
private List<MethodWithArgs> findAddressMethods(ProgramLocationActionContext context) {
@ -215,23 +224,59 @@ public class TraceRecorderTarget extends AbstractTarget {
return method.getName();
}
private ActionEntry makeEntry(TargetMethod method, Map<String, ?> arguments) {
return new ActionEntry(method.getDisplay(), null, null, false, () -> true, () -> {
return method.invoke(arguments).thenAccept(result -> {
DebuggerConsoleService consoleService =
tool.getService(DebuggerConsoleService.class);
if (consoleService != null && method.getReturnType() != Void.class) {
consoleService.log(null, getDisplay(method) + " returned " + result);
private Map<String, ?> promptArgs(TargetMethod method, Map<String, ?> defaults) {
DebuggerMethodInvocationDialog dialog = new DebuggerMethodInvocationDialog(tool,
method.getDisplay(), method.getDisplay(), null);
while (true) {
for (ParameterDescription<?> param : method.getParameters().values()) {
Object val = defaults.get(param.name);
if (val != null) {
dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class),
val);
}
});
}
Map<String, ?> args = dialog.promptArguments(method.getParameters());
if (args == null) {
// Cancelled
return null;
}
if (dialog.isResetRequested()) {
continue;
}
return args;
}
}
private CompletableFuture<?> invokeMethod(boolean prompt, TargetMethod method,
Map<String, ?> arguments) {
Map<String, ?> chosenArgs;
if (prompt) {
chosenArgs = promptArgs(method, arguments);
}
else {
chosenArgs = arguments;
}
return method.invoke(chosenArgs).thenAccept(result -> {
DebuggerConsoleService consoleService =
tool.getService(DebuggerConsoleService.class);
if (consoleService != null && method.getReturnType() != Void.class) {
consoleService.log(null, getDisplay(method) + " returned " + result);
}
});
}
private ActionEntry makeEntry(boolean requiresPrompt, TargetMethod method,
Map<String, ?> arguments) {
return new ActionEntry(method.getDisplay(), null, null, requiresPrompt, () -> true,
prompt -> invokeMethod(prompt, method, arguments));
}
@Override
public Map<String, ActionEntry> collectAddressActions(ProgramLocationActionContext context) {
Map<String, ActionEntry> result = new HashMap<>();
for (MethodWithArgs mwa : findAddressMethods(context)) {
result.put(mwa.method.getJoinedPath("."), makeEntry(mwa.method, mwa.arguments));
result.put(mwa.method.getJoinedPath("."),
makeEntry(mwa.requiresPrompt(), mwa.method, mwa.arguments));
}
return result;
}
@ -244,7 +289,7 @@ public class TraceRecorderTarget extends AbstractTarget {
return Map.of();
}
return Map.of(display, new ActionEntry(display, name, description, false,
() -> enabled.test(object), () -> action.apply(object)));
() -> enabled.test(object), prompt -> action.apply(object)));
}
private TargetExecutionState getStateOf(TargetObject object) {
@ -541,6 +586,12 @@ public class TraceRecorderTarget extends AbstractTarget {
return spec.toggle(enabled);
}
@Override
public CompletableFuture<Void> forceTerminateAsync() {
recorder.stopRecording();
return AsyncUtils.nil();
}
@Override
public CompletableFuture<Void> disconnectAsync() {
return recorder.getTarget()

View file

@ -58,6 +58,10 @@ public abstract class AbstractTarget implements Target {
this.tool = tool;
}
public PluginTool getTool() {
return tool;
}
private Address staticToDynamicAddress(ProgramLocation location) {
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
@ -73,10 +77,14 @@ public abstract class AbstractTarget implements Target {
}
protected Address findAddress(Navigatable nav) {
if (nav.isDynamic()) {
return nav.getLocation().getAddress();
ProgramLocation location = nav.getLocation();
if (location == null) {
return null;
}
return staticToDynamicAddress(nav.getLocation());
if (nav.isDynamic()) {
return location.getAddress();
}
return staticToDynamicAddress(location);
}
protected Address findAddress(MarkerLocation location) {
@ -118,7 +126,7 @@ public abstract class AbstractTarget implements Target {
}
protected AddressRange singleRange(AddressSetView set) {
if (set.getNumAddressRanges() != 1) {
if (set == null || set.getNumAddressRanges() != 1) {
return null;
}
return set.getFirstRange();
@ -386,6 +394,11 @@ public abstract class AbstractTarget implements Target {
runSync(msg, () -> toggleBreakpointAsync(breakpoint, enabled));
}
@Override
public void forceTerminate() {
runSync("force terminate", () -> forceTerminateAsync());
}
@Override
public void disconnect() {
runSync("disconnect", this::disconnectAsync);

View file

@ -463,7 +463,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
}
@AutoServiceConsumed
private void setModelService(DebuggerTargetService targetService) {
private void setTargetService(DebuggerTargetService targetService) {
if (this.targetService != null) {
this.targetService.removeTargetPublicationListener(forTargetsListener);
}

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