From fb9a7c62a282e204599de542d62bc36248b6f381 Mon Sep 17 00:00:00 2001
From: Dan <46821332+nsadeveloper789@users.noreply.github.com>
Date: Thu, 6 Feb 2025 13:15:45 +0000
Subject: [PATCH] GP-4546: Add 'Set breakpoint' multi-action to Breakpoints
window.
---
.../src/main/py/src/ghidradbg/methods.py | 52 ++--
.../src/main/py/src/ghidragdb/methods.py | 30 ++-
.../src/main/py/src/ghidralldb/methods.py | 42 +--
.../ghidra/debug/api/target/ActionName.java | 244 ++++++++++++++----
.../java/ghidra/debug/api/target/Target.java | 94 +++++--
.../debug/api/tracermi/RemoteMethod.java | 16 ++
.../ghidra/debug/flatapi/FlatDebuggerAPI.java | 13 +-
.../ghidra/dbg/jdi/rmi/jpda/JdiConnector.java | 14 +-
.../core/debug/client/tracermi/RmiClient.java | 6 +-
.../client/tracermi/RmiMethodRegistry.java | 4 +
.../client/tracermi/RmiRemoteMethod.java | 31 ++-
.../service/tracermi/RecordRemoteMethod.java | 25 +-
.../service/tracermi/TraceRmiHandler.java | 5 +-
.../service/tracermi/TraceRmiTarget.java | 155 +++++++----
.../src/main/proto/trace-rmi.proto | 6 +-
.../src/main/py/src/ghidratrace/client.py | 28 +-
.../tracermi/TestTraceRmiConnection.java | 17 ++
.../DebuggerBreakpointsPlugin.html | 7 +
.../images/DebuggerBreakpointsPlugin.png | Bin 28715 -> 30412 bytes
.../debug/gui/InvokeActionEntryAction.java | 39 +++
.../DebuggerBreakpointsProvider.java | 78 ++++++
.../gui/control/DebuggerControlPlugin.java | 5 +-
.../control/DebuggerMethodActionsPlugin.java | 37 +--
.../gui/control/TargetDockingAction.java | 6 +-
.../trace/model/TraceExecutionState.java | 68 +----
.../DebuggerMethodActionsPluginTest.java | 12 +-
.../Debugger/A1-GettingStarted.html | 3 +-
.../GhidraClass/Debugger/A2-UITour.html | 3 +-
.../GhidraClass/Debugger/A3-Breakpoints.html | 31 ++-
.../GhidraClass/Debugger/A3-Breakpoints.md | 3 +-
.../GhidraClass/Debugger/A4-MachineState.html | 3 +-
.../GhidraClass/Debugger/A5-Navigation.html | 3 +-
.../GhidraClass/Debugger/A6-MemoryMap.html | 3 +-
.../Debugger/B1-RemoteTargets.html | 3 +-
.../GhidraClass/Debugger/B2-Emulation.html | 3 +-
.../GhidraClass/Debugger/B3-Scripting.html | 5 +-
.../GhidraClass/Debugger/B3-Scripting.md | 2 +-
.../GhidraClass/Debugger/B4-Modeling.html | 85 +++---
.../ghidra_scripts/ZeroTimerScript.java | 6 +-
.../images/Breakpoints_EmptyAfterLaunch.png | Bin 14679 -> 16326 bytes
.../images/Breakpoints_PopAfterSRandRand.png | Bin 24384 -> 26447 bytes
.../Breakpoints_SyncedAfterImportLibC.png | Bin 218844 -> 217291 bytes
.../GettingStarted_DisassemblyAfterLaunch.png | Bin 203605 -> 210345 bytes
.../images/GettingStarted_ToolWSpecimen.png | Bin 129396 -> 136294 bytes
44 files changed, 785 insertions(+), 402 deletions(-)
create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/InvokeActionEntryAction.java
diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/methods.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/methods.py
index 5326cc5be5..b98cfa1c93 100644
--- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/methods.py
+++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/methods.py
@@ -144,8 +144,8 @@ def find_thread_by_regs_obj(object):
def find_frame_by_level(level):
for f in util.dbg._base.backtrace_list():
if f.FrameNumber == level:
- return f
- #return dbg().backtrace_list()[level]
+ return f
+ # return dbg().backtrace_list()[level]
def find_frame_by_pattern(pattern, object, err_msg):
@@ -206,8 +206,8 @@ def execute(cmd: str, to_string: bool=False):
@REGISTRY.method(action='evaluate', display='Evaluate')
# @util.dbg.eng_thread
def evaluate(
- session: sch.Schema('Session'),
- expr: ParamDesc(str, display='Expr')):
+ session: sch.Schema('Session'),
+ expr: ParamDesc(str, display='Expr')):
"""Evaluate a Python3 expression."""
return str(eval(expr, shared_globals))
@@ -328,8 +328,8 @@ def remove_process(process: sch.Schema('Process')):
@REGISTRY.method(action='connect', display='Connect')
@util.dbg.eng_thread
def target(
- session: sch.Schema('Session'),
- cmd: ParamDesc(str, display='Command')):
+ session: sch.Schema('Session'),
+ cmd: ParamDesc(str, display='Command')):
"""Connect to a target machine or process."""
dbg().attach_kernel(cmd)
@@ -345,8 +345,8 @@ def attach_obj(target: sch.Schema('Attachable')):
@REGISTRY.method(action='attach', display='Attach by pid')
@util.dbg.eng_thread
def attach_pid(
- session: sch.Schema('Session'),
- pid: ParamDesc(str, display='PID')):
+ session: sch.Schema('Session'),
+ pid: ParamDesc(str, display='PID')):
"""Attach the process to the given target."""
dbg().attach_proc(int(pid))
@@ -354,8 +354,8 @@ def attach_pid(
@REGISTRY.method(action='attach', display='Attach by name')
@util.dbg.eng_thread
def attach_name(
- session: sch.Schema('Session'),
- name: ParamDesc(str, display='Name')):
+ session: sch.Schema('Session'),
+ name: ParamDesc(str, display='Name')):
"""Attach the process to the given target."""
dbg().attach_proc(name)
@@ -369,7 +369,7 @@ def detach(process: sch.Schema('Process')):
@REGISTRY.method(action='launch', display='Launch')
def launch_loader(
- session: sch.Schema('Session'),
+ session: sch.Schema('Session'),
file: ParamDesc(str, display='File'),
args: ParamDesc(str, display='Arguments')=''):
"""
@@ -383,7 +383,7 @@ def launch_loader(
@REGISTRY.method(action='launch', display='LaunchEx')
def launch(
- session: sch.Schema('Session'),
+ session: sch.Schema('Session'),
file: ParamDesc(str, display='File'),
args: ParamDesc(str, display='Arguments')='',
initial_break: ParamDesc(bool, display='Initial Break')=True,
@@ -405,7 +405,7 @@ def kill(process: sch.Schema('Process')):
commands.ghidra_trace_kill()
-@REGISTRY.method(action='resume')
+@REGISTRY.method(action='resume', display="Go")
def go(process: sch.Schema('Process')):
"""Continue execution of the process."""
util.dbg.run_async(lambda: dbg().go())
@@ -456,7 +456,7 @@ def break_address(process: sch.Schema('Process'), address: Address):
dbg().bp(expr=address.offset)
-@REGISTRY.method(action='break_sw_execute')
+@REGISTRY.method(action='break_ext', display='Set Breakpoint')
@util.dbg.eng_thread
def break_expression(expression: str):
"""Set a breakpoint."""
@@ -472,7 +472,7 @@ def break_hw_address(process: sch.Schema('Process'), address: Address):
dbg().ba(expr=address.offset)
-@REGISTRY.method(action='break_hw_execute')
+@REGISTRY.method(action='break_ext', display='Set Hardware Breakpoint')
@util.dbg.eng_thread
def break_hw_expression(expression: str):
"""Set a hardware-assisted breakpoint."""
@@ -482,50 +482,50 @@ def break_hw_expression(expression: str):
@REGISTRY.method(action='break_read')
@util.dbg.eng_thread
def break_read_range(process: sch.Schema('Process'), range: AddressRange):
- """Set a read watchpoint."""
+ """Set a read breakpoint."""
find_proc_by_obj(process)
dbg().ba(expr=range.min, size=range.length(), access=DbgEng.DEBUG_BREAK_READ)
-@REGISTRY.method(action='break_read')
+@REGISTRY.method(action='break_ext', display='Set Read Breakpoint')
@util.dbg.eng_thread
def break_read_expression(expression: str):
- """Set a read watchpoint."""
+ """Set a read breakpoint."""
dbg().ba(expr=expression, access=DbgEng.DEBUG_BREAK_READ)
@REGISTRY.method(action='break_write')
@util.dbg.eng_thread
def break_write_range(process: sch.Schema('Process'), range: AddressRange):
- """Set a watchpoint."""
+ """Set a write breakpoint."""
find_proc_by_obj(process)
dbg().ba(expr=range.min, size=range.length(), access=DbgEng.DEBUG_BREAK_WRITE)
-@REGISTRY.method(action='break_write')
+@REGISTRY.method(action='break_ext', display='Set Write Breakpoint')
@util.dbg.eng_thread
def break_write_expression(expression: str):
- """Set a watchpoint."""
+ """Set a write breakpoint."""
dbg().ba(expr=expression, access=DbgEng.DEBUG_BREAK_WRITE)
@REGISTRY.method(action='break_access')
@util.dbg.eng_thread
def break_access_range(process: sch.Schema('Process'), range: AddressRange):
- """Set an access watchpoint."""
+ """Set an access breakpoint."""
find_proc_by_obj(process)
dbg().ba(expr=range.min, size=range.length(),
access=DbgEng.DEBUG_BREAK_READ | DbgEng.DEBUG_BREAK_WRITE)
-@REGISTRY.method(action='break_access')
+@REGISTRY.method(action='break_ext', display='Set Access Breakpoint')
@util.dbg.eng_thread
def break_access_expression(expression: str):
- """Set an access watchpoint."""
+ """Set an access breakpoint."""
dbg().ba(expr=expression, access=DbgEng.DEBUG_BREAK_READ | DbgEng.DEBUG_BREAK_WRITE)
-@REGISTRY.method(action='toggle')
+@REGISTRY.method(action='toggle', display='Toggle Breakpoint')
@util.dbg.eng_thread
def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool):
"""Toggle a breakpoint."""
@@ -536,7 +536,7 @@ def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool):
dbg().bd(bpt.GetId())
-@REGISTRY.method(action='delete')
+@REGISTRY.method(action='delete', display='Delete Breakpoint')
@util.dbg.eng_thread
def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')):
"""Delete a breakpoint."""
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 a019d4b160..30263b3fe0 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
@@ -391,19 +391,19 @@ def refresh_sections(node: sch.Schema('Module')):
gdb.execute(f'ghidra trace put-sections "{modname}"')
-@REGISTRY.method(action='activate')
+@REGISTRY.method(action='activate', display="Activate Inferior")
def activate_inferior(inferior: sch.Schema('Inferior')):
"""Switch to the inferior."""
switch_inferior(find_inf_by_obj(inferior))
-@REGISTRY.method(action='activate')
+@REGISTRY.method(action='activate', display="Activate Thread")
def activate_thread(thread: sch.Schema('Thread')):
"""Switch to the thread."""
find_thread_by_obj(thread).switch()
-@REGISTRY.method(action='activate')
+@REGISTRY.method(action='activate', display="Activate Frame")
def activate_frame(frame: sch.Schema('StackFrame')):
"""Select the frame."""
find_frame_by_obj(frame).select()
@@ -415,7 +415,7 @@ def add_inferior(container: sch.Schema('InferiorContainer')):
gdb.execute('add-inferior')
-@REGISTRY.method(action='delete')
+@REGISTRY.method(action='delete', display="Delete Inferior")
def delete_inferior(inferior: sch.Schema('Inferior')):
"""Remove the inferior."""
inf = find_inf_by_obj(inferior)
@@ -433,7 +433,7 @@ def connect(inferior: sch.Schema('Inferior'), spec: str):
@REGISTRY.method(action='attach', display='Attach')
def attach_obj(target: sch.Schema('Attachable')):
"""Attach the inferior to the given target."""
- #switch_inferior(find_inf_by_obj(inferior))
+ # switch_inferior(find_inf_by_obj(inferior))
pid = find_availpid_by_obj(target)
gdb.execute(f'attach {pid}')
@@ -577,7 +577,7 @@ def break_sw_execute_address(inferior: sch.Schema('Inferior'), address: Address)
gdb.execute(f'break *0x{offset:x}')
-@REGISTRY.method(action='break_sw_execute')
+@REGISTRY.method(action='break_ext', display="Set Breakpoint")
def break_sw_execute_expression(expression: str):
"""Set a breakpoint (break)."""
# TODO: Escape?
@@ -592,7 +592,7 @@ def break_hw_execute_address(inferior: sch.Schema('Inferior'), address: Address)
gdb.execute(f'hbreak *0x{offset:x}')
-@REGISTRY.method(action='break_hw_execute')
+@REGISTRY.method(action='break_ext', display="Set Hardware Breakpoint")
def break_hw_execute_expression(expression: str):
"""Set a hardware-assisted breakpoint (hbreak)."""
# TODO: Escape?
@@ -609,7 +609,7 @@ def break_read_range(inferior: sch.Schema('Inferior'), range: AddressRange):
f'rwatch -location *((char(*)[{range.length()}]) 0x{offset_start:x})')
-@REGISTRY.method(action='break_read')
+@REGISTRY.method(action='break_ext', display="Set Read Watchpoint")
def break_read_expression(expression: str):
"""Set a read watchpoint (rwatch)."""
gdb.execute(f'rwatch {expression}')
@@ -625,7 +625,7 @@ def break_write_range(inferior: sch.Schema('Inferior'), range: AddressRange):
f'watch -location *((char(*)[{range.length()}]) 0x{offset_start:x})')
-@REGISTRY.method(action='break_write')
+@REGISTRY.method(action='break_ext', display="Set Watchpoint")
def break_write_expression(expression: str):
"""Set a watchpoint (watch)."""
gdb.execute(f'watch {expression}')
@@ -641,7 +641,7 @@ def break_access_range(inferior: sch.Schema('Inferior'), range: AddressRange):
f'awatch -location *((char(*)[{range.length()}]) 0x{offset_start:x})')
-@REGISTRY.method(action='break_access')
+@REGISTRY.method(action='break_ext', display="Set Access Watchpoint")
def break_access_expression(expression: str):
"""Set an access watchpoint (awatch)."""
gdb.execute(f'awatch {expression}')
@@ -653,21 +653,23 @@ def break_event(inferior: sch.Schema('Inferior'), spec: str):
gdb.execute(f'catch {spec}')
-@REGISTRY.method(action='toggle')
+@REGISTRY.method(action='toggle', display="Toggle Breakpoint")
def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool):
"""Toggle a breakpoint."""
bpt = find_bpt_by_obj(breakpoint)
bpt.enabled = enabled
-@REGISTRY.method(action='toggle', condition=util.GDB_VERSION.major >= 13)
+@REGISTRY.method(action='toggle', display="Toggle Breakpoint Location",
+ condition=util.GDB_VERSION.major >= 13)
def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool):
"""Toggle a breakpoint location."""
loc = find_bpt_loc_by_obj(location)
loc.enabled = enabled
-@REGISTRY.method(action='toggle', condition=util.GDB_VERSION.major < 13)
+@REGISTRY.method(action='toggle', display="Toggle Breakpoint Location",
+ condition=util.GDB_VERSION.major < 13)
def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool):
"""Toggle a breakpoint location."""
bptnum, locnum = find_bptlocnum_by_obj(location)
@@ -675,7 +677,7 @@ def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabl
gdb.execute(f'{cmd} {bptnum}.{locnum}')
-@REGISTRY.method(action='delete')
+@REGISTRY.method(action='delete', display="Delete Breakpoint")
def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')):
"""Delete a breakpoint."""
bpt = find_bpt_by_obj(breakpoint)
diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/methods.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/methods.py
index 66592d6d9a..4a0e04c2fe 100644
--- a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/methods.py
+++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/methods.py
@@ -17,11 +17,11 @@ from concurrent.futures import Future, ThreadPoolExecutor
import re
import sys
+import lldb
+
from ghidratrace import sch
from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange
-import lldb
-
from . import commands, util
@@ -248,7 +248,7 @@ def execute(cmd: str, to_string: bool=False):
return exec_convert_errors(cmd, to_string)
-@REGISTRY.method
+@REGISTRY.method(display='Evaluate')
def evaluate(expr: str):
"""Evaluate an expression."""
value = util.get_target().EvaluateExpression(expr)
@@ -257,7 +257,7 @@ def evaluate(expr: str):
return commands.convert_value(value)
-@REGISTRY.method
+@REGISTRY.method(display="Python Evaluate")
def pyeval(expr: str):
return eval(expr)
@@ -345,28 +345,28 @@ def refresh_modules(node: sch.Schema('ModuleContainer')):
exec_convert_errors('ghidra trace put-modules')
-@REGISTRY.method(action='activate')
+@REGISTRY.method(action='activate', display='Activate Process')
def activate_process(process: sch.Schema('Process')):
"""Switch to the process."""
# TODO
return
-@REGISTRY.method(action='activate')
+@REGISTRY.method(action='activate', display='Activate Thread')
def activate_thread(thread: sch.Schema('Thread')):
"""Switch to the thread."""
t = find_thread_by_obj(thread)
t.process.SetSelectedThread(t)
-@REGISTRY.method(action='activate')
+@REGISTRY.method(action='activate', display='Activate Frame')
def activate_frame(frame: sch.Schema('StackFrame')):
"""Select the frame."""
f = find_frame_by_obj(frame)
f.thread.SetSelectedFrame(f.GetFrameID())
-@REGISTRY.method(action='delete')
+@REGISTRY.method(action='delete', display='Remove Process')
def remove_process(process: sch.Schema('Process')):
"""Remove the process."""
proc = find_proc_by_obj(process)
@@ -442,7 +442,7 @@ def kill(process: sch.Schema('Process')):
exec_convert_errors('process kill')
-@REGISTRY.method(name='continue', action='resume')
+@REGISTRY.method(name='continue', action='resume', display="Continue")
def _continue(process: sch.Schema('Process')):
"""Continue execution of the process."""
exec_convert_errors('process continue')
@@ -510,7 +510,7 @@ def break_address(process: sch.Schema('Process'), address: Address):
exec_convert_errors(f'breakpoint set -a 0x{offset:x}')
-@REGISTRY.method(action='break_sw_execute')
+@REGISTRY.method(action='break_ext', display='Set Breakpoint')
def break_expression(expression: str):
"""Set a breakpoint."""
# TODO: Escape?
@@ -525,7 +525,7 @@ def break_hw_address(process: sch.Schema('Process'), address: Address):
exec_convert_errors(f'breakpoint set -H -a 0x{offset:x}')
-@REGISTRY.method(action='break_hw_execute')
+@REGISTRY.method(action='break_ext', display='Set Hardware Breakpoint')
def break_hw_expression(expression: str):
"""Set a hardware-assisted breakpoint."""
# TODO: Escape?
@@ -543,7 +543,7 @@ def break_read_range(process: sch.Schema('Process'), range: AddressRange):
f'watchpoint set expression -s {sz} -w read -- {offset_start}')
-@REGISTRY.method(action='break_read')
+@REGISTRY.method(action='break_ext', display='Set Read Watchpoint')
def break_read_expression(expression: str, size=None):
"""Set a read watchpoint."""
size_part = '' if size is None else f'-s {size}'
@@ -562,7 +562,7 @@ def break_write_range(process: sch.Schema('Process'), range: AddressRange):
f'watchpoint set expression -s {sz} -- {offset_start}')
-@REGISTRY.method(action='break_write')
+@REGISTRY.method(action='break_ext', display='Set Watchpoint')
def break_write_expression(expression: str, size=None):
"""Set a watchpoint."""
size_part = '' if size is None else f'-s {size}'
@@ -572,7 +572,7 @@ def break_write_expression(expression: str, size=None):
@REGISTRY.method(action='break_access')
def break_access_range(process: sch.Schema('Process'), range: AddressRange):
- """Set an access watchpoint."""
+ """Set a read/write watchpoint."""
proc = find_proc_by_obj(process)
offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min))
@@ -581,9 +581,9 @@ def break_access_range(process: sch.Schema('Process'), range: AddressRange):
f'watchpoint set expression -s {sz} -w read_write -- {offset_start}')
-@REGISTRY.method(action='break_access')
+@REGISTRY.method(action='break_ext', display='Set Read/Write Watchpoint')
def break_access_expression(expression: str, size=None):
- """Set an access watchpoint."""
+ """Set a read/write watchpoint."""
size_part = '' if size is None else f'-s {size}'
exec_convert_errors(
f'watchpoint set expression {size_part} -w read_write -- {expression}')
@@ -595,7 +595,7 @@ def break_exception(lang: str):
exec_convert_errors(f'breakpoint set -E {lang}')
-@REGISTRY.method(action='toggle')
+@REGISTRY.method(action='toggle', display='Toggle Watchpoint')
def toggle_watchpoint(watchpoint: sch.Schema('WatchpointSpec'), enabled: bool):
"""Toggle a watchpoint."""
wpt = find_wpt_by_obj(watchpoint)
@@ -604,7 +604,7 @@ def toggle_watchpoint(watchpoint: sch.Schema('WatchpointSpec'), enabled: bool):
exec_convert_errors(f'watchpoint {cmd} {wpt.GetID()}')
-@REGISTRY.method(action='toggle')
+@REGISTRY.method(action='toggle', display='Toggle Breakpoint')
def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool):
"""Toggle a breakpoint."""
bpt = find_bpt_by_obj(breakpoint)
@@ -612,7 +612,7 @@ def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool):
exec_convert_errors(f'breakpoint {cmd} {bpt.GetID()}')
-@REGISTRY.method(action='toggle')
+@REGISTRY.method(action='toggle', display='Toggle Breakpoint Location')
def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool):
"""Toggle a breakpoint location."""
bptnum, locnum = find_bptlocnum_by_obj(location)
@@ -620,7 +620,7 @@ def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabl
exec_convert_errors(f'breakpoint {cmd} {bptnum}.{locnum}')
-@REGISTRY.method(action='delete')
+@REGISTRY.method(action='delete', display='Delete Watchpoint')
def delete_watchpoint(watchpoint: sch.Schema('WatchpointSpec')):
"""Delete a watchpoint."""
wpt = find_wpt_by_obj(watchpoint)
@@ -628,7 +628,7 @@ def delete_watchpoint(watchpoint: sch.Schema('WatchpointSpec')):
exec_convert_errors(f'watchpoint delete {wptnum}')
-@REGISTRY.method(action='delete')
+@REGISTRY.method(action='delete', display='Delete Breakpoint')
def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')):
"""Delete a breakpoint."""
bpt = find_bpt_by_obj(breakpoint)
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 eecb36a0ef..214c4631e1 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
@@ -4,9 +4,9 @@
* 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.
@@ -15,8 +15,16 @@
*/
package ghidra.debug.api.target;
-import java.util.HashMap;
-import java.util.Map;
+import java.awt.event.InputEvent;
+import java.util.*;
+
+import javax.swing.Icon;
+
+import docking.ActionContext;
+import generic.theme.GIcon;
+import ghidra.app.context.ProgramLocationActionContext;
+import ghidra.trace.model.TraceExecutionState;
+import ghidra.trace.model.target.TraceObject;
/**
* A name for a commonly-recognized target action.
@@ -35,21 +43,137 @@ import java.util.Map;
* the UI to decide what is presented where.
*
* @param name the name of the action (given as the action attribute on method annotations)
- * @param builtIn true if the action should not be presented in generic contexts, but
- * reserved for built-in, purpose-specific actions
+ * @param show when to show the general UI action for this debugger action
+ * @param enabler determines when the action is enabled, based on the object
+ * @param display the default text to display
+ * @param icon the default icon for menus and dialogs
+ * @param okText the default text for confirm buttons in dialogs
*/
-public record ActionName(String name, boolean builtIn) {
+public record ActionName(String name, Show show, Enabler enabler, String display, Icon icon,
+ String okText) {
+
+ private static final Icon ICON_ATTACH = new GIcon("icon.debugger.attach");
+ private static final Icon ICON_CONNECT = new GIcon("icon.debugger.connect");
+ private static final Icon ICON_DETACH = new GIcon("icon.debugger.detach");
+ private static final Icon ICON_INTERRUPT = new GIcon("icon.debugger.interrupt");
+ private static final Icon ICON_KILL = new GIcon("icon.debugger.kill");
+ private static final Icon ICON_LAUNCH = new GIcon("icon.debugger.launch");
+ private static final Icon ICON_REFRESH = new GIcon("icon.debugger.refresh");
+ private static final Icon ICON_RESUME = new GIcon("icon.debugger.resume");
+ private static final Icon ICON_STEP_BACK = new GIcon("icon.debugger.step.back");
+ private static final Icon ICON_STEP_INTO = new GIcon("icon.debugger.step.into");
+ private static final Icon ICON_STEP_LAST = new GIcon("icon.debugger.step.last");
+ private static final Icon ICON_STEP_OUT = new GIcon("icon.debugger.step.finish");
+ private static final Icon ICON_STEP_OVER = new GIcon("icon.debugger.step.over");
+ private static final Icon ICON_SKIP_OVER = new GIcon("icon.debugger.skip.over");
+ private static final Icon ICON_SET_BREAKPOINT = new GIcon("icon.debugger.breakpoint.set");
+
private static final Map NAMES = new HashMap<>();
+ /**
+ * Specifies when an action should appear in the menus. For diagnostics, a user may override
+ * this by holding SHIFT when right-clicking, causing all applicable general actions to appear.
+ */
+ public enum Show {
+ /**
+ * Don't show general actions. The tool has built-in actions that already know how to invoke
+ * this.
+ */
+ BUILTIN {
+ @Override
+ boolean doIsShowing(ActionContext context) {
+ return false;
+ }
+ },
+ /**
+ * Only show general actions in address-based context, e.g., when right-clicking in the
+ * listing.
+ */
+ ADDRESS {
+ @Override
+ boolean doIsShowing(ActionContext context) {
+ return context instanceof ProgramLocationActionContext;
+ }
+ },
+ /**
+ * Show in all contexts. This is the default.
+ */
+ EXTENDED {
+ @Override
+ boolean doIsShowing(ActionContext context) {
+ return true;
+ }
+ };
+
+ public boolean isShowing(ActionContext context) {
+ if (isOverriden(context)) {
+ return true;
+ }
+ return doIsShowing(context);
+ }
+
+ abstract boolean doIsShowing(ActionContext context);
+
+ private boolean isOverriden(ActionContext context) {
+ return (context.getEventClickModifiers() & InputEvent.SHIFT_DOWN_MASK) != 0;
+ }
+ }
+
+ public enum Enabler {
+ ALWAYS {
+ @Override
+ public boolean isEnabled(TraceObject obj, long snap) {
+ return true;
+ }
+ },
+ NOT_RUNNING {
+ @Override
+ boolean doIsEnabled(TraceExecutionState state) {
+ return state != null && state != TraceExecutionState.RUNNING;
+ }
+ },
+ NOT_STOPPED {
+ @Override
+ boolean doIsEnabled(TraceExecutionState state) {
+ return state != TraceExecutionState.STOPPED;
+ }
+ },
+ NOT_DEAD {
+ @Override
+ boolean doIsEnabled(TraceExecutionState state) {
+ return state != TraceExecutionState.TERMINATED;
+ }
+ };
+
+ private TraceExecutionState getState(TraceObject obj, long snap) {
+ try {
+ return obj.getExecutionState(snap);
+ }
+ catch (NoSuchElementException e) {
+ return TraceExecutionState.TERMINATED;
+ }
+ }
+
+ boolean doIsEnabled(TraceExecutionState state) {
+ return true;
+ }
+
+ public boolean isEnabled(TraceObject obj, long snap) {
+ return doIsEnabled(getState(obj, snap));
+ }
+ }
+
public static ActionName name(String name) {
synchronized (NAMES) {
- return NAMES.computeIfAbsent(name, n -> new ActionName(n, false));
+ return NAMES.computeIfAbsent(name,
+ n -> new ActionName(n, Show.EXTENDED, Enabler.ALWAYS, n, null, "OK"));
}
}
- private static ActionName builtIn(String name) {
+ private static ActionName create(String name, Show show, Enabler enabler, String display,
+ Icon icon, String okText) {
synchronized (NAMES) {
- ActionName action = new ActionName(name, true);
+ ActionName action = new ActionName(name, show, enabler, display, icon, okText);
if (NAMES.put(name, action) != null) {
throw new AssertionError();
}
@@ -57,24 +181,16 @@ public record ActionName(String name, boolean builtIn) {
}
}
- 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 = extended("refresh");
+ public static final ActionName REFRESH =
+ create("refresh", Show.EXTENDED, Enabler.ALWAYS, "Refresh", ICON_REFRESH, "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 = builtIn("activate");
+ public static final ActionName ACTIVATE =
+ create("activate", Show.BUILTIN, Enabler.ALWAYS, "Activate", null, "Activate");
/**
* A weaker form of activate.
*
@@ -83,9 +199,12 @@ public record ActionName(String name, boolean builtIn) {
* 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 = builtIn("focus");
- public static final ActionName TOGGLE = builtIn("toggle");
- public static final ActionName DELETE = builtIn("delete");
+ public static final ActionName FOCUS =
+ create("focus", Show.BUILTIN, Enabler.ALWAYS, "Focus", null, "Focus");
+ public static final ActionName TOGGLE =
+ create("toggle", Show.BUILTIN, Enabler.ALWAYS, "Toggle", null, "Toggle");
+ public static final ActionName DELETE =
+ create("delete", Show.BUILTIN, Enabler.ALWAYS, "Delete", null, "Delete");
/**
* Execute a CLI command
@@ -93,7 +212,8 @@ public record ActionName(String name, boolean builtIn) {
*
* Forms: (cmd:STRING):STRING; Optional arguments: capture:BOOL
*/
- public static final ActionName EXECUTE = builtIn("execute");
+ public static final ActionName EXECUTE =
+ create("execute", Show.BUILTIN, Enabler.ALWAYS, "Execute", null, "Execute");
/**
* Connect the back-end to a (usually remote) target
@@ -101,23 +221,31 @@ public record ActionName(String name, boolean builtIn) {
*
* Forms: (spec:STRING)
*/
- public static final ActionName CONNECT = extended("connect");
+ public static final ActionName CONNECT =
+ create("connect", Show.EXTENDED, Enabler.ALWAYS, "Connect", ICON_CONNECT, "Connect");
/**
* Forms: (target:Attachable), (pid:INT), (spec:STRING)
*/
- public static final ActionName ATTACH = extended("attach");
- public static final ActionName DETACH = extended("detach");
+ public static final ActionName ATTACH =
+ create("attach", Show.EXTENDED, Enabler.ALWAYS, "Attach", ICON_ATTACH, "Attach");
+ public static final ActionName DETACH =
+ create("detach", Show.EXTENDED, Enabler.ALWAYS, "Detach", ICON_DETACH, "Detach");
/**
* Forms: (command_line:STRING), (file:STRING,args:STRING), (file:STRING,args:STRING_ARRAY),
* (ANY*)
*/
- public static final ActionName LAUNCH = extended("launch");
- public static final ActionName KILL = builtIn("kill");
+ public static final ActionName LAUNCH =
+ create("launch", Show.EXTENDED, Enabler.ALWAYS, "Launch", ICON_LAUNCH, "Launch");
+ public static final ActionName KILL =
+ create("kill", Show.BUILTIN, Enabler.NOT_DEAD, "Kill", ICON_KILL, "Kill");
- public static final ActionName RESUME = builtIn("resume");
- public static final ActionName INTERRUPT = builtIn("interrupt");
+ public static final ActionName RESUME =
+ create("resume", Show.BUILTIN, Enabler.NOT_RUNNING, "Resume", ICON_RESUME, "Resume");
+ public static final ActionName INTERRUPT =
+ create("interrupt", Show.BUILTIN, Enabler.NOT_STOPPED, "Interrupt", ICON_INTERRUPT,
+ "Interrupt");
/**
* All of these will show in the "step" portion of the control toolbar, if present. The
@@ -128,25 +256,31 @@ public record ActionName(String name, boolean builtIn) {
* 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 = builtIn("step_into");
- public static final ActionName STEP_OVER = builtIn("step_over");
- public static final ActionName STEP_OUT = builtIn("step_out");
+ public static final ActionName STEP_INTO =
+ create("step_into", Show.BUILTIN, Enabler.NOT_RUNNING, "Step Into", ICON_STEP_INTO, "Step");
+ public static final ActionName STEP_OVER =
+ create("step_over", Show.BUILTIN, Enabler.NOT_RUNNING, "Step Over", ICON_STEP_OVER, "Step");
+ public static final ActionName STEP_OUT =
+ create("step_out", Show.BUILTIN, Enabler.NOT_RUNNING, "Step Out", ICON_STEP_OUT, "Step");
/**
* 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 = builtIn("step_skip");
+ public static final ActionName STEP_SKIP =
+ create("step_skip", Show.BUILTIN, Enabler.NOT_RUNNING, "Skip Over", ICON_SKIP_OVER, "Skip");
/**
* Step back is not typically available, except in emulators and timeless (or time-travel)
* debuggers.
*/
- public static final ActionName STEP_BACK = builtIn("step_back");
+ public static final ActionName STEP_BACK =
+ create("step_back", Show.BUILTIN, Enabler.NOT_RUNNING, "Step Back", ICON_STEP_BACK, "Back");
/**
* The action for steps that don't fit one of the common stepping actions.
*/
- public static final ActionName STEP_EXT = extended("step_ext");
+ public static final ActionName STEP_EXT =
+ create("step_ext", Show.ADDRESS, Enabler.NOT_RUNNING, null, ICON_STEP_LAST, "Step");
/**
* Forms: (addr:ADDRESS), R/W(rng:RANGE), (expr:STRING)
@@ -158,25 +292,39 @@ public record ActionName(String name, boolean builtIn) {
* 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 = 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");
+ public static final ActionName BREAK_SW_EXECUTE =
+ create("break_sw_execute", Show.BUILTIN, Enabler.ALWAYS, "Set Software Breakpoint",
+ ICON_SET_BREAKPOINT, "Set");
+ public static final ActionName BREAK_HW_EXECUTE =
+ create("break_hw_execute", Show.BUILTIN, Enabler.ALWAYS, "Set Hardware Breakpoint",
+ ICON_SET_BREAKPOINT, "Set");
+ public static final ActionName BREAK_READ =
+ create("break_read", Show.BUILTIN, Enabler.ALWAYS, "Set Read Breakpoint",
+ ICON_SET_BREAKPOINT, "Set");
+ public static final ActionName BREAK_WRITE =
+ create("break_write", Show.BUILTIN, Enabler.ALWAYS, "Set Write Breakpoint",
+ ICON_SET_BREAKPOINT, "Set");
+ public static final ActionName BREAK_ACCESS =
+ create("break_access", Show.BUILTIN, Enabler.ALWAYS, "Set Access Breakpont",
+ ICON_SET_BREAKPOINT, "Set");
+ public static final ActionName BREAK_EXT =
+ create("break_ext", Show.BUILTIN, Enabler.ALWAYS, null, ICON_SET_BREAKPOINT, "Set");
/**
* Forms: (rng:RANGE)
*/
- public static final ActionName READ_MEM = builtIn("read_mem");
+ public static final ActionName READ_MEM =
+ create("read_mem", Show.BUILTIN, Enabler.ALWAYS, "Read Memory", null, "Read");
/**
* Forms: (addr:ADDRESS,data:BYTES)
*/
- public static final ActionName WRITE_MEM = builtIn("write_mem");
+ public static final ActionName WRITE_MEM =
+ create("write_mem", Show.BUILTIN, Enabler.ALWAYS, "Write Memory", null, "Write");
// 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 = builtIn("write_reg");
+ public static final ActionName WRITE_REG =
+ create("write_reg", Show.BUILTIN, Enabler.NOT_RUNNING, "Write Register", null, "Write");
}
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 133f186892..4f6eb5e6cd 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
@@ -18,16 +18,17 @@ package ghidra.debug.api.target;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
-import java.util.function.BooleanSupplier;
-import java.util.function.Function;
+
+import javax.swing.Icon;
import docking.ActionContext;
+import ghidra.debug.api.target.ActionName.Show;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.Register;
import ghidra.program.model.lang.RegisterValue;
-import ghidra.trace.model.TraceExecutionState;
import ghidra.trace.model.Trace;
+import ghidra.trace.model.TraceExecutionState;
import ghidra.trace.model.breakpoint.TraceBreakpoint;
import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.trace.model.guest.TracePlatform;
@@ -58,28 +59,72 @@ public interface Target {
* 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 specificity a relative score of specificity. These are only meaningful when compared
- * among entries returned in the same collection.
- * @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,
- long specificity, BooleanSupplier enabled,
- Function> action) {
+ interface ActionEntry {
+
+ /**
+ * Get the text to display on UI actions associated with this entry
+ *
+ * @return the display
+ */
+ String display();
+
+ /**
+ * Get the name of a common debugger command this action implements
+ *
+ * @return the name
+ */
+ ActionName name();
+
+ /**
+ * Get the icon to display in menus and dialogs
+ *
+ * @return the icon
+ */
+ Icon icon();
+
+ /**
+ * Get the text providing more details, usually displayed in a tool tip
+ *
+ * @return the details
+ */
+ String details();
+
+ /**
+ * Check whether invoking the action requires further user interaction
+ *
+ * @return true if prompting is required
+ */
+ boolean requiresPrompt();
+
+ /**
+ * Get a relative score of specificity.
+ *
+ *
+ * These are only meaningful when compared among entries returned in the same collection.
+ *
+ * @return the specificity
+ */
+ long specificity();
+
+ /**
+ * Invoke the action asynchronously, prompting if desired.
+ *
+ *
+ * The implementation is not required to provide a timeout; however, downstream components
+ * may.
+ *
+ * @param prompt whether or not to prompt the user for arguments
+ * @return the future result, often {@link Void}
+ */
+ CompletableFuture> invokeAsyncWithoutTimeout(boolean prompt);
/**
* Check if this action is currently enabled
*
* @return true if enabled
*/
- public boolean isEnabled() {
- return enabled.getAsBoolean();
- }
+ boolean isEnabled();
/**
* Invoke the action asynchronously, prompting if desired
@@ -90,8 +135,9 @@ public interface Target {
* @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.apply(prompt).orTimeout(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ default CompletableFuture> invokeAsync(boolean prompt) {
+ return invokeAsyncWithoutTimeout(prompt).orTimeout(TIMEOUT_MILLIS,
+ TimeUnit.MILLISECONDS);
}
/**
@@ -103,7 +149,7 @@ public interface Target {
*
* @param prompt whether or not to prompt the user for arguments
*/
- public void run(boolean prompt) {
+ default void run(boolean prompt) {
get(prompt);
}
@@ -113,7 +159,7 @@ public interface Target {
* @param prompt whether or not to prompt the user for arguments
* @return the resulting value, if applicable
*/
- public Object get(boolean prompt) {
+ default Object get(boolean prompt) {
if (Swing.isSwingThread()) {
throw new AssertionError("Refusing to block the Swing thread. Use a Task.");
}
@@ -130,8 +176,8 @@ public interface Target {
*
* @return true if built in.
*/
- public boolean builtIn() {
- return name != null && name.builtIn();
+ default Show getShow() {
+ return name() == null ? Show.EXTENDED : name().show();
}
}
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/RemoteMethod.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/RemoteMethod.java
index 4838df5716..e4325badd7 100644
--- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/RemoteMethod.java
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/RemoteMethod.java
@@ -20,6 +20,8 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
+import javax.swing.Icon;
+
import ghidra.async.AsyncUtils;
import ghidra.debug.api.target.ActionName;
import ghidra.trace.model.Trace;
@@ -62,6 +64,20 @@ public interface RemoteMethod {
*/
String display();
+ /**
+ * The icon to display in menu's and in the prompt dialog.
+ *
+ * @return the icon
+ */
+ Icon icon();
+
+ /**
+ * Text to display in the OK button of any prompt dialog.
+ *
+ * @return the text
+ */
+ String okText();
+
/**
* A description of the method.
*
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java
index d36d03eab5..f72685d120 100644
--- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java
@@ -49,7 +49,8 @@ import ghidra.trace.model.guest.TracePlatform;
import ghidra.trace.model.memory.TraceMemoryOperations;
import ghidra.trace.model.memory.TraceMemorySpace;
import ghidra.trace.model.program.TraceProgramView;
-import ghidra.trace.model.target.*;
+import ghidra.trace.model.target.TraceObject;
+import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.target.path.KeyPath;
import ghidra.trace.model.thread.TraceObjectThread;
import ghidra.trace.model.thread.TraceThread;
@@ -1740,7 +1741,7 @@ public interface FlatDebuggerAPI {
* @return true if alive
*/
default boolean isTargetAlive(Trace trace) {
- return getExecutionState(trace).isAlive();
+ return getExecutionState(trace) != TraceExecutionState.TERMINATED;
}
/**
@@ -1763,7 +1764,7 @@ public interface FlatDebuggerAPI {
* @return true if alive
*/
default boolean isThreadAlive(TraceThread thread) {
- return getExecutionState(thread).isAlive();
+ return getExecutionState(thread) != TraceExecutionState.TERMINATED;
}
/**
@@ -1792,7 +1793,7 @@ public interface FlatDebuggerAPI {
* @throws TimeoutException if the timeout expires
*/
default void waitForBreak(Trace trace, long timeout, TimeUnit unit) throws TimeoutException {
- if (!getExecutionState(trace).isRunning()) {
+ if (getExecutionState(trace) != TraceExecutionState.RUNNING) {
return;
}
var listener = new DomainObjectListener() {
@@ -1800,14 +1801,14 @@ public interface FlatDebuggerAPI {
@Override
public void domainObjectChanged(DomainObjectChangedEvent ev) {
- if (!getExecutionState(trace).isRunning()) {
+ if (getExecutionState(trace) != TraceExecutionState.RUNNING) {
future.complete(null);
}
}
};
trace.addListener(listener);
try {
- if (!getExecutionState(trace).isRunning()) {
+ if (getExecutionState(trace) != TraceExecutionState.RUNNING) {
return;
}
listener.future.get(timeout, unit);
diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/rmi/jpda/JdiConnector.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/rmi/jpda/JdiConnector.java
index c7fce87c50..7b85272d16 100644
--- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/rmi/jpda/JdiConnector.java
+++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/rmi/jpda/JdiConnector.java
@@ -144,20 +144,10 @@ public class JdiConnector {
}
public void registerRemoteMethod(JdiMethods methods, java.lang.reflect.Method m, String name) {
- String action = name;
- String display = name;
- String description = name;
TraceMethod annot = m.getAnnotation(TraceMethod.class);
if (annot == null) {
return;
}
- action = annot.action();
- if (annot.display() != null) {
- display = annot.display();
- }
- if (annot.description() != null) {
- description = annot.description();
- }
int pcount = m.getParameterCount();
if (pcount < 1) {
return;
@@ -167,8 +157,8 @@ public class JdiConnector {
* collection routines currently use the return type, so just use ANY for now.
*/
TraceObjectSchema schema = PrimitiveTraceObjectSchema.ANY;
- RmiRemoteMethod method = new RmiRemoteMethod(rootSchema.getContext(), name, action, display,
- description, schema, methods, m);
+ RmiRemoteMethod method = new RmiRemoteMethod(rootSchema.getContext(), name, annot.action(),
+ annot.display(), annot.description(), annot.okText(), annot.icon(), schema, methods, m);
remoteMethodRegistry.putMethod(name, method);
}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiClient.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiClient.java
index 405b35c3fc..c9fada1ef8 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiClient.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiClient.java
@@ -605,9 +605,11 @@ public class RmiClient {
private Method buildMethod(RmiRemoteMethod method) {
Method.Builder builder = Method.newBuilder()
.setName(method.getName())
- .setDescription(method.getDescription())
.setAction(method.getAction())
- .setDisplay(method.getDisplay());
+ .setDisplay(method.getDisplay())
+ .setDescription(method.getDescription())
+ .setOkText(method.getOkText())
+ .setIcon(method.getIcon());
int i = 0;
for (RmiRemoteMethodParameter p : method.getParameters()) {
MethodParameter param = buildParameter(p);
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiMethodRegistry.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiMethodRegistry.java
index c00d65f37a..a929e0b578 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiMethodRegistry.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiMethodRegistry.java
@@ -32,6 +32,10 @@ public class RmiMethodRegistry {
String display() default "";
String description() default "";
+
+ String okText() default "";
+
+ String icon() default "";
}
Map map = new HashMap<>();
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiRemoteMethod.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiRemoteMethod.java
index 54e1034c35..f7473316e0 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiRemoteMethod.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/client/tracermi/RmiRemoteMethod.java
@@ -25,22 +25,27 @@ import ghidra.trace.model.target.schema.TraceObjectSchema.SchemaName;
public class RmiRemoteMethod {
private final SchemaContext schemaContext;
- private String name;
- private String action;
- private String display;
- private String description;
- private RmiRemoteMethodParameter[] params;
- private TraceObjectSchema schema;
- private RmiMethods instance;
- private Method m;
+ private final String name;
+ private final String action;
+ private final String display;
+ private final String description;
+ private final String okText;
+ private final String icon;
+ private final RmiRemoteMethodParameter[] params;
+ private final TraceObjectSchema schema;
+ private final RmiMethods instance;
+ private final Method m;
public RmiRemoteMethod(SchemaContext schemaContext, String name, String action, String display,
- String description, TraceObjectSchema schema, RmiMethods instance, Method m) {
+ String description, String okText, String icon, TraceObjectSchema schema,
+ RmiMethods instance, Method m) {
this.schemaContext = schemaContext;
this.name = name;
this.action = action;
this.display = display;
this.description = description;
+ this.okText = okText;
+ this.icon = icon;
this.params = new RmiRemoteMethodParameter[m.getParameterCount()];
this.schema = schema;
this.instance = instance;
@@ -69,6 +74,14 @@ public class RmiRemoteMethod {
return description;
}
+ public String getOkText() {
+ return okText;
+ }
+
+ public String getIcon() {
+ return icon;
+ }
+
public String getAction() {
return action;
}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/RecordRemoteMethod.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/RecordRemoteMethod.java
index ae57681c30..7742df15df 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/RecordRemoteMethod.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/RecordRemoteMethod.java
@@ -17,14 +17,33 @@ package ghidra.app.plugin.core.debug.service.tracermi;
import java.util.Map;
+import javax.swing.Icon;
+
import ghidra.debug.api.target.ActionName;
-import ghidra.debug.api.tracermi.*;
+import ghidra.debug.api.tracermi.RemoteMethod;
+import ghidra.debug.api.tracermi.RemoteParameter;
import ghidra.trace.model.Trace;
import ghidra.trace.model.target.schema.TraceObjectSchema.SchemaName;
public record RecordRemoteMethod(TraceRmiHandler handler, String name, ActionName action,
- String display, String description, Map parameters,
- SchemaName retType) implements RemoteMethod {
+ String display, Icon icon, String okText, String description,
+ Map parameters, SchemaName retType) implements RemoteMethod {
+
+ public RecordRemoteMethod {
+ if (display == null || display.isBlank()) {
+ display = action.display();
+ }
+ if (display == null || display.isBlank()) { // still
+ display = name;
+ }
+ if (icon == null) {
+ icon = action.icon();
+ }
+ if (okText.isEmpty() || okText.isBlank()) {
+ okText = action.okText();
+ }
+ }
+
@Override
public DefaultRemoteAsyncResult invokeAsync(Map arguments) {
Trace trace = validate(arguments);
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java
index bb2d959c9b..8ffb81edee 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java
@@ -31,6 +31,7 @@ import org.apache.commons.lang3.ArrayUtils;
import com.google.protobuf.ByteString;
import db.Transaction;
+import generic.theme.GIcon;
import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin;
import ghidra.app.plugin.core.debug.disassemble.TraceDisassembleCommand;
import ghidra.app.services.DebuggerControlService;
@@ -1027,7 +1028,9 @@ public class TraceRmiHandler extends AbstractTraceRmiConnection {
}
for (Method m : req.getMethodsList()) {
RemoteMethod rm = new RecordRemoteMethod(this, m.getName(),
- ActionName.name(m.getAction()), m.getDisplay(), m.getDescription(),
+ ActionName.name(m.getAction()), m.getDisplay(),
+ m.getIcon().isBlank() ? null : new GIcon(m.getIcon()), m.getOkText(),
+ m.getDescription(),
m.getParametersList()
.stream()
.collect(Collectors.toMap(MethodParameter::getName, this::makeParameter)),
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java
index 5c91394553..327bea8d65 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java
@@ -19,9 +19,11 @@ import java.io.IOException;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.CompletableFuture;
-import java.util.function.*;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
+import javax.swing.Icon;
+
import org.apache.commons.lang3.exception.ExceptionUtils;
import docking.ActionContext;
@@ -60,6 +62,70 @@ import ghidra.util.Msg;
import ghidra.util.task.TaskMonitor;
public class TraceRmiTarget extends AbstractTarget {
+
+ class TraceRmiActionEntry implements ActionEntry {
+ private final RemoteMethod method;
+ private final Map args;
+ private final boolean requiresPrompt;
+ private final long specificity;
+ private final ParamAndObjectArg first;
+
+ public TraceRmiActionEntry(RemoteMethod method, Map args) {
+ this.method = method;
+ this.args = args;
+
+ this.requiresPrompt = args.values().contains(Missing.MISSING);
+ this.specificity = computeSpecificity(args);
+ this.first = getFirstObjectArgument(method, args);
+ }
+
+ @Override
+ public String display() {
+ return method.display();
+ }
+
+ @Override
+ public ActionName name() {
+ return method.action();
+ }
+
+ @Override
+ public Icon icon() {
+ return method.icon();
+ }
+
+ @Override
+ public String details() {
+ return method.description();
+ }
+
+ @Override
+ public boolean requiresPrompt() {
+ return requiresPrompt;
+ }
+
+ @Override
+ public long specificity() {
+ return specificity;
+ }
+
+ @Override
+ public CompletableFuture> invokeAsyncWithoutTimeout(boolean prompt) {
+ return invokeMethod(prompt, method, args);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ if (first == null) {
+ return true;
+ }
+ if (first.obj == null) {
+ return false;
+ }
+ return name().enabler().isEnabled(first.obj, getSnap());
+ }
+ }
+
private final TraceRmiConnection connection;
private final Trace trace;
@@ -224,8 +290,12 @@ public class TraceRmiTarget extends AbstractTarget {
return null;
}
+ /**
+ * A singleton to indicate missing arguments
+ */
public enum Missing {
- MISSING; // The argument requires a prompt
+ /** The argument requires a prompt */
+ MISSING;
}
protected Object findArgument(ActionName action, RemoteParameter parameter,
@@ -266,28 +336,7 @@ public class TraceRmiTarget extends AbstractTarget {
return args;
}
- private TraceExecutionState getStateOf(TraceObject object) {
- try {
- return object.getExecutionState(getSnap());
- }
- catch (NoSuchElementException e) {
- return TraceExecutionState.TERMINATED;
- }
- }
-
- private boolean whenState(TraceObject object,
- Predicate predicate) {
- try {
- TraceExecutionState state = getStateOf(object);
- return state == null || predicate.test(state);
- }
- catch (Exception e) {
- Msg.error(this, "Could not get state: " + e);
- return false;
- }
- }
-
- protected long computeSpecificity(Map args) {
+ protected static long computeSpecificity(Map args) {
long score = 0;
for (Object o : args.values()) {
if (o instanceof TraceObject obj) {
@@ -297,46 +346,34 @@ public class TraceRmiTarget extends AbstractTarget {
return score;
}
- protected BooleanSupplier chooseEnabler(RemoteMethod method, Map args) {
- ActionName name = method.action();
+ protected RemoteParameter getFirstObjectParameter(RemoteMethod method) {
SchemaContext ctx = getSchemaContext();
if (ctx == null) {
- return () -> true;
+ return null;
}
- RemoteParameter firstParam = method.parameters()
+ return method.parameters()
.values()
.stream()
.filter(
p -> TraceObjectInterfaceUtils.isTraceObject(ctx.getSchema(p.type()).getType()))
.findFirst()
.orElse(null);
+ }
+
+ record ParamAndObjectArg(RemoteParameter param, TraceObject obj) {}
+
+ protected ParamAndObjectArg getFirstObjectArgument(RemoteMethod method,
+ Map args) {
+ RemoteParameter firstParam = getFirstObjectParameter(method);
if (firstParam == null) {
- return () -> true;
+ return null;
}
Object firstArg = args.get(firstParam.name());
- if (firstArg == null || firstArg == Missing.MISSING) {
+ if (firstArg == null || !(firstArg instanceof TraceObject obj)) {
Msg.trace(this, "MISSING first argument for " + method + "(" + firstParam + ")");
- return () -> false;
+ return new ParamAndObjectArg(firstParam, null);
}
- TraceObject obj = (TraceObject) firstArg;
- if (ActionName.RESUME.equals(name) ||
- ActionName.STEP_BACK.equals(name) ||
- ActionName.STEP_EXT.equals(name) ||
- ActionName.STEP_INTO.equals(name) ||
- ActionName.STEP_OUT.equals(name) ||
- ActionName.STEP_OVER.equals(name) ||
- ActionName.STEP_SKIP.equals(name)) {
- return () -> whenState(obj,
- state -> state != null && (state.isStopped() || state.isUnknown()));
- }
- else if (ActionName.INTERRUPT.equals(name)) {
- return () -> whenState(obj,
- state -> state == null || state.isRunning() || state.isUnknown());
- }
- else if (ActionName.KILL.equals(name)) {
- return () -> whenState(obj, state -> state == null || !state.isTerminated());
- }
- return () -> true;
+ return new ParamAndObjectArg(firstParam, obj);
}
private Map promptArgs(RemoteMethod method, Map defaults) {
@@ -346,7 +383,7 @@ public class TraceRmiTarget extends AbstractTarget {
*/
Map> defs = ValStr.fromPlainMap(defaults);
RemoteMethodInvocationDialog dialog = new RemoteMethodInvocationDialog(tool,
- getSchemaContext(), method.display(), method.display(), null);
+ getSchemaContext(), method.display(), method.okText(), method.icon());
Map> args = dialog.promptArguments(method.parameters(), defs, defs);
return args == null ? null : ValStr.toPlainMap(args);
}
@@ -374,10 +411,7 @@ public class TraceRmiTarget extends AbstractTarget {
boolean allowContextObject, boolean allowCoordsObject, boolean allowSuitableObject) {
Map args = collectArguments(method, context, allowContextObject,
allowCoordsObject, allowSuitableObject);
- boolean requiresPrompt = args.values().contains(Missing.MISSING);
- return new ActionEntry(method.display(), method.action(), method.description(),
- requiresPrompt, computeSpecificity(args), chooseEnabler(method, args),
- prompt -> invokeMethod(prompt, method, args));
+ return new TraceRmiActionEntry(method, args);
}
protected Map collectFromMethods(Collection methods,
@@ -432,6 +466,17 @@ public class TraceRmiTarget extends AbstractTarget {
true);
}
+ @Override
+ public Map collectActions(ActionName name, ActionContext context) {
+ if (name == null) {
+ if (context instanceof ProgramLocationActionContext ctx) {
+ return collectAddressActions(ctx);
+ }
+ return collectAllActions(context);
+ }
+ return collectByName(name, context);
+ }
+
@Override
protected Map collectResumeActions(ActionContext context) {
return collectByName(ActionName.RESUME, context);
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/proto/trace-rmi.proto b/Ghidra/Debug/Debugger-rmi-trace/src/main/proto/trace-rmi.proto
index 0ff04e0fb1..f469723ea8 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/proto/trace-rmi.proto
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/proto/trace-rmi.proto
@@ -4,9 +4,9 @@
* 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.
@@ -423,6 +423,8 @@ message Method {
// I'd like to make them all void, but I think executing a command and capturing its output
// justifies being able to return a result. It should be used very sparingly.
ValueType return_type = 6;
+ string ok_text = 7;
+ string icon = 8;
}
message RequestNegotiate {
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py
index 6b3c2d10ac..40784b67a1 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/py/src/ghidratrace/client.py
@@ -27,10 +27,11 @@ from . import sch
from . import trace_rmi_pb2 as bufs
from .util import send_delimited, recv_delimited
+
# This need not be incremented every Ghidra release. When a breaking protocol
# change is made, this should be updated to match the first Ghidra release that
# includes the change.
-#
+#
# Other places to change:
# * every pyproject.toml file (incl. deps)
# * TraceRmiHandler.VERSION
@@ -98,7 +99,8 @@ class Receiver(Thread):
while not self._is_shutdown:
#print("Receiving message")
try:
- reply = recv_delimited(self.client.s, bufs.RootMessage(), dbg_seq)
+ reply = recv_delimited(
+ self.client.s, bufs.RootMessage(), dbg_seq)
except BaseException as e:
self._is_shutdown = True
return
@@ -448,6 +450,8 @@ class RemoteMethod:
name: str
action: str
display: str
+ icon: str
+ ok_text: str
description: str
parameters: List[RemoteParameter]
return_schema: sch.Schema
@@ -508,30 +512,28 @@ class MethodRegistry(object):
@classmethod
def create_method(cls, function, name=None, action=None, display=None,
- description=None) -> RemoteMethod:
+ icon=None, ok_text=None, description=None) -> RemoteMethod:
if name is None:
name = function.__name__
if action is None:
action = name
- if display is None:
- display = name
if description is None:
- description = function.__doc__ or ''
+ description = function.__doc__
sig = inspect.signature(function)
params = []
for p in sig.parameters.values():
params.append(cls._make_param(p))
return_schema = cls._to_schema(sig, sig.return_annotation)
- return RemoteMethod(name, action, display, description, params,
- return_schema, function)
+ return RemoteMethod(name, action, display, icon, ok_text, description,
+ params, return_schema, function)
def method(self, func=None, *, name=None, action=None, display=None,
- description='', condition=True):
+ icon=None, ok_text=None, description=None, condition=True):
def _method(func):
if condition:
method = self.create_method(func, name, action, display,
- description)
+ icon, ok_text, description)
self.register_method(method)
return func
@@ -705,8 +707,10 @@ class Client(object):
def _write_method(to: bufs.Method, method: RemoteMethod):
to.name = method.name
to.action = method.action
- to.display = method.display
- to.description = method.description
+ to.display = method.display or ''
+ to.icon = method.icon or ''
+ to.ok_text = method.ok_text or ''
+ to.description = method.description or ''
Client._write_parameters(to.parameters, method.parameters)
to.return_type.name = method.return_schema.name
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java
index 12901a480a..3ba6116ba0 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java
@@ -24,6 +24,8 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import javax.swing.Icon;
+
import db.Transaction;
import ghidra.app.services.DebuggerTargetService;
import ghidra.async.*;
@@ -56,6 +58,11 @@ public abstract class TestTraceRmiConnection extends AbstractTraceRmiConnection
String description, Map parameters, SchemaName retType,
AsyncPairingQueue
+
Set Breakpoint
+
+
This is a dropdown of actions provided by the back-end debugger, usually for setting
+ breakpoints by symbol, expression, etc. Setting breakpoints by address is typically done from
+ the Listings. If no such actions are available, or there is no live target, this action is
+ disabled.