diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py
index c88f74cbd8..a6997f76f7 100644
--- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py
+++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py
@@ -62,12 +62,15 @@ SECTION_ADD_PATTERN = SECTIONS_ADD_PATTERN + SECTION_KEY_PATTERN
# TODO: Symbols
+
class ErrorWithCode(Exception):
- def __init__(self,code):
+ def __init__(self, code):
self.code = code
+
def __str__(self)->str:
return repr(self.code)
+
class State(object):
def __init__(self):
@@ -114,7 +117,8 @@ class State(object):
STATE = State()
-
+
+
def ghidra_trace_connect(address=None):
"""
Connect Python to Ghidra for tracing
@@ -124,8 +128,9 @@ def ghidra_trace_connect(address=None):
STATE.require_no_client()
if address is None:
- raise RuntimeError("'ghidra_trace_connect': missing required argument 'address'")
-
+ raise RuntimeError(
+ "'ghidra_trace_connect': missing required argument 'address'")
+
parts = address.split(':')
if len(parts) != 2:
raise RuntimeError("address must be in the form 'host:port'")
@@ -133,11 +138,13 @@ def ghidra_trace_connect(address=None):
try:
c = socket.socket()
c.connect((host, int(port)))
- STATE.client = Client(c, methods.REGISTRY)
- except ValueError:
+ # TODO: Can we get version info from the DLL?
+ STATE.client = Client(c, "dbgeng.dll", methods.REGISTRY)
+ print(f"Connected to {STATE.client.description} at {address}")
+ except ValueError:
raise RuntimeError("port must be numeric")
-
+
def ghidra_trace_listen(address='0.0.0.0:0'):
"""
Listen for Ghidra to connect for tracing
@@ -243,7 +250,8 @@ def ghidra_trace_create(command=None, initial_break=True, timeout=None, start_tr
if timeout != None:
util.base._client.CreateProcess(command, DbgEng.DEBUG_PROCESS)
if initial_break:
- util.base._control.AddEngineOptions(DbgEng.DEBUG_ENGINITIAL_BREAK)
+ util.base._control.AddEngineOptions(
+ DbgEng.DEBUG_ENGINITIAL_BREAK)
util.base.wait(timeout)
else:
util.base.create(command, initial_break)
@@ -267,7 +275,7 @@ def ghidra_trace_info():
print("Not connected to Ghidra\n")
return
host, port = STATE.client.s.getpeername()
- print("Connected to Ghidra at {}:{}\n".format(host, port))
+ print(f"Connected to {STATE.client.description} at {host}:{port}\n")
if STATE.trace is None:
print("No trace\n")
return
@@ -363,7 +371,7 @@ def put_bytes(start, end, pages, display_result):
if end - start <= 0:
return {'count': 0}
buf = dbg().read(start, end - start)
-
+
count = 0
if buf != None:
base, addr = trace.memory_mapper.map(nproc, start)
@@ -405,7 +413,7 @@ def ghidra_trace_putmem(items):
address = items[0]
length = items[1]
pages = items[2] if len(items) > 2 else True
-
+
STATE.require_tx()
return putmem(address, length, pages, True)
@@ -418,7 +426,7 @@ def ghidra_trace_putval(items):
items = items.split(" ")
value = items[0]
pages = items[1] if len(items) > 1 else True
-
+
STATE.require_tx()
try:
start = util.parse_and_eval(value)
@@ -565,28 +573,29 @@ def ghidra_trace_remove_obj(path):
def to_bytes(value):
- return bytes(ord(value[i]) if type(value[i]) == str else int(value[i]) for i in range(0,len(value)))
+ return bytes(ord(value[i]) if type(value[i]) == str else int(value[i]) for i in range(0, len(value)))
def to_string(value, encoding):
- b = bytes(ord(value[i]) if type(value[i]) == str else int(value[i]) for i in range(0,len(value)))
+ b = bytes(ord(value[i]) if type(value[i]) == str else int(
+ value[i]) for i in range(0, len(value)))
return str(b, encoding)
def to_bool_list(value):
- return [bool(value[i]) for i in range(0,len(value))]
+ return [bool(value[i]) for i in range(0, len(value))]
def to_int_list(value):
- return [ord(value[i]) if type(value[i]) == str else int(value[i]) for i in range(0,len(value))]
+ return [ord(value[i]) if type(value[i]) == str else int(value[i]) for i in range(0, len(value))]
def to_short_list(value):
- return [ord(value[i]) if type(value[i]) == str else int(value[i]) for i in range(0,len(value))]
+ return [ord(value[i]) if type(value[i]) == str else int(value[i]) for i in range(0, len(value))]
def to_string_list(value, encoding):
- return [to_string(value[i], encoding) for i in range(0,len(value))]
+ return [to_string(value[i], encoding) for i in range(0, len(value))]
def eval_value(value, schema=None):
@@ -614,9 +623,9 @@ def eval_value(value, schema=None):
return to_string_list(value, 'utf-8'), schema
if schema == sch.CHAR_ARR:
return to_string(value, 'utf-8'), sch.CHAR_ARR
- if schema == sch.STRING:
+ if schema == sch.STRING:
return to_string(value, 'utf-8'), sch.STRING
-
+
return value, schema
@@ -796,7 +805,7 @@ def ghidra_trace_activate(path=None):
This has no effect if the current trace is not current in Ghidra. If path is
omitted, this will activate the current frame.
"""
-
+
activate(path)
@@ -822,7 +831,7 @@ def ghidra_trace_disassemble(address):
def compute_proc_state(nproc=None):
status = dbg()._control.GetExecutionStatus()
- if status == DbgEng.DEBUG_STATUS_BREAK:
+ if status == DbgEng.DEBUG_STATUS_BREAK:
return 'STOPPED'
return 'RUNNING'
@@ -841,13 +850,15 @@ def put_processes(running=False):
procobj.set_value('_state', istate)
if running == False:
procobj.set_value('_pid', p[0])
- pidstr = ('0x{:x}' if radix == 16 else '0{:o}' if radix == 8 else '{}').format(p[0])
+ pidstr = ('0x{:x}' if radix ==
+ 16 else '0{:o}' if radix == 8 else '{}').format(p[0])
procobj.set_value('_display', pidstr)
procobj.set_value('Name', str(p[1]))
procobj.set_value('PEB', hex(p[2]))
procobj.insert()
STATE.trace.proxy_object_path(PROCESSES_PATH).retain_values(keys)
+
def put_state(event_process):
STATE.require_no_tx()
STATE.tx = STATE.require_trace().start_tx("state", undoable=False)
@@ -873,10 +884,10 @@ def ghidra_trace_put_processes():
def put_available():
radix = util.get_convenience_variable('output-radix')
keys = []
- result = dbg().cmd(".tlist")
+ result = dbg().cmd(".tlist")
lines = result.split("\n")
for i in lines:
- i = i.strip();
+ i = i.strip()
if i == "":
continue
if i.startswith("0n") is False:
@@ -930,10 +941,10 @@ def put_single_breakpoint(bp, ibobj, nproc, ikeys):
if bp.GetType()[0] == DbgEng.DEBUG_BREAKPOINT_DATA:
width, prot = bp.GetDataParameters()
width = str(width)
- prot = {4: 'HW_EXECUTE', 2: 'READ', 1: 'WRITE'}[prot]
+ prot = {4: 'HW_EXECUTE', 2: 'READ', 1: 'WRITE'}[prot]
else:
width = ' '
- prot = 'SW_EXECUTE'
+ prot = 'SW_EXECUTE'
if address is not None: # Implies execution break
base, addr = mapper.map(nproc, address)
@@ -968,7 +979,6 @@ def put_single_breakpoint(bp, ibobj, nproc, ikeys):
ikeys.append(k)
-
def put_breakpoints():
target = util.get_target()
nproc = util.selected_process()
@@ -998,7 +1008,7 @@ def ghidra_trace_put_breakpoints():
STATE.require_tx()
with STATE.client.batch() as b:
put_breakpoints()
-
+
def put_environment():
epath = ENV_PATTERN.format(procnum=util.selected_process())
@@ -1039,9 +1049,12 @@ def put_regions():
if start_base != start_addr.space:
STATE.trace.create_overlay_space(start_base, start_addr.space)
regobj.set_value('_range', start_addr.extend(r.RegionSize))
- regobj.set_value('_readable', r.Protect == None or r.Protect&0x66 != 0)
- regobj.set_value('_writable', r.Protect == None or r.Protect&0xCC != 0)
- regobj.set_value('_executable', r.Protect == None or r.Protect&0xF0 != 0)
+ regobj.set_value('_readable', r.Protect ==
+ None or r.Protect & 0x66 != 0)
+ regobj.set_value('_writable', r.Protect ==
+ None or r.Protect & 0xCC != 0)
+ regobj.set_value('_executable', r.Protect ==
+ None or r.Protect & 0xF0 != 0)
regobj.set_value('_offset', hex(r.BaseAddress))
regobj.set_value('Base', hex(r.BaseAddress))
regobj.set_value('Size', hex(r.RegionSize))
@@ -1089,13 +1102,13 @@ def put_modules():
modobj.set_value('Size', hex(size))
modobj.set_value('Flags', hex(size))
modobj.insert()
-
- # TODO: would be nice to list sections, but currently we have no API for
+
+ # TODO: would be nice to list sections, but currently we have no API for
# it as far as I am aware
# sec_keys = []
# STATE.trace.proxy_object_path(
# mpath + SECTIONS_ADD_PATTERN).retain_values(sec_keys)
-
+
STATE.trace.proxy_object_path(MODULES_PATTERN.format(
procnum=nproc)).retain_values(mod_keys)
@@ -1146,7 +1159,7 @@ def put_threads(running=False):
tid = t[0]
tobj.set_value('_tid', tid)
tidstr = ('0x{:x}' if radix ==
- 16 else '0{:o}' if radix == 8 else '{}').format(tid)
+ 16 else '0{:o}' if radix == 8 else '{}').format(tid)
tobj.set_value('_short_display', '[{}.{}:{}]'.format(
nproc, i, tidstr))
tobj.set_value('_display', compute_thread_display(tidstr, t))
@@ -1201,7 +1214,8 @@ def put_frames():
fobj.set_value('StackOffset', hex(f.StackOffset))
fobj.set_value('ReturnOffset', hex(f.ReturnOffset))
fobj.set_value('FrameOffset', hex(f.FrameOffset))
- fobj.set_value('_display', "#{} {}".format(f.FrameNumber, hex(f.InstructionOffset)))
+ fobj.set_value('_display', "#{} {}".format(
+ f.FrameNumber, hex(f.InstructionOffset)))
fobj.insert()
STATE.trace.proxy_object_path(STACK_PATTERN.format(
procnum=nproc, tnum=nthrd)).retain_values(keys)
@@ -1326,7 +1340,7 @@ def repl():
dbg().wait()
else:
pass
- #dbg().dispatch_events()
+ # dbg().dispatch_events()
except KeyboardInterrupt as e:
print("")
print("You have left the dbgeng REPL and are now at the Python3 interpreter.")
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 d809d9e844..e218b87df6 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
@@ -201,7 +201,9 @@ def ghidra_trace_connect(address, *, is_mi, **kwargs):
try:
c = socket.socket()
c.connect((host, int(port)))
- STATE.client = Client(c, methods.REGISTRY)
+ STATE.client = Client(
+ c, "gdb-" + util.GDB_VERSION.full, methods.REGISTRY)
+ print(f"Connected to {STATE.client.description} at {address}")
except ValueError:
raise gdb.GdbError("port must be numeric")
@@ -320,9 +322,11 @@ def ghidra_trace_info(*, is_mi, **kwargs):
return
host, port = STATE.client.s.getpeername()
if is_mi:
- result['connection'] = "{}:{}".format(host, port)
+ result['description'] = STATE.client.description
+ result['address'] = f"{host}:{port}"
else:
- gdb.write("Connected to Ghidra at {}:{}\n".format(host, port))
+ gdb.write(
+ f"Connected to {STATE.client.description} at {host}:{port}\n")
if STATE.trace is None:
if is_mi:
result['tracing'] = False
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerConsoleService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerConsoleService.java
index 745e87bac2..27db228776 100644
--- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerConsoleService.java
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerConsoleService.java
@@ -21,12 +21,11 @@ import javax.swing.Icon;
import docking.ActionContext;
import docking.action.DockingActionIf;
-import ghidra.dbg.DebuggerConsoleLogger;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.util.HTMLUtilities;
@ServiceInfo(defaultProviderName = "ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin")
-public interface DebuggerConsoleService extends DebuggerConsoleLogger {
+public interface DebuggerConsoleService {
/**
* Log a message to the console
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java
index 450a7b1199..a78c3ab4ab 100644
--- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java
@@ -19,6 +19,7 @@ import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import ghidra.async.AsyncReference;
+import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.model.DomainFile;
import ghidra.framework.plugintool.ServiceInfo;
@@ -330,6 +331,24 @@ public interface DebuggerTraceManagerService {
activate(resolveTrace(trace));
}
+ /**
+ * Resolve coordinates for the given target using the manager's "best judgment"
+ *
+ * @see #resolveTrace(Trace)
+ * @param target the target
+ * @return the best coordinates
+ */
+ DebuggerCoordinates resolveTarget(Target target);
+
+ /**
+ * Activate the given target
+ *
+ * @param target the desired target
+ */
+ default void activateTarget(Target target) {
+ activate(resolveTarget(target));
+ }
+
/**
* Resolve coordinates for the given platform using the manager's "best judgment"
*
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java
new file mode 100644
index 0000000000..b99c776630
--- /dev/null
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java
@@ -0,0 +1,102 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.services;
+
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+
+import ghidra.debug.api.progress.*;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.framework.plugintool.ServiceInfo;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * A service for publishing and subscribing to tasks and progress notifications.
+ *
+ *
+ * This is an attempt to de-couple the concepts of task monitoring and task execution. The
+ * {@link PluginTool} has a system for submitting background tasks. It queues the task. When it
+ * reaches the front of the queue, it creates a {@link TaskMonitor}, starts a thread, and executes
+ * the task. Unfortunately, this tightly couples the progress reporting system with the execution
+ * model, which is not ideal. Moreover, the task queuing system is the only simple way to obtain a
+ * {@link TaskMonitor} with any semblance of central management or consistent presentation.
+ * Providers can (and often do) create their own {@link TaskMonitor}s, usually placed at the bottom
+ * of the provider when it is, e.g., updating a table.
+ *
+ *
+ * This service attempts to provide a centralized system for creating and presenting
+ * {@link TaskMonitor}s separate from the execution model. No particular execution model is
+ * required. Nor is the task implicitly associated to a specific thread. A client may use a single
+ * thread for all tasks, a single thread for each task, many threads for a task, etc. In fact, a
+ * client could even use an {@link ExecutorService}, without any care to how tasks are executed.
+ * Instead, a task need simply request a monitor, pass its handle as needed, and close it when
+ * finished. The information generated by such monitors is then forwarded to the subscriber which
+ * can determine how to present them.
+ */
+@ServiceInfo(
+ defaultProviderName = "ghidra.app.plugin.core.debug.service.progress.ProgressServicePlugin")
+public interface ProgressService {
+ /**
+ * Publish a task and create a monitor for it
+ *
+ *
+ * This and the methods on {@link TaskMonitor} are the mechanism for clients to publish task and
+ * progress information. The monitor returned also extends {@link AutoCloseable}, allowing it to
+ * be used fairly safely when the execution model involves a single thread.
+ *
+ *
+ * try (CloseableTaskMonitor monitor = progressService.publishTask()) {
+ * // Do the computation and update the monitor accordingly.
+ * }
+ *
+ *
+ *
+ * If the above idiom is not used, e.g., because the monitor is passed among several
+ * {@link CompletableFuture}s, the client must take care to close it. While the service may make
+ * some effort to clean up dropped handles, this is just a safeguard to prevent stale monitors
+ * from being presented indefinitely. The service may complain loudly when it detects dropped
+ * monitor handles.
+ *
+ * @return the monitor
+ */
+ CloseableTaskMonitor publishTask();
+
+ /**
+ * Collect all the tasks currently in progress
+ *
+ *
+ * The subscriber ought to call this immediately after adding its listener, in order to catch up
+ * on tasks already in progress.
+ *
+ * @return a collection of in-progress monitor proxies
+ */
+ Collection getAllMonitors();
+
+ /**
+ * Subscribe to task and progress events
+ *
+ * @param listener the listener
+ */
+ void addProgressListener(ProgressListener listener);
+
+ /**
+ * Un-subscribe from task and progress events
+ *
+ * @param listener the listener
+ */
+ void removeProgressListener(ProgressListener listener);
+}
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiService.java
index 35f65acf67..5ddf5750b1 100644
--- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiService.java
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiService.java
@@ -19,8 +19,7 @@ import java.io.IOException;
import java.net.SocketAddress;
import java.util.Collection;
-import ghidra.debug.api.tracermi.TraceRmiAcceptor;
-import ghidra.debug.api.tracermi.TraceRmiConnection;
+import ghidra.debug.api.tracermi.*;
import ghidra.framework.plugintool.ServiceInfo;
/**
@@ -105,4 +104,26 @@ public interface TraceRmiService {
* @return the connections
*/
Collection getAllConnections();
+
+ /**
+ * Get all of the acceptors currently listening for a connection
+ *
+ * @return the acceptors
+ */
+ Collection getAllAcceptors();
+
+ /**
+ * Add a listener for events on the Trace RMI service
+ *
+ * @param listener the listener to add
+ */
+ void addTraceServiceListener(TraceRmiServiceListener listener);
+
+ /**
+ * Remove a listener for events on the Trace RMI service
+ *
+ * @param listener the listener to remove
+ */
+ void removeTraceServiceListener(TraceRmiServiceListener listener);
+
}
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/progress/CloseableTaskMonitor.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/progress/CloseableTaskMonitor.java
new file mode 100644
index 0000000000..58ced490e6
--- /dev/null
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/progress/CloseableTaskMonitor.java
@@ -0,0 +1,23 @@
+/* ###
+ * 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.progress;
+
+import ghidra.util.task.TaskMonitor;
+
+public interface CloseableTaskMonitor extends TaskMonitor, AutoCloseable {
+ @Override
+ void close();
+}
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/progress/MonitorReceiver.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/progress/MonitorReceiver.java
new file mode 100644
index 0000000000..1d33f848a1
--- /dev/null
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/progress/MonitorReceiver.java
@@ -0,0 +1,113 @@
+/* ###
+ * 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.progress;
+
+import ghidra.debug.api.progress.ProgressListener.Disposal;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * The subscriber side of a published {@link TaskMonitor}
+ *
+ *
+ * This only gives a subset of the expected task monitor interface. This is the subset a
+ * user would need to monitor and/or cancel the task. All the mechanisms for updating the
+ * monitor are only available to the publishing client.
+ */
+public interface MonitorReceiver {
+ /**
+ * Get the current message for the monitor
+ *
+ * @return the message
+ */
+ String getMessage();
+
+ /**
+ * Check if the monitor indicates progress at all
+ *
+ *
+ * If the task is indeterminate, then its {@link #getMaximum()} and {@link #getProgress()}
+ * methods are meaningless.
+ *
+ * @return true if indeterminate (no progress shown), false if determinate (progress shown)
+ */
+ boolean isIndeterminate();
+
+ /**
+ * Get the maximum value of progress
+ *
+ *
+ * The implication is that when {@link #getProgress()} returns the maximum, the task is
+ * complete.
+ *
+ * @return the maximum progress
+ */
+ long getMaximum();
+
+ /**
+ * Get the progress value, if applicable
+ *
+ * @return the progress, or {@link TaskMonitor#NO_PROGRESS_VALUE} if un-set or not applicable
+ */
+ long getProgress();
+
+ /**
+ * Check if the task can be cancelled
+ *
+ * @return true if cancel is enabled, false if not
+ */
+ boolean isCancelEnabled();
+
+ /**
+ * Request the task be cancelled
+ *
+ *
+ * Note it is up to the client publishing the task to adhere to this request. In general, the
+ * computation should occasionally call {@link TaskMonitor#checkCancelled()}. In particular, the
+ * subscribing client cannot presume the task is cancelled purely by virtue of calling
+ * this method successfully. Instead, it should listen for
+ * {@link ProgressListener#monitorDisposed(MonitorReceiver, Disposal)}.
+ */
+ void cancel();
+
+ /**
+ * Check if the task is cancelled
+ *
+ * @return true if cancelled, false if not
+ */
+ boolean isCancelled();
+
+ /**
+ * Check if the monitor is still valid
+ *
+ *
+ * A monitor becomes invalid when it is closed or cleaned.
+ *
+ * @return true if still valid, false if invalid
+ */
+ boolean isValid();
+
+ /**
+ * Check if the monitor should be rendered with the progress value
+ *
+ *
+ * Regardless of this value, the monitor will render a progress bar and a numeric percentage. If
+ * this is set to true (the default), the it will also display "{progress} of {maximum}" in
+ * text.
+ *
+ * @return true to render the actual progress value, false for only a percentage.
+ */
+ boolean isShowProgressValue();
+}
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/progress/ProgressListener.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/progress/ProgressListener.java
new file mode 100644
index 0000000000..60749983f7
--- /dev/null
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/progress/ProgressListener.java
@@ -0,0 +1,52 @@
+/* ###
+ * 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.progress;
+
+public interface ProgressListener {
+ enum Disposal {
+ /**
+ * The monitor was properly closed
+ */
+ CLOSED,
+ /**
+ * The monitor was not closed. Instead, it was cleaned by the garbage collector.
+ */
+ CLEANED;
+ }
+
+ void monitorCreated(MonitorReceiver monitor);
+
+ void monitorDisposed(MonitorReceiver monitor, Disposal disposal);
+
+ void messageUpdated(MonitorReceiver monitor, String message);
+
+ void progressUpdated(MonitorReceiver monitor, long progress);
+
+ /**
+ * Some other attribute has been updated
+ *
+ *
+ *
cancelled
+ *
cancel enabled
+ *
indeterminate
+ *
maximum
+ *
show progress value in percent string
+ *
+ *
+ * @param monitor the monitor
+ */
+ void attributeUpdated(MonitorReceiver monitor);
+}
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiAcceptor.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiAcceptor.java
index d36d8fdefb..3dcd7bb8ca 100644
--- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiAcceptor.java
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiAcceptor.java
@@ -19,6 +19,8 @@ import java.io.IOException;
import java.net.SocketAddress;
import java.net.SocketException;
+import ghidra.util.exception.CancelledException;
+
/**
* An acceptor to receive a single Trace RMI connection from a back-end
*/
@@ -27,12 +29,13 @@ public interface TraceRmiAcceptor {
* Accept a single connection
*
*
- * This acceptor is no longer valid after the connection is accepted.
+ * This acceptor is no longer valid after the connection is accepted. If accepting the
+ * connection fails, e.g., because of a timeout, this acceptor is no longer valid.
*
* @return the connection, if successful
* @throws IOException if there was an error
*/
- TraceRmiConnection accept() throws IOException;
+ TraceRmiConnection accept() throws IOException, CancelledException;
/**
* Get the address (and port) where the acceptor is listening
@@ -48,4 +51,14 @@ public interface TraceRmiAcceptor {
* @throws SocketException if there's a protocol error
*/
void setTimeout(int millis) throws SocketException;
+
+ /**
+ * Cancel the connection
+ *
+ *
+ * If a different thread has called {@link #accept()}, it will fail. In this case, both
+ * {@linkplain TraceRmiServiceListener#acceptCancelled(TraceRmiAcceptor)} and
+ * {@linkplain TraceRmiServiceListener#acceptFailed(Exception)} may be invoked.
+ */
+ void cancel();
}
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 decddb02b8..6deb65eab0 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,9 +17,11 @@ package ghidra.debug.api.tracermi;
import java.io.IOException;
import java.net.SocketAddress;
+import java.util.Collection;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeoutException;
+import ghidra.debug.api.target.Target;
import ghidra.trace.model.Trace;
/**
@@ -37,6 +39,16 @@ import ghidra.trace.model.Trace;
* to both parent and child, then it should create and publish a second target.
*/
public interface TraceRmiConnection extends AutoCloseable {
+ /**
+ * Get the client-given description of this connection
+ *
+ *
+ * If the connection is still being negotiated, this will return a string indicating that.
+ *
+ * @return the description
+ */
+ String getDescription();
+
/**
* Get the address of the back end debugger
*
@@ -137,4 +149,11 @@ public interface TraceRmiConnection extends AutoCloseable {
* @return true if the trace is a target, false otherwise.
*/
boolean isTarget(Trace trace);
+
+ /**
+ * Get all the valid targets created by this connection
+ *
+ * @return the collection of valid targets
+ */
+ Collection getTargets();
}
diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiServiceListener.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiServiceListener.java
new file mode 100644
index 0000000000..a88ab0964e
--- /dev/null
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiServiceListener.java
@@ -0,0 +1,127 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.debug.api.tracermi;
+
+import java.net.SocketAddress;
+
+import ghidra.app.services.TraceRmiService;
+import ghidra.debug.api.target.Target;
+import ghidra.debug.api.target.TargetPublicationListener;
+
+/**
+ * A listener for Trace RMI Service events
+ */
+public interface TraceRmiServiceListener {
+ /**
+ * The mechanism for creating a connection
+ */
+ enum ConnectMode {
+ /**
+ * The connection was established via {@link TraceRmiService#connect(SocketAddress)}
+ */
+ CONNECT,
+ /**
+ * The connection was established via {@link TraceRmiService#acceptOne(SocketAddress)}
+ */
+ ACCEPT_ONE,
+ /**
+ * The connection was established by the server. See {@link TraceRmiService#startServer()}
+ */
+ SERVER;
+ }
+
+ /**
+ * The server has been started on the given address
+ *
+ * @param address the server's address
+ */
+ default void serverStarted(SocketAddress address) {
+ }
+
+ /**
+ * The server has been stopped
+ */
+ default void serverStopped() {
+ }
+
+ /**
+ * A new connection has been established
+ *
+ * @param connection the new connection
+ * @param mode the mechanism creating the connection
+ * @param if by {@link TraceRmiService#acceptOne(SocketAddress)}, the acceptor that created this
+ * connection
+ */
+ default void connected(TraceRmiConnection connection, ConnectMode mode,
+ TraceRmiAcceptor acceptor) {
+ }
+
+ /**
+ * A connection was lost or closed
+ *
+ *
+ * TODO: Do we care to indicate why?
+ *
+ * @param connection the connection that has been closed
+ */
+ default void disconnected(TraceRmiConnection connection) {
+ }
+
+ /**
+ * The service is waiting for an inbound connection
+ *
+ *
+ * The acceptor remains valid until one of three events occurs:
+ * {@linkplain} #connected(TraceRmiConnection, ConnectMode, TraceRmiAcceptor)},
+ * {@linkplain} #acceptCancelled(TraceRmiAcceptor)}, or {@linkplain} #acceptFailed(Exception)}.
+ *
+ * @param acceptor the acceptor waiting
+ */
+ default void waitingAccept(TraceRmiAcceptor acceptor) {
+ }
+
+ /**
+ * The client cancelled an inbound acceptor via {@link TraceRmiAcceptor#cancel()}
+ *
+ * @param acceptor the acceptor that was cancelled
+ */
+ default void acceptCancelled(TraceRmiAcceptor acceptor) {
+ }
+
+ /**
+ * The service failed to complete an inbound connection
+ *
+ * @param acceptor the acceptor that failed
+ * @param e the exception causing the failure
+ */
+ default void acceptFailed(TraceRmiAcceptor acceptor, Exception e) {
+ }
+
+ /**
+ * A new target was created by a Trace RMI connection
+ *
+ *
+ * The added benefit of this method compared to the {@link TargetPublicationListener} is that it
+ * identifies which connection
+ *
+ * @param connection the connection creating the target
+ * @param target the target
+ * @see TargetPublicationListener#targetPublished(Target)
+ * @see TargetPublicationListener#targetWithdrawn(Target)
+ */
+ default void targetPublished(TraceRmiConnection connection, Target target) {
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ConnectTraceRmiScript.java b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ConnectTraceRmiScript.java
index c97af91aef..429c82e96c 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ConnectTraceRmiScript.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ConnectTraceRmiScript.java
@@ -16,7 +16,7 @@
import java.net.InetSocketAddress;
import java.util.Objects;
-import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin;
+import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin;
import ghidra.app.script.GhidraScript;
import ghidra.app.services.TraceRmiService;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ListenTraceRmiScript.java b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ListenTraceRmiScript.java
index 3014952495..43cf4e43ac 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ListenTraceRmiScript.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/ListenTraceRmiScript.java
@@ -16,7 +16,7 @@
import java.util.Map;
import java.util.Objects;
-import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin;
+import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin;
import ghidra.app.script.GhidraScript;
import ghidra.app.services.TraceRmiService;
import ghidra.debug.api.tracermi.TraceRmiAcceptor;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerPlugin.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerPlugin.java
index 1b15dd8dce..81640171bf 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerPlugin.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerPlugin.java
@@ -17,6 +17,8 @@ package ghidra.app.plugin.core.debug.gui.tracermi.connection;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
+import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
+import ghidra.app.plugin.core.debug.event.TraceInactiveCoordinatesPluginEvent;
import ghidra.app.services.TraceRmiService;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
@@ -29,14 +31,30 @@ import ghidra.framework.plugintool.util.PluginStatus;
""",
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
- status = PluginStatus.RELEASED,
+ status = PluginStatus.STABLE,
+ eventsConsumed = {
+ TraceActivatedPluginEvent.class,
+ TraceInactiveCoordinatesPluginEvent.class,
+ },
servicesRequired = {
TraceRmiService.class,
})
public class TraceRmiConnectionManagerPlugin extends Plugin {
+ private final TraceRmiConnectionManagerProvider provider;
+
public TraceRmiConnectionManagerPlugin(PluginTool tool) {
super(tool);
+ this.provider = new TraceRmiConnectionManagerProvider(this);
}
- // TODO: Add the actual provider. This will probably replace DebuggerTargetsPlugin.
+ @Override
+ public void processEvent(PluginEvent event) {
+ super.processEvent(event);
+ if (event instanceof TraceActivatedPluginEvent evt) {
+ provider.coordinates(evt.getActiveCoordinates());
+ }
+ if (event instanceof TraceInactiveCoordinatesPluginEvent evt) {
+ provider.coordinates(evt.getCoordinates());
+ }
+ }
}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProvider.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProvider.java
new file mode 100644
index 0000000000..676b6e5a1a
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProvider.java
@@ -0,0 +1,532 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.tracermi.connection;
+
+import java.awt.AWTEvent;
+import java.awt.BorderLayout;
+import java.awt.event.*;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import javax.swing.*;
+import javax.swing.tree.TreeSelectionModel;
+
+import docking.ActionContext;
+import docking.WindowPosition;
+import docking.action.DockingAction;
+import docking.action.builder.ActionBuilder;
+import docking.widgets.tree.*;
+import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
+import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
+import ghidra.app.plugin.core.debug.gui.tracermi.connection.tree.*;
+import ghidra.app.services.*;
+import ghidra.dbg.target.TargetMethod.ParameterDescription;
+import ghidra.dbg.target.TargetMethod.TargetParameterMap;
+import ghidra.debug.api.control.ControlMode;
+import ghidra.debug.api.target.Target;
+import ghidra.debug.api.tracemgr.DebuggerCoordinates;
+import ghidra.debug.api.tracermi.TraceRmiAcceptor;
+import ghidra.debug.api.tracermi.TraceRmiConnection;
+import ghidra.framework.plugintool.*;
+import ghidra.framework.plugintool.AutoService.Wiring;
+import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
+import ghidra.framework.plugintool.util.PluginUtils;
+import ghidra.util.HelpLocation;
+import ghidra.util.Msg;
+import ghidra.util.exception.CancelledException;
+
+public class TraceRmiConnectionManagerProvider extends ComponentProviderAdapter {
+ public static final String TITLE = "Connections";
+ public static final HelpLocation HELP =
+ new HelpLocation(PluginUtils.getPluginNameFromClass(TraceRmiConnectionManagerPlugin.class),
+ DebuggerResources.HELP_ANCHOR_PLUGIN);
+
+ private static final String GROUP_SERVER = "2. Server";
+ private static final String GROUP_CONNECT = "1. Connect";
+ private static final String GROUP_MAINTENANCE = "3. Maintenance";
+
+ private static final ParameterDescription PARAM_ADDRESS =
+ ParameterDescription.create(String.class, "address", true, "localhost",
+ "Host/Address", "Address or hostname for interface(s) to listen on");
+ private static final ParameterDescription PARAM_PORT =
+ ParameterDescription.create(Integer.class, "port", true, 0,
+ "Port", "TCP port number, 0 for ephemeral");
+ private static final TargetParameterMap PARAMETERS = TargetParameterMap.ofEntries(
+ Map.entry(PARAM_ADDRESS.name, PARAM_ADDRESS),
+ Map.entry(PARAM_PORT.name, PARAM_PORT));
+
+ interface StartServerAction {
+ String NAME = "Start Server";
+ String DESCRIPTION = "Start a TCP server for incoming connections (indefinitely)";
+ String GROUP = GROUP_SERVER;
+ String HELP_ANCHOR = "start_server";
+
+ static ActionBuilder builder(Plugin owner) {
+ String ownerName = owner.getName();
+ return new ActionBuilder(NAME, ownerName)
+ .description(DESCRIPTION)
+ .menuPath(NAME)
+ .menuGroup(GROUP)
+ .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
+ }
+ }
+
+ interface StopServerAction {
+ String NAME = "Stop Server";
+ String DESCRIPTION = "Close the TCP server";
+ String GROUP = GROUP_SERVER;
+ String HELP_ANCHOR = "stop_server";
+
+ static ActionBuilder builder(Plugin owner) {
+ String ownerName = owner.getName();
+ return new ActionBuilder(NAME, ownerName)
+ .description(DESCRIPTION)
+ .menuPath(NAME)
+ .menuGroup(GROUP)
+ .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
+ }
+ }
+
+ interface ConnectAcceptAction {
+ String NAME = "Connect by Accept";
+ String DESCRIPTION = "Accept a single inbound TCP connection";
+ String GROUP = GROUP_CONNECT;
+ Icon ICON = DebuggerResources.ICON_CONNECT_ACCEPT;
+ String HELP_ANCHOR = "connect_accept";
+
+ static ActionBuilder builder(Plugin owner) {
+ String ownerName = owner.getName();
+ return new ActionBuilder(NAME, ownerName)
+ .description(DESCRIPTION)
+ .toolBarIcon(ICON)
+ .toolBarGroup(GROUP)
+ .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
+ }
+ }
+
+ interface ConnectOutboundAction {
+ String NAME = "Connect Outbound";
+ String DESCRIPTION = "Connect to a listening agent/plugin by TCP";
+ String GROUP = GROUP_CONNECT;
+ Icon ICON = DebuggerResources.ICON_CONNECT_OUTBOUND;
+ String HELP_ANCHOR = "connect_outbound";
+
+ static ActionBuilder builder(Plugin owner) {
+ String ownerName = owner.getName();
+ return new ActionBuilder(NAME, ownerName)
+ .description(DESCRIPTION)
+ .toolBarIcon(ICON)
+ .toolBarGroup(GROUP)
+ .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
+ }
+ }
+
+ interface CloseConnectionAction {
+ String NAME = "Close";
+ String DESCRIPTION = "Close a connection or server";
+ String GROUP = GROUP_MAINTENANCE;
+ String HELP_ANCHOR = "close";
+
+ static ActionBuilder builder(Plugin owner) {
+ String ownerName = owner.getName();
+ return new ActionBuilder(NAME, ownerName)
+ .description(DESCRIPTION)
+ .menuPath(NAME)
+ .popupMenuPath(NAME)
+ .menuGroup(GROUP)
+ .popupMenuGroup(GROUP)
+ .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
+ }
+ }
+
+ interface CloseAllAction {
+ String NAME = "Close All";
+ String DESCRIPTION = "Close all connections and the server";
+ String GROUP = GROUP_MAINTENANCE;
+ String HELP_ANCHOR = "close_all";
+
+ static ActionBuilder builder(Plugin owner) {
+ String ownerName = owner.getName();
+ return new ActionBuilder(NAME, ownerName)
+ .description(DESCRIPTION)
+ .menuPath(NAME)
+ .menuGroup(GROUP)
+ .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
+ }
+ }
+
+ class InjectableGTree extends GTree {
+ public InjectableGTree(GTreeNode root) {
+ super(root);
+ }
+
+ /**
+ * This allows the test framework to use reflection to access this method.
+ */
+ @Override
+ protected void processEvent(AWTEvent e) {
+ super.processEvent(e);
+ }
+ }
+
+ private final TraceRmiConnectionManagerPlugin plugin;
+
+ // @AutoServiceConsumed via method
+ TraceRmiService traceRmiService;
+ // @AutoServiceConsumed via method
+ DebuggerTargetService targetService;
+ @AutoServiceConsumed
+ DebuggerConsoleService consoleService;
+ @AutoServiceConsumed
+ DebuggerTraceManagerService traceManagerService;
+ @AutoServiceConsumed
+ DebuggerControlService controlService;
+ @SuppressWarnings("unused")
+ private final Wiring autoServiceWiring;
+
+ private JPanel mainPanel;
+ protected GTree tree;
+ protected TraceRmiServiceNode rootNode = new TraceRmiServiceNode(this);
+
+ DockingAction actionStartServer;
+ DockingAction actionStopServer;
+ DockingAction actionConnectAccept;
+ DockingAction actionConnectOutbound;
+ DockingAction actionCloseConnection;
+ DockingAction actionCloseAll;
+
+ TraceRmiManagerActionContext myActionContext;
+
+ public TraceRmiConnectionManagerProvider(TraceRmiConnectionManagerPlugin plugin) {
+ super(plugin.getTool(), TITLE, plugin.getName());
+ this.plugin = plugin;
+
+ this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
+ setTitle(TITLE);
+ setIcon(DebuggerResources.ICON_PROVIDER_TARGETS);
+ setHelpLocation(HELP);
+ setWindowMenuGroup(DebuggerPluginPackage.NAME);
+
+ buildMainPanel();
+
+ setDefaultWindowPosition(WindowPosition.LEFT);
+ setVisible(true);
+ createActions();
+ }
+
+ private void buildMainPanel() {
+ mainPanel = new JPanel(new BorderLayout());
+
+ tree = new InjectableGTree(rootNode);
+ tree.setRootVisible(false);
+ tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+ mainPanel.add(tree);
+
+ tree.getGTSelectionModel().addGTreeSelectionListener(evt -> {
+ setContext();
+ });
+ tree.addGTModelListener((AnyChangeTreeModelListener) e -> {
+ setContext();
+ });
+ // TODO: Double-click or ENTER (activate) should open and/or activate trace/snap
+ tree.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) {
+ activateSelectedNode();
+ e.consume();
+ }
+ }
+ });
+ tree.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+ activateSelectedNode();
+ e.consume();
+ }
+ }
+ });
+ }
+
+ private void activateSelectedNode() {
+ List selList = tree.getSelectedNodes();
+ if (selList.isEmpty()) {
+ return;
+ }
+ assert selList.size() == 1;
+ GTreeNode sel = selList.get(0);
+ nodeActivated((TraceRmiManagerNode) sel);
+ }
+
+ private void nodeActivated(TraceRmiManagerNode node) {
+ if (node instanceof TraceRmiTargetNode tNode) {
+ if (traceManagerService == null) {
+ return;
+ }
+ Target target = tNode.getTarget();
+ traceManagerService.activateTarget(target);
+ if (controlService == null) {
+ return;
+ }
+ if (!controlService.getCurrentMode(target.getTrace()).isTarget()) {
+ controlService.setCurrentMode(target.getTrace(), ControlMode.RO_TARGET);
+ }
+ }
+ }
+
+ private void createActions() {
+ actionStartServer = StartServerAction.builder(plugin)
+ .enabledWhen(this::isActionStartServerEnabled)
+ .onAction(this::doActionStartServerActivated)
+ .buildAndInstallLocal(this);
+ actionStopServer = StopServerAction.builder(plugin)
+ .enabledWhen(this::isActionStopServerEnabled)
+ .onAction(this::doActionStopServerActivated)
+ .buildAndInstallLocal(this);
+
+ actionConnectAccept = ConnectAcceptAction.builder(plugin)
+ .enabledWhen(this::isActionConnectAcceptEnabled)
+ .onAction(this::doActionConnectAcceptActivated)
+ .buildAndInstallLocal(this);
+ actionConnectOutbound = ConnectOutboundAction.builder(plugin)
+ .enabledWhen(this::isActionConnectOutboundEnabled)
+ .onAction(this::doActionConnectOutboundActivated)
+ .buildAndInstallLocal(this);
+
+ actionCloseConnection = CloseConnectionAction.builder(plugin)
+ .withContext(TraceRmiManagerActionContext.class)
+ .enabledWhen(this::isActionCloseConnectionEnabled)
+ .onAction(this::doActionCloseConnectionActivated)
+ .buildAndInstallLocal(this);
+ actionCloseAll = CloseAllAction.builder(plugin)
+ .enabledWhen(this::isActionCloseAllEnabled)
+ .onAction(this::doActionCloseAllActivated)
+ .buildAndInstallLocal(this);
+ }
+
+ @Override
+ public ActionContext getActionContext(MouseEvent event) {
+ if (myActionContext == null) {
+ return super.getActionContext(event);
+ }
+ return myActionContext;
+ }
+
+ @Override
+ public JComponent getComponent() {
+ return mainPanel;
+ }
+
+ private void setContext() {
+ myActionContext = new TraceRmiManagerActionContext(this, tree.getSelectionPath(), tree);
+ contextChanged();
+ }
+
+ private boolean isActionStartServerEnabled(ActionContext __) {
+ return traceRmiService != null && !traceRmiService.isServerStarted();
+ }
+
+ private InetSocketAddress promptSocketAddress(String title, String okText) {
+ DebuggerMethodInvocationDialog dialog = new DebuggerMethodInvocationDialog(tool,
+ title, okText, DebuggerResources.ICON_CONNECTION);
+ Map arguments;
+ do {
+ dialog.forgetMemorizedArguments();
+ arguments = dialog.promptArguments(PARAMETERS);
+ }
+ while (dialog.isResetRequested());
+ if (arguments == null) {
+ return null;
+ }
+ String address = PARAM_ADDRESS.get(arguments);
+ int port = PARAM_PORT.get(arguments);
+ return new InetSocketAddress(address, port);
+ }
+
+ private void doActionStartServerActivated(ActionContext __) {
+ InetSocketAddress sockaddr = promptSocketAddress("Start Trace RMI Server", "Start");
+ if (sockaddr == null) {
+ return;
+ }
+ try {
+ traceRmiService.setServerAddress(sockaddr);
+ traceRmiService.startServer();
+ if (consoleService != null) {
+ consoleService.log(DebuggerResources.ICON_CONNECTION,
+ "TraceRmi Server listening at " + traceRmiService.getServerAddress());
+ }
+ }
+ catch (Exception e) {
+ Msg.error(this, "Could not start TraceRmi server: " + e);
+ }
+ }
+
+ private boolean isActionStopServerEnabled(ActionContext __) {
+ return traceRmiService != null && traceRmiService.isServerStarted();
+ }
+
+ private void doActionStopServerActivated(ActionContext __) {
+ traceRmiService.stopServer();
+ if (consoleService != null) {
+ consoleService.log(DebuggerResources.ICON_DISCONNECT, "TraceRmi Server stopped");
+ }
+ }
+
+ private boolean isActionConnectAcceptEnabled(ActionContext __) {
+ return traceRmiService != null;
+ }
+
+ private void doActionConnectAcceptActivated(ActionContext __) {
+ InetSocketAddress sockaddr = promptSocketAddress("Accept Trace RMI Connection", "Listen");
+ if (sockaddr == null) {
+ return;
+ }
+ CompletableFuture.runAsync(() -> {
+ // TODO: Progress entry
+ try {
+ TraceRmiAcceptor acceptor = traceRmiService.acceptOne(sockaddr);
+ acceptor.accept();
+ }
+ catch (CancelledException e) {
+ // Nothing. User should already know.
+ }
+ catch (Throwable e) {
+ Msg.showError(this, null, "Accept",
+ "Could not accept Trace RMI Connection on " + sockaddr + ": " + e);
+ }
+ });
+ }
+
+ private boolean isActionConnectOutboundEnabled(ActionContext __) {
+ return traceRmiService != null;
+ }
+
+ private void doActionConnectOutboundActivated(ActionContext __) {
+ InetSocketAddress sockaddr = promptSocketAddress("Connect to Trace RMI", "Connect");
+ if (sockaddr == null) {
+ return;
+ }
+ CompletableFuture.runAsync(() -> {
+ // TODO: Progress entry?
+ try {
+ traceRmiService.connect(sockaddr);
+ }
+ catch (Throwable e) {
+ Msg.showError(this, null, "Connect",
+ "Could connect to Trace RMI at " + sockaddr + ": " + e.getMessage());
+ }
+ });
+ }
+
+ private boolean isActionCloseConnectionEnabled(TraceRmiManagerActionContext context) {
+ TraceRmiManagerNode node = context.getSelectedNode();
+ if (node instanceof TraceRmiConnectionNode) {
+ return true;
+ }
+ if (node instanceof TraceRmiAcceptorNode) {
+ return true;
+ }
+ return false;
+ }
+
+ private void doActionCloseConnectionActivated(TraceRmiManagerActionContext context) {
+ TraceRmiManagerNode node = context.getSelectedNode();
+ if (node instanceof TraceRmiConnectionNode cxNode) {
+ try {
+ cxNode.getConnection().close();
+ }
+ catch (IOException e) {
+ Msg.showError(this, null, "Close Connection",
+ "Could not close Trace RMI connection: " + e);
+ }
+ }
+ else if (node instanceof TraceRmiAcceptorNode acNode) {
+ acNode.getAcceptor().cancel();
+ }
+ }
+
+ private boolean isActionCloseAllEnabled(ActionContext __) {
+ return traceRmiService != null;
+ }
+
+ private void doActionCloseAllActivated(ActionContext __) {
+ try {
+ doActionStopServerActivated(__);
+ }
+ catch (Throwable e) {
+ Msg.error(this, "Could not close server: " + e);
+ }
+ for (TraceRmiConnection connection : traceRmiService.getAllConnections()) {
+ try {
+ connection.close();
+ }
+ catch (Throwable e) {
+ Msg.error(this, "Could not close " + connection + ": " + e);
+ }
+ }
+ for (TraceRmiAcceptor acceptor : traceRmiService.getAllAcceptors()) {
+ try {
+ acceptor.cancel();
+ }
+ catch (Throwable e) {
+ Msg.error(this, "Could not cancel " + acceptor + ": " + e);
+ }
+ }
+ }
+
+ @AutoServiceConsumed
+ private void setTraceRmiService(TraceRmiService traceRmiService) {
+ if (this.traceRmiService != null) {
+ this.traceRmiService.removeTraceServiceListener(rootNode);
+ }
+ this.traceRmiService = traceRmiService;
+ if (this.traceRmiService != null) {
+ this.traceRmiService.addTraceServiceListener(rootNode);
+ }
+ }
+
+ @AutoServiceConsumed
+ private void setTargetService(DebuggerTargetService targetService) {
+ if (this.targetService != null) {
+ this.targetService.removeTargetPublicationListener(rootNode);
+ }
+ this.targetService = targetService;
+ if (this.targetService != null) {
+ this.targetService.addTargetPublicationListener(rootNode);
+ }
+ }
+
+ public TraceRmiService getTraceRmiService() {
+ return traceRmiService;
+ }
+
+ /**
+ * Coordinates, whether active or inactive, for a trace changed
+ *
+ * @param coordinates the coordinates
+ */
+ public void coordinates(DebuggerCoordinates coordinates) {
+ if (rootNode == null) {
+ return;
+ }
+ rootNode.coordinates(coordinates);
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiManagerActionContext.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiManagerActionContext.java
new file mode 100644
index 0000000000..5595915ca2
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiManagerActionContext.java
@@ -0,0 +1,39 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.tracermi.connection;
+
+import javax.swing.tree.TreePath;
+
+import docking.DefaultActionContext;
+import docking.widgets.tree.GTree;
+import ghidra.app.plugin.core.debug.gui.tracermi.connection.tree.TraceRmiManagerNode;
+
+public class TraceRmiManagerActionContext extends DefaultActionContext {
+ private final TreePath path;
+
+ public TraceRmiManagerActionContext(TraceRmiConnectionManagerProvider provider,
+ TreePath path, GTree tree) {
+ super(provider, path, tree);
+ this.path = path;
+ }
+
+ public TraceRmiManagerNode getSelectedNode() {
+ if (path == null) {
+ return null;
+ }
+ return (TraceRmiManagerNode) path.getLastPathComponent();
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/AbstractTraceRmiManagerNode.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/AbstractTraceRmiManagerNode.java
new file mode 100644
index 0000000000..6d5cd856c9
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/AbstractTraceRmiManagerNode.java
@@ -0,0 +1,34 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
+
+import docking.widgets.tree.GTreeNode;
+import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
+
+public abstract class AbstractTraceRmiManagerNode extends GTreeNode implements TraceRmiManagerNode {
+ protected final TraceRmiConnectionManagerProvider provider;
+ protected final String name;
+
+ public AbstractTraceRmiManagerNode(TraceRmiConnectionManagerProvider provider, String name) {
+ this.provider = provider;
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiAcceptorNode.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiAcceptorNode.java
new file mode 100644
index 0000000000..936dfa1ad0
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiAcceptorNode.java
@@ -0,0 +1,54 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
+
+import javax.swing.Icon;
+
+import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
+import ghidra.debug.api.tracermi.TraceRmiAcceptor;
+
+public class TraceRmiAcceptorNode extends AbstractTraceRmiManagerNode {
+
+ private static final Icon ICON = DebuggerResources.ICON_CONNECT_ACCEPT;
+
+ private final TraceRmiAcceptor acceptor;
+
+ public TraceRmiAcceptorNode(TraceRmiConnectionManagerProvider provider,
+ TraceRmiAcceptor acceptor) {
+ super(provider, "ACCEPTING: " + acceptor.getAddress());
+ this.acceptor = acceptor;
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return ICON;
+ }
+
+ @Override
+ public String getToolTip() {
+ return "Trace RMI Acceptor listening at " + acceptor.getAddress();
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+
+ public TraceRmiAcceptor getAcceptor() {
+ return acceptor;
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiConnectionNode.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiConnectionNode.java
new file mode 100644
index 0000000000..e39fbe3b95
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiConnectionNode.java
@@ -0,0 +1,97 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.Icon;
+
+import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
+import ghidra.debug.api.target.Target;
+import ghidra.debug.api.tracermi.TraceRmiConnection;
+
+public class TraceRmiConnectionNode extends AbstractTraceRmiManagerNode {
+ private static final Icon ICON = DebuggerResources.ICON_CONNECTION;
+
+ private final TraceRmiConnection connection;
+ private final Map targetNodes = new HashMap<>();
+
+ public TraceRmiConnectionNode(TraceRmiConnectionManagerProvider provider,
+ TraceRmiConnection connection) {
+ // TODO: Can the connector identify/describe itself for this display?
+ super(provider, "Connected: " + connection.getRemoteAddress());
+ this.connection = connection;
+ }
+
+ @Override
+ public String getDisplayText() {
+ return connection.getDescription() + " at " + connection.getRemoteAddress();
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return ICON;
+ }
+
+ @Override
+ public String getToolTip() {
+ return "Trace RMI Connection to " + connection.getDescription() + " at " +
+ connection.getRemoteAddress();
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+
+ private TraceRmiTargetNode newTargetNode(Target target) {
+ return new TraceRmiTargetNode(provider, this, target);
+ }
+
+ private TraceRmiTargetNode addTargetNode(Target target) {
+ TraceRmiTargetNode node;
+ synchronized (targetNodes) {
+ node = targetNodes.computeIfAbsent(target, this::newTargetNode);
+ }
+ addNode(node);
+ return node;
+ }
+
+ private void removeTargetNode(Target target) {
+ TraceRmiTargetNode node;
+ synchronized (targetNodes) {
+ node = targetNodes.remove(target);
+ }
+ if (node == null) {
+ return;
+ }
+ removeNode(node);
+ }
+
+ public TraceRmiTargetNode targetPublished(Target target) {
+ return addTargetNode(target);
+ }
+
+ public void targetWithdrawn(Target target) {
+ removeTargetNode(target);
+ }
+
+ public TraceRmiConnection getConnection() {
+ return connection;
+ }
+}
diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerConsoleLogger.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiManagerNode.java
similarity index 84%
rename from Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerConsoleLogger.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiManagerNode.java
index 95b53b58e4..9ee26f526b 100644
--- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerConsoleLogger.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiManagerNode.java
@@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.dbg;
+package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
-public interface DebuggerConsoleLogger {
+public interface TraceRmiManagerNode {
}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiServerNode.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiServerNode.java
new file mode 100644
index 0000000000..e956fc49dd
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiServerNode.java
@@ -0,0 +1,57 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
+
+import javax.swing.Icon;
+
+import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
+import ghidra.app.services.TraceRmiService;
+
+public class TraceRmiServerNode extends AbstractTraceRmiManagerNode {
+ private static final Icon ICON = DebuggerResources.ICON_THREAD; // TODO: Different name?
+
+ public TraceRmiServerNode(TraceRmiConnectionManagerProvider provider) {
+ super(provider, "Server");
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return ICON;
+ }
+
+ @Override
+ public String getDisplayText() {
+ TraceRmiService service = provider.getTraceRmiService();
+ if (service == null) {
+ return "";
+ }
+ if (!service.isServerStarted()) {
+ return "Server: CLOSED";
+ }
+ return "Server: LISTENING " + service.getServerAddress();
+ }
+
+ @Override
+ public String getToolTip() {
+ return getDisplayText();
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiServiceNode.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiServiceNode.java
new file mode 100644
index 0000000000..d37c9f704e
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiServiceNode.java
@@ -0,0 +1,205 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
+
+import java.net.SocketAddress;
+import java.util.*;
+
+import javax.swing.Icon;
+
+import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
+import ghidra.debug.api.target.Target;
+import ghidra.debug.api.target.TargetPublicationListener;
+import ghidra.debug.api.tracemgr.DebuggerCoordinates;
+import ghidra.debug.api.tracermi.*;
+import ghidra.util.Msg;
+
+public class TraceRmiServiceNode extends AbstractTraceRmiManagerNode
+ implements TraceRmiServiceListener, TargetPublicationListener {
+ private static final String DESCRIPTION = "The TraceRmi service";
+
+ final TraceRmiServerNode serverNode;
+ final Map connectionNodes = new HashMap<>();
+ final Map acceptorNodes = new HashMap<>();
+ // weak because each connection node keeps the strong map
+ final Map targetNodes = new WeakHashMap<>();
+
+ public TraceRmiServiceNode(TraceRmiConnectionManagerProvider provider) {
+ super(provider, "");
+ this.serverNode = new TraceRmiServerNode(provider);
+
+ addNode(serverNode);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return null;
+ }
+
+ @Override
+ public String getToolTip() {
+ return DESCRIPTION;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+
+ private TraceRmiConnectionNode newConnectionNode(TraceRmiConnection connection) {
+ return new TraceRmiConnectionNode(provider, connection);
+ }
+
+ private void addConnectionNode(TraceRmiConnection connection) {
+ TraceRmiConnectionNode node;
+ synchronized (connectionNodes) {
+ node = connectionNodes.computeIfAbsent(connection, this::newConnectionNode);
+ }
+ addNode(node);
+ }
+
+ private void removeConnectionNode(TraceRmiConnection connection) {
+ TraceRmiConnectionNode node;
+ synchronized (connectionNodes) {
+ node = connectionNodes.remove(connection);
+ }
+ if (node == null) {
+ return;
+ }
+ removeNode(node);
+ }
+
+ private TraceRmiAcceptorNode newAcceptorNode(TraceRmiAcceptor acceptor) {
+ return new TraceRmiAcceptorNode(provider, acceptor);
+ }
+
+ private void addAcceptorNode(TraceRmiAcceptor acceptor) {
+ TraceRmiAcceptorNode node;
+ synchronized (acceptorNodes) {
+ node = acceptorNodes.computeIfAbsent(acceptor, this::newAcceptorNode);
+ }
+ addNode(node);
+ }
+
+ private void removeAcceptorNode(TraceRmiAcceptor acceptor) {
+ TraceRmiAcceptorNode node;
+ synchronized (acceptorNodes) {
+ node = acceptorNodes.remove(acceptor);
+ }
+ if (node == null) {
+ return;
+ }
+ removeNode(node);
+ }
+
+ @Override
+ public void serverStarted(SocketAddress address) {
+ serverNode.fireNodeChanged();
+ provider.contextChanged();
+ }
+
+ @Override
+ public void serverStopped() {
+ serverNode.fireNodeChanged();
+ provider.contextChanged();
+ }
+
+ @Override
+ public void connected(TraceRmiConnection connection, ConnectMode mode,
+ TraceRmiAcceptor acceptor) {
+ addConnectionNode(connection);
+ removeAcceptorNode(acceptor);
+ provider.contextChanged();
+ }
+
+ @Override
+ public void disconnected(TraceRmiConnection connection) {
+ removeConnectionNode(connection);
+ provider.contextChanged();
+ }
+
+ @Override
+ public void waitingAccept(TraceRmiAcceptor acceptor) {
+ addAcceptorNode(acceptor);
+ provider.contextChanged();
+ }
+
+ @Override
+ public void acceptCancelled(TraceRmiAcceptor acceptor) {
+ removeAcceptorNode(acceptor);
+ provider.contextChanged();
+ }
+
+ @Override
+ public void acceptFailed(TraceRmiAcceptor acceptor, Exception e) {
+ removeAcceptorNode(acceptor);
+ provider.contextChanged();
+ }
+
+ @Override
+ public void targetPublished(TraceRmiConnection connection, Target target) {
+ TraceRmiConnectionNode cxNode;
+ synchronized (connectionNodes) {
+ cxNode = connectionNodes.get(connection);
+ }
+ if (cxNode == null) {
+ Msg.warn(this,
+ "Target published on a connection I don't have! " + connection + " " + target);
+ return;
+ }
+ TraceRmiTargetNode tNode = cxNode.targetPublished(target);
+ if (tNode == null) {
+ return;
+ }
+ synchronized (targetNodes) {
+ targetNodes.put(target, tNode);
+ }
+ provider.contextChanged();
+ }
+
+ @Override
+ public void targetPublished(Target target) {
+ // Dont care. Using targetPublished(connection, target) instead
+ }
+
+ @Override
+ public void targetWithdrawn(Target target) {
+ TraceRmiTargetNode node;
+ synchronized (targetNodes) {
+ node = targetNodes.remove(target);
+ }
+ if (node == null) {
+ return;
+ }
+ node.getConnectionNode().targetWithdrawn(target);
+ provider.contextChanged();
+ }
+
+ public void coordinates(DebuggerCoordinates coordinates) {
+ Target target = coordinates.getTarget();
+ if (target == null) {
+ return;
+ }
+ TraceRmiTargetNode node;
+ synchronized (targetNodes) {
+ node = targetNodes.get(target);
+ }
+ if (node == null) {
+ return;
+ }
+ node.fireNodeChanged();
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiTargetNode.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiTargetNode.java
new file mode 100644
index 0000000000..31958f557c
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiTargetNode.java
@@ -0,0 +1,64 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
+
+import javax.swing.Icon;
+
+import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.app.plugin.core.debug.gui.tracermi.connection.TraceRmiConnectionManagerProvider;
+import ghidra.debug.api.target.Target;
+
+public class TraceRmiTargetNode extends AbstractTraceRmiManagerNode {
+ private static final Icon ICON = DebuggerResources.ICON_RECORD;
+
+ private final TraceRmiConnectionNode connectionNode;
+ private final Target target;
+
+ public TraceRmiTargetNode(TraceRmiConnectionManagerProvider provider,
+ TraceRmiConnectionNode connectionNode, Target target) {
+ super(provider, target.getTrace().getName());
+ this.connectionNode = connectionNode;
+ this.target = target;
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return ICON;
+ }
+
+ @Override
+ public String getDisplayText() {
+ return target.getTrace().getName() + " (snap=" + target.getSnap() + ")";
+ }
+
+ @Override
+ public String getToolTip() {
+ return "Target: " + target.getTrace().getName();
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+
+ public TraceRmiConnectionNode getConnectionNode() {
+ return connectionNode;
+ }
+
+ public Target getTarget() {
+ return target;
+ }
+}
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 dfcab73170..063cf34de0 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
@@ -33,8 +33,8 @@ 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.debug.service.tracermi.DefaultTraceRmiAcceptor;
+import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler;
import ghidra.app.plugin.core.terminal.TerminalListener;
import ghidra.app.services.*;
import ghidra.app.services.DebuggerTraceManagerService.ActivationCause;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/DefaultTraceRmiAcceptor.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/DefaultTraceRmiAcceptor.java
deleted file mode 100644
index 0dff0d177f..0000000000
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/DefaultTraceRmiAcceptor.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/* ###
- * IP: GHIDRA
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package ghidra.app.plugin.core.debug.service.rmi.trace;
-
-import java.io.IOException;
-import java.net.ServerSocket;
-import java.net.SocketAddress;
-
-import ghidra.debug.api.tracermi.TraceRmiAcceptor;
-
-public class DefaultTraceRmiAcceptor extends TraceRmiServer implements TraceRmiAcceptor {
-
- public DefaultTraceRmiAcceptor(TraceRmiPlugin plugin, SocketAddress address) {
- super(plugin, address);
- }
-
- @Override
- public void start() throws IOException {
- socket = new ServerSocket();
- bind();
- }
-
- @Override
- protected void bind() throws IOException {
- socket.bind(address, 1);
- }
-
- @Override
- public TraceRmiHandler accept() throws IOException {
- TraceRmiHandler handler = super.accept();
- close();
- return handler;
- }
-}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiServer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/AbstractTraceRmiListener.java
similarity index 69%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiServer.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/AbstractTraceRmiListener.java
index 0cb0ca34a0..8bd86a68ea 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiServer.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/AbstractTraceRmiListener.java
@@ -13,38 +13,43 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import java.io.IOException;
import java.net.*;
+import ghidra.debug.api.tracermi.TraceRmiAcceptor;
+import ghidra.debug.api.tracermi.TraceRmiServiceListener.ConnectMode;
import ghidra.util.Msg;
+import ghidra.util.exception.CancelledException;
-public class TraceRmiServer {
+public abstract class AbstractTraceRmiListener {
protected final TraceRmiPlugin plugin;
protected final SocketAddress address;
protected ServerSocket socket;
- public TraceRmiServer(TraceRmiPlugin plugin, SocketAddress address) {
+ public AbstractTraceRmiListener(TraceRmiPlugin plugin, SocketAddress address) {
this.plugin = plugin;
this.address = address;
}
- protected void bind() throws IOException {
- socket.bind(address);
- }
+ protected abstract void bind() throws IOException;
public void start() throws IOException {
socket = new ServerSocket();
bind();
- new Thread(this::serviceLoop, "trace-rmi server " + socket.getLocalSocketAddress()).start();
+ startServiceLoop();
}
+ protected abstract void startServiceLoop();
+
public void setTimeout(int millis) throws SocketException {
socket.setSoTimeout(millis);
}
+ protected abstract ConnectMode getConnectMode();
+
/**
* Accept a connection and handle its requests.
*
@@ -54,36 +59,17 @@ public class TraceRmiServer {
*
* @return the handler
* @throws IOException on error
+ * @throws CancelledException if the accept is cancelled
*/
@SuppressWarnings("resource")
- protected TraceRmiHandler accept() throws IOException {
+ protected TraceRmiHandler doAccept(TraceRmiAcceptor acceptor) throws IOException {
Socket client = socket.accept();
TraceRmiHandler handler = new TraceRmiHandler(plugin, client);
handler.start();
+ plugin.listeners.invoke().connected(handler, getConnectMode(), acceptor);
return handler;
}
- protected void serviceLoop() {
- try {
- accept();
- }
- catch (IOException e) {
- if (socket.isClosed()) {
- return;
- }
- Msg.error("Error accepting TraceRmi client", e);
- return;
- }
- finally {
- try {
- socket.close();
- }
- catch (IOException e) {
- Msg.error("Error closing TraceRmi service", e);
- }
- }
- }
-
public void close() {
try {
socket.close();
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/DefaultRemoteAsyncResult.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultRemoteAsyncResult.java
similarity index 96%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/DefaultRemoteAsyncResult.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultRemoteAsyncResult.java
index ee9fdec583..0fe1d89577 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/DefaultRemoteAsyncResult.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultRemoteAsyncResult.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import java.util.concurrent.*;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/DefaultRemoteMethodRegistry.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultRemoteMethodRegistry.java
similarity index 96%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/DefaultRemoteMethodRegistry.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultRemoteMethodRegistry.java
index 0fa90b77c8..643c51aef9 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/DefaultRemoteMethodRegistry.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultRemoteMethodRegistry.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import java.util.*;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultTraceRmiAcceptor.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultTraceRmiAcceptor.java
new file mode 100644
index 0000000000..ab52c32911
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultTraceRmiAcceptor.java
@@ -0,0 +1,77 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.service.tracermi;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+
+import ghidra.debug.api.tracermi.TraceRmiAcceptor;
+import ghidra.debug.api.tracermi.TraceRmiServiceListener.ConnectMode;
+import ghidra.util.exception.CancelledException;
+
+public class DefaultTraceRmiAcceptor extends AbstractTraceRmiListener implements TraceRmiAcceptor {
+ private boolean cancelled = false;
+
+ public DefaultTraceRmiAcceptor(TraceRmiPlugin plugin, SocketAddress address) {
+ super(plugin, address);
+ }
+
+ @Override
+ protected void startServiceLoop() {
+ // Don't. Instead, client calls accept()
+ }
+
+ @Override
+ protected void bind() throws IOException {
+ socket.bind(address, 1);
+ plugin.addAcceptor(this);
+ }
+
+ @Override
+ protected ConnectMode getConnectMode() {
+ return ConnectMode.ACCEPT_ONE;
+ }
+
+ @Override
+ public TraceRmiHandler accept() throws IOException, CancelledException {
+ try {
+ TraceRmiHandler handler = doAccept(this);
+ close();
+ return handler;
+ }
+ catch (Exception e) {
+ close();
+ if (cancelled) {
+ throw new CancelledException();
+ }
+ plugin.listeners.invoke().acceptFailed(this, e);
+ throw e;
+ }
+ }
+
+ @Override
+ public void close() {
+ plugin.removeAcceptor(this);
+ super.close();
+ }
+
+ @Override
+ public void cancel() {
+ cancelled = true;
+ close();
+ plugin.listeners.invoke().acceptCancelled(this);
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/OpenTrace.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/OpenTrace.java
similarity index 96%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/OpenTrace.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/OpenTrace.java
index 9e0f0a38e9..d93b907a62 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/OpenTrace.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/OpenTrace.java
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
-import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler.*;
+import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler.*;
import ghidra.debug.api.tracermi.TraceRmiError;
import ghidra.program.model.address.*;
import ghidra.program.model.lang.Register;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RecordRemoteMethod.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/RecordRemoteMethod.java
similarity index 95%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RecordRemoteMethod.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/RecordRemoteMethod.java
index 90adf56e8c..d217f1e83f 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RecordRemoteMethod.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/RecordRemoteMethod.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import java.util.Map;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RecordRemoteParameter.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/RecordRemoteParameter.java
similarity index 96%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RecordRemoteParameter.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/RecordRemoteParameter.java
index b855904bf4..d45971bc94 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/RecordRemoteParameter.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/RecordRemoteParameter.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.debug.api.tracermi.RemoteParameter;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java
similarity index 95%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java
index 821e8d96db..52c9f9ae63 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiHandler.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import java.io.*;
import java.math.BigInteger;
@@ -41,9 +41,12 @@ import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.dbg.target.schema.XmlSchemaContext;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils;
+import ghidra.debug.api.progress.CloseableTaskMonitor;
import ghidra.debug.api.target.ActionName;
+import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.debug.api.tracermi.*;
+import ghidra.framework.Application;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.AutoService.Wiring;
@@ -65,7 +68,6 @@ import ghidra.trace.model.time.TraceSnapshot;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
-import ghidra.util.task.TaskMonitor;
public class TraceRmiHandler implements TraceRmiConnection {
public static final String VERSION = "10.4";
@@ -180,7 +182,14 @@ public class TraceRmiHandler implements TraceRmiConnection {
byTrace.put(openTrace.trace, openTrace);
first.complete(openTrace);
- plugin.publishTarget(openTrace.target);
+ plugin.publishTarget(TraceRmiHandler.this, openTrace.target);
+ }
+
+ public synchronized List getTargets() {
+ return byId.values()
+ .stream()
+ .map(ot -> ot.target)
+ .collect(Collectors.toUnmodifiableList());
}
public CompletableFuture getFirstAsync() {
@@ -192,7 +201,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
private final Socket socket;
private final InputStream in;
private final OutputStream out;
- private final CompletableFuture negotiate = new CompletableFuture<>();
+ private final CompletableFuture negotiate = new CompletableFuture<>();
private final CompletableFuture closed = new CompletableFuture<>();
private final Set terminals = new LinkedHashSet<>();
@@ -276,8 +285,8 @@ public class TraceRmiHandler implements TraceRmiConnection {
DoId nextKey = openTraces.idSet().iterator().next();
OpenTrace open = openTraces.removeById(nextKey);
if (traceManager == null || traceManager.isSaveTracesByDefault()) {
- try {
- open.trace.save("Save on Disconnect", plugin.getTaskMonitor());
+ try (CloseableTaskMonitor monitor = plugin.createMonitor()) {
+ open.trace.save("Save on Disconnect", monitor);
}
catch (IOException e) {
Msg.error(this, "Could not save " + open.trace);
@@ -289,6 +298,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
open.trace.release(this);
}
closed.complete(null);
+ plugin.listeners.invoke().disconnected(this);
}
@Override
@@ -344,18 +354,19 @@ public class TraceRmiHandler implements TraceRmiConnection {
protected DomainFile createDeconflictedFile(DomainFolder parent, DomainObject object)
throws InvalidNameException, CancelledException, IOException {
String name = object.getName();
- TaskMonitor monitor = plugin.getTaskMonitor();
- for (int nextId = 1; nextId < 100; nextId++) {
- try {
- return parent.createFile(name, object, monitor);
- }
- catch (DuplicateFileException e) {
- name = object.getName() + "." + nextId;
+ try (CloseableTaskMonitor monitor = plugin.createMonitor()) {
+ for (int nextId = 1; nextId < 100; nextId++) {
+ try {
+ return parent.createFile(name, object, monitor);
+ }
+ catch (DuplicateFileException e) {
+ name = object.getName() + "." + nextId;
+ }
}
+ name = object.getName() + "." + System.currentTimeMillis();
+ // Don't catch it this last time
+ return parent.createFile(name, object, monitor);
}
- name = object.getName() + "." + System.currentTimeMillis();
- // Don't catch it this last time
- return parent.createFile(name, object, monitor);
}
public void start() {
@@ -911,16 +922,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
OpenTrace open = requireOpenTrace(req.getOid());
long snap = req.getSnap().getSnap();
- /**
- * TODO: Is this composition of laziness upon laziness efficient enough?
- *
- *
- * Can experiment with ordering of address-set-view "expression" to optimize early
- * termination.
- *
- *
- * Want addresses satisfying {@code known | (readOnly & everKnown)}
- */
+ // Want addresses satisfying {@code known | (readOnly & everKnown)}
TraceMemoryManager memoryManager = open.trace.getMemoryManager();
AddressSetView readOnly =
memoryManager.getRegionsAddressSetWith(snap, r -> !r.isWrite());
@@ -939,8 +941,9 @@ public class TraceRmiHandler implements TraceRmiConnection {
dis.setInitialContext(DebuggerDisassemblerPlugin.deriveAlternativeDefaultContext(
host.getLanguage(), host.getLanguage().getLanguageID(), start));
- TaskMonitor monitor = plugin.getTaskMonitor();
- dis.applyToTyped(open.trace.getFixedProgramView(snap), monitor);
+ try (CloseableTaskMonitor monitor = plugin.createMonitor()) {
+ dis.applyToTyped(open.trace.getFixedProgramView(snap), monitor);
+ }
return ReplyDisassemble.newBuilder()
.setLength(dis.getDisassembledAddressSet().getNumAddresses())
@@ -1027,8 +1030,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
new SchemaName(m.getReturnType().getName()));
methodRegistry.add(rm);
}
- negotiate.complete(null);
- return ReplyNegotiate.getDefaultInstance();
+ negotiate.complete(req.getDescription());
+ return ReplyNegotiate.newBuilder()
+ .setDescription(Application.getName() + " " + Application.getApplicationVersion())
+ .build();
}
protected ReplyPutBytes handlePutBytes(RequestPutBytes req) {
@@ -1110,7 +1115,9 @@ public class TraceRmiHandler implements TraceRmiConnection {
protected ReplySaveTrace handleSaveTrace(RequestSaveTrace req)
throws CancelledException, IOException {
OpenTrace open = requireOpenTrace(req.getOid());
- open.trace.save("TraceRMI", plugin.getTaskMonitor());
+ try (CloseableTaskMonitor monitor = plugin.createMonitor()) {
+ open.trace.save("TraceRMI", monitor);
+ }
return ReplySaveTrace.getDefaultInstance();
}
@@ -1271,9 +1278,25 @@ public class TraceRmiHandler implements TraceRmiConnection {
return openTraces.getByTrace(trace) != null;
}
+ @Override
+ public Collection getTargets() {
+ return openTraces.getTargets();
+ }
+
public void registerTerminals(Collection terminals) {
synchronized (this.terminals) {
this.terminals.addAll(terminals);
}
}
+
+ @Override
+ public String getDescription() {
+ // NOTE: Negotiation happens during construction, so unless this is called internally,
+ // or there's some error, we should always have a read description.
+ String description = negotiate.getNow("(Negotiating...)");
+ if (description.isBlank()) {
+ return "Trace RMI";
+ }
+ return description;
+ }
}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiPlugin.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiPlugin.java
similarity index 66%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiPlugin.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiPlugin.java
index c776a1929e..7750e34924 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiPlugin.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiPlugin.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import java.io.IOException;
import java.net.*;
@@ -24,14 +24,16 @@ import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
import ghidra.app.services.*;
-import ghidra.debug.api.tracermi.TraceRmiConnection;
+import ghidra.debug.api.progress.CloseableTaskMonitor;
+import ghidra.debug.api.tracermi.*;
+import ghidra.debug.api.tracermi.TraceRmiServiceListener.ConnectMode;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.AutoService.Wiring;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.util.Swing;
+import ghidra.util.datastruct.ListenerSet;
import ghidra.util.task.ConsoleTaskMonitor;
-import ghidra.util.task.TaskMonitor;
@PluginInfo(
shortDescription = "Connect to back-end debuggers via Trace RMI",
@@ -56,26 +58,41 @@ import ghidra.util.task.TaskMonitor;
public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
private static final int DEFAULT_PORT = 15432;
+ static class FallbackTaskMonitor extends ConsoleTaskMonitor implements CloseableTaskMonitor {
+ @Override
+ public void close() {
+ // Nothing
+ }
+ }
+
@AutoServiceConsumed
private DebuggerTargetService targetService;
+ @AutoServiceConsumed
+ private ProgressService progressService;
@SuppressWarnings("unused")
private final Wiring autoServiceWiring;
- private final TaskMonitor monitor = new ConsoleTaskMonitor();
-
private SocketAddress serverAddress = new InetSocketAddress("0.0.0.0", DEFAULT_PORT);
private TraceRmiServer server;
private final Set handlers = new LinkedHashSet<>();
+ private final Set acceptors = new LinkedHashSet<>();
+
+ final ListenerSet listeners =
+ new ListenerSet<>(TraceRmiServiceListener.class, true);
+
+ private final CloseableTaskMonitor fallbackMonitor = new FallbackTaskMonitor();
public TraceRmiPlugin(PluginTool tool) {
super(tool);
autoServiceWiring = AutoService.wireServicesProvidedAndConsumed(this);
}
- public TaskMonitor getTaskMonitor() {
- // TODO: Create one in the Debug Console?
- return monitor;
+ protected CloseableTaskMonitor createMonitor() {
+ if (progressService == null) {
+ return fallbackMonitor;
+ }
+ return progressService.publishTask();
}
@Override
@@ -102,14 +119,16 @@ public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
}
server = new TraceRmiServer(this, serverAddress);
server.start();
+ listeners.invoke().serverStarted(server.getAddress());
}
@Override
public void stopServer() {
if (server != null) {
server.close();
+ server = null;
+ listeners.invoke().serverStopped();
}
- server = null;
}
@Override
@@ -124,6 +143,7 @@ public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
socket.connect(address);
TraceRmiHandler handler = new TraceRmiHandler(this, socket);
handler.start();
+ listeners.invoke().connected(handler, ConnectMode.CONNECT, null);
return handler;
}
@@ -131,25 +151,52 @@ public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
public DefaultTraceRmiAcceptor acceptOne(SocketAddress address) throws IOException {
DefaultTraceRmiAcceptor acceptor = new DefaultTraceRmiAcceptor(this, address);
acceptor.start();
+ listeners.invoke().waitingAccept(acceptor);
return acceptor;
}
void addHandler(TraceRmiHandler handler) {
- handlers.add(handler);
+ synchronized (handlers) {
+ handlers.add(handler);
+ }
}
void removeHandler(TraceRmiHandler handler) {
- handlers.remove(handler);
+ synchronized (handlers) {
+ handlers.remove(handler);
+ }
}
@Override
public Collection getAllConnections() {
- return List.copyOf(handlers);
+ synchronized (handlers) {
+ return List.copyOf(handlers);
+ }
}
- void publishTarget(TraceRmiTarget target) {
+ void addAcceptor(DefaultTraceRmiAcceptor acceptor) {
+ synchronized (acceptors) {
+ acceptors.add(acceptor);
+ }
+ }
+
+ void removeAcceptor(DefaultTraceRmiAcceptor acceptor) {
+ synchronized (acceptors) {
+ acceptors.remove(acceptor);
+ }
+ }
+
+ @Override
+ public Collection getAllAcceptors() {
+ synchronized (acceptors) {
+ return List.copyOf(acceptors);
+ }
+ }
+
+ void publishTarget(TraceRmiHandler handler, TraceRmiTarget target) {
Swing.runIfSwingOrRunLater(() -> {
targetService.publishTarget(target);
+ listeners.invoke().targetPublished(handler, target);
});
}
@@ -158,4 +205,14 @@ public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
targetService.withdrawTarget(target);
});
}
+
+ @Override
+ public void addTraceServiceListener(TraceRmiServiceListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeTraceServiceListener(TraceRmiServiceListener listener) {
+ listeners.remove(listener);
+ }
}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiServer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiServer.java
new file mode 100644
index 0000000000..b59a7ec4fc
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiServer.java
@@ -0,0 +1,65 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.service.tracermi;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+
+import ghidra.debug.api.tracermi.TraceRmiServiceListener.ConnectMode;
+import ghidra.util.Msg;
+
+public class TraceRmiServer extends AbstractTraceRmiListener {
+ public TraceRmiServer(TraceRmiPlugin plugin, SocketAddress address) {
+ super(plugin, address);
+ }
+
+ @Override
+ protected void bind() throws IOException {
+ socket.bind(address);
+ }
+
+ @Override
+ protected void startServiceLoop() {
+ new Thread(this::serviceLoop, "trace-rmi server " + socket.getLocalSocketAddress()).start();
+ }
+
+ @Override
+ protected ConnectMode getConnectMode() {
+ return ConnectMode.SERVER;
+ }
+
+ @SuppressWarnings("resource")
+ protected void serviceLoop() {
+ try {
+ doAccept(null);
+ }
+ catch (IOException e) {
+ if (socket.isClosed()) {
+ return;
+ }
+ Msg.error("Error accepting TraceRmi client", e);
+ return;
+ }
+ finally {
+ try {
+ socket.close();
+ }
+ catch (IOException e) {
+ Msg.error("Error closing TraceRmi service", e);
+ }
+ }
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiTarget.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java
similarity index 99%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiTarget.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java
index cc50c3d321..0583adb39b 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiTarget.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import java.io.IOException;
import java.util.*;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueDecoder.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/ValueDecoder.java
similarity index 98%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueDecoder.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/ValueDecoder.java
index 4d45549116..2d5bd1fb60 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueDecoder.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/ValueDecoder.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import org.apache.commons.lang3.ArrayUtils;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueSupplier.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/ValueSupplier.java
similarity index 93%
rename from Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueSupplier.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/ValueSupplier.java
index e4e9d7cbf8..22ea14119d 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/ValueSupplier.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/ValueSupplier.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import ghidra.program.model.address.AddressOverflowException;
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/services/InternalTraceRmiService.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/services/InternalTraceRmiService.java
index 056f0d74a0..be10ea13a8 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/services/InternalTraceRmiService.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/services/InternalTraceRmiService.java
@@ -18,8 +18,8 @@ package ghidra.app.services;
import java.io.IOException;
import java.net.SocketAddress;
-import ghidra.app.plugin.core.debug.service.rmi.trace.DefaultTraceRmiAcceptor;
-import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
+import ghidra.app.plugin.core.debug.service.tracermi.DefaultTraceRmiAcceptor;
+import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler;
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
/**
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/debug/spi/tracermi/TraceRmiLaunchOpinion.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/debug/spi/tracermi/TraceRmiLaunchOpinion.java
index 28e1f57164..6d9388d074 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/debug/spi/tracermi/TraceRmiLaunchOpinion.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/debug/spi/tracermi/TraceRmiLaunchOpinion.java
@@ -18,7 +18,7 @@ package ghidra.debug.spi.tracermi;
import java.util.Collection;
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin;
-import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler;
+import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler;
import ghidra.app.services.InternalTraceRmiService;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.framework.options.Options;
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 21220af85e..9506f8d549 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
@@ -427,9 +427,11 @@ message Method {
message RequestNegotiate {
string version = 1;
repeated Method methods = 2;
+ string description = 3;
}
message ReplyNegotiate {
+ string description = 1;
}
message XRequestInvokeMethod {
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 acc1eb4da2..0b40632531 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
@@ -720,7 +720,7 @@ class Client(object):
return Client._read_obj_desc(msg.child_desc), sch.OBJECT
raise ValueError("Could not read value: {}".format(msg))
- def __init__(self, s, method_registry: MethodRegistry):
+ def __init__(self, s, description: str, method_registry: MethodRegistry):
self._traces = {}
self._next_trace_id = 1
self.tlock = Lock()
@@ -732,7 +732,7 @@ class Client(object):
self.slock = Lock()
self.receiver.start()
self._method_registry = method_registry
- self._negotiate()
+ self.description = self._negotiate(description)
def close(self):
self.s.close()
@@ -1083,15 +1083,16 @@ class Client(object):
return reply.length
return self._batch_or_now(root, 'reply_disassemble', _handle)
- def _negotiate(self):
+ def _negotiate(self, description: str):
root = bufs.RootMessage()
root.request_negotiate.version = VERSION
+ root.request_negotiate.description = description
self._write_methods(root.request_negotiate.methods,
self._method_registry._methods.values())
def _handle(reply):
- pass
- self._now(root, 'reply_negotiate', _handle)
+ return reply.description
+ return self._now(root, 'reply_negotiate', _handle)
def _handle_invoke_method(self, request):
if request.HasField('oid'):
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProviderTest.java b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProviderTest.java
new file mode 100644
index 0000000000..ea280b5762
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/TraceRmiConnectionManagerProviderTest.java
@@ -0,0 +1,416 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.tracermi.connection;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.Map;
+import java.util.concurrent.*;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+import generic.Unique;
+import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
+import ghidra.app.plugin.core.debug.gui.objects.components.InvocationDialogHelper;
+import ghidra.app.plugin.core.debug.gui.tracermi.connection.tree.*;
+import ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin;
+import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiClient;
+import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiClient.Tx;
+import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin;
+import ghidra.app.services.DebuggerControlService;
+import ghidra.app.services.TraceRmiService;
+import ghidra.dbg.target.schema.SchemaContext;
+import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
+import ghidra.dbg.target.schema.XmlSchemaContext;
+import ghidra.debug.api.control.ControlMode;
+import ghidra.debug.api.target.Target;
+import ghidra.debug.api.tracermi.TraceRmiAcceptor;
+import ghidra.debug.api.tracermi.TraceRmiConnection;
+import ghidra.util.exception.CancelledException;
+
+public class TraceRmiConnectionManagerProviderTest extends AbstractGhidraHeadedDebuggerTest {
+ TraceRmiConnectionManagerProvider provider;
+ TraceRmiService traceRmiService;
+ DebuggerControlService controlService;
+
+ @Before
+ public void setUpConnectionManager() throws Exception {
+ controlService = addPlugin(tool, DebuggerControlServicePlugin.class);
+ traceRmiService = addPlugin(tool, TraceRmiPlugin.class);
+ addPlugin(tool, TraceRmiConnectionManagerPlugin.class);
+ provider = waitForComponentProvider(TraceRmiConnectionManagerProvider.class);
+ }
+
+ @Test
+ public void testActionAccept() throws Exception {
+ performEnabledAction(provider, provider.actionConnectAccept, false);
+ InvocationDialogHelper helper = InvocationDialogHelper.waitFor();
+ helper.dismissWithArguments(Map.ofEntries(
+ Map.entry("address", "localhost"),
+ Map.entry("port", 0)));
+ waitForPass(() -> Unique.assertOne(traceRmiService.getAllAcceptors()));
+ }
+
+ @Test
+ public void testActionConnect() throws Exception {
+ try (ServerSocketChannel server = ServerSocketChannel.open()) {
+ server.bind(new InetSocketAddress("localhost", 0), 1);
+ if (!(server.getLocalAddress() instanceof InetSocketAddress sockaddr)) {
+ throw new AssertionError();
+ }
+ performEnabledAction(provider, provider.actionConnectOutbound, false);
+ InvocationDialogHelper helper = InvocationDialogHelper.waitFor();
+ helper.dismissWithArguments(Map.ofEntries(
+ Map.entry("address", sockaddr.getHostString()),
+ Map.entry("port", sockaddr.getPort())));
+ try (SocketChannel channel = server.accept()) {
+ TestTraceRmiClient client = new TestTraceRmiClient(channel);
+ client.sendNegotiate("Test client");
+ client.recvNegotiate();
+ waitForPass(() -> Unique.assertOne(traceRmiService.getAllConnections()));
+ }
+ }
+ }
+
+ @Test
+ public void testActionStartServer() throws Exception {
+ performEnabledAction(provider, provider.actionStartServer, false);
+ InvocationDialogHelper helper = InvocationDialogHelper.waitFor();
+ helper.dismissWithArguments(Map.ofEntries(
+ Map.entry("address", "localhost"),
+ Map.entry("port", 0)));
+ waitForPass(() -> assertTrue(traceRmiService.isServerStarted()));
+ waitForPass(() -> assertFalse(provider.actionStartServer.isEnabled()));
+
+ traceRmiService.stopServer();
+ waitForPass(() -> assertTrue(provider.actionStartServer.isEnabled()));
+ }
+
+ @Test
+ public void testActionStopServer() throws Exception {
+ waitForPass(() -> assertFalse(provider.actionStopServer.isEnabled()));
+ traceRmiService.startServer();
+ waitForSwing();
+ performEnabledAction(provider, provider.actionStopServer, true);
+ assertFalse(traceRmiService.isServerStarted());
+
+ waitForPass(() -> assertFalse(provider.actionStopServer.isEnabled()));
+ }
+
+ @Test
+ public void testActionCloseOnAcceptor() throws Exception {
+ TraceRmiAcceptor acceptor =
+ traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
+ TraceRmiAcceptorNode node =
+ TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode).get(acceptor);
+ assertNotNull(node);
+ provider.tree.setSelectedNode(node);
+ // Tree uses a task queue for selection requests
+ waitForPass(() -> assertEquals(node, Unique.assertOne(provider.tree.getSelectedNodes())));
+
+ performEnabledAction(provider, provider.actionCloseConnection, true);
+ try {
+ acceptor.accept();
+ fail();
+ }
+ catch (CancelledException e) {
+ // pass
+ }
+ }
+
+ @Test
+ public void testActionCloseOnConnection() throws Exception {
+ try (Cx cx = Cx.connect(traceRmiService, "Test client")) {
+ TraceRmiConnectionNode node =
+ TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
+ .get(cx.connection);
+ assertNotNull(node);
+ provider.tree.setSelectedNode(node);
+ // Tree uses a task queue for selection requests
+ waitForPass(
+ () -> assertEquals(node, Unique.assertOne(provider.tree.getSelectedNodes())));
+
+ performEnabledAction(provider, provider.actionCloseConnection, true);
+ waitForPass(() -> assertTrue(cx.connection.isClosed()));
+ }
+ }
+
+ @Test
+ public void testActionCloseAll() throws Exception {
+ traceRmiService.startServer();
+ TraceRmiAcceptor acceptor =
+ traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
+ try (Cx cx = Cx.connect(traceRmiService, "Test client")) {
+ performEnabledAction(provider, provider.actionCloseAll, true);
+
+ waitForPass(() -> assertFalse(traceRmiService.isServerStarted()));
+ waitForPass(() -> assertTrue(cx.connection.isClosed()));
+ try {
+ acceptor.accept();
+ fail();
+ }
+ catch (CancelledException e) {
+ // pass
+ }
+ }
+ }
+
+ @Test
+ public void testServerNode() throws Exception {
+ TraceRmiServerNode node = TraceRmiConnectionTreeHelper.getServerNode(provider.rootNode);
+ assertEquals("Server: CLOSED", node.getDisplayText());
+ traceRmiService.startServer();
+ waitForPass(() -> assertEquals("Server: LISTENING " + traceRmiService.getServerAddress(),
+ node.getDisplayText()));
+ traceRmiService.stopServer();
+ waitForPass(() -> assertEquals("Server: CLOSED", node.getDisplayText()));
+ }
+
+ @Test
+ public void testAcceptHasNode() throws Exception {
+ TraceRmiAcceptor acceptor =
+ traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
+ TraceRmiAcceptorNode node =
+ TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode).get(acceptor);
+ assertNotNull(node);
+ assertEquals("ACCEPTING: " + acceptor.getAddress(), node.getDisplayText());
+ }
+
+ @Test
+ public void testAcceptThenCancelNoNode() throws Exception {
+ TraceRmiAcceptor acceptor =
+ traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
+ assertNotNull(
+ TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode).get(acceptor));
+
+ acceptor.cancel();
+ waitForPass(() -> traceRmiService.getAllAcceptors().isEmpty());
+ assertNull(
+ TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode).get(acceptor));
+ }
+
+ record Cx(SocketChannel channel, TestTraceRmiClient client,
+ TraceRmiConnection connection)
+ implements AutoCloseable {
+ public static Cx complete(TraceRmiAcceptor acceptor, String description)
+ throws IOException, CancelledException {
+ SocketChannel channel = null;
+ TraceRmiConnection connection = null;
+ try {
+ channel = SocketChannel.open(acceptor.getAddress());
+ TestTraceRmiClient client = new TestTraceRmiClient(channel);
+ client.sendNegotiate(description);
+ connection = acceptor.accept();
+ client.recvNegotiate();
+ return new Cx(channel, client, connection);
+ }
+ catch (Throwable t) {
+ if (channel != null) {
+ channel.close();
+ }
+ if (connection != null) {
+ connection.close();
+ }
+ throw t;
+ }
+ }
+
+ public static Cx toServer(TraceRmiService service, String description) throws IOException {
+ SocketChannel channel = null;
+ try {
+ channel = SocketChannel.open(service.getServerAddress());
+ TestTraceRmiClient client = new TestTraceRmiClient(channel);
+ client.sendNegotiate(description);
+ client.recvNegotiate();
+ return new Cx(channel, client,
+ waitForPass(() -> Unique.assertOne(service.getAllConnections())));
+ }
+ catch (Throwable t) {
+ if (channel != null) {
+ channel.close();
+ }
+ throw t;
+ }
+ }
+
+ public static Cx connect(TraceRmiService service, String description)
+ throws IOException, InterruptedException, ExecutionException, TimeoutException {
+ SocketChannel channel = null;
+ CompletableFuture future = null;
+ try (ServerSocketChannel server = ServerSocketChannel.open()) {
+ server.bind(new InetSocketAddress("localhost", 0), 1);
+ future = CompletableFuture.supplyAsync(() -> {
+ try {
+ return service.connect(server.getLocalAddress());
+ }
+ catch (IOException e) {
+ return ExceptionUtils.rethrow(e);
+ }
+ });
+ channel = server.accept();
+ TestTraceRmiClient client = new TestTraceRmiClient(channel);
+ client.sendNegotiate(description);
+ client.recvNegotiate();
+ return new Cx(channel, client, future.get(1, TimeUnit.SECONDS));
+ }
+ catch (Throwable t) {
+ if (channel != null) {
+ channel.close();
+ }
+ throw t;
+ }
+ }
+
+ @Override
+ public void close() throws Exception {
+ connection.close();
+ channel.close();
+ }
+ }
+
+ @Test
+ public void testAcceptThenSuccessNodes() throws Exception {
+ TraceRmiAcceptor acceptor =
+ traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
+ assertNotNull(
+ TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode).get(acceptor));
+
+ try (Cx cx = Cx.complete(acceptor, "Test client")) {
+ waitForPass(() -> traceRmiService.getAllAcceptors().isEmpty());
+ waitForPass(() -> assertNull(
+ TraceRmiConnectionTreeHelper.getAcceptorNodeMap(provider.rootNode)
+ .get(acceptor)));
+ waitForPass(() -> assertEquals(cx.connection,
+ Unique.assertOne(traceRmiService.getAllConnections())));
+
+ TraceRmiConnectionNode node =
+ TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
+ .get(cx.connection);
+ assertNotNull(node);
+ assertEquals("Test client at " + cx.connection.getRemoteAddress(),
+ node.getDisplayText());
+ }
+ }
+
+ @Test
+ public void testServerConnectNode() throws Exception {
+ traceRmiService.startServer();
+ try (Cx cx = Cx.toServer(traceRmiService, "Test client")) {
+ waitForPass(() -> traceRmiService.getAllAcceptors().isEmpty());
+
+ TraceRmiConnectionNode node = waitForValue(
+ () -> TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
+ .get(cx.connection));
+ assertEquals("Test client at " + cx.connection.getRemoteAddress(),
+ node.getDisplayText());
+ }
+ }
+
+ @Test
+ public void testConnectThenSuccessNodes() throws Exception {
+ try (Cx cx = Cx.connect(traceRmiService, "Test client")) {
+ waitForPass(() -> assertEquals(cx.connection,
+ Unique.assertOne(traceRmiService.getAllConnections())));
+
+ TraceRmiConnectionNode node =
+ TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
+ .get(cx.connection);
+ assertNotNull(node);
+ assertEquals("Test client at " + cx.connection.getRemoteAddress(),
+ node.getDisplayText());
+ }
+ }
+
+ @Test
+ public void testFrontEndCloseNoNodes() throws Exception {
+ TraceRmiAcceptor acceptor =
+ traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
+ try (Cx cx = Cx.complete(acceptor, "Test client")) {
+ assertNotNull(TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
+ .get(cx.connection));
+
+ cx.connection.close();
+ waitForPass(() -> assertTrue(traceRmiService.getAllConnections().isEmpty()));
+ waitForPass(() -> assertNull(
+ TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
+ .get(cx.connection)));
+ }
+ }
+
+ @Test
+ public void testBackEndCloseNoNodes() throws Exception {
+ TraceRmiAcceptor acceptor =
+ traceRmiService.acceptOne(new InetSocketAddress("localhost", 0));
+ try (Cx cx = Cx.complete(acceptor, "Test client")) {
+ assertNotNull(TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
+ .get(cx.connection));
+
+ cx.channel.close();
+ waitForPass(() -> assertTrue(traceRmiService.getAllConnections().isEmpty()));
+ waitForPass(() -> assertNull(
+ TraceRmiConnectionTreeHelper.getConnectionNodeMap(provider.rootNode)
+ .get(cx.connection)));
+ }
+ }
+
+ @Test
+ public void testActivateTargetNode() throws Exception {
+ SchemaContext ctx = XmlSchemaContext.deserialize("""
+
+
+
+ """);
+ try (Cx cx = Cx.connect(traceRmiService, "Test client")) {
+ cx.client.createTrace(1, "bash");
+ try (Tx tx = cx.client.new Tx(1, 1, "Create snapshots")) {
+ cx.client.snapshot(1, 0, "First snapshot");
+ cx.client.createRootObject(1, ctx.getSchema(new SchemaName("Root")));
+ cx.client.snapshot(1, 1, "Stepped");
+ }
+ cx.client.activate(1, "");
+ Target target = waitForValue(() -> traceManager.getCurrent().getTarget());
+
+ TraceRmiTargetNode node =
+ TraceRmiConnectionTreeHelper.getTargetNodeMap(provider.rootNode).get(target);
+ assertEquals("bash (snap=1)", node.getDisplayText());
+
+ provider.tree.setSelectedNode(node);
+ // Tree uses a task queue for selection requests
+ waitForPass(
+ () -> assertEquals(node, Unique.assertOne(provider.tree.getSelectedNodes())));
+
+ traceManager.activateSnap(0);
+ waitForPass(() -> {
+ assertEquals(0, traceManager.getCurrentSnap());
+ assertEquals(ControlMode.RO_TRACE,
+ controlService.getCurrentMode(target.getTrace()));
+ });
+
+ triggerEnter(provider.tree);
+ waitForPass(() -> {
+ assertEquals(1, traceManager.getCurrentSnap());
+ assertEquals(ControlMode.RO_TARGET,
+ controlService.getCurrentMode(target.getTrace()));
+ });
+ }
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiConnectionTreeHelper.java b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiConnectionTreeHelper.java
new file mode 100644
index 0000000000..7cceb5dc8a
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/connection/tree/TraceRmiConnectionTreeHelper.java
@@ -0,0 +1,43 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.gui.tracermi.connection.tree;
+
+import java.util.Map;
+
+import ghidra.debug.api.target.Target;
+import ghidra.debug.api.tracermi.TraceRmiAcceptor;
+import ghidra.debug.api.tracermi.TraceRmiConnection;
+
+public class TraceRmiConnectionTreeHelper {
+ public static Map getAcceptorNodeMap(
+ TraceRmiServiceNode serviceNode) {
+ return serviceNode.acceptorNodes;
+ }
+
+ public static Map getConnectionNodeMap(
+ TraceRmiServiceNode serviceNode) {
+ return serviceNode.connectionNodes;
+ }
+
+ public static Map getTargetNodeMap(
+ TraceRmiServiceNode serviceNode) {
+ return serviceNode.targetNodes;
+ }
+
+ public static TraceRmiServerNode getServerNode(TraceRmiServiceNode serviceNode) {
+ return serviceNode.serverNode;
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/ProtobufSocket.java b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/ProtobufSocket.java
new file mode 100644
index 0000000000..baf3a70165
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/ProtobufSocket.java
@@ -0,0 +1,69 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.service.tracermi;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+public class ProtobufSocket {
+ public interface Decoder {
+ T decode(ByteBuffer buf) throws InvalidProtocolBufferException;
+ }
+
+ private final ByteBuffer lenSend = ByteBuffer.allocate(4);
+ private final ByteBuffer lenRecv = ByteBuffer.allocate(4);
+ private final SocketChannel channel;
+ private final Decoder decoder;
+
+ public ProtobufSocket(SocketChannel channel, Decoder decoder) {
+ this.channel = channel;
+ this.decoder = decoder;
+ }
+
+ public void send(T msg) throws IOException {
+ synchronized (lenSend) {
+ lenSend.clear();
+ lenSend.putInt(msg.getSerializedSize());
+ lenSend.flip();
+ channel.write(lenSend);
+ for (ByteBuffer buf : msg.toByteString().asReadOnlyByteBufferList()) {
+ channel.write(buf);
+ }
+ }
+ }
+
+ public T recv() throws IOException {
+ synchronized (lenRecv) {
+ lenRecv.clear();
+ while (lenRecv.hasRemaining()) {
+ channel.read(lenRecv);
+ }
+ lenRecv.flip();
+ int len = lenRecv.getInt();
+ // This is just for testing, so littering on the heap is okay.
+ ByteBuffer buf = ByteBuffer.allocate(len);
+ while (buf.hasRemaining()) {
+ channel.read(buf);
+ }
+ buf.flip();
+ return decoder.decode(buf);
+ }
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiClient.java b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiClient.java
new file mode 100644
index 0000000000..2a6bdac2ae
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiClient.java
@@ -0,0 +1,162 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.service.tracermi;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.channels.SocketChannel;
+
+import ghidra.dbg.target.schema.TargetObjectSchema;
+import ghidra.dbg.target.schema.XmlSchemaContext;
+import ghidra.framework.Application;
+import ghidra.rmi.trace.TraceRmi.*;
+import ghidra.rmi.trace.TraceRmi.Compiler;
+
+public class TestTraceRmiClient {
+ final ProtobufSocket socket;
+
+ public TestTraceRmiClient(SocketChannel channel) {
+ this.socket = new ProtobufSocket<>(channel, RootMessage::parseFrom);
+ }
+
+ public void sendNegotiate(String description) throws IOException {
+ socket.send(RootMessage.newBuilder()
+ .setRequestNegotiate(RequestNegotiate.newBuilder()
+ .setVersion(TraceRmiHandler.VERSION)
+ .setDescription(description))
+ .build());
+ }
+
+ public void recvNegotiate() throws IOException {
+ assertEquals(RootMessage.newBuilder()
+ .setReplyNegotiate(ReplyNegotiate.newBuilder()
+ .setDescription(
+ Application.getName() + " " +
+ Application.getApplicationVersion()))
+ .build(),
+ socket.recv());
+ }
+
+ public void createTrace(int id, String name) throws IOException {
+ socket.send(RootMessage.newBuilder()
+ .setRequestCreateTrace(RequestCreateTrace.newBuilder()
+ .setOid(DomObjId.newBuilder()
+ .setId(id))
+ .setLanguage(Language.newBuilder()
+ .setId("Toy:BE:64:default"))
+ .setCompiler(Compiler.newBuilder()
+ .setId("default"))
+ .setPath(FilePath.newBuilder()
+ .setPath("test/" + name)))
+ .build());
+ assertEquals(RootMessage.newBuilder()
+ .setReplyCreateTrace(ReplyCreateTrace.newBuilder())
+ .build(),
+ socket.recv());
+ }
+
+ public void startTx(int traceId, int txId, String description) throws IOException {
+ socket.send(RootMessage.newBuilder()
+ .setRequestStartTx(RequestStartTx.newBuilder()
+ .setOid(DomObjId.newBuilder()
+ .setId(traceId))
+ .setTxid(TxId.newBuilder().setId(txId))
+ .setDescription(description))
+ .build());
+ assertEquals(RootMessage.newBuilder()
+ .setReplyStartTx(ReplyStartTx.newBuilder())
+ .build(),
+ socket.recv());
+ }
+
+ public void endTx(int traceId, int txId) throws IOException {
+ socket.send(RootMessage.newBuilder()
+ .setRequestEndTx(RequestEndTx.newBuilder()
+ .setOid(DomObjId.newBuilder()
+ .setId(traceId))
+ .setTxid(TxId.newBuilder().setId(txId))
+ .setAbort(false))
+ .build());
+ assertEquals(RootMessage.newBuilder()
+ .setReplyEndTx(ReplyEndTx.newBuilder())
+ .build(),
+ socket.recv());
+ }
+
+ public class Tx implements AutoCloseable {
+ private final int traceId;
+ private final int txId;
+
+ public Tx(int traceId, int txId, String description) throws IOException {
+ this.traceId = traceId;
+ this.txId = txId;
+ startTx(traceId, txId, description);
+ }
+
+ @Override
+ public void close() throws Exception {
+ endTx(traceId, txId);
+ }
+ }
+
+ public void snapshot(int traceId, long snap, String description) throws IOException {
+ socket.send(RootMessage.newBuilder()
+ .setRequestSnapshot(RequestSnapshot.newBuilder()
+ .setOid(DomObjId.newBuilder()
+ .setId(traceId))
+ .setSnap(Snap.newBuilder()
+ .setSnap(snap))
+ .setDescription(description))
+ .build());
+ assertEquals(RootMessage.newBuilder()
+ .setReplySnapshot(ReplySnapshot.newBuilder())
+ .build(),
+ socket.recv());
+ }
+
+ public void createRootObject(int traceId, TargetObjectSchema schema) throws IOException {
+ String xmlCtx = XmlSchemaContext.serialize(schema.getContext());
+ socket.send(RootMessage.newBuilder()
+ .setRequestCreateRootObject(RequestCreateRootObject.newBuilder()
+ .setOid(DomObjId.newBuilder()
+ .setId(traceId))
+ .setSchemaContext(xmlCtx)
+ .setRootSchema(schema.getName().toString()))
+ .build());
+ assertEquals(RootMessage.newBuilder()
+ .setReplyCreateObject(ReplyCreateObject.newBuilder()
+ .setObject(ObjSpec.newBuilder()
+ .setId(0)))
+ .build(),
+ socket.recv());
+ }
+
+ public void activate(int traceId, String path) throws IOException {
+ socket.send(RootMessage.newBuilder()
+ .setRequestActivate(RequestActivate.newBuilder()
+ .setOid(DomObjId.newBuilder()
+ .setId(traceId))
+ .setObject(ObjSpec.newBuilder()
+ .setPath(ObjPath.newBuilder()
+ .setPath(path))))
+ .build());
+ assertEquals(RootMessage.newBuilder()
+ .setReplyActivate(ReplyActivate.newBuilder())
+ .build(),
+ socket.recv());
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/rmi/trace/TestTraceRmiConnection.java b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java
similarity index 95%
rename from Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/rmi/trace/TestTraceRmiConnection.java
rename to Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java
index 088f29763f..a54fc9c2a7 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/rmi/trace/TestTraceRmiConnection.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.app.plugin.core.debug.service.rmi.trace;
+package ghidra.app.plugin.core.debug.service.tracermi;
import java.io.IOException;
import java.net.InetSocketAddress;
@@ -28,6 +28,7 @@ import ghidra.async.AsyncPairingQueue;
import ghidra.async.AsyncUtils;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.debug.api.target.ActionName;
+import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracermi.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
@@ -89,6 +90,11 @@ public class TestTraceRmiConnection implements TraceRmiConnection {
}
}
+ @Override
+ public String getDescription() {
+ return "Test Trace RMI connnection";
+ }
+
@Override
public SocketAddress getRemoteAddress() {
return new InetSocketAddress("localhost", 0);
@@ -185,4 +191,9 @@ public class TestTraceRmiConnection implements TraceRmiConnection {
throw new AssertionError(e);
}
}
+
+ @Override
+ public Collection getTargets() {
+ return List.copyOf(targets.values());
+ }
}
diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest
index bf1ca8156f..6210059af1 100644
--- a/Ghidra/Debug/Debugger/certification.manifest
+++ b/Ghidra/Debug/Debugger/certification.manifest
@@ -107,6 +107,8 @@ src/main/resources/images/breakpoints-disable-all.png||GHIDRA||||END|
src/main/resources/images/breakpoints-enable-all.png||GHIDRA||||END|
src/main/resources/images/breakpoints-make-effective.png||GHIDRA||||END|
src/main/resources/images/conf.png||GHIDRA||||END|
+src/main/resources/images/connect-accept.png||GHIDRA||||END|
+src/main/resources/images/connect-outbound.png||GHIDRA||||END|
src/main/resources/images/connect.png||GHIDRA||||END|
src/main/resources/images/console.png||GHIDRA||||END|
src/main/resources/images/debugger.png||GHIDRA||||END|
@@ -157,6 +159,8 @@ src/main/svg/breakpoints-clear-all.svg||GHIDRA||||END|
src/main/svg/breakpoints-disable-all.svg||GHIDRA||||END|
src/main/svg/breakpoints-enable-all.svg||GHIDRA||||END|
src/main/svg/breakpoints-make-effective.svg||GHIDRA||||END|
+src/main/svg/connect-accept.svg||GHIDRA||||END|
+src/main/svg/connect-outbound.svg||GHIDRA||||END|
src/main/svg/connect.svg||GHIDRA||||END|
src/main/svg/console.svg||GHIDRA||||END|
src/main/svg/debugger.svg||GHIDRA||||END|
diff --git a/Ghidra/Debug/Debugger/data/debugger.theme.properties b/Ghidra/Debug/Debugger/data/debugger.theme.properties
index 9c160f1bed..412c00bf7a 100644
--- a/Ghidra/Debug/Debugger/data/debugger.theme.properties
+++ b/Ghidra/Debug/Debugger/data/debugger.theme.properties
@@ -89,6 +89,8 @@ icon.debugger.tree.object = icon.debugger.object.unpopulated
icon.debugger = debugger.png
icon.debugger.connect = connect.png
+icon.debugger.connect.accept = connect-accept.png
+icon.debugger.connect.outbound = connect-outbound.png
icon.debugger.disconnect = disconnect.png
icon.debugger.process = process.png
icon.debugger.thread = thread.png
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java
index 0b5c9b7ba5..6521466d92 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java
@@ -68,6 +68,8 @@ public interface DebuggerResources {
Icon ICON_DEBUGGER = new GIcon("icon.debugger");
Icon ICON_CONNECTION = new GIcon("icon.debugger.connect");
+ Icon ICON_CONNECT_ACCEPT = new GIcon("icon.debugger.connect.accept");
+ Icon ICON_CONNECT_OUTBOUND = new GIcon("icon.debugger.connect.outbound");
Icon ICON_DISCONNECT = new GIcon("icon.debugger.disconnect");
Icon ICON_PROCESS = new GIcon("icon.debugger.process");
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellEditor.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellEditor.java
index 866fb4f53b..c2b8708d5c 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellEditor.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellEditor.java
@@ -15,6 +15,7 @@
*/
package ghidra.app.plugin.core.debug.gui.console;
+import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@@ -22,8 +23,10 @@ import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
+import javax.swing.border.Border;
import javax.swing.table.TableCellEditor;
+import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.ActionList;
import ghidra.app.plugin.core.debug.gui.console.DebuggerConsoleProvider.BoundAction;
@@ -36,8 +39,13 @@ public class ConsoleActionsCellEditor extends AbstractCellEditor
protected ActionList value;
+ protected Color bg = new Color(0); // Initial cached value
+
public ConsoleActionsCellEditor() {
ConsoleActionsCellRenderer.configureBox(box);
+ Border innerBorder = BorderFactory.createEmptyBorder(0, 4, 0, 4);
+ Border outerBorder = BorderFactory.createLineBorder(Palette.YELLOW, 1);
+ box.setBorder(BorderFactory.createCompoundBorder(outerBorder, innerBorder));
}
@Override
@@ -49,7 +57,11 @@ public class ConsoleActionsCellEditor extends AbstractCellEditor
public Component getTableCellEditorComponent(JTable table, Object v, boolean isSelected,
int row, int column) {
// I can't think of when you'd be "editing" a non-selected cell.
- box.setBackground(table.getSelectionBackground());
+ if (bg.getRGB() != table.getSelectionBackground().getRGB()) {
+ bg = new Color(table.getSelectionBackground().getRGB());
+ }
+ box.setBackground(bg);
+ box.setOpaque(true);
value = (ActionList) v;
ConsoleActionsCellRenderer.populateBox(box, buttonCache, value,
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java
index 531c03eb3d..d7e513eef2 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/ConsoleActionsCellRenderer.java
@@ -84,6 +84,7 @@ public class ConsoleActionsCellRenderer extends AbstractGhidraColumnRenderer {
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java
index 1c4162222c..d17a64a152 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsolePlugin.java
@@ -152,7 +152,7 @@ public class DebuggerConsolePlugin extends Plugin implements DebuggerConsoleServ
* @param ctx the context
* @return the the log entry
*/
- public LogRow getLogRow(ActionContext ctx) {
+ public LogRow> getLogRow(ActionContext ctx) {
return provider.getLogRow(ctx);
}
}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java
index 0b2a694df7..4b4e9a534e 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProvider.java
@@ -32,23 +32,29 @@ import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.LogEvent;
import docking.*;
-import docking.action.DockingAction;
-import docking.action.DockingActionIf;
+import docking.action.*;
import docking.actions.PopupActionProvider;
import docking.widgets.table.ColumnSortState.SortDirection;
import docking.widgets.table.CustomToStringCellRenderer;
import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn;
+import generic.theme.GIcon;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.ClearAction;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.SelectNoneAction;
import ghidra.app.plugin.core.debug.utils.DebouncedRowWrappedEnumeratedColumnTableModel;
+import ghidra.app.services.ProgressService;
+import ghidra.debug.api.progress.MonitorReceiver;
+import ghidra.debug.api.progress.ProgressListener;
import ghidra.framework.options.AutoOptions;
import ghidra.framework.options.annotation.*;
import ghidra.framework.plugintool.*;
+import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.util.*;
import ghidra.util.table.GhidraTable;
import ghidra.util.table.GhidraTableFilterPanel;
+import ghidra.util.table.column.GColumnRenderer;
+import resources.Icons;
public class DebuggerConsoleProvider extends ComponentProviderAdapter
implements PopupActionProvider {
@@ -57,19 +63,37 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
new Dimension(ACTION_BUTTON_SIZE, ACTION_BUTTON_SIZE);
static final int MIN_ROW_HEIGHT = 16;
- protected enum LogTableColumns implements EnumeratedTableColumn {
- LEVEL("Level", Icon.class, LogRow::getIcon, SortDirection.ASCENDING, false),
- MESSAGE("Message", String.class, LogRow::getMessage, SortDirection.ASCENDING, false),
- ACTIONS("Actions", ActionList.class, LogRow::getActions, SortDirection.DESCENDING, true),
- TIME("Time", Date.class, LogRow::getDate, SortDirection.DESCENDING, false);
+ protected enum LogTableColumns implements EnumeratedTableColumn> {
+ ICON("Icon", Icon.class, LogRow::getIcon, SortDirection.ASCENDING, false),
+ MESSAGE("Message", Object.class, LogRow::getMessage, SortDirection.ASCENDING, false) {
+ @Override
+ public GColumnRenderer> getRenderer() {
+ return HtmlOrProgressCellRenderer.INSTANCE;
+ }
+ },
+ ACTIONS("Actions", ActionList.class, LogRow::getActions, SortDirection.DESCENDING, true) {
+ private static final ConsoleActionsCellRenderer RENDERER =
+ new ConsoleActionsCellRenderer();
+
+ @Override
+ public GColumnRenderer> getRenderer() {
+ return RENDERER;
+ }
+ },
+ TIME("Time", Date.class, LogRow::getDate, SortDirection.DESCENDING, false) {
+ @Override
+ public GColumnRenderer> getRenderer() {
+ return CustomToStringCellRenderer.TIME_24HMSms;
+ }
+ };
private final String header;
- private final Function getter;
+ private final Function, ?> getter;
private final Class> cls;
private final SortDirection defaultSortDirection;
private final boolean editable;
- LogTableColumns(String header, Class cls, Function getter,
+ LogTableColumns(String header, Class cls, Function, T> getter,
SortDirection defaultSortDirection, boolean editable) {
this.header = header;
this.cls = cls;
@@ -89,17 +113,17 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
}
@Override
- public Object getValueOf(LogRow row) {
+ public Object getValueOf(LogRow> row) {
return getter.apply(row);
}
@Override
- public boolean isEditable(LogRow row) {
+ public boolean isEditable(LogRow> row) {
return editable;
}
@Override
- public void setValueOf(LogRow row, Object value) {
+ public void setValueOf(LogRow> row, Object value) {
}
@Override
@@ -164,14 +188,26 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
*
* This class is public for access by test cases only.
*/
- public static class LogRow {
+ public interface LogRow {
+ Icon getIcon();
+
+ T getMessage();
+
+ ActionList getActions();
+
+ Date getDate();
+
+ ActionContext getActionContext();
+ }
+
+ static class MessageLogRow implements LogRow {
private final Icon icon;
private final String message;
private final Date date;
private final ActionContext context;
private final ActionList actions;
- public LogRow(Icon icon, String message, Date date, ActionContext context,
+ public MessageLogRow(Icon icon, String message, Date date, ActionContext context,
ActionList actions) {
this.icon = icon;
this.message = message;
@@ -180,32 +216,154 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
this.actions = Objects.requireNonNull(actions);
}
+ @Override
public Icon getIcon() {
return icon;
}
+ @Override
public String getMessage() {
return message;
}
+ @Override
public Date getDate() {
return date;
}
+ @Override
public ActionContext getActionContext() {
return context;
}
+ @Override
public ActionList getActions() {
return actions;
}
}
+ static class MonitorLogRow implements LogRow {
+ static final GIcon ICON = new GIcon("icon.pending");
+
+ private final MonitorReceiver monitor;
+ private final Date date;
+ private final ActionContext context;
+ private final ActionList actions;
+
+ public MonitorLogRow(MonitorReceiver monitor, Date date, ActionContext context,
+ ActionList actions) {
+ this.monitor = monitor;
+ this.date = date;
+ this.context = context;
+ this.actions = Objects.requireNonNull(actions);
+ }
+
+ @Override
+ public Icon getIcon() {
+ return ICON;
+ }
+
+ @Override
+ public MonitorReceiver getMessage() {
+ return monitor;
+ }
+
+ @Override
+ public ActionList getActions() {
+ return actions;
+ }
+
+ @Override
+ public Date getDate() {
+ return date;
+ }
+
+ @Override
+ public ActionContext getActionContext() {
+ return context;
+ }
+ }
+
+ private class ListenerForProgress implements ProgressListener {
+ final Map contexts = new HashMap<>();
+ CancelAction cancelAction = new CancelAction(plugin);
+
+ ActionContext contextFor(MonitorReceiver monitor) {
+ return contexts.computeIfAbsent(monitor, MonitorRowConsoleActionContext::new);
+ }
+
+ ActionList bindActions(ActionContext context) {
+ ActionList actions = new ActionList();
+ actions.add(new BoundAction(cancelAction, context));
+ return actions;
+ }
+
+ @Override
+ public void monitorCreated(MonitorReceiver monitor) {
+ ActionContext context = contextFor(monitor);
+ logRow(new MonitorLogRow(monitor, new Date(), context, bindActions(context)));
+ }
+
+ @Override
+ public void monitorDisposed(MonitorReceiver monitor, Disposal disposal) {
+ ActionContext context = contexts.remove(monitor);
+ if (context == null) {
+ context = new MonitorRowConsoleActionContext(monitor);
+ }
+ removeFromLog(context);
+ }
+
+ @Override
+ public void messageUpdated(MonitorReceiver monitor, String message) {
+ LogRow> logRow = logTableModel.getMap().get(contextFor(monitor));
+ logTableModel.updateItem(logRow);
+ }
+
+ @Override
+ public void progressUpdated(MonitorReceiver monitor, long progress) {
+ LogRow> logRow = logTableModel.getMap().get(contextFor(monitor));
+ logTableModel.updateItem(logRow);
+ }
+
+ @Override
+ public void attributeUpdated(MonitorReceiver monitor) {
+ LogRow> logRow = logTableModel.getMap().get(contextFor(monitor));
+ logTableModel.updateItem(logRow);
+ }
+ }
+
+ static class CancelAction extends DockingAction {
+ static final Icon ICON = Icons.STOP_ICON;
+
+ public CancelAction(Plugin owner) {
+ super("Cancel", owner.getName());
+ setToolBarData(new ToolBarData(ICON));
+ }
+
+ @Override
+ public void actionPerformed(ActionContext context) {
+ if (!(context instanceof MonitorRowConsoleActionContext ctx)) {
+ return;
+ }
+ ctx.getMonitor().cancel();
+ }
+
+ @Override
+ public boolean isEnabledForContext(ActionContext context) {
+ if (!(context instanceof MonitorRowConsoleActionContext ctx)) {
+ return false;
+ }
+ MonitorReceiver monitor = ctx.getMonitor();
+ return monitor.isCancelEnabled() && !monitor.isCancelled();
+ }
+ }
+
protected static class LogTableModel extends DebouncedRowWrappedEnumeratedColumnTableModel< //
- LogTableColumns, ActionContext, LogRow, LogRow> {
+ LogTableColumns, ActionContext, LogRow>, LogRow>> {
public LogTableModel(PluginTool tool) {
- super(tool, "Log", LogTableColumns.class, r -> r.getActionContext(), r -> r, r -> r);
+ super(tool, "Log", LogTableColumns.class, r -> r == null ? null : r.getActionContext(),
+ r -> r, r -> r);
}
@Override
@@ -215,7 +373,6 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
}
protected static class LogTable extends GhidraTable {
-
public LogTable(LogTableModel model) {
super(model);
}
@@ -255,12 +412,11 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
ActionList actions =
(ActionList) getModel().getValueAt(r, convertColumnIndexToModel(c));
if (actions != null && !actions.isEmpty()) {
- return ACTION_BUTTON_SIZE;
+ return ACTION_BUTTON_SIZE + 2;
}
return 0;
}
- if (renderer instanceof CustomToStringCellRenderer>) {
- CustomToStringCellRenderer> custom = (CustomToStringCellRenderer>) renderer;
+ if (renderer instanceof HtmlOrProgressCellRenderer custom) {
int colWidth = getColumnModel().getColumn(c).getWidth();
prepareRenderer(renderer, r, c);
return custom.getRowHeight(colWidth);
@@ -271,6 +427,8 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
private final DebuggerConsolePlugin plugin;
+ // @AutoServiceConsumed via method
+ private ProgressService progressService;
@SuppressWarnings("unused")
private final AutoService.Wiring autoServiceWiring;
@@ -287,18 +445,21 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
protected final LogTableModel logTableModel;
protected GhidraTable logTable;
- private GhidraTableFilterPanel logFilterPanel;
+ private GhidraTableFilterPanel> logFilterPanel;
- private Deque buffer = new ArrayDeque<>();
+ private Deque> buffer = new ArrayDeque<>();
private final JPanel mainPanel = new JPanel(new BorderLayout());
+ private final ListenerForProgress progressListener;
+
DockingAction actionClear;
DockingAction actionSelectNone;
public DebuggerConsoleProvider(DebuggerConsolePlugin plugin) {
super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_CONSOLE, plugin.getName());
this.plugin = plugin;
+ this.progressListener = new ListenerForProgress();
logTableModel = new LogTableModel(tool);
@@ -329,24 +490,21 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
logFilterPanel = new GhidraTableFilterPanel<>(logTable, logTableModel);
mainPanel.add(logFilterPanel, BorderLayout.NORTH);
- logTable.setRowHeight(ACTION_BUTTON_SIZE);
+ logTable.setRowHeight(ACTION_BUTTON_SIZE + 2);
TableColumnModel columnModel = logTable.getColumnModel();
- TableColumn levelCol = columnModel.getColumn(LogTableColumns.LEVEL.ordinal());
- levelCol.setMaxWidth(24);
- levelCol.setMinWidth(24);
+ TableColumn iconCol = columnModel.getColumn(LogTableColumns.ICON.ordinal());
+ iconCol.setMaxWidth(24);
+ iconCol.setMinWidth(24);
TableColumn msgCol = columnModel.getColumn(LogTableColumns.MESSAGE.ordinal());
msgCol.setPreferredWidth(150);
- msgCol.setCellRenderer(CustomToStringCellRenderer.HTML);
TableColumn actCol = columnModel.getColumn(LogTableColumns.ACTIONS.ordinal());
actCol.setPreferredWidth(50);
- actCol.setCellRenderer(new ConsoleActionsCellRenderer());
actCol.setCellEditor(new ConsoleActionsCellEditor());
TableColumn timeCol = columnModel.getColumn(LogTableColumns.TIME.ordinal());
- timeCol.setCellRenderer(CustomToStringCellRenderer.TIME_24HMSms);
timeCol.setPreferredWidth(15);
}
@@ -362,8 +520,8 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
private void activatedClear(ActionContext ctx) {
synchronized (buffer) {
- logTableModel.clear();
- buffer.clear();
+ logTableModel.deleteItemsWith(r -> !(r instanceof MonitorLogRow));
+ buffer.removeIf(r -> !(r instanceof MonitorLogRow));
}
}
@@ -376,7 +534,7 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
if (logTable.getSelectedRowCount() != 1) {
return super.getActionContext(event);
}
- LogRow sel = logFilterPanel.getSelectedItem();
+ LogRow> sel = logFilterPanel.getSelectedItem();
if (sel == null) {
// I guess this can happen because of timing?
return super.getActionContext(event);
@@ -407,12 +565,13 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
}
protected void log(Icon icon, String message, ActionContext context) {
- logRow(new LogRow(icon, message, new Date(), context, computeToolbarActions(context)));
+ logRow(
+ new MessageLogRow(icon, message, new Date(), context, computeToolbarActions(context)));
}
- protected void logRow(LogRow row) {
+ protected void logRow(LogRow> row) {
synchronized (buffer) {
- LogRow old = logTableModel.deleteKey(row.getActionContext());
+ LogRow> old = logTableModel.deleteKey(row.getActionContext());
if (old != null) {
buffer.remove(old);
}
@@ -438,14 +597,14 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
protected void logEvent(LogEvent event) {
ActionContext context = new LogRowConsoleActionContext();
- logRow(new LogRow(iconForLevel(event.getLevel()),
+ logRow(new MessageLogRow(iconForLevel(event.getLevel()),
"" + HTMLUtilities.escapeHTML(event.getMessage().getFormattedMessage()),
new Date(event.getTimeMillis()), context, computeToolbarActions(context)));
}
protected void removeFromLog(ActionContext context) {
synchronized (buffer) {
- LogRow r = logTableModel.deleteKey(context);
+ LogRow> r = logTableModel.deleteKey(context);
buffer.remove(r);
}
}
@@ -508,7 +667,7 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
}
@Override
- public java.util.List getPopupActions(Tool tool, ActionContext context) {
+ public List getPopupActions(Tool tool, ActionContext context) {
return streamActions(context)
.filter(a -> a.isAddToPopup(context))
.collect(Collectors.toList());
@@ -518,14 +677,41 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
synchronized (buffer) {
return logTableModel.getModelData()
.stream()
- .filter(r -> ctxCls.isInstance(r.context))
+ .filter(r -> ctxCls.isInstance(r.getActionContext()))
.count();
}
}
- public LogRow getLogRow(ActionContext ctx) {
+ public LogRow> getLogRow(ActionContext ctx) {
synchronized (buffer) {
return logTableModel.getMap().get(ctx);
}
}
+
+ @AutoServiceConsumed
+ private void setProgressService(ProgressService progressService) {
+ if (this.progressService != null) {
+ this.progressService.removeProgressListener(progressListener);
+ }
+ this.progressService = progressService;
+ if (this.progressService != null) {
+ this.progressService.addProgressListener(progressListener);
+ }
+ resyncProgressRows();
+ }
+
+ private void resyncProgressRows() {
+ synchronized (buffer) {
+ logTableModel.deleteItemsWith(r -> r instanceof MonitorLogRow);
+ if (progressService == null) {
+ return;
+ }
+ for (MonitorReceiver monitor : progressService.getAllMonitors()) {
+ if (!monitor.isValid()) {
+ continue;
+ }
+ progressListener.monitorCreated(monitor);
+ }
+ }
+ }
}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/HtmlOrProgressCellRenderer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/HtmlOrProgressCellRenderer.java
new file mode 100644
index 0000000000..cce99b0047
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/HtmlOrProgressCellRenderer.java
@@ -0,0 +1,69 @@
+/* ###
+ * 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.console;
+
+import java.awt.Component;
+
+import javax.swing.JTable;
+
+import docking.widgets.table.CustomToStringCellRenderer;
+import ghidra.debug.api.progress.MonitorReceiver;
+import ghidra.docking.settings.Settings;
+import ghidra.util.table.column.GColumnRenderer;
+
+public enum HtmlOrProgressCellRenderer implements GColumnRenderer