diff --git a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/local-gdb.sh b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/local-gdb.sh index a4fe7b665b..e27b0210ff 100755 --- a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/local-gdb.sh +++ b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/local-gdb.sh @@ -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" diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/arch.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/arch.py index 473639b076..afceea5fee 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/arch.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/arch.py @@ -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') diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py index 3eca459ef1..d809d9e844 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py @@ -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 diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py index 4ae8a135d9..dc1654a7de 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py @@ -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) diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py index 3779016263..9a38dd3f2e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py @@ -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}') diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiLauncherService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiLauncherService.java index cc2ab03359..8d769d7ad2 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiLauncherService.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiLauncherService.java @@ -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 getOpinions(); - /** * Get all offers for the given program * diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/action/LocationTracker.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/action/LocationTracker.java index 28e6267046..49247ebb9d 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/action/LocationTracker.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/action/LocationTracker.java @@ -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. + * + *

+ * Essentially, is this tracking the program counter? + * + * @return true to disassemble, false not to + */ + boolean shouldDisassemble(); } diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/ActionName.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/ActionName.java index 93161f2647..9d31777cea 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/ActionName.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/ActionName.java @@ -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 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 * *

* 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) { *

* 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) { *

* 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"); } diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java index 108f43f64c..96822accca 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java @@ -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. + * + *

+ * 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. + * + *

+ * 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> action) { + BooleanSupplier enabled, Function> 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 + * + *

+ * 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 + * + *

+ * 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 + * + *

+ * 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 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 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 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 { * NOTE: 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 invalidateMemoryCachesAsync(); - - /** - * See {@link #invalidateMemoryCachesAsync()} - */ void invalidateMemoryCaches(); /** @@ -135,11 +246,11 @@ public interface Target { * *

* The target may read more than the requested memory, usually because it will read all pages - * containing any portion of the requested set. - * - *

- * 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 before this method returns. Only if the request is entirely + * 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 writeMemoryAsync(Address address, byte[] data); + /** + * Write data to the target's memory + * + *

+ * 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 before this method returns. Only if the + * request is entirely 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 readRegistersAsync(TracePlatform platform, TraceThread thread, int frame, Set registers); + /** + * Read and capture the named target registers for the given platform, thread, and frame. + * + *

+ * Target target should read the registers and, to the extent it is successful, cause the values + * to be recorded into the trace before this method returns. Only if the request is + * entirely 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 registers); + /** + * @see #readRegistersAsync(TracePlatform, TraceThread, int, AddressSetView) + */ CompletableFuture readRegistersAsync(TracePlatform platform, TraceThread thread, int frame, AddressSetView guestSet); + /** + * Read and capture the target registers in the given address set. + * + *

+ * 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 writeRegisterAsync(TracePlatform platform, TraceThread thread, int frame, RegisterValue value); + /** + * Write a value to a target register for the given platform, thread, and frame + * + *

+ * The target should attempt to write the register. If successful, it must cause the effects to + * be recorded into the trace before 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 writeRegisterAsync(TracePlatform platform, TraceThread thread, int frame, Address address, byte[] data); + /** + * Write a value to a target register by its address + * + *

+ * 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 getSupportedBreakpointKinds(); + /** + * @see #placeBreakpoint(AddressRange, Set, String, String) + */ CompletableFuture placeBreakpointAsync(AddressRange range, Set kinds, String condition, String commands); + /** + * Place a new breakpoint of the given kind(s) over the given range + * + *

+ * 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 kinds, String condition, String commands); @@ -227,14 +429,61 @@ public interface Target { */ boolean isBreakpointValid(TraceBreakpoint breakpoint); + /** + * @see #deleteBreakpoint(TraceBreakpoint) + */ CompletableFuture deleteBreakpointAsync(TraceBreakpoint breakpoint); + /** + * Delete the given breakpoint from the target + * + *

+ * 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 toggleBreakpointAsync(TraceBreakpoint breakpoint, boolean enabled); + /** + * Toggle the given breakpoint on the target + * + *

+ * 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 forceTerminateAsync(); + + /** + * Forcefully terminate the target + * + *

+ * 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 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 disconnectAsync(); - - /** - * Terminate the target and its connection - * *

* NOTE: This method cannot be invoked on the Swing thread, because it may block on I/O. * diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/TargetPublicationListener.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/TargetPublicationListener.java index 6a4a63787d..cfd114e290 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/TargetPublicationListener.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/TargetPublicationListener.java @@ -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); } diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java index bb896cf43d..665ea747dd 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java @@ -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; diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerWorkflowFrontEndService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TerminalSession.java similarity index 50% rename from Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerWorkflowFrontEndService.java rename to Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TerminalSession.java index 7c82695c0b..c9269758d8 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerWorkflowFrontEndService.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TerminalSession.java @@ -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 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(); } diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiConnection.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiConnection.java index fd0afe7ca0..decddb02b8 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiConnection.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiConnection.java @@ -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 + * + *

+ * 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. + * + *

+ * 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. + * + *

+ * 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. + * + *

+ * 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. + * + *

+ * NOTE: 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. + * + *

+ * {@inheritDoc} + * + *

+ * 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. + * + *

+ * 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); } diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java index 4a510e9963..9ef8efb73a 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java @@ -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 * diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/workflow/DebuggerBot.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/workflow/DebuggerBot.java deleted file mode 100644 index b4ee473e6d..0000000000 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/workflow/DebuggerBot.java +++ /dev/null @@ -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 - * - *

- * 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." - * - *

- * 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 - * - *

- * 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 ""; - } - 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 - * - *

- * 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 - * - *

- * 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 - * - *

- * 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) { - } -} diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/workflow/DebuggerBotInfo.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/workflow/DebuggerBotInfo.java deleted file mode 100644 index 2fb237accb..0000000000 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/workflow/DebuggerBotInfo.java +++ /dev/null @@ -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; -} diff --git a/Ghidra/Debug/Debugger-rmi-trace/DEVNOTES.txt b/Ghidra/Debug/Debugger-rmi-trace/DEVNOTES.txt index 0c6907099d..9815847397 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/DEVNOTES.txt +++ b/Ghidra/Debug/Debugger-rmi-trace/DEVNOTES.txt @@ -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. diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/RemoteMethodInvocationDialog.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/RemoteMethodInvocationDialog.java new file mode 100644 index 0000000000..2e0f36bd5f --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/RemoteMethodInvocationDialog.java @@ -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 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 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 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 parameters; + private Map defaults; + + // TODO: Not sure this is the best keying, but I think it works. + private Map memorized = new HashMap<>(); + private Map 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 promptArguments(SchemaContext ctx, + Map parameterMap, Map defaults) { + setParameters(ctx, parameterMap); + setDefaults(defaults); + tool.showDialog(this); + + return getArguments(); + } + + public void setParameters(SchemaContext ctx, Map parameterMap) { + this.ctx = ctx; + this.parameters = parameterMap; + populateOptions(); + } + + public void setDefaults(Map 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 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 collectArguments() { + Map 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 getArguments() { + return arguments; + } + + public void setMemorizedArgument(String name, Class type, T value) { + if (value == null) { + return; + } + memorized.put(new NameTypePair(name, type), value); + } + + public T getMemorizedArgument(String name, Class 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 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); + } + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java index 9d8d73c8e4..a223d5a894 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java @@ -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 listenForMapping( - DebuggerStaticMappingService mappingService, TraceRmiConnection connection, - Trace trace) { + protected CompletableFuture 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 sessions, Map 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 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 = """ + +

Failed to launch %s due to an exception:

+ + %s + +

Troubleshooting

+

+ Check the Terminal! + If no terminal is visible, check the menus: Window → Terminals → + .... + 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.

+ +

These resources remain after the failed launch:

+ + +

Do you want to keep these resources?

+