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

+ * + * @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 { + INSTANCE; + + static final CustomToStringCellRenderer FOR_STRING = + CustomToStringCellRenderer.HTML; + static final MonitorCellRenderer FOR_MONITOR = MonitorCellRenderer.INSTANCE; + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + if (value == null) { + return FOR_STRING.getTableCellRendererComponent(table, value, isSelected, + hasFocus, row, column); + } + if (value instanceof String message) { + return FOR_STRING.getTableCellRendererComponent(table, message, isSelected, + hasFocus, row, column); + } + if (value instanceof MonitorReceiver monitor) { + return FOR_MONITOR.getTableCellRendererComponent(table, monitor, isSelected, + hasFocus, row, column); + } + throw new AssertionError(); + } + + int getRowHeight(int colWidth) { + return FOR_STRING.getRowHeight(colWidth); + } + + @Override + public String getFilterString(Object t, Settings settings) { + if (t == null) { + return FOR_STRING.getFilterString(null, settings); + } + if (t instanceof String message) { + return FOR_STRING.getFilterString(message, settings); + } + if (t instanceof MonitorReceiver monitor) { + return FOR_MONITOR.getFilterString(monitor, settings); + } + throw new AssertionError(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/MonitorCellRenderer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/MonitorCellRenderer.java new file mode 100644 index 0000000000..401c601b48 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/MonitorCellRenderer.java @@ -0,0 +1,157 @@ +/* ### + * 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.*; +import java.text.NumberFormat; + +import javax.swing.*; +import javax.swing.border.Border; + +import generic.theme.GColor; +import generic.theme.GThemeDefaults.Colors.Palette; +import ghidra.debug.api.progress.MonitorReceiver; +import ghidra.docking.settings.Settings; +import ghidra.util.table.column.GColumnRenderer; +import ghidra.util.task.TaskMonitor; + +public class MonitorCellRenderer extends JPanel + implements GColumnRenderer { + static final MonitorCellRenderer INSTANCE = new MonitorCellRenderer(); + + private static final Color BACKGROUND_COLOR = new GColor("color.bg.table.row"); + private static final Color ALT_BACKGROUND_COLOR = new GColor("color.bg.table.row.alt"); + private static final String DISABLE_ALTERNATING_ROW_COLORS_PROPERTY = + "disable.alternating.row.colors"; + + private static boolean getAlternateRowColors() { + return !Boolean.getBoolean(DISABLE_ALTERNATING_ROW_COLORS_PROPERTY); + } + + static class CachedColor { + Color cached; + + Color copy(Color c) { + if (cached == null || cached.getRGB() != c.getRGB()) { + cached = new Color(c.getRGB()); + } + return cached; + } + } + + protected CachedColor selFg = new CachedColor(); + protected CachedColor selBg = new CachedColor(); + + protected final Border focusBorder; + protected final Border noFocusBorder; + protected final JProgressBar bar = new JProgressBar(); + protected final JLabel label = new JLabel(); + + public MonitorCellRenderer() { + super(new BorderLayout()); + noFocusBorder = BorderFactory.createEmptyBorder(1, 5, 1, 5); + Border innerBorder = BorderFactory.createEmptyBorder(0, 4, 0, 4); + Border outerBorder = BorderFactory.createLineBorder(Palette.YELLOW, 1); + focusBorder = BorderFactory.createCompoundBorder(outerBorder, innerBorder); + + add(bar); + add(label, BorderLayout.SOUTH); + } + + protected Color getAlternatingBackgroundColor(int row) { + if (!getAlternateRowColors() || (row & 1) == 1) { + return BACKGROUND_COLOR; + } + return ALT_BACKGROUND_COLOR; + } + + @Override + public final Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + setOpaque(true); + if (isSelected) { + setForeground(selFg.copy(table.getSelectionForeground())); + label.setForeground(selFg.copy(table.getSelectionForeground())); + setBackground(selBg.copy(table.getSelectionBackground())); + } + else { + setForeground(table.getForeground()); + label.setForeground(table.getForeground()); + setBackground(getAlternatingBackgroundColor(row)); + } + setBorder(hasFocus ? focusBorder : noFocusBorder); + + if (!(value instanceof MonitorReceiver monitor)) { + return this; + } + + if (monitor.isCancelled()) { + label.setText("(cancelled) " + monitor.getMessage()); + } + else { + label.setText(monitor.getMessage()); + } + + StringBuilder sb = new StringBuilder(); + long progress = monitor.getProgress(); + long maximum = monitor.getMaximum(); + if (progress != TaskMonitor.NO_PROGRESS_VALUE) { + if (progress <= 0) { + sb.append("0%"); + } + else if (progress >= maximum) { + sb.append("100%"); + } + else { + sb.append(NumberFormat.getPercentInstance().format((float) progress / maximum)); + } + if (monitor.isShowProgressValue()) { + sb.append(" ("); + sb.append(progress); + sb.append(" of "); + sb.append(maximum); + sb.append(")"); + } + } + bar.setString(sb.toString()); + bar.setStringPainted(true); + BoundedRangeModel model = bar.getModel(); + try { + model.setValueIsAdjusting(true); + model.setMaximum(Integer.MAX_VALUE); + if (progress == TaskMonitor.NO_PROGRESS_VALUE) { + bar.setIndeterminate(true); + model.setValue(0); + } + else { + bar.setIndeterminate(monitor.isIndeterminate()); + double val = Integer.MAX_VALUE; + val *= progress; + val /= maximum; + model.setValue((int) val); + } + } + finally { + model.setValueIsAdjusting(false); + } + return this; + } + + @Override + public String getFilterString(MonitorReceiver t, Settings settings) { + return t.getMessage(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/MonitorRowConsoleActionContext.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/MonitorRowConsoleActionContext.java new file mode 100644 index 0000000000..071b60d4cf --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/console/MonitorRowConsoleActionContext.java @@ -0,0 +1,49 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.console; + +import java.util.Objects; + +import docking.DefaultActionContext; +import ghidra.debug.api.progress.MonitorReceiver; + +public class MonitorRowConsoleActionContext extends DefaultActionContext { + private MonitorReceiver monitor; + + public MonitorRowConsoleActionContext(MonitorReceiver monitor) { + this.monitor = monitor; + } + + @Override + public int hashCode() { + return Objects.hashCode(monitor); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MonitorRowConsoleActionContext that)) { + return false; + } + if (!Objects.equals(this.monitor, that.monitor)) { + return false; + } + return true; + } + + public MonitorReceiver getMonitor() { + return monitor; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java index 5947928fa0..4bd69ecb31 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java @@ -205,7 +205,7 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider protected boolean resetRequested; private final PluginTool tool; - private Map> parameters; + Map> parameters; // TODO: Not sure this is the best keying, but I think it works. private Map memorized = new HashMap<>(); @@ -275,13 +275,13 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider close(); } - private void invoke(ActionEvent evt) { + void invoke(ActionEvent evt) { this.arguments = TargetMethod.validateArguments(parameters, collectArguments(), false); this.resetRequested = false; close(); } - private void reset(ActionEvent evt) { + void reset(ActionEvent evt) { this.arguments = new LinkedHashMap<>(); this.resetRequested = true; close(); @@ -343,6 +343,10 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider return type.cast(memorized.get(new NameTypePair(name, type))); } + public void forgetMemorizedArguments() { + memorized.clear(); + } + @Override public void propertyChange(PropertyChangeEvent evt) { PropertyEditor editor = (PropertyEditor) evt.getSource(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/DefaultCloseableTaskMonitor.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/DefaultCloseableTaskMonitor.java new file mode 100644 index 0000000000..6366d0f76e --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/DefaultCloseableTaskMonitor.java @@ -0,0 +1,159 @@ +/* ### + * 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.progress; + +import java.lang.ref.Cleaner; + +import javax.help.UnsupportedOperationException; + +import ghidra.debug.api.progress.CloseableTaskMonitor; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.CancelledListener; + +public class DefaultCloseableTaskMonitor implements CloseableTaskMonitor { + private static final Cleaner CLEANER = Cleaner.create(); + + static class State implements Runnable { + private final DefaultMonitorReceiver receiver; + + State(DefaultMonitorReceiver receiver) { + this.receiver = receiver; + } + + @Override + public void run() { + receiver.clean(); + } + } + + private final DefaultMonitorReceiver receiver; + private final State state; + @SuppressWarnings("unused") + private final Cleaner.Cleanable cleanable; + + public DefaultCloseableTaskMonitor(ProgressServicePlugin plugin) { + this.receiver = new DefaultMonitorReceiver(plugin); + this.state = new State(receiver); + this.cleanable = CLEANER.register(this, state); + } + + DefaultMonitorReceiver getReceiver() { + return receiver; + } + + @Override + public boolean isCancelled() { + return receiver.isCancelled(); + } + + @Override + public void setShowProgressValue(boolean showProgressValue) { + receiver.setShowProgressValue(showProgressValue); + } + + @Override + public void setMessage(String message) { + receiver.setMessage(message); + } + + @Override + public String getMessage() { + return receiver.getMessage(); + } + + @Override + public void setProgress(long value) { + receiver.setProgress(value); + } + + @Override + public void initialize(long max) { + receiver.setProgress(0); + receiver.setMaximum(max); + } + + @Override + public void setMaximum(long max) { + receiver.setMaximum(max); + } + + @Override + public long getMaximum() { + return receiver.getMaximum(); + } + + @Override + public void setIndeterminate(boolean indeterminate) { + receiver.setIndeterminate(indeterminate); + } + + @Override + public boolean isIndeterminate() { + return receiver.isIndeterminate(); + } + + @Override + public void checkCanceled() throws CancelledException { + if (receiver.isCancelled()) { + throw new CancelledException(); + } + } + + @Override + public void incrementProgress(long incrementAmount) { + receiver.incrementProgress(incrementAmount); + } + + @Override + public long getProgress() { + return receiver.getProgress(); + } + + @Override + public void cancel() { + receiver.cancel(); + } + + @Override + public void addCancelledListener(CancelledListener listener) { + receiver.addCancelledListener(listener); + } + + @Override + public void removeCancelledListener(CancelledListener listener) { + receiver.removeCancelledListener(listener); + } + + @Override + public void setCancelEnabled(boolean enable) { + receiver.setCancelEnabled(enable); + } + + @Override + public boolean isCancelEnabled() { + return receiver.isCancelEnabled(); + } + + @Override + public void clearCanceled() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + receiver.close(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/DefaultMonitorReceiver.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/DefaultMonitorReceiver.java new file mode 100644 index 0000000000..8b42f7ed86 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/DefaultMonitorReceiver.java @@ -0,0 +1,189 @@ +/* ### + * 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.progress; + +import ghidra.debug.api.progress.MonitorReceiver; +import ghidra.debug.api.progress.ProgressListener.Disposal; +import ghidra.util.datastruct.ListenerSet; +import ghidra.util.task.CancelledListener; + +public class DefaultMonitorReceiver implements MonitorReceiver { + private final ProgressServicePlugin plugin; + private final ListenerSet listeners = + new ListenerSet<>(CancelledListener.class, true); + private final Object lock = new Object(); + + private boolean cancelled = false; + private boolean indeterminate = false; + private boolean cancelEnabled = true; + private boolean showProgressValue = true; + + private boolean valid = true; + + private String message; + private long maximum; + private long progress; + + public DefaultMonitorReceiver(ProgressServicePlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void cancel() { + synchronized (lock) { + if (this.cancelled == true) { + return; + } + this.cancelled = true; + } + listeners.invoke().cancelled(); + plugin.listeners.invoke().attributeUpdated(this); + } + + void setShowProgressValue(boolean showProgressValue) { + synchronized (lock) { + if (this.showProgressValue == showProgressValue) { + return; + } + this.showProgressValue = showProgressValue; + } + plugin.listeners.invoke().attributeUpdated(this); + } + + void setMessage(String message) { + synchronized (lock) { + this.message = message; + } + plugin.listeners.invoke().messageUpdated(this, message); + } + + @Override + public String getMessage() { + synchronized (lock) { + return message; + } + } + + void setProgress(long progress) { + synchronized (lock) { + this.progress = progress; + } + plugin.listeners.invoke().progressUpdated(this, progress); + } + + void incrementProgress(long amount) { + long progress; + synchronized (lock) { + progress = this.progress + amount; + this.progress = progress; + } + plugin.listeners.invoke().progressUpdated(this, progress); + } + + @Override + public long getProgress() { + return progress; + } + + void setMaximum(long maximum) { + synchronized (lock) { + if (this.maximum == maximum) { + return; + } + this.maximum = maximum; + } + plugin.listeners.invoke().attributeUpdated(this); + } + + @Override + public long getMaximum() { + synchronized (lock) { + return maximum; + } + } + + void setIndeterminate(boolean indeterminate) { + synchronized (lock) { + if (this.indeterminate == indeterminate) { + return; + } + this.indeterminate = indeterminate; + } + plugin.listeners.invoke().attributeUpdated(this); + } + + @Override + public boolean isIndeterminate() { + return indeterminate; + } + + void addCancelledListener(CancelledListener listener) { + listeners.add(listener); + } + + void removeCancelledListener(CancelledListener listener) { + listeners.remove(listener); + } + + void setCancelEnabled(boolean cancelEnabled) { + synchronized (lock) { + if (this.cancelEnabled == cancelEnabled) { + return; + } + this.cancelEnabled = cancelEnabled; + } + plugin.listeners.invoke().attributeUpdated(this); + } + + @Override + public boolean isCancelEnabled() { + return cancelEnabled; + } + + public boolean isShowProgressValue() { + return showProgressValue; + } + + public void close() { + synchronized (lock) { + if (!this.valid) { + return; + } + this.valid = false; + } + plugin.disposeMonitor(this, Disposal.CLOSED); + } + + public void clean() { + synchronized (lock) { + if (!this.valid) { + return; + } + this.valid = false; + } + plugin.disposeMonitor(this, Disposal.CLEANED); + } + + @Override + public boolean isValid() { + return this.valid; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/ProgressServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/ProgressServicePlugin.java new file mode 100644 index 0000000000..8a4e59988b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/progress/ProgressServicePlugin.java @@ -0,0 +1,85 @@ +/* ### + * 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.progress; + +import java.util.*; + +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.app.services.ProgressService; +import ghidra.debug.api.progress.*; +import ghidra.debug.api.progress.ProgressListener.Disposal; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.util.datastruct.ListenerSet; + +@PluginInfo( + category = PluginCategoryNames.MISC, + shortDescription = "Service for monitoring task progress", + description = """ + Implements a pub-sub model for notifying of tasks and progress. Publishers can create + task monitors and update them using the TaskMonitor interface. Subscribers (there ought + to only be one) are notified of the tasks and render progress in a component provider. + """, + servicesProvided = { ProgressService.class }, + packageName = DebuggerPluginPackage.NAME, + status = PluginStatus.STABLE) +public class ProgressServicePlugin extends Plugin implements ProgressService { + ListenerSet listeners = new ListenerSet<>(ProgressListener.class, true); + + Set monitors = new HashSet<>(); + + public ProgressServicePlugin(PluginTool tool) { + super(tool); + } + + @Override + public CloseableTaskMonitor publishTask() { + DefaultCloseableTaskMonitor monitor = new DefaultCloseableTaskMonitor(this); + synchronized (monitors) { + monitors.add(monitor.getReceiver()); + } + listeners.invoke().monitorCreated(monitor.getReceiver()); + return monitor; + } + + @Override + public Collection getAllMonitors() { + synchronized (monitors) { + return Set.copyOf(monitors); + } + } + + @Override + public void addProgressListener(ProgressListener listener) { + listeners.add(listener); + } + + @Override + public void removeProgressListener(ProgressListener listener) { + listeners.remove(listener); + } + + void disposeMonitor(DefaultMonitorReceiver monitor, Disposal disposal) { + boolean changed; + synchronized (monitors) { + changed = monitors.remove(monitor); + } + if (changed) { + listeners.invoke().monitorDisposed(monitor, disposal); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/target/AbstractTarget.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/target/AbstractTarget.java index f19c46b5f1..92df10029b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/target/AbstractTarget.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/target/AbstractTarget.java @@ -102,7 +102,7 @@ public abstract class AbstractTarget implements Target { return address; } } - if (context.getContextObject() instanceof MarkerLocation ml) { + if (context != null && context.getContextObject() instanceof MarkerLocation ml) { Address address = findAddress(ml); if (address != null) { return address; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java index aa1beb54ef..d936c3cf24 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java @@ -1084,6 +1084,12 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return getCurrentFor(trace).trace(trace); } + @Override + public DebuggerCoordinates resolveTarget(Target target) { + Trace trace = target == null ? null : target.getTrace(); + return getCurrentFor(trace).target(target).snap(target.getSnap()); + } + @Override public DebuggerCoordinates resolvePlatform(TracePlatform platform) { Trace trace = platform == null ? null : platform.getTrace(); diff --git a/Ghidra/Debug/Debugger/src/main/resources/images/connect-accept.png b/Ghidra/Debug/Debugger/src/main/resources/images/connect-accept.png new file mode 100644 index 0000000000..b9fce86d5e Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/resources/images/connect-accept.png differ diff --git a/Ghidra/Debug/Debugger/src/main/resources/images/connect-outbound.png b/Ghidra/Debug/Debugger/src/main/resources/images/connect-outbound.png new file mode 100644 index 0000000000..bce5e880b2 Binary files /dev/null and b/Ghidra/Debug/Debugger/src/main/resources/images/connect-outbound.png differ diff --git a/Ghidra/Debug/Debugger/src/main/svg/connect-accept.svg b/Ghidra/Debug/Debugger/src/main/svg/connect-accept.svg new file mode 100644 index 0000000000..13340e5b64 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/svg/connect-accept.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/Ghidra/Debug/Debugger/src/main/svg/connect-outbound.svg b/Ghidra/Debug/Debugger/src/main/svg/connect-outbound.svg new file mode 100644 index 0000000000..5c90e9ccfa --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/svg/connect-outbound.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProviderTest.java index b38f64e376..afb4a13720 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/console/DebuggerConsoleProviderTest.java @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.debug.gui.console; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Test; @@ -24,7 +24,13 @@ import docking.DefaultActionContext; import docking.action.builder.ActionBuilder; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.service.progress.ProgressServicePlugin; +import ghidra.app.services.ProgressService; +import ghidra.debug.api.progress.CloseableTaskMonitor; import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.Task; +import ghidra.util.task.TaskMonitor; public class DebuggerConsoleProviderTest extends AbstractGhidraHeadedDebuggerTest { DebuggerConsolePlugin consolePlugin; @@ -43,18 +49,18 @@ public class DebuggerConsoleProviderTest extends AbstractGhidraHeadedDebuggerTes @Test public void testActions() throws Exception { consolePlugin.addResolutionAction(new ActionBuilder("Add", name.getMethodName()) - .toolBarIcon(DebuggerResources.ICON_ADD) - .description("Add") - .withContext(TestConsoleActionContext.class) - .onAction(ctx -> Msg.info(this, "Add clicked")) - .build()); + .toolBarIcon(DebuggerResources.ICON_ADD) + .description("Add") + .withContext(TestConsoleActionContext.class) + .onAction(ctx -> Msg.info(this, "Add clicked")) + .build()); consolePlugin.addResolutionAction(new ActionBuilder("Delete", name.getMethodName()) - .popupMenuIcon(DebuggerResources.ICON_DELETE) - .popupMenuPath("Delete") - .description("Delete") - .withContext(TestConsoleActionContext.class) - .onAction(ctx -> Msg.info(this, "Delete clicked")) - .build()); + .popupMenuIcon(DebuggerResources.ICON_DELETE) + .popupMenuPath("Delete") + .description("Delete") + .withContext(TestConsoleActionContext.class) + .onAction(ctx -> Msg.info(this, "Delete clicked")) + .build()); consolePlugin.log(DebuggerResources.ICON_DEBUGGER, "Test message", new TestConsoleActionContext()); @@ -75,4 +81,40 @@ public class DebuggerConsoleProviderTest extends AbstractGhidraHeadedDebuggerTes waitForPass(() -> assertEquals(2, consoleProvider.logTable.getRowCount())); } + + @Test + public void testProgress() throws Exception { + ProgressService progressService = addPlugin(tool, ProgressServicePlugin.class); + try (CloseableTaskMonitor monitor1 = progressService.publishTask(); + CloseableTaskMonitor monitor2 = progressService.publishTask()) { + monitor1.initialize(10, "Testing 1"); + monitor2.initialize(10, "Testing 2"); + for (int i = 0; i < 10; i++) { + Thread.sleep(100); + monitor1.increment(); + Thread.sleep(100); + monitor2.increment(); + } + } + } + + @Test + public void testRefTaskMonitor() throws Exception { + tool.execute(new Task("Test") { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + monitor.initialize(10, "Testing"); + for (int i = 0; i < 10; i++) { + try { + Thread.sleep(100); + } + catch (InterruptedException e) { + throw new AssertionError(e); + } + monitor.increment(); + } + } + }); + Thread.sleep(100); + } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java index 6206e93250..f23956d211 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTest.java @@ -1598,7 +1598,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerTes DebuggerOpenProgramActionContext ctx = new DebuggerOpenProgramActionContext(df); waitForPass(() -> assertTrue(consolePlugin.logContains(ctx))); - assertTrue(consolePlugin.getLogRow(ctx).getMessage().contains("recovery")); + assertTrue(consolePlugin.getLogRow(ctx).getMessage() instanceof String message && + message.contains("recovery")); } @Test @@ -1626,7 +1627,8 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerTes DebuggerOpenProgramActionContext ctx = new DebuggerOpenProgramActionContext(df); waitForPass(() -> assertTrue(consolePlugin.logContains(ctx))); - assertTrue(consolePlugin.getLogRow(ctx).getMessage().contains("version")); + assertTrue(consolePlugin.getLogRow(ctx).getMessage() instanceof String message && + message.contains("version")); } @Test @@ -1643,7 +1645,7 @@ public class DebuggerListingProviderTest extends AbstractGhidraHeadedDebuggerTes consolePlugin.log(DebuggerResources.ICON_MODULES, "Test resolution", ctx); waitForSwing(); - LogRow row = consolePlugin.getLogRow(ctx); + LogRow row = consolePlugin.getLogRow(ctx); assertEquals(1, row.getActions().size()); BoundAction boundAction = row.getActions().get(0); assertEquals(listingProvider.actionOpenProgram, boundAction.action); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/objects/components/InvocationDialogHelper.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/objects/components/InvocationDialogHelper.java new file mode 100644 index 0000000000..1b1c43af88 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/objects/components/InvocationDialogHelper.java @@ -0,0 +1,48 @@ +/* ### + * 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.objects.components; + +import static org.junit.Assert.assertNotNull; + +import java.util.Map; + +import docking.test.AbstractDockingTest; +import ghidra.dbg.target.TargetMethod.ParameterDescription; +import ghidra.util.Swing; + +public class InvocationDialogHelper { + + public static InvocationDialogHelper waitFor() { + DebuggerMethodInvocationDialog dialog = + AbstractDockingTest.waitForDialogComponent(DebuggerMethodInvocationDialog.class); + return new InvocationDialogHelper(dialog); + } + + private final DebuggerMethodInvocationDialog dialog; + + public InvocationDialogHelper(DebuggerMethodInvocationDialog dialog) { + this.dialog = dialog; + } + + public void dismissWithArguments(Map args) { + for (Map.Entry a : args.entrySet()) { + ParameterDescription p = dialog.parameters.get(a.getKey()); + assertNotNull(p); + dialog.setMemorizedArgument(a.getKey(), p.type.asSubclass(Object.class), a.getValue()); + } + Swing.runNow(() -> dialog.invoke(null)); + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java index a2f7a3eebd..e507856185 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java @@ -25,7 +25,6 @@ import org.junit.experimental.categories.Category; import db.Transaction; import generic.test.category.NightlyCategory; -import generic.test.rule.Repeated; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; import ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin; import ghidra.app.services.DebuggerControlService; diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java index b56447baa3..c935a2fc07 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java @@ -150,11 +150,19 @@ public class XmlSchemaContext extends DefaultSchemaContext { return names.computeIfAbsent(name, SchemaName::new); } + private String requireAttributeValue(Element elem, String name) { + String value = elem.getAttributeValue(name); + if (value == null) { + throw new IllegalArgumentException("Missing attribute " + name + " in " + elem); + } + return value; + } + public TargetObjectSchema schemaFromXml(Element schemaElem) { SchemaBuilder builder = builder(name(schemaElem.getAttributeValue("name", ""))); for (Element ifaceElem : XmlUtilities.getChildren(schemaElem, "interface")) { - String ifaceName = ifaceElem.getAttributeValue("name"); + String ifaceName = requireAttributeValue(ifaceElem, "name"); Class iface = TargetObject.INTERFACES_BY_NAME.get(ifaceName); if (iface == null) { Msg.warn(this, "Unknown interface name: '" + ifaceName + "'"); @@ -166,18 +174,18 @@ public class XmlSchemaContext extends DefaultSchemaContext { builder.setCanonicalContainer(parseBoolean(schemaElem, "canonical")); builder.setElementResyncMode( - ResyncMode.valueOf(schemaElem.getAttributeValue("elementResync"))); + ResyncMode.valueOf(requireAttributeValue(schemaElem, "elementResync"))); builder.setAttributeResyncMode( - ResyncMode.valueOf(schemaElem.getAttributeValue("attributeResync"))); + ResyncMode.valueOf(requireAttributeValue(schemaElem, "attributeResync"))); for (Element elemElem : XmlUtilities.getChildren(schemaElem, "element")) { - SchemaName schema = name(elemElem.getAttributeValue("schema")); + SchemaName schema = name(requireAttributeValue(elemElem, "schema")); String index = elemElem.getAttributeValue("index", ""); builder.addElementSchema(index, schema, elemElem); } for (Element attrElem : XmlUtilities.getChildren(schemaElem, "attribute")) { - SchemaName schema = name(attrElem.getAttributeValue("schema")); + SchemaName schema = name(requireAttributeValue(attrElem, "schema")); boolean required = parseBoolean(attrElem, "required"); boolean fixed = parseBoolean(attrElem, "fixed"); boolean hidden = parseBoolean(attrElem, "hidden"); diff --git a/Ghidra/Framework/Docking/data/docking.theme.properties b/Ghidra/Framework/Docking/data/docking.theme.properties index f3eb15fdff..e984f54c65 100644 --- a/Ghidra/Framework/Docking/data/docking.theme.properties +++ b/Ghidra/Framework/Docking/data/docking.theme.properties @@ -63,6 +63,7 @@ icon.run = play.png icon.spreadsheet = application-vnd.oasis.opendocument.spreadsheet-template.png icon.pulldown = menu16.gif icon.window = application_xp.png +icon.pending = hourglass.png icon.zoom.in = zoom_in.png icon.zoom.out = zoom_out.png @@ -102,7 +103,7 @@ icon.widget.pathmanager.reset = trash-empty.png icon.widget.table.header.help = info_small.png icon.widget.table.header.help.hovered = info_small_hover.png -icon.widget.table.header.pending = hourglass.png +icon.widget.table.header.pending = icon.pending icon.dialog.error.expandable.report = icon.spreadsheet icon.dialog.error.expandable.exception = program_obj.png diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/dbgeng/rmi/AbstractDbgEngTraceRmiTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/dbgeng/rmi/AbstractDbgEngTraceRmiTest.java index 9a6897f483..08113a1eee 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/dbgeng/rmi/AbstractDbgEngTraceRmiTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/dbgeng/rmi/AbstractDbgEngTraceRmiTest.java @@ -29,7 +29,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.Before; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; -import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin; +import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin; import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; import ghidra.app.services.TraceRmiService; import ghidra.dbg.testutil.DummyProc; diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/gdb/rmi/AbstractGdbTraceRmiTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/gdb/rmi/AbstractGdbTraceRmiTest.java index 5645238e66..ca010d1856 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/gdb/rmi/AbstractGdbTraceRmiTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/gdb/rmi/AbstractGdbTraceRmiTest.java @@ -33,7 +33,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.Before; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; -import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin; +import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin; import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; import ghidra.app.services.TraceRmiService; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/lldb/rmi/AbstractLldbTraceRmiTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/lldb/rmi/AbstractLldbTraceRmiTest.java index 62e63eba09..20dbb249d1 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/lldb/rmi/AbstractLldbTraceRmiTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/lldb/rmi/AbstractLldbTraceRmiTest.java @@ -32,7 +32,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.Before; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; -import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin; +import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin; import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; import ghidra.app.services.TraceRmiService; import ghidra.dbg.testutil.DummyProc; diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java index aebd8507b6..773c4f3afd 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java @@ -18,8 +18,8 @@ package ghidra.app.plugin.core.debug.gui; import java.util.Set; import db.Transaction; -import ghidra.app.plugin.core.debug.service.rmi.trace.TestTraceRmiConnection; -import ghidra.app.plugin.core.debug.service.rmi.trace.TestTraceRmiConnection.*; +import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiConnection; +import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiConnection.*; import ghidra.dbg.target.schema.*; import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; import ghidra.debug.api.target.ActionName; diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerRmiBreakpointMarkerPluginTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerRmiBreakpointMarkerPluginTest.java index 9fcf0f7f09..ca4a19ed74 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerRmiBreakpointMarkerPluginTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerRmiBreakpointMarkerPluginTest.java @@ -24,7 +24,7 @@ import org.junit.experimental.categories.Category; import db.Transaction; import generic.test.category.NightlyCategory; -import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiTarget; +import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget; import ghidra.trace.database.target.DBTraceObjectManager; import ghidra.trace.model.Lifespan; import ghidra.trace.model.Trace; diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerRmiBreakpointsProviderTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerRmiBreakpointsProviderTest.java index a7697b0a78..e9adcfcbd7 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerRmiBreakpointsProviderTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerRmiBreakpointsProviderTest.java @@ -22,7 +22,7 @@ import java.util.*; import org.junit.Before; import db.Transaction; -import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiTarget; +import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget; import ghidra.trace.database.ToyDBTraceBuilder; import ghidra.trace.model.Lifespan; import ghidra.trace.model.Trace; diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java index d6f8b59ebd..5c6ed99460 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPluginTest.java @@ -46,7 +46,7 @@ import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; import ghidra.app.plugin.core.debug.mapping.ObjectBasedDebuggerTargetTraceMapper; import ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin; import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin; -import ghidra.app.plugin.core.debug.service.rmi.trace.TestTraceRmiConnection.TestRemoteMethod; +import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiConnection.TestRemoteMethod; import ghidra.app.services.DebuggerControlService; import ghidra.app.services.DebuggerEmulationService; import ghidra.app.services.DebuggerEmulationService.CachedEmulator; diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerRmiLogicalBreakpointServiceTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerRmiLogicalBreakpointServiceTest.java index e4d95f0dee..0ec8b8c5b6 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerRmiLogicalBreakpointServiceTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerRmiLogicalBreakpointServiceTest.java @@ -23,7 +23,7 @@ import org.junit.Before; import db.Transaction; import ghidra.app.plugin.core.debug.service.modules.DebuggerStaticMappingUtils; -import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiTarget; +import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation;