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

View File

@ -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)
@ -876,7 +887,7 @@ def put_available():
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:
@ -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))
@ -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.")

View File

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

View File

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

View File

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

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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) {
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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());
}
}
}

View File

@ -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<String> PARAM_ADDRESS =
ParameterDescription.create(String.class, "address", true, "localhost",
"Host/Address", "Address or hostname for interface(s) to listen on");
private static final ParameterDescription<Integer> 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<GTreeNode> 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<String, ?> 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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 {
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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.*;

View File

@ -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.*;

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,7 +354,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
protected DomainFile createDeconflictedFile(DomainFolder parent, DomainObject object)
throws InvalidNameException, CancelledException, IOException {
String name = object.getName();
TaskMonitor monitor = plugin.getTaskMonitor();
try (CloseableTaskMonitor monitor = plugin.createMonitor()) {
for (int nextId = 1; nextId < 100; nextId++) {
try {
return parent.createFile(name, object, monitor);
@ -357,6 +367,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
// Don't catch it this last time
return parent.createFile(name, object, monitor);
}
}
public void start() {
new Thread(this::receiveLoop, "trace-rmi handler " + socket.getRemoteSocketAddress())
@ -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();
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;
}
}

View File

@ -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();
}
}
@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) {
synchronized (handlers) {
handlers.add(handler);
}
}
void removeHandler(TraceRmiHandler handler) {
synchronized (handlers) {
handlers.remove(handler);
}
}
@Override
public Collection<TraceRmiConnection> getAllConnections() {
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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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.*;

View File

@ -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;

View File

@ -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;

View File

@ -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;
/**

View File

@ -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;

View File

@ -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 {

View File

@ -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'):

View File

@ -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()));
});
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

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

View File

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

View File

@ -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");

View File

@ -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,

View File

@ -84,6 +84,7 @@ public class ConsoleActionsCellRenderer extends AbstractGhidraColumnRenderer<Act
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
super.getTableCellRendererComponent(data); // A bit of a waste, but sets the background
box.setBackground(getBackground());
box.setBorder(getBorder());
ActionList value = (ActionList) data.getValue();
populateBox(box, buttonCache, value, button -> {

View File

@ -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);
}
}

View File

@ -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<LogTableColumns, LogRow> {
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<LogTableColumns, LogRow<?>> {
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<LogRow, ?> getter;
private final Function<LogRow<?>, ?> getter;
private final Class<?> cls;
private final SortDirection defaultSortDirection;
private final boolean editable;
<T> LogTableColumns(String header, Class<T> cls, Function<LogRow, T> getter,
<T> LogTableColumns(String header, Class<T> cls, Function<LogRow<?>, 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
* <p>
* This class is public for access by test cases only.
*/
public static class LogRow {
public interface LogRow<T> {
Icon getIcon();
T getMessage();
ActionList getActions();
Date getDate();
ActionContext getActionContext();
}
static class MessageLogRow implements LogRow<String> {
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<MonitorReceiver> {
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<MonitorReceiver, MonitorRowConsoleActionContext> 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<LogRow> logFilterPanel;
private GhidraTableFilterPanel<LogRow<?>> logFilterPanel;
private Deque<LogRow> buffer = new ArrayDeque<>();
private Deque<LogRow<?>> 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()),
"<html>" + 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<DockingActionIf> getPopupActions(Tool tool, ActionContext context) {
public List<DockingActionIf> 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);
}
}
}
}

View File

@ -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<Object> {
INSTANCE;
static final CustomToStringCellRenderer<String> 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();
}
}

View File

@ -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<MonitorReceiver> {
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();
}
}

View File

@ -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;
}
}

View File

@ -205,7 +205,7 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider
protected boolean resetRequested;
private final PluginTool tool;
private Map<String, ParameterDescription<?>> parameters;
Map<String, ParameterDescription<?>> parameters;
// TODO: Not sure this is the best keying, but I think it works.
private Map<NameTypePair, Object> 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();

View File

@ -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();
}
}

View File

@ -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<CancelledListener> 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;
}
}

View File

@ -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<ProgressListener> listeners = new ListenerSet<>(ProgressListener.class, true);
Set<MonitorReceiver> 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<MonitorReceiver> 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);
}
}
}

View File

@ -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;

View File

@ -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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
id="svg4819"
version="1.1"
viewBox="0 0 16 16">
<defs
id="defs4821" />
<metadata
id="metadata4824">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<path
id="path1465"
d="M 8,1.5 C 4.4160714,1.5 1.5,4.4160714 1.5,8 c 0.00175,1.1999382 0.3850235,2.351348 1.0097656,3.369141 l -2.21679685,2.216797 2.12109375,2.121093 2.21875,-2.21875 C 5.6498713,14.113396 6.7985466,14.497314 7.9980469,14.5 H 8 c 3.583929,0 6.5,-2.916071 6.5,-6.5 V 7.5 h -3 V 8 C 11.5,9.9389189 9.9389189,11.5 8,11.5 6.0610811,11.5 4.5,9.9389189 4.5,8 4.5,6.0610811 6.0610811,4.5 8,4.5 h 0.5 v -3 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="circle842"
d="m 8,2.0000001 a 6,6 0 0 0 -6,6 6,6 0 0 0 1.113282,3.4726559 L 1,13.585938 2.414063,15 4.529297,12.884766 A 6,6 0 0 0 8,14 6,6 0 0 0 14,8.0000001 H 12 A 4,4 0 0 1 8,12 4,4 0 0 1 4,8.0000001 a 4,4 0 0 1 4,-4 z"
style="opacity:1;fill:#d4aa00;fill-opacity:1;stroke:none;stroke-width:1.99999976;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
version="1.1"
id="svg4819"
height="16"
width="16">
<defs
id="defs4821" />
<metadata
id="metadata4824">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<path
id="path878"
d="M 13.585938,0.29296875 13.232422,0.64648438 9.1230469,4.7558594 C 8.757941,4.6294826 8.3901339,4.5007931 8.0019531,4.5 H 8 C 6.0729256,4.5 4.4999999,6.0729256 4.5,8 c 1e-7,1.9270743 1.5729257,3.5 3.5,3.5 1.9270743,0 3.5,-1.5729257 3.5,-3.5 V 7.99805 C 11.499207,7.6098662 11.370517,7.2420589 11.244141,6.8769531 l 4.46289,-4.4628906 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="opacity:1;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 13.585938,1 9.291016,5.2949219 A 3,3 0 0 0 8,5 3,3 0 0 0 5,8.0000001 3,3 0 0 0 8,11 3,3 0 0 0 11,8.0000001 3,3 0 0 0 10.705078,6.7089844 L 15,2.4140625 Z"
id="path838" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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;
@ -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);
}
}

View File

@ -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);

View File

@ -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<String, Object> args) {
for (Map.Entry<String, Object> 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));
}
}

View File

@ -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;

View File

@ -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<? extends TargetObject> 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");

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;