GP-3836: Add Trace RMI 'Connections' pane.

This commit is contained in:
Dan
2023-12-01 09:10:12 -05:00
parent 5fd01c739d
commit bf8f7c8f78
82 changed files with 3836 additions and 270 deletions
@@ -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):
@@ -115,6 +118,7 @@ class State(object):
STATE = State()
def ghidra_trace_connect(address=None):
"""
Connect Python to Ghidra for tracing
@@ -124,7 +128,8 @@ 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:
@@ -133,7 +138,9 @@ def ghidra_trace_connect(address=None):
try:
c = socket.socket()
c.connect((host, int(port)))
STATE.client = Client(c, methods.REGISTRY)
# 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")
@@ -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
@@ -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):
@@ -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:
@@ -933,7 +944,7 @@ def put_single_breakpoint(bp, ibobj, nproc, ikeys):
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()
@@ -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))
@@ -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.")
@@ -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
@@ -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
@@ -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"
*
@@ -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.
*
* <p>
* 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.
*
* <p>
* 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
*
* <p>
* 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.
*
* <pre>
* try (CloseableTaskMonitor monitor = progressService.publishTask()) {
* // Do the computation and update the monitor accordingly.
* }
* </pre>
*
* <p>
* 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
*
* <p>
* 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<MonitorReceiver> 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);
}
@@ -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<TraceRmiConnection> getAllConnections();
/**
* Get all of the acceptors currently listening for a connection
*
* @return the acceptors
*/
Collection<TraceRmiAcceptor> 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);
}
@@ -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();
}
@@ -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}
*
* <p>
* This only gives a subset of the expected task monitor interface. This is the subset a
* <em>user</em> 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
*
* <p>
* 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
*
* <p>
* 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
*
* <p>
* 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 <em>cannot</em> 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
*
* <p>
* 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
*
* <p>
* 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();
}
@@ -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 <em>not</em> 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
*
* <ul>
* <li>cancelled</li>
* <li>cancel enabled</li>
* <li>indeterminate</li>
* <li>maximum</li>
* <li>show progress value in percent string</li>
* </ul>
*
* @param monitor the monitor
*/
void attributeUpdated(MonitorReceiver monitor);
}
@@ -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
*
* <p>
* 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
*
* <p>
* 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();
}
@@ -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
*
* <p>
* 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<Target> getTargets();
}
@@ -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
*
* <p>
* <b>TODO</b>: 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
*
* <p>
* 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
*
* <p>
* The added benefit of this method compared to the {@link TargetPublicationListener} is that it
* identifies <em>which connection</em>
*
* @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) {
}
}
@@ -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;
@@ -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;
@@ -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());
}
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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<Target, TraceRmiTargetNode> 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;
}
}
@@ -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 {
}
@@ -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 "<SERVICE MISSING>";
}
if (!service.isServerStarted()) {
return "Server: CLOSED";
}
return "Server: LISTENING " + service.getServerAddress();
}
@Override
public String getToolTip() {
return getDisplayText();
}
@Override
public boolean isLeaf() {
return true;
}
}
@@ -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<TraceRmiConnection, TraceRmiConnectionNode> connectionNodes = new HashMap<>();
final Map<TraceRmiAcceptor, TraceRmiAcceptorNode> acceptorNodes = new HashMap<>();
// weak because each connection node keeps the strong map
final Map<Target, TraceRmiTargetNode> targetNodes = new WeakHashMap<>();
public TraceRmiServiceNode(TraceRmiConnectionManagerProvider provider) {
super(provider, "<root>");
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();
}
}
@@ -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;
}
}
@@ -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;
@@ -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;
}
}
@@ -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();
@@ -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.*;
@@ -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.*;
@@ -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);
}
}
@@ -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;
@@ -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;
@@ -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;
@@ -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<Target> getTargets() {
return byId.values()
.stream()
.map(ot -> ot.target)
.collect(Collectors.toUnmodifiableList());
}
public CompletableFuture<OpenTrace> 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<Void> negotiate = new CompletableFuture<>();
private final CompletableFuture<String> negotiate = new CompletableFuture<>();
private final CompletableFuture<Void> closed = new CompletableFuture<>();
private final Set<TerminalSession> 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?
*
* <p>
* Can experiment with ordering of address-set-view "expression" to optimize early
* termination.
*
* <p>
* 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<Target> getTargets() {
return openTraces.getTargets();
}
public void registerTerminals(Collection<TerminalSession> 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;
}
}
@@ -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<TraceRmiHandler> handlers = new LinkedHashSet<>();
private final Set<DefaultTraceRmiAcceptor> acceptors = new LinkedHashSet<>();
final ListenerSet<TraceRmiServiceListener> 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<TraceRmiConnection> 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<TraceRmiAcceptor> 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);
}
}
@@ -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);
}
}
}
}
@@ -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.*;
@@ -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;
@@ -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;
@@ -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;
/**
@@ -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;
@@ -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 {
@@ -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'):
@@ -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<TraceRmiConnection> 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("""
<context>
<schema name="Root" elementResync="NEVER" attributeResync="NEVER" />
</context>
""");
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()));
});
}
}
}
@@ -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<TraceRmiAcceptor, TraceRmiAcceptorNode> getAcceptorNodeMap(
TraceRmiServiceNode serviceNode) {
return serviceNode.acceptorNodes;
}
public static Map<TraceRmiConnection, TraceRmiConnectionNode> getConnectionNodeMap(
TraceRmiServiceNode serviceNode) {
return serviceNode.connectionNodes;
}
public static Map<Target, TraceRmiTargetNode> getTargetNodeMap(
TraceRmiServiceNode serviceNode) {
return serviceNode.targetNodes;
}
public static TraceRmiServerNode getServerNode(TraceRmiServiceNode serviceNode) {
return serviceNode.serverNode;
}
}
@@ -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<T extends AbstractMessage> {
public interface Decoder<T> {
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<T> decoder;
public ProtobufSocket(SocketChannel channel, Decoder<T> 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);
}
}
}
@@ -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<RootMessage> 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());
}
}
@@ -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<Target> getTargets() {
return List.copyOf(targets.values());
}
}
@@ -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|
@@ -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

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