GP-739,741,742,666,681,823: combine listener interfaces, remove attribute-specific callbacks, update-mode schema, recorder refactor, model testing, and double-launch fix

This commit is contained in:
Dan 2021-04-01 10:15:17 -04:00
parent e83a893493
commit 015858b5d3
533 changed files with 29293 additions and 8011 deletions

View file

@ -15,25 +15,38 @@
*/
#include <stdio.h>
#include <Windows.h>
#include <debugapi.h>
#include <shellapi.h>
int __declspec(dllexport) func(char* msg) {
printf("%s\n", msg);
}
int main(int argc, char** argv) {
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) {
OutputDebugStringW(L"Starting\n");
LPWSTR *argvW;
int argc = 0;
argvW = CommandLineToArgvW(GetCommandLine(), &argc);
if (argvW != NULL) {
LocalFree(argvW);
} else {
return 0;
}
wprintf(L"argc: %d\n", argc);
if (argc != 1) {
func("I'm the child");
return 1;
}
STARTUPINFO sStartupInfo = {sizeof(sStartupInfo)};
PROCESS_INFORMATION sProcessInformation = {0};
BOOL result = CreateProcess(argv[0], "expCreateProcess child", NULL, NULL, FALSE, 0, NULL, NULL, &sStartupInfo, &sProcessInformation);
wprintf(L"Me: %s\n", argvW[0]);
BOOL result = CreateProcessW(argvW[0], L"expCreateProcess child", NULL, NULL, FALSE, 0, NULL, NULL, &sStartupInfo, &sProcessInformation);
if (result == FALSE) {
DWORD le = GetLastError();
fprintf(stderr, "Could not create child process: %d\n", le);
char err[1024];
wchar_t err[1024];
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, le, 0, err, sizeof(err), NULL);
fprintf(stderr, " Message: '%s'\n", err);
fwprintf(stderr, L" Message: '%s'\n", err);
DebugBreak();
return -1;
}

View file

@ -26,7 +26,7 @@ __declspec(dllexport) unsigned int WINAPI work(DWORD* param) {
}
}
int main(int argc, char** argv) {
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) {
DWORD zero = 0;
DWORD one = 1;
HANDLE thread = _beginthreadex(NULL, 0, work, &one, 0, NULL);

View file

@ -24,7 +24,7 @@ __declspec(dllexport) unsigned int WINAPI work(DWORD* param) {
}
}
int main(int argc, char** argv) {
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) {
DWORD zero = 0;
DWORD one = 1;
HANDLE thread = _beginthreadex(NULL, 0, work, &one, 0, NULL);

View file

@ -16,14 +16,21 @@
#include <stdio.h>
#ifdef WIN32
#include <Windows.h>
#include <debugapi.h>
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#define OutputDebugString(out) printf("%s\n", out)
#endif
DLLEXPORT volatile char overwrite[] = "Hello, World!";
#ifdef WIN32
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) {
#else
int main(int argc, char** argv) {
printf("%s\n", overwrite);
#endif
OutputDebugString(overwrite);
return overwrite[0];
}

View file

@ -0,0 +1,33 @@
/* ###
* 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.
*/
#ifdef WIN32
#include <Windows.h>
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#endif
int DLLEXPORT break_here(int val) {
return val;
}
#ifdef WIN32
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) {
#else
int main(int argc, char** argv) {
#endif
return break_here(0);
}

View file

@ -15,7 +15,7 @@
*/
#include <Windows.h>
__declspec(dllexport) int main(int argc, char** argv) {
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) {
for (int i = 0; i < 10; i++) {
Sleep(1000);
}

View file

@ -0,0 +1,45 @@
/* ###
* 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.
*/
#ifdef WIN32
#include <Windows.h>
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#endif
int DLLEXPORT break_here() {
return 0;
}
int DLLEXPORT funcC() {
return break_here();
}
int DLLEXPORT funcB() {
return funcC();
}
int DLLEXPORT funcA() {
return funcB();
}
#ifdef WIN32
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) {
#else
int main(int argc, char** argv) {
#endif
return funcA();
}

View file

@ -0,0 +1,126 @@
/* ###
* 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.dbg;
import java.lang.annotation.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import ghidra.dbg.target.TargetObject;
import ghidra.util.Msg;
public abstract class AnnotatedDebuggerAttributeListener implements DebuggerModelListener {
private static final String ATTR_METHODS =
"@" + AttributeCallback.class.getSimpleName() + "-annotated methods";
private static final String PARAMS_ERR =
ATTR_METHODS + " must accept 2 parameters: (TargetObject, T)";
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
protected @interface AttributeCallback {
String value();
}
private static class Wiring {
private final Map<String, Set<MethodHandle>> handles = new HashMap<>();
private Wiring(Class<?> cls, Lookup lookup) {
try {
collect(cls, lookup);
}
catch (IllegalAccessException e) {
throw new IllegalArgumentException("Lookup must have access " + ATTR_METHODS, e);
}
}
private void collectFromClass(Class<?> cls, Lookup lookup) throws IllegalAccessException {
for (Method m : cls.getDeclaredMethods()) {
AttributeCallback annot = m.getAnnotation(AttributeCallback.class);
if (annot == null) {
continue;
}
Parameter[] parameters = m.getParameters();
if (parameters.length != 2) {
throw new IllegalArgumentException(PARAMS_ERR);
}
if (!parameters[0].getType().isAssignableFrom(TargetObject.class)) {
throw new IllegalArgumentException(PARAMS_ERR);
}
MethodHandle handle = lookup.unreflect(m);
handles.computeIfAbsent(annot.value(), __ -> new HashSet<>()).add(handle);
}
}
private void collect(Class<?> cls, Lookup lookup) throws IllegalAccessException {
collectFromClass(cls, lookup);
Class<?> s = cls.getSuperclass();
if (s != null) {
collect(s, lookup);
}
for (Class<?> i : cls.getInterfaces()) {
collect(i, lookup);
}
}
private void fireChange(AnnotatedDebuggerAttributeListener l, TargetObject object,
String name, Object value) {
Set<MethodHandle> set = handles.get(name);
if (set == null) {
return;
}
for (MethodHandle h : set) {
try {
h.invoke(l, object, value);
}
catch (Throwable e) {
Msg.error(this, "Error invoking " + h + ": " + e);
}
}
}
}
private static final Map<Class<? extends AnnotatedDebuggerAttributeListener>, Wiring> WIRINGS_BY_CLASS =
new HashMap<>();
private final Wiring wiring;
public AnnotatedDebuggerAttributeListener(Lookup lookup) {
wiring = WIRINGS_BY_CLASS.computeIfAbsent(getClass(), cls -> new Wiring(cls, lookup));
}
protected boolean checkFire(TargetObject object) {
return true;
}
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
if (!checkFire(object)) {
return;
}
for (String name : removed) {
wiring.fireChange(this, object, name, null);
}
for (Map.Entry<String, ?> ent : added.entrySet()) {
wiring.fireChange(this, object, ent.getKey(), ent.getValue());
}
}
}

View file

@ -15,6 +15,7 @@
*/
package ghidra.dbg;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
@ -22,9 +23,9 @@ import java.util.stream.Collectors;
import ghidra.async.*;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.TargetObject.TargetObjectListener;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.util.PathPredicates;
import ghidra.dbg.util.PathUtils;
import ghidra.dbg.util.PathUtils.PathComparator;
import ghidra.util.Msg;
@ -80,7 +81,9 @@ public enum DebugModelConventions {
* @param seed the starting object
* @return a future which completes with the discovered object or completes with null, if not
* found.
* @deprecated use {@link #suitable(Class, TargetObject)} instead
*/
@Deprecated(forRemoval = true)
public static <T extends TargetObject> CompletableFuture<T> findSuitable(Class<T> iface,
TargetObject seed) {
if (iface.isAssignableFrom(seed.getClass())) {
@ -97,6 +100,56 @@ public enum DebugModelConventions {
return findParentSuitable(iface, seed);
}
/**
* Search for a suitable object implementing the given interface, starting at a given seed.
*
* <p>
* This performs an n-up-m-down search starting at the given seed, seeking an object which
* implements the given interface. The m-down part is only applied from objects implementing
* {@link TargetAggregate}. See {@link TargetObject} for the specifics of expected model
* conventions.
*
* <p>
* Note that many a debugger target object interface type require a self-referential {@code T}
* parameter referring to the implementing class type. To avoid referring to a particular
* implementation, it becomes necessary to leave {@code T} as {@code ?}, but that can never
* satisfy the constraints of this method. To work around this, such interfaces must provide a
* static {@code tclass} field, which can properly satisfy the type constraints of this method
* for such self-referential type variables. The returned value must be ascribed to the
* wild-carded type, because the work-around involves a hidden class. Perhaps a little verbose
* (hey, it's Java!), the following is the recommended pattern, e.g., to discover the
* environment of a given process:
*
* <pre>
* CompletableFuture<? extends TargetEnvironment<?>> futureEnv =
* DebugModelConventions.suitable(TargetEnvironment.tclass, aProcess);
* </pre>
*
* @param <T> the desired interface type.
* @param iface the (probably {@code tclass}) of the desired interface type
* @param seed the starting object
* @return a future which completes with the discovered object or completes with null, if not
* found.
*/
public static <T extends TargetObject> CompletableFuture<T> suitable(Class<T> iface,
TargetObject seed) {
List<String> path =
seed.getModel().getRootSchema().searchForSuitable(iface, seed.getPath());
if (path == null) {
return null;
}
return seed.getModel().fetchModelObject(path).thenApply(obj -> iface.cast(obj));
}
public static <T extends TargetObject> T ancestor(Class<T> iface, TargetObject seed) {
List<String> path =
seed.getModel().getRootSchema().searchForAncestor(iface, seed.getPath());
if (path == null) {
return null;
}
return iface.cast(seed.getModel().getModelObject(path));
}
private static <T extends TargetObject> CompletableFuture<T> findParentSuitable(Class<T> iface,
TargetObject obj) {
TargetObject parent = obj.getParent();
@ -285,8 +338,11 @@ public enum DebugModelConventions {
* @param seed the starting point (root of subtree to inspect)
* @param iface the class of the interface
* @return the collection of successor elements supporting the interface
* @deprecated use {@link TargetObjectSchema#searchFor(Class, boolean)} and
* {@link PathPredicates#collectSuccessorRefs(TargetObject)} instead.
*/
// TODO: Test this method
@Deprecated(forRemoval = true)
public static <T extends TargetObject> CompletableFuture<Collection<T>> collectSuccessors(
TargetObject seed, Class<T> iface) {
Collection<T> result =
@ -366,7 +422,29 @@ public enum DebugModelConventions {
}
/**
* Check if a target is a live process
* Check if the given process is alive
*
* @param process the process
* @return true if alive
*/
public static boolean isProcessAlive(TargetProcess process) {
if (!process.isValid()) {
return false;
}
if (!(process instanceof TargetExecutionStateful)) {
return true;
}
TargetExecutionStateful exe = (TargetExecutionStateful) process;
TargetExecutionState state = exe.getExecutionState();
if (state == null) {
Msg.error(null, "null state for " + exe);
return false;
}
return state.isAlive();
}
/**
* Check if a target is a live process, and cast if so
*
* @param target the potential process
* @return the process if live, or null
@ -375,27 +453,22 @@ public enum DebugModelConventions {
if (!(target instanceof TargetProcess)) {
return null;
}
// TODO: When schemas are introduced, we'll better handle "associated"
// For now, require "implements"
if (!(target instanceof TargetExecutionStateful)) {
return (TargetProcess) target;
}
TargetExecutionStateful exe = (TargetExecutionStateful) target;
TargetExecutionState state = exe.getExecutionState();
if (!state.isAlive()) {
return null;
}
return (TargetProcess) target;
TargetProcess process = (TargetProcess) target;
return isProcessAlive(process) ? process : null;
}
/**
* A convenience for listening to selected portions (possible all) of a sub-tree of a model
*/
public abstract static class SubTreeListenerAdapter implements TargetObjectListener {
public abstract static class SubTreeListenerAdapter extends AnnotatedDebuggerAttributeListener {
protected boolean disposed = false;
protected final NavigableMap<List<String>, TargetObject> objects =
new TreeMap<>(PathComparator.KEYED);
public SubTreeListenerAdapter() {
super(MethodHandles.lookup());
}
/**
* An object has been removed from the sub-tree
*
@ -494,7 +567,7 @@ public enum DebugModelConventions {
}
}
private void considerAttributes(TargetObject obj, Map<String, ?> attributes) {
protected void considerAttributes(TargetObject obj, Map<String, ?> attributes) {
synchronized (objects) {
if (disposed) {
return;
@ -612,20 +685,28 @@ public enum DebugModelConventions {
}
}
/**
* A variable that is updated whenever access changes according to the (now deprecated)
* "every-ancestor" convention.
*
* @deprecated The "every-ancestor" thing doesn't add any flexibility to model implementations.
* It might even restrict it. Not to mention it's obtuse to implement.
*/
@Deprecated(forRemoval = true)
public static class AllRequiredAccess extends AsyncReference<Boolean, Void> {
protected class ListenerForAccess implements TargetAccessibilityListener {
protected class ListenerForAccess extends AnnotatedDebuggerAttributeListener {
protected final TargetAccessConditioned access;
private boolean accessible;
public ListenerForAccess(TargetAccessConditioned access) {
super(MethodHandles.lookup());
this.access = access;
this.access.addListener(this);
this.accessible = access.isAccessible();
}
@Override
public void accessibilityChanged(TargetAccessConditioned object,
boolean accessibility) {
@AttributeCallback(TargetAccessConditioned.ACCESSIBLE_ATTRIBUTE_NAME)
public void accessibilityChanged(TargetObject object, boolean accessibility) {
//Msg.debug(this, "Obj " + object + " has become " + accessibility);
synchronized (AllRequiredAccess.this) {
this.accessible = accessibility;
@ -651,6 +732,59 @@ public enum DebugModelConventions {
}
}
public static class AsyncAttribute<T> extends AsyncReference<T, Void>
implements DebuggerModelListener {
private final TargetObject obj;
private final String name;
@SuppressWarnings("unchecked")
public AsyncAttribute(TargetObject obj, String name) {
this.name = name;
this.obj = obj;
obj.addListener(this);
obj.fetchAttribute(name).thenAccept(t -> {
set((T) t, null);
}).exceptionally(ex -> {
Msg.error(this, "Could not get initial value of " + name + " for " + obj, ex);
return null;
});
}
@Override
@SuppressWarnings("unchecked")
public void attributesChanged(TargetObject parent, Collection<String> removed,
Map<String, ?> added) {
if (added.containsKey(name)) {
set((T) added.get(name), null);
}
else if (removed.contains(name)) {
set(null, null);
}
}
public void dispose() {
this.dispose(new AssertionError("disposed"));
}
@Override
public void dispose(Throwable reason) {
super.dispose(reason);
obj.removeListener(this);
}
}
public static class AsyncState extends AsyncAttribute<TargetExecutionState> {
public AsyncState(TargetExecutionStateful stateful) {
super(stateful, TargetExecutionStateful.STATE_ATTRIBUTE_NAME);
}
}
public static class AsyncAccess extends AsyncAttribute<Boolean> {
public AsyncAccess(TargetAccessConditioned ac) {
super(ac, TargetAccessConditioned.ACCESSIBLE_ATTRIBUTE_NAME);
}
}
/**
* Obtain an object which tracks accessibility for a given target object.
*
@ -669,7 +803,10 @@ public enum DebugModelConventions {
* @param obj the object whose accessibility to track
* @return a future which completes with an {@link AsyncReference} of the objects effective
* accessibility.
* @deprecated Just listen on the nearest {@link TargetAccessConditioned} ancestor instead. The
* "every-ancestor" convention is deprecated.
*/
@Deprecated
public static CompletableFuture<AllRequiredAccess> trackAccessibility(TargetObject obj) {
CompletableFuture<? extends Collection<? extends TargetAccessConditioned>> collectAncestors =
collectAncestors(obj, TargetAccessConditioned.class);

View file

@ -15,29 +15,21 @@
*/
package ghidra.dbg;
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointListener;
import ghidra.dbg.target.TargetEventScope.TargetEventScopeListener;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionStateListener;
import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener;
import ghidra.dbg.target.TargetInterpreter.TargetInterpreterListener;
import ghidra.dbg.target.TargetMemory.TargetMemoryListener;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetObject.TargetObjectListener;
import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener;
import java.util.*;
import ghidra.dbg.error.DebuggerMemoryAccessException;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
import ghidra.util.Msg;
/**
* A listener for events related to the debugger model, usually a connection
*
* <p>
* TODO: Most (non-client) models do not implement this. Even the client ones do not implement
* {@link #modelStateChanged()}
* A listener for events related to the debugger model, including its connection and objects
*/
public interface DebuggerModelListener
extends TargetObjectListener, TargetAccessibilityListener, TargetBreakpointListener,
TargetInterpreterListener, TargetEventScopeListener, TargetExecutionStateListener,
TargetFocusScopeListener, TargetMemoryListener, TargetRegisterBankListener {
public interface DebuggerModelListener {
/**
* An error occurred such that this listener will no longer receive events
@ -58,14 +50,9 @@ public interface DebuggerModelListener
}
/**
* The root object has been added to the model
*
* <p>
* This indicates the root is ready, not just {@link #created(TargetObject)}.
*
* @param root the root object
* The model's state has changed, prompting an update to its description
*/
default public void rootAdded(TargetObject root) {
default public void modelStateChanged() {
}
/**
@ -81,8 +68,253 @@ public interface DebuggerModelListener
}
/**
* The model's state has changed, prompting an update to its description
* An object was created
*
* <p>
* This can only be received by listening on the model. While the created object can now appear
* in other callbacks, it should not be used aside from those callbacks, until it is added to
* the model. Until that time, the object may not adhere to the schema, since its children are
* still being initialized.
*
* @param object the newly-created object
*/
default public void modelStateChanged() {
default void created(TargetObject object) {
}
/**
* An object is no longer valid
*
* <p>
* This should be the final callback ever issued for this object. Invalidation of an object
* implies invalidation of all its successors; nevertheless, the implementation MUST explicitly
* invoke this callback for those successors in preorder. Users need only listen for
* invalidation by installing a listener on the object of interest. However, a user must be able
* to ignore invalidation events on an object it has already removed and/or invalidated. The
* {@code branch} parameter will identify the branch node of the sub-tree being removed. For
* models that are managed by a client connection, disconnecting or otherwise terminating the
* session should invalidate the root, and thus every object must receive this callback.
*
* <p>
* If an invalidated object is replaced (i.e., a new object with the same path is added to the
* model), the implementation must be careful to issue all invalidations related to the removed
* object before the replacement is added, so that delayed invalidations are not mistakenly
* applied to the replacement or its successors.
*
* @param object the now-invalid object
* @param branch the root of the sub-tree being invalidated
* @param reason an informational, human-consumable reason, if applicable
*/
default void invalidated(TargetObject object, TargetObject branch, String reason) {
}
/**
* The root object has been added to the model
*
* <p>
* This indicates the root is ready, not just {@link #created(TargetObject)}. Note this callback
* indicates the root being "added to the model."
*
* @param root the root object
*/
default public void rootAdded(TargetObject root) {
}
/**
* The object's elements changed
*
* <p>
* The listener must have received a prior {@link #created(TargetObject)} callback for the
* parent and all (object-valued) elements being added. Assuming {@code object} has already been
* "added the model," this callback indicates all objects in the {@code added} parameter being
* "added to the model" along with their successors.
*
* @param object the object whose children changed
* @param removed the list of removed children
* @param added a map of indices to new children references
*/
default void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
}
/**
* The object's attributes changed
*
* <p>
* In the case of an object-valued attribute, changes to that object do not constitute a changed
* attribute. The attribute is considered changed only when that attribute is assigned to a
* completely different object.
*
* @param object the object whose attributes changed
* @param removed the list of removed attributes
* @param added a map of names to new/changed attributes
*/
default void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
}
/**
* The model has requested the client invalidate (non-tree) caches associated with an object
*
* <p>
* For objects with methods exposing contents other than elements and attributes (e.g., memory
* and register contents), this callback requests that any caches associated with that content
* be invalidated. Most notably, this usually occurs when an object (e.g., thread) enters the
* {@link TargetExecutionState#RUNNING} state, to inform proxies that they should invalidate
* their memory and register caches. In most cases, clients need not worry about this callback.
* Protocol implementations that use the model, however, should forward this request to the
* client-side peer.
*
* <p>
* Note caches of elements and attributes are not affected by this callback. See
* {@link TargetObject#invalidateCaches()}.
*
* @param object the object whose caches must be invalidated
*/
default void invalidateCacheRequested(TargetObject object) {
}
/**
* A breakpoint trapped execution
*
* <p>
* The program counter can be obtained in a few ways. The most reliable is to get the address of
* the breakpoint location. If available, the frame will also contain the program counter.
* Finally, the trapped object or one of its relatives may offer the program counter.
*
* @param container the container whose breakpoint trapped execution
* @param trapped the object whose execution was trapped, usually a {@link TargetThread}
* @param frame the innermost stack frame, if available, of the trapped object
* @param spec the breakpoint specification
* @param breakpoint the breakpoint location that actually trapped execution
*/
default void breakpointHit(TargetObject container, TargetObject trapped,
TargetStackFrame frame, TargetBreakpointSpec spec,
TargetBreakpointLocation breakpoint) {
}
/**
* A console has produced output (given as bytes)
*
* <p>
* Note that "captured" outputs will not be reported in this callback. See
* {@link TargetInterpreter#executeCapture(String)}.
*
* @param console the console producing the output
* @param channel identifies the "output stream", stdout or stderr
* @param data the output data
*/
default void consoleOutput(TargetObject console, Channel channel, byte[] data) {
}
/**
* A console has produced output (given as a string)
*
* @implNote Overriding this method is not a substitute for overriding
* {@link #consoleOutput(TargetObject, Channel, byte[])}. Some models may invoke this
* {@code String} variant as a convenience, which by default, invokes the
* {@code byte[]} variant, but models are only expected to invoke the {@code byte[]}
* variant. A client may override this method simply to avoid back-and-forth
* conversions between {@code String}s and {@code byte[]}s.
*
* @param console the console producing the output
* @param channel identifies the "output stream", stdout or stderr
* @param text the output text
*/
default void consoleOutput(TargetObject console, Channel channel, String text) {
consoleOutput(console, channel, text.getBytes(TargetConsole.CHARSET));
}
/**
* A "special" event has occurred
*
* <p>
* When present, this callback must be invoked before any other callback which results from this
* event, except creation events. E.g., for PROCESS_EXITED, this must be called before the
* affected process is invalidated.
*
* <p>
* Whenever possible, event thread must be given. This is often the thread given focus by the
* debugger immediately upon stopping for the event. Parameters are not (yet) strictly
* specified, but it should include the stopped target, if that target is not already given by
* the event thread. It may optionally contain other useful information, such as an exit code,
* but no client should depend on that information being given.
*
* <p>
* The best way to communicate to users what has happened is via the description. Almost every
* other result of an event is communicated by other means in the model, e.g., state changes,
* object creation, invalidation. The description should contain as much information as possible
* to cue users as to why the other changes have occurred, and point them to relevant objects.
* For example, if trapped on a breakpoint, the description might contain the breakpoint's
* identifier. If the debugger prints a message for this event, that message is probably a
* sufficient description.
*
* @param object the event scope
* @param eventThread if applicable, the thread causing the event
* @param type the type of event
* @param description a human-readable description of the event
* @param parameters extra parameters for the event. TODO: Specify these for each type, or break
* this into other callbacks.
*/
default void event(TargetObject object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
}
/**
* Memory was successfully read or written
*
* <p>
* This implies memory caches should be updated. If the implementation employs a cache, then it
* need only report reads or writes which updated that cache. However, that cache must be
* invalidated whenever any other event occurs which could change memory, e.g., the target
* stepping or running. See {@link #invalidateCacheRequested(TargetObject)}. If the
* implementation does not employ a cache, then it must report <em>every</em> successful
* client-driven read or write. If the implementation can detect <em>debugger-driven</em> memory
* reads and writes, then it is recommended to call this method for those events. However, this
* method <em>must not</em> be called for <em>target-driven</em> memory changes. In other words,
* this method should only be called for reads or writes requested by the user.
*
* @param memory this memory object
* @param address the starting address of the affected range
* @param data the new data for the affected range
*/
default void memoryUpdated(TargetObject memory, Address address, byte[] data) {
}
/**
* An attempt to read memory failed
*
* <p>
* Like {@link #memoryUpdated(TargetMemory, Address, byte[])}, this should only be invoked for
* <em>user-driven</em> requests. Failure of the <em>target</em> to read its own memory would
* likely be reported via an exception, not this callback.
*
* @param memory the memory object
* @param range the range for the read which generated the error
* @param e the error
*/
default void memoryReadError(TargetObject memory, AddressRange range,
DebuggerMemoryAccessException e) {
}
/**
* Registers were successfully read or written
*
* <p>
* This implies register caches should be updated. If the implementation employs a cache, then
* it need only report reads or writes which updated that cache. However, that cache must be
* invalidated whenever any other event occurs which could change register values, e.g., the
* target stepping or running. See {@link #invalidateCacheRequested(TargetObject)}. If the
* implementation doe not employ a cache, then it must report <em>every</em> successful
* client-driven read or write. If the implementation can detect <em>debugger-driven</em>
* register reads and writes, then it recommended to call this method for those events. However,
* this method <em>must not</em> be called for <em>target-driven</em> register changes, except
* perhaps when the target becomes suspended. Note that some models may additionally provide a
* {@code value} attribute on each register -- when the register bank is its own description
* container -- however, updating those attributes is not a substitute for this callback.
*
* @param bank this register bank object
* @param updates a name-value map of updated registers
*/
default void registersUpdated(TargetObject bank, Map<String, byte[]> updates) {
}
}

View file

@ -15,10 +15,10 @@
*/
package ghidra.dbg;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.RejectedExecutionException;
import java.util.function.Predicate;
import ghidra.async.AsyncUtils;
import ghidra.async.TypeSpec;
@ -27,6 +27,7 @@ import ghidra.dbg.target.TargetMemory;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode;
import ghidra.dbg.util.PathUtils;
import ghidra.program.model.address.*;
import ghidra.util.Msg;
@ -223,16 +224,16 @@ public interface DebuggerObjectModel {
*
* @param <T> the required implementation-specific type
* @param cls the class for the required type
* @param ref the reference (or object) to check
* @param obj the object to check
* @return the object, cast to the desired typed
* @throws IllegalArgumentException if -ref- does not belong to this model
* @throws DebuggerIllegalArgumentException if {@code obj} does not belong to this model
*/
default <T extends TargetObject> T assertMine(Class<T> cls, TargetObject ref) {
if (ref.getModel() != this) {
throw new IllegalArgumentException(
"TargetObject (or ref)" + ref + " does not belong to this model");
default <T extends TargetObject> T assertMine(Class<T> cls, TargetObject obj) {
if (obj.getModel() != this) {
throw new DebuggerIllegalArgumentException(
"TargetObject " + obj + " does not belong to this model");
}
return cls.cast(ref);
return cls.cast(obj);
}
/**
@ -304,18 +305,19 @@ public interface DebuggerObjectModel {
*
* <p>
* The root is a virtual object to contain all the top-level objects of the model tree. This
* object represents the debugger itself.
* object represents the debugger itself. Note in most cases {@link #getModelRoot()} is
* sufficient; however, if you've just created the model, it is prudent to wait for it to create
* its root. For asynchronous cases, just listen for the root-creation and -added events. This
* method returns a future which completes after the root-added event.
*
* @return the root
* @deprecated use {@link #getModelRoot()} instead
* @return a future which completes with the root
*/
@Deprecated(forRemoval = true)
public CompletableFuture<? extends TargetObject> fetchModelRoot();
/**
* Get the root object of the model
*
* @return the root
* @return the root or {@code null} if it hasn't been created, yet
*/
public TargetObject getModelRoot();
@ -426,9 +428,15 @@ public interface DebuggerObjectModel {
}
/**
* @see #fetchModelObject(List)
* @deprecated Use {@link #getModelObject(List)} instead, or {@link #fetchModelObject(List)} if
* a refresh is needed
* Get an object from the model, resyncing according to the schema
*
* <p>
* This is necessary when an object in the path has a resync mode other than
* {@link ResyncMode#NEVER} for the child being retrieved. Please note that some synchronization
* may still be required on the client side, since accessing the object before it is created
* will cause a {@code null} completion.
*
* @return a future that completes with the object or with {@code null} if it doesn't exist
*/
@Deprecated
public default CompletableFuture<? extends TargetObject> fetchModelObject(List<String> path) {
@ -441,13 +449,25 @@ public interface DebuggerObjectModel {
* <p>
* Note this may return an object which is still being constructed, i.e., between being created
* and being added to the model. This differs from {@link #getModelValue(List)}, which will only
* return an object after it has been added.
* return an object after it has been added. This method also never follows links.
*
* @param path the path of the object
* @return the object
* @return the object or {@code null} if it doesn't exist
*/
public TargetObject getModelObject(List<String> path);
/**
* Get all created objects matching a given predicate
*
* <p>
* Note the predicate is executed while holding an internal model-wide lock. Be careful and keep
* it simple.
*
* @param predicate the predicate
* @return the set of matching objects
*/
public Set<TargetObject> getModelObjects(Predicate<? super TargetObject> predicate);
/**
* @see #fetchModelObject(List)
*/
@ -455,6 +475,9 @@ public interface DebuggerObjectModel {
return fetchModelObject(List.of(path));
}
/**
* @see #getModelObject(List)
*/
public default TargetObject getModelObject(String... path) {
return getModelObject(List.of(path));
}

View file

@ -0,0 +1,26 @@
/* ###
* 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.dbg;
import ghidra.dbg.target.TargetMemory;
import ghidra.dbg.target.TargetObject;
import ghidra.program.model.address.Address;
public interface DebuggerObjectModelWithMemory {
TargetMemory getMemory(TargetObject target, Address address, int length);
}

View file

@ -17,16 +17,24 @@ package ghidra.dbg.agent;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.util.PathUtils;
import ghidra.util.Msg;
import ghidra.util.datastruct.ListenerSet;
public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectModel {
protected final Object lock = new Object();
protected final ExecutorService clientExecutor = Executors.newSingleThreadExecutor();
public final Object lock = new Object();
protected final ExecutorService clientExecutor =
Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder()
.namingPattern(getClass().getSimpleName() + "-thread-%d")
.build());
protected final ListenerSet<DebuggerModelListener> listeners =
new ListenerSet<>(DebuggerModelListener.class, clientExecutor);
@ -50,19 +58,18 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo
}
protected void objectInvalidated(TargetObject object) {
synchronized (lock) {
creationLog.remove(object);
}
creationLog.remove(object.getPath());
}
protected void addModelRoot(SpiTargetObject root) {
assert root == this.root;
synchronized (lock) {
rootAdded = true;
root.getSchema()
.validateTypeAndInterfaces(root, null, null, root.enforcesStrictSchema());
this.completedRoot.completeAsync(() -> root, clientExecutor);
listeners.fire.rootAdded(root);
}
root.getSchema().validateTypeAndInterfaces(root, null, null, root.enforcesStrictSchema());
this.completedRoot.completeAsync(() -> root, clientExecutor);
listeners.fire.rootAdded(root);
}
@Override
@ -77,20 +84,27 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo
}
}
protected void onClientExecutor(DebuggerModelListener listener, Runnable r) {
CompletableFuture.runAsync(r, clientExecutor).exceptionally(t -> {
Msg.error(this, "Listener " + listener + " caused unexpected exception", t);
return null;
});
}
protected void replayTreeEvents(DebuggerModelListener listener) {
if (root == null) {
assert creationLog.isEmpty();
return;
}
for (SpiTargetObject object : creationLog.values()) {
listener.created(object);
onClientExecutor(listener, () -> listener.created(object));
}
Set<SpiTargetObject> visited = new HashSet<>();
for (SpiTargetObject object : creationLog.values()) {
replayAddEvents(listener, object, visited);
}
if (rootAdded) {
listener.rootAdded(root);
onClientExecutor(listener, () -> listener.rootAdded(root));
}
}
@ -99,34 +113,42 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo
if (!visited.add(object)) {
return;
}
for (Object val : object.getCachedAttributes().values()) {
Map<String, ?> cachedAttributes = object.getCachedAttributes();
for (Object val : cachedAttributes.values()) {
if (!(val instanceof TargetObject)) {
continue;
}
assert val instanceof SpiTargetObject;
replayAddEvents(listener, (SpiTargetObject) val, visited);
}
listener.attributesChanged(object, List.of(), object.getCachedAttributes());
for (TargetObject elem : object.getCachedElements().values()) {
if (!cachedAttributes.isEmpty()) {
onClientExecutor(listener,
() -> listener.attributesChanged(object, List.of(), cachedAttributes));
}
Map<String, ? extends TargetObject> cachedElements = object.getCachedElements();
for (TargetObject elem : cachedElements.values()) {
assert elem instanceof SpiTargetObject;
replayAddEvents(listener, (SpiTargetObject) elem, visited);
}
listener.elementsChanged(object, List.of(), object.getCachedElements());
if (!cachedElements.isEmpty()) {
onClientExecutor(listener,
() -> listener.elementsChanged(object, List.of(), cachedElements));
}
}
@Override
public void addModelListener(DebuggerModelListener listener, boolean replay) {
CompletableFuture.runAsync(() -> {
try {
synchronized (lock) {
if (replay) {
replayTreeEvents(listener);
}
listeners.add(listener);
}
}, clientExecutor).exceptionally(ex -> {
}
catch (Throwable ex) {
listener.catastrophic(ex);
return null;
});
}
}
@Override
@ -137,30 +159,27 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo
/**
* Ensure that dependent computations occur on the client executor
*
* <p>
* Use as a method reference in a final call to
* {@link CompletableFuture#thenCompose(java.util.function.Function)} to ensure the final stage
* completes on the client executor.
*
* @param <T> the type of the future value
* @param v the value
* @return a future while completes with the given value on the client executor
* @param v the future
* @return a future which completes after the given one on the client executor
*/
public <T> CompletableFuture<T> gateFuture(T v) {
//Msg.debug(this, "Gate requested @" + System.identityHashCode(clientExecutor));
//Msg.debug(this, " rvalue: " + v);
return CompletableFuture.supplyAsync(() -> {
//Msg.debug(this, "Gate completing @" + System.identityHashCode(clientExecutor));
//Msg.debug(this, " cvalue: " + v);
return v;
public <T> CompletableFuture<T> gateFuture(CompletableFuture<T> future) {
return future.whenCompleteAsync((t, ex) -> {
}, clientExecutor);
}
@Override
public CompletableFuture<Void> flushEvents() {
return CompletableFuture.supplyAsync(() -> null, clientExecutor);
}
/*
@Override
public CompletableFuture<Void> flushEvents() {
return gateFuture(null);
//return CompletableFuture.supplyAsync(() -> gateFuture((Void) null)).thenCompose(f -> f);
}
*/
@Override
public CompletableFuture<Void> close() {
@ -197,7 +216,17 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo
@Override
public TargetObject getModelObject(List<String> path) {
synchronized (lock) {
if (path.isEmpty()) {
return root;
}
return creationLog.get(path);
}
}
@Override
public Set<TargetObject> getModelObjects(Predicate<? super TargetObject> predicate) {
synchronized (lock) {
return creationLog.values().stream().filter(predicate).collect(Collectors.toSet());
}
}
}

View file

@ -19,13 +19,13 @@ import java.lang.reflect.Proxy;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.agent.AbstractTargetObject.ProxyFactory;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetObject.TargetObjectListener;
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.util.PathUtils;
import ghidra.lifecycle.Internal;
import ghidra.util.datastruct.ListenerSet;
/**
@ -40,8 +40,7 @@ import ghidra.util.datastruct.ListenerSet;
*
* @param <P> the type of the parent
*/
public abstract class AbstractTargetObject<P extends TargetObject>
implements SpiTargetObject {
public abstract class AbstractTargetObject<P extends TargetObject> implements SpiTargetObject {
public static interface ProxyFactory<I> {
SpiTargetObject createProxy(AbstractTargetObject<?> delegate, I info);
}
@ -64,12 +63,12 @@ public abstract class AbstractTargetObject<P extends TargetObject>
protected boolean valid = true;
// TODO: Remove these, and just do invocations on model's listeners?
protected final ListenerSet<TargetObjectListener> listeners;
protected final ListenerSet<DebuggerModelListener> listeners;
public <I> AbstractTargetObject(ProxyFactory<I> proxyFactory, I proxyInfo,
AbstractDebuggerObjectModel model, P parent, String key, String typeHint,
TargetObjectSchema schema) {
this.listeners = new ListenerSet<>(TargetObjectListener.class, model.clientExecutor);
this.listeners = new ListenerSet<>(DebuggerModelListener.class, model.clientExecutor);
this.model = model;
listeners.addChained(model.listeners);
this.parent = parent;
@ -129,6 +128,11 @@ public abstract class AbstractTargetObject<P extends TargetObject>
return DebuggerObjectModel.requireIface(iface, getProxy(), path);
}
@Override
public Collection<String> getInterfaceNames() {
return Protected.getInterfaceNamesOf(getProxy().getClass());
}
/**
* Check if this object strictly conforms to the schema
*
@ -162,11 +166,11 @@ public abstract class AbstractTargetObject<P extends TargetObject>
@Override
public String toString() {
if (schema == null) {
return String.format("<%s: path=%s model=%s schema=NULL>", getClass().getSimpleName(),
path, getModel());
return String.format("<%s: path=%s model=%s schema=<null>>", getClass().getSimpleName(),
getJoinedPath("."), getModel());
}
return String.format("<%s: path=%s model=%s schema=%s>", getClass().getSimpleName(), path,
getModel(), schema.getName());
return String.format("<%s: path=%s model=%s schema=%s>", getClass().getSimpleName(),
getJoinedPath("."), getModel(), schema.getName());
}
@Override
@ -185,7 +189,7 @@ public abstract class AbstractTargetObject<P extends TargetObject>
}
@Override
public void addListener(TargetObjectListener l) {
public void addListener(DebuggerModelListener l) {
if (!valid) {
throw new IllegalStateException("Object is no longer valid: " + getProxy());
}
@ -193,7 +197,7 @@ public abstract class AbstractTargetObject<P extends TargetObject>
}
@Override
public void removeListener(TargetObjectListener l) {
public void removeListener(DebuggerModelListener l) {
listeners.remove(l);
}
@ -242,35 +246,43 @@ public abstract class AbstractTargetObject<P extends TargetObject>
model.objectInvalidated(getProxy());
listeners.fire.invalidated(getProxy(), branch, reason);
listeners.clear();
listeners.clearChained();
}
protected void doInvalidateElements(Collection<?> elems, String reason) {
for (Object e : elems) {
protected void doInvalidateElements(Map<String, ?> elems, String reason) {
for (Map.Entry<String, ?> ent : elems.entrySet()) {
String name = ent.getKey();
Object e = ent.getValue();
if (e instanceof InvalidatableTargetObjectIf && e instanceof TargetObject) {
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) e;
obj.invalidateSubtree((TargetObject) e, reason);
if (!PathUtils.isElementLink(getPath(), name, obj.getPath())) {
obj.doInvalidateSubtree((TargetObject) e, reason);
}
}
}
}
protected void doInvalidateElements(TargetObject branch, Collection<?> elems, String reason) {
for (Object e : elems) {
protected void doInvalidateElements(TargetObject branch, Map<String, ?> elems, String reason) {
for (Map.Entry<String, ?> ent : elems.entrySet()) {
String name = ent.getKey();
Object e = ent.getValue();
if (e instanceof InvalidatableTargetObjectIf) {
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) e;
obj.invalidateSubtree(branch, reason);
if (!PathUtils.isElementLink(getPath(), name, obj.getPath())) {
obj.doInvalidateSubtree(branch, reason);
}
}
}
}
protected void doInvalidateAttributes(Map<String, ?> attrs,
String reason) {
protected void doInvalidateAttributes(Map<String, ?> attrs, String reason) {
for (Map.Entry<String, ?> ent : attrs.entrySet()) {
String name = ent.getKey();
Object a = ent.getValue();
if (a instanceof InvalidatableTargetObjectIf && a instanceof TargetObject) {
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) a;
if (!PathUtils.isLink(getPath(), name, obj.getPath())) {
obj.invalidateSubtree((TargetObject) a, reason);
obj.doInvalidateSubtree((TargetObject) a, reason);
}
}
}
@ -284,21 +296,40 @@ public abstract class AbstractTargetObject<P extends TargetObject>
if (a instanceof InvalidatableTargetObjectIf) {
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) a;
if (!PathUtils.isLink(getPath(), name, obj.getPath())) {
obj.invalidateSubtree(branch, reason);
obj.doInvalidateSubtree(branch, reason);
}
}
}
}
@Override
public void invalidateSubtree(TargetObject branch, String reason) {
public void doInvalidateSubtree(TargetObject branch, String reason) {
// Pre-ordered traversal
doInvalidate(branch, reason);
doInvalidateElements(branch, getCachedElements().values(), reason);
doInvalidateElements(branch, getCachedElements(), reason);
doInvalidateAttributes(branch, getCachedAttributes(), reason);
}
public ListenerSet<TargetObjectListener> getListeners() {
@Override
public void invalidateSubtree(TargetObject branch, String reason) {
synchronized (model.lock) {
doInvalidateSubtree(branch, reason);
}
}
/**
* Get the listener set
*
* <p>
* TODO: This method should only be used by the internal implementation. It's not exposed on the
* {@link TargetObject} interface, but it could be dangerous to have it here, since clients
* could cast to {@link AbstractTargetObject} and get at it, even if the implementation's jar is
* excluded from the compile-time classpath.
*
* @return the listener set
*/
@Internal
public ListenerSet<DebuggerModelListener> getListeners() {
return listeners;
}
}

View file

@ -17,15 +17,18 @@ package ghidra.dbg.agent;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode;
import ghidra.dbg.util.CollectionUtils.Delta;
import ghidra.dbg.util.PathUtils;
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
import ghidra.util.Msg;
import ghidra.util.datastruct.ListenerSet;
/**
* A default implementation of {@link TargetObject} suitable for cases where the implementation
@ -39,14 +42,44 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
extends AbstractTargetObject<P> {
/** Note modifying this directly subverts notifications */
protected final Map<String, E> elements = new TreeMap<>(TargetObjectKeyComparator.ELEMENT);
protected final Map<String, E> elements =
new TreeMap<>(TargetObjectKeyComparator.ELEMENT);
protected final Map<String, E> cbElements =
new TreeMap<>(TargetObjectKeyComparator.ELEMENT);
protected final Map<String, E> roCbElements =
Collections.unmodifiableMap(cbElements);
protected CompletableFuture<Void> curElemsRequest;
/** Note modifying this directly subverts notifications */
protected final Map<String, Object> attributes =
new TreeMap<>(TargetObjectKeyComparator.ATTRIBUTE);
protected final Map<String, Object> cbAttributes =
new TreeMap<>(TargetObjectKeyComparator.ATTRIBUTE);
protected final Map<String, Object> roCbAttributes =
Collections.unmodifiableMap(cbAttributes);
protected CompletableFuture<Void> curAttrsRequest;
/*protected static Set<Class<?>> dependencySet = Set.of(//
TargetProcess.class, //
TargetThread.class, //
TargetStack.class, //
TargetStackFrame.class, //
TargetRegisterBank.class, //
TargetRegisterContainer.class, //
TargetRegister.class, //
TargetMemory.class, //
TargetMemoryRegion.class, //
TargetModule.class, //
TargetModuleContainer.class, //
TargetSection.class, //
TargetBreakpointSpecContainer.class, //
TargetBreakpointSpec.class, //
TargetBreakpointLocation.class, //
TargetEventScope.class, //
TargetFocusScope.class, //
TargetExecutionStateful.class //
);*/
/**
* Construct a new default target object whose schema is derived from the parent
*
@ -117,10 +150,9 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
AbstractDebuggerObjectModel model, P parent, String key, String typeHint,
TargetObjectSchema schema) {
super(proxyFactory, proxyInfo, model, parent, key, typeHint, schema);
changeAttributes(List.of(), List.of(), Map.ofEntries(
Map.entry(DISPLAY_ATTRIBUTE_NAME, key == null ? "<root>" : key),
Map.entry(UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED)),
"Initialized");
changeAttributes(List.of(), List.of(),
Map.ofEntries(Map.entry(DISPLAY_ATTRIBUTE_NAME, key == null ? "<root>" : key)),
"Default");
}
public <I> DefaultTargetObject(ProxyFactory<I> proxyFactory, I proxyInfo,
@ -157,8 +189,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
@Override
public CompletableFuture<Void> resync(boolean refreshAttributes, boolean refreshElements) {
return CompletableFuture.allOf(
fetchAttributes(refreshAttributes),
return CompletableFuture.allOf(fetchAttributes(refreshAttributes),
fetchElements(refreshElements));
}
@ -186,6 +217,14 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
return AsyncUtils.NIL;
}
private boolean shouldRequestElements(boolean refresh) {
if (refresh) {
return true;
}
ResyncMode resync = getSchema().getElementResyncMode();
return resync.shouldResync(curElemsRequest);
}
/**
* {@inheritDoc}
*
@ -199,11 +238,10 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
public CompletableFuture<? extends Map<String, ? extends E>> fetchElements(boolean refresh) {
CompletableFuture<Void> req;
synchronized (elements) {
if (refresh || curElemsRequest == null || curElemsRequest.isCompletedExceptionally() ||
getUpdateMode() == TargetUpdateMode.SOLICITED) {
curElemsRequest = requestElements(refresh).thenCompose(model::gateFuture);
if (shouldRequestElements(refresh)) {
curElemsRequest = model.gateFuture(requestElements(refresh));
}
req = curElemsRequest;
req = curElemsRequest == null ? AsyncUtils.NIL : curElemsRequest;
}
return req.thenApply(__ -> getCachedElements());
}
@ -220,6 +258,11 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
}
}
@Override
public Map<String, E> getCallbackElements() {
return roCbElements;
}
/**
* {@inheritDoc}
*
@ -230,22 +273,13 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
return fetchElements().thenApply(elems -> elems.get(index));
}
protected Map<String, E> combineElements(Collection<? extends E> canonical,
Map<String, ? extends E> links) {
protected Map<String, E> combineElements(Collection<? extends E> autoKeyed,
Map<String, ? extends E> mapKeyed) {
Map<String, E> asMap = new LinkedHashMap<>();
for (E e : canonical) {
if (!PathUtils.parent(e.getPath()).equals(getPath())) {
Msg.error(this, "Link found in canonical elements of " + parent + ": " + e);
}
for (E e : autoKeyed) {
asMap.put(e.getIndex(), e);
}
for (Map.Entry<String, ? extends E> ent : links.entrySet()) {
if (!PathUtils.isLink(getPath(), PathUtils.makeKey(ent.getKey()),
ent.getValue().getPath())) {
//Msg.error(this, "Canonical element found in links: " + ent);
}
asMap.put(ent.getKey(), ent.getValue());
}
asMap.putAll(mapKeyed);
return asMap;
}
@ -256,14 +290,14 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
* An existing element is left in place if it's identical to its replacement as in {@code ==}.
* This method also invalidates the sub-trees of removed elements, if any.
*
* @param canonical the desired set of canonical elements
* @param links the desired map of linked elements
* @param autoKeyed the desired set of elements where keys are given by the elements
* @param mapKeyed the desired map of elements with specified keys (usually for links)
* @param reason the reason for the change (used as the reason for invalidation)
* @return the delta from the previous elements
*/
public Delta<E, E> setElements(Collection<? extends E> canonical,
Map<String, ? extends E> links, String reason) {
Map<String, E> elements = combineElements(canonical, links);
public Delta<E, E> setElements(Collection<? extends E> autoKeyed,
Map<String, ? extends E> mapKeyed, String reason) {
Map<String, E> elements = combineElements(autoKeyed, mapKeyed);
return setElements(elements, reason);
}
@ -274,6 +308,15 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
return setElements(elements, Map.of(), reason);
}
private void updateCallbackElements(Delta<E, E> delta) {
CompletableFuture.runAsync(() -> {
delta.apply(this.cbElements, Delta.SAME);
}, model.clientExecutor).exceptionally(ex -> {
Msg.error(this, "Error updating elements before callback");
return null;
});
}
private Delta<E, E> setElements(Map<String, E> elements, String reason) {
Delta<E, E> delta;
synchronized (model.lock) {
@ -283,8 +326,9 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
if (schemax != null) {
schemax.validateElementDelta(getPath(), delta, enforcesStrictSchema());
}
doInvalidateElements(delta.removed.values(), reason);
doInvalidateElements(delta.removed, reason);
if (!delta.isEmpty()) {
updateCallbackElements(delta);
listeners.fire.elementsChanged(getProxy(), delta.getKeysRemoved(), delta.added);
}
return delta;
@ -298,15 +342,14 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
* This method also invalidates the sub-trees of removed elements, if any.
*
* @param remove the set of indices to remove
* @param addCanonical the set of canonical elements to add
* @param addLinks the map of linked elements to add
* @param autoKeyed the set of elements to add with the elements' keys
* @param mapKeyed the map of elements to add with given keys (usually for links)
* @param reason the reason for the change (used as the reason for invalidation)
* @return the actual delta from the previous to the current elements
*/
public Delta<E, E> changeElements(Collection<String> remove,
Collection<? extends E> addCanonical, Map<String, ? extends E> addLinks,
String reason) {
Map<String, E> add = combineElements(addCanonical, addLinks);
public Delta<E, E> changeElements(Collection<String> remove, Collection<? extends E> autoKeyed,
Map<String, ? extends E> mapKeyed, String reason) {
Map<String, E> add = combineElements(autoKeyed, mapKeyed);
return changeElements(remove, add, reason);
}
@ -318,7 +361,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
return changeElements(remove, add, Map.of(), reason);
}
private Delta<E, E> changeElements(Collection<String> remove, Map<String, E> add,
public Delta<E, E> changeElements(Collection<String> remove, Map<String, E> add,
String reason) {
Delta<E, E> delta;
synchronized (model.lock) {
@ -328,8 +371,9 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
if (schemax != null) {
schemax.validateElementDelta(getPath(), delta, enforcesStrictSchema());
}
doInvalidateElements(delta.removed.values(), reason);
doInvalidateElements(delta.removed, reason);
if (!delta.isEmpty()) {
updateCallbackElements(delta);
listeners.fire.elementsChanged(getProxy(), delta.getKeysRemoved(), delta.added);
}
return delta;
@ -353,6 +397,14 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
return AsyncUtils.NIL;
}
private boolean shouldRequestAttributes(boolean refresh) {
if (refresh) {
return true;
}
ResyncMode resync = getSchema().getAttributeResyncMode();
return resync.shouldResync(curAttrsRequest);
}
/**
* {@inheritDoc}
*
@ -367,10 +419,10 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
CompletableFuture<Void> req;
synchronized (attributes) {
// update_mode does not affect attributes. They always behave as if UNSOLICITED.
if (refresh || curAttrsRequest == null || curAttrsRequest.isCompletedExceptionally()) {
curAttrsRequest = requestAttributes(refresh).thenCompose(model::gateFuture);
if (shouldRequestAttributes(refresh)) {
curAttrsRequest = model.gateFuture(requestAttributes(refresh));
}
req = curAttrsRequest;
req = curAttrsRequest == null ? AsyncUtils.NIL : curAttrsRequest;
}
return req.thenApply(__ -> {
synchronized (model.lock) {
@ -394,6 +446,11 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
}
}
@Override
public Map<String, ?> getCallbackAttributes() {
return roCbAttributes;
}
@Override
public Object getCachedAttribute(String name) {
synchronized (model.lock) {
@ -401,25 +458,13 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
}
}
protected Map<String, Object> combineAttributes(
Collection<? extends TargetObject> canonicalObjects, Map<String, ?> linksAndValues) {
protected Map<String, Object> combineAttributes(Collection<? extends TargetObject> autoKeyed,
Map<String, ?> mapKeyed) {
Map<String, Object> asMap = new LinkedHashMap<>();
for (TargetObject ca : canonicalObjects) {
if (!PathUtils.parent(ca.getPath()).equals(getPath())) {
Msg.error(this, "Link found in canonical attributes: " + ca);
}
for (TargetObject ca : autoKeyed) {
asMap.put(ca.getName(), ca);
}
for (Map.Entry<String, ?> ent : linksAndValues.entrySet()) {
Object av = ent.getValue();
/*if (av instanceof TargetObject) {
TargetObject link = (TargetObject) av;
if (!PathUtils.isLink(getPath(), ent.getKey(), link.getPath())) {
//Msg.error(this, "Canonical attribute found in links: " + ent);
}
}*/
asMap.put(ent.getKey(), av);
}
asMap.putAll(mapKeyed);
return asMap;
}
@ -431,22 +476,31 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
* defined by {@link Objects#equals(Object, Object)}. This method also invalidates the sub-trees
* of removed non-reference object-valued attributes.
*
* @param canonicalObjects the desired set of canonical object-valued attributes
* @param linksAndValues the desired map of other attributes
* @param autoKeyed the desired set of object-valued attributes using the objects' keys
* @param mapKeyed the desired map of other attributes (usually links and primitive values)
* @param reason the reason for the change (used as the reason for invalidation)
* @return the delta from the previous attributes
*/
public Delta<?, ?> setAttributes(Collection<? extends TargetObject> canonicalObjects,
Map<String, ?> linksAndValues, String reason) {
Map<String, ?> attributes = combineAttributes(canonicalObjects, linksAndValues);
public Delta<?, ?> setAttributes(Collection<? extends TargetObject> autoKeyed,
Map<String, ?> mapKeyed, String reason) {
Map<String, ?> attributes = combineAttributes(autoKeyed, mapKeyed);
return setAttributes(attributes, reason);
}
private void updateCallbackAttributes(Delta<Object, ?> delta) {
CompletableFuture.runAsync(() -> {
delta.apply(this.cbAttributes, Delta.EQUAL);
}, model.clientExecutor).exceptionally(ex -> {
Msg.error(this, "Error updating elements before callback");
return null;
});
}
/**
* TODO: Document me.
*/
public Delta<?, ?> setAttributes(Map<String, ?> attributes, String reason) {
Delta<?, ?> delta;
Delta<Object, ?> delta;
synchronized (model.lock) {
delta = Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL);
}
@ -456,6 +510,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
}
doInvalidateAttributes(delta.removed, reason);
if (!delta.isEmpty()) {
updateCallbackAttributes(delta);
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
}
return delta;
@ -470,18 +525,33 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
* of removed non-reference object-valued attributes.
*
* @param remove the set of names to remove
* @param addCanonicalObjects the set of canonical object-valued attributes to add
* @param addLinksAndValues the map of other attributes to add
* @param autoKeyed the set of object-valued attributes to add using the objects' keys
* @param mapKeyed the map of other attributes to add (usually links and primitives)
* @param reason the reason for the change (used as the reason for invalidation)
* @return the actual delta from the previous to the current attributes
*/
public Delta<?, ?> changeAttributes(List<String> remove,
Collection<? extends TargetObject> addCanonicalObjects,
Map<String, ?> addLinksAndValues, String reason) {
Map<String, ?> add = combineAttributes(addCanonicalObjects, addLinksAndValues);
Collection<? extends TargetObject> autoKeyed, Map<String, ?> mapKeyed, String reason) {
Map<String, ?> add = combineAttributes(autoKeyed, mapKeyed);
return changeAttributes(remove, add, reason);
}
public <T> Map<String, T> filterValid(String name, Map<String, T> map) {
return map.entrySet().stream().filter(ent -> {
T val = ent.getValue();
if (!(val instanceof TargetObject)) {
return true;
}
TargetObject obj = (TargetObject) val;
if (obj.isValid()) {
return true;
}
Msg.error(this, name + " " + ent.getKey() + " of " + getJoinedPath(".") +
" linked to invalid object: " + obj.getJoinedPath("."));
return false;
}).collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
}
/**
* This method may soon be made private. Consider
* {@link DefaultTargetObject#changeAttributes(List, Collection, Map, String)} instead.
@ -491,7 +561,8 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
* used to specify the "canonical" location.
*/
public Delta<?, ?> changeAttributes(List<String> remove, Map<String, ?> add, String reason) {
Delta<?, ?> delta;
// add = filterValid("Attribute", add);
Delta<Object, ?> delta;
synchronized (model.lock) {
delta = Delta.apply(this.attributes, remove, add, Delta.EQUAL);
}
@ -500,9 +571,87 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
}
doInvalidateAttributes(delta.removed, reason);
if (!delta.isEmpty()) {
if (!delta.isEmpty()/* && !reason.equals("Default")*/) {
updateCallbackAttributes(delta);
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
}
return delta;
}
@Override
public ListenerSet<DebuggerModelListener> getListeners() {
return listeners;
}
/*
private CompletableFuture<Void> findDependencies(TargetObjectListener l) {
//System.err.println("findDependencies " + this);
Map<String, TargetObject> resultAttrs = new HashMap<>();
Map<String, TargetObject> resultElems = new HashMap<>();
AsyncFence fence = new AsyncFence();
fence.include(fetchAttributes(false).thenCompose(attrs -> {
AsyncFence af = new AsyncFence();
for (String key : attrs.keySet()) { //requiredObjKeys) {
Object object = attrs.get(key);
if (!(object instanceof TargetObjectRef)) {
continue;
}
TargetObjectRef ref = (TargetObjectRef) object;
if (PathUtils.isLink(getPath(), key, ref.getPath())) {
continue;
}
af.include(ref.fetch().thenAccept(obj -> {
if (isDependency(obj)) {
synchronized (this) {
resultAttrs.put(key, obj);
obj.addListener(l);
}
}
}));
}
return af.ready();
}));
fence.include(fetchElements(false).thenCompose(elems -> {
AsyncFence ef = new AsyncFence();
for (Entry<String, ? extends TargetObjectRef> entry : elems.entrySet()) {
ef.include(entry.getValue().fetch().thenAccept(obj -> {
synchronized (this) {
resultElems.put(entry.getKey(), obj);
obj.addListener(l);
}
}));
}
return ef.ready();
}));
return fence.ready();
}
public boolean isDependency(TargetObject object) {
String name = object.getName();
if (name != null) {
if (name.equals("Debug"))
return true;
if (name.equals("Stack"))
return true;
}
Set<Class<? extends TargetObject>> interfaces = object.getSchema().getInterfaces();
for (Class<? extends TargetObject> ifc : interfaces) {
if (dependencySet.contains(ifc)) {
return true;
}
}
return false;
}
*/
@Override
public void addListener(DebuggerModelListener l) {
listeners.add(l);
/*
if (isDependency(this)) {
findDependencies(l);
}
*/
}
}

View file

@ -59,4 +59,16 @@ public interface InvalidatableTargetObjectIf extends TargetObject {
* @param reason a human-consumable explanation for the removal
*/
void invalidateSubtree(TargetObject branch, String reason);
/**
* Invalidate this subtree, without locking
*
* <p>
* This really only exists to avoid reentering a lock. This should be called when a thread has
* already acquired the relevant lock(s).
*
* @param branch
* @param reason
*/
void doInvalidateSubtree(TargetObject branch, String reason);
}

View file

@ -36,10 +36,4 @@ public interface TargetAccessConditioned extends TargetObject {
public default boolean isAccessible() {
return getTypedAttributeNowByName(ACCESSIBLE_ATTRIBUTE_NAME, Boolean.class, true);
}
public interface TargetAccessibilityListener extends TargetObjectListener {
default void accessibilityChanged(TargetAccessConditioned object,
boolean accessibe) {
}
}
}

View file

@ -16,7 +16,6 @@
package ghidra.dbg.target;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.attributes.TargetObjectList;
import ghidra.dbg.target.schema.TargetAttributeType;
import ghidra.program.model.address.Address;
@ -25,13 +24,14 @@ import ghidra.program.model.address.Address;
*
* <p>
* If the native debugger does not separate the concepts of specification and location, then
* breakpoint objects should implement both the specification and location interfaces.
* breakpoint objects should implement both the specification and location interfaces. If the
* location is user-togglable independent of its specification, it should implement
* {@link TargetTogglable} as well.
*/
@DebuggerTargetObjectIface("BreakpointLocation")
public interface TargetBreakpointLocation extends TargetObject {
String ADDRESS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "address";
String AFFECTS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "affects";
// NOTE: address and length are treated separately (not using AddressRange)
// On GDB, e.g., the length may not be offered immediately.
String LENGTH_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "length";
@ -47,21 +47,6 @@ public interface TargetBreakpointLocation extends TargetObject {
return getTypedAttributeNowByName(ADDRESS_ATTRIBUTE_NAME, Address.class, null);
}
/**
* A list of object to which this breakpoint applies
*
* <p>
* This list may be empty, in which case, this location is conventionally assumed to apply
* everywhere its container's location/scope suggests.
*
* @return the list of affected objects' references
*/
@TargetAttributeType(name = AFFECTS_ATTRIBUTE_NAME, hidden = true)
public default TargetObjectList<?> getAffects() {
return getTypedAttributeNowByName(AFFECTS_ATTRIBUTE_NAME, TargetObjectList.class,
TargetObjectList.of());
}
/**
* If available, get the length in bytes, of the range covered by the breakpoint.
*

View file

@ -0,0 +1,33 @@
/* ###
* 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.dbg.target;
import ghidra.dbg.DebuggerTargetObjectIface;
/**
* A place for breakpoint locations to reside
*
* <p>
* This is just a marker interface for finding where a target's breakpoints are given. In some
* models, notably GDB, the locations belong to a global set of specifications. The only way to
* indicate that a location applies to a target is for it to be a successor of that target, at least
* by linking. To ease discovery, the breakpoint location container for the target must be a
* canonical successor of the target. The locations in the container may be canonical or links.
*/
@DebuggerTargetObjectIface("BreakpointLocationContainer")
public interface TargetBreakpointLocationContainer extends TargetObject {
// Nothing here aside from a marker
}

View file

@ -21,7 +21,7 @@ import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet;
import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet;
import ghidra.dbg.target.schema.TargetAttributeType;
/**
@ -35,18 +35,40 @@ import ghidra.dbg.target.schema.TargetAttributeType;
* object include the resolved {@link TargetBreakpointLocation}s. If the debugger does not share
* this same concept, then its breakpoints should implement both the specification and the location;
* the specification need not have any children.
*
* <p>
* This object extends {@link TargetTogglable} for a transitional period only. Implementations whose
* breakpoint specifications can be toggled should declare this interface explicitly. When the
* specification is user togglable, toggling it should effectively toggle all locations -- whether
* or not the locations are user togglable.
*/
@DebuggerTargetObjectIface("BreakpointSpec")
public interface TargetBreakpointSpec extends TargetObject {
public interface TargetBreakpointSpec extends TargetObject, /*@Transitional*/ TargetTogglable {
public enum TargetBreakpointKind {
READ, WRITE, EXECUTE, SOFTWARE;
/**
* A read breakpoint, likely implemented in hardware
*/
READ,
/**
* A write breakpoint, likely implemented in hardware
*/
WRITE,
/**
* An execution breakpoint implemented in hardware, i.e., without modifying the target's
* program memory
*/
HW_EXECUTE,
/**
* An execution breakpoint implemented in software, i.e., by modifying the target's program
* memory
*/
SW_EXECUTE;
}
String CONTAINER_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "container";
String EXPRESSION_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "expression";
String KINDS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "kinds";
String ENABLED_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "enabled";
/**
* Get the container of this breakpoint.
@ -59,8 +81,9 @@ public interface TargetBreakpointSpec extends TargetObject {
* @return a reference to the container
*/
@TargetAttributeType(name = CONTAINER_ATTRIBUTE_NAME, required = true, hidden = true)
public default TargetBreakpointContainer getContainer() {
return getTypedAttributeNowByName(CONTAINER_ATTRIBUTE_NAME, TargetBreakpointContainer.class,
public default TargetBreakpointSpecContainer getContainer() {
return getTypedAttributeNowByName(CONTAINER_ATTRIBUTE_NAME,
TargetBreakpointSpecContainer.class,
null);
}
@ -89,16 +112,6 @@ public interface TargetBreakpointSpec extends TargetObject {
TargetBreakpointKindSet.EMPTY);
}
/**
* Check if the breakpoint is enabled
*
* @return true if enabled, false otherwise
*/
@TargetAttributeType(name = ENABLED_ATTRIBUTE_NAME, required = true, hidden = true)
public default boolean isEnabled() {
return getTypedAttributeNowByName(ENABLED_ATTRIBUTE_NAME, Boolean.class, false);
}
/**
* Add an action to execute locally when this breakpoint traps execution
*
@ -132,25 +145,6 @@ public interface TargetBreakpointSpec extends TargetObject {
TargetBreakpointLocation breakpoint);
}
/**
* Disable all breakpoints resulting from this specification
*/
public CompletableFuture<Void> disable();
/**
* Enable all breakpoints resulting from this specification
*/
public CompletableFuture<Void> enable();
/**
* Enable or disable all breakpoints resulting from this specification
*
* @param enabled true to enable, false to disable
*/
public default CompletableFuture<Void> toggle(boolean enabled) {
return enabled ? enable() : disable();
}
/**
* Get the locations created by this specification.
*
@ -171,9 +165,4 @@ public interface TargetBreakpointSpec extends TargetObject {
}
// TODO: Make hit count part of the common interface?
public interface TargetBreakpointSpecListener extends TargetObjectListener {
default void breakpointToggled(TargetBreakpointSpec spec, boolean enabled) {
}
}
}

View file

@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.schema.TargetAttributeType;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.util.CollectionUtils.AbstractEmptySet;
import ghidra.dbg.util.CollectionUtils.AbstractNSet;
import ghidra.program.model.address.*;
@ -31,9 +32,17 @@ import ghidra.program.model.address.*;
* <p>
* This interface provides for the placement (creation) of breakpoints and as a listening point for
* breakpoint events. Typically, it is implemented by an object whose elements are breakpoints.
*
* <p>
* TODO: Rename this to {@code TargetBreakpointOperations}. Conventionally, it is a container of
* breakpoints, but it doesn't technically have to be. A client searching for the breakpoint
* (location) container should use {@link TargetObjectSchema#searchForCanonicalContainer(Class)},
* passing {@link TargetBreakpointLocation}. A client seeking to place breakpoints should use
* {@link TargetObjectSchema#searchForSuitable(Class, java.util.List)}, passing
* {@link TargetBreakpointSpecContainer}.
*/
@DebuggerTargetObjectIface("BreakpointContainer")
public interface TargetBreakpointContainer extends TargetObject {
@DebuggerTargetObjectIface("BreakpointSpecContainer")
public interface TargetBreakpointSpecContainer extends TargetObject {
String SUPPORTED_BREAK_KINDS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "supported_breakpoint_kinds";
@ -132,26 +141,4 @@ public interface TargetBreakpointContainer extends TargetObject {
Set<TargetBreakpointKind> kinds) {
return placeBreakpoint(new AddressRangeImpl(address, address), kinds);
}
public interface TargetBreakpointListener extends TargetObjectListener {
/**
* A breakpoint trapped execution
*
* <p>
* The program counter can be obtained in a few ways. The most reliable is to get the
* address of the effective breakpoint. If available, the frame will also contain the
* program counter. Finally, the trapped object or one of its relatives may offer the
* program counter.
*
* @param container the container whose breakpoint trapped execution
* @param trapped the object whose execution was trapped
* @param frame the innermost stack frame, if available, of the trapped object
* @param spec the breakpoint specification
* @param breakpoint the breakpoint location that actually trapped execution
*/
default void breakpointHit(TargetBreakpointContainer container, TargetObject trapped,
TargetStackFrame frame, TargetBreakpointSpec spec,
TargetBreakpointLocation breakpoint) {
}
}
}

View file

@ -54,37 +54,4 @@ public interface TargetConsole extends TargetObject {
* @return a future which completes when the data is sent
*/
public CompletableFuture<Void> write(byte[] data);
public interface TargetConsoleListener extends TargetObjectListener {
/**
* The console has produced output
*
* @param console the console producing the output
* @param channel identifies the "output stream", stdout or stderr
* @param data the output data
*/
default void consoleOutput(TargetObject console, Channel channel, byte[] data) {
}
/**
* The console has produced output
*
* @implNote Overriding this method is not a substitute for overriding
* {@link #consoleOutput(TargetObject, Channel, byte[])}. Some models may invoke
* this {@code String} variant as a convenience, which by default, invokes the
* {@code byte[]} variant, but models are only expected to invoke the
* {@code byte[]} variant. A client may override this method simply to avoid
* back-and-forth conversions between {@code String}s and {@code byte[]}s.
*
* @param console the console producing the output
* @param channel identifies the "output stream", stdout or stderr
* @param text the output text
*/
default void consoleOutput(TargetObject console, Channel channel, String text) {
consoleOutput(console, channel, text.getBytes(CHARSET));
}
}
public interface TargetTextConsoleListener extends TargetConsoleListener {
}
}

View file

@ -15,8 +15,6 @@
*/
package ghidra.dbg.target;
import java.util.List;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.target.schema.TargetAttributeType;
@ -36,7 +34,7 @@ public interface TargetEventScope extends TargetObject {
/**
* The session has stopped for an unspecified reason
*/
STOPPED,
STOPPED(true),
/**
* The session is running for an unspecified reason
*
@ -44,40 +42,40 @@ public interface TargetEventScope extends TargetObject {
* Note that execution state changes are communicated via {@link TargetExecutionStateful},
* since the sessiopn may specify such state on a per-target and/or per-thread basis.
*/
RUNNING,
RUNNING(false),
/**
* A new target process was created by this session
*
* <p>
* If the new process is part of the session, too, it must be passed as a parameter.
*/
PROCESS_CREATED,
PROCESS_CREATED(false),
/**
* A target process in this session has exited
*/
PROCESS_EXITED,
PROCESS_EXITED(false),
/**
* A new target thread was created by this session
*
* <p>
* The new thread must be part of the session, too, and must be given as the event thread.
*/
THREAD_CREATED,
THREAD_CREATED(false),
/**
* A target thread in this session has exited
*/
THREAD_EXITED,
THREAD_EXITED(false),
/**
* A new module has been loaded by this session
*
* <p>
* The new module must be passed as a parameter.
*/
MODULE_LOADED,
MODULE_LOADED(false),
/**
* A module has been unloaded by this session
*/
MODULE_UNLOADED,
MODULE_UNLOADED(false),
/**
* The session has stopped, because one if its targets was trapped by a breakpoint
*
@ -85,7 +83,7 @@ public interface TargetEventScope extends TargetObject {
* If the breakpoint (specification) is part of the session, too, it must be passed as a
* parameter. The trapped target must also be passed as a parameter.
*/
BREAKPOINT_HIT,
BREAKPOINT_HIT(true),
/**
* The session has stopped, because a stepping command has completed
*
@ -93,7 +91,7 @@ public interface TargetEventScope extends TargetObject {
* The target completing the command must also be passed as a parameter, unless it is the
* event thread. If it is a thread, it must be given as the event thread.
*/
STEP_COMPLETED,
STEP_COMPLETED(true),
/**
* The session has stopped, because one if its targets was trapped on an exception
*
@ -101,7 +99,7 @@ public interface TargetEventScope extends TargetObject {
* The trapped target must also be passed as a parameter, unless it is the event thread. If
* it is a thread, it must be given as the event thread.
*/
EXCEPTION,
EXCEPTION(false),
/**
* The session has stopped, because one of its targets was trapped on a signal
*
@ -109,7 +107,13 @@ public interface TargetEventScope extends TargetObject {
* The trapped target must also be passed as a parameter, unless it is the event thread. If
* it is a thread, it must be given as the event thread.
*/
SIGNAL,
SIGNAL(false);
public final boolean impliesStop;
private TargetEventType(boolean impliesStop) {
this.impliesStop = impliesStop;
}
}
/**
@ -125,7 +129,7 @@ public interface TargetEventScope extends TargetObject {
* @return the process or reference
*/
@TargetAttributeType(name = EVENT_PROCESS_ATTRIBUTE_NAME, hidden = true)
public default /*TODO: TypedTargetObjectRef<? extends TargetProcess<?>>*/ String getEventProcess() {
public default /*TODO: TargetProcess*/ String getEventProcess() {
return getTypedAttributeNowByName(EVENT_PROCESS_ATTRIBUTE_NAME, String.class, null);
}
@ -138,43 +142,7 @@ public interface TargetEventScope extends TargetObject {
* @return the thread or reference
*/
@TargetAttributeType(name = EVENT_THREAD_ATTRIBUTE_NAME, hidden = true)
public default /*TODO: TypedTargetObjectRef<? extends TargetThread<?>>*/ String getEventThread() {
public default /*TODO: TargetThread*/ String getEventThread() {
return getTypedAttributeNowByName(EVENT_THREAD_ATTRIBUTE_NAME, String.class, null);
}
public interface TargetEventScopeListener extends TargetObjectListener {
/**
* An event affecting a target in this scope has occurred
*
* <p>
* When present, this callback must be invoked before any other callback which results from
* this event, except creation events. E.g., for PROCESS_EXITED, this must be called before
* the affected process is removed from the tree.
*
* <p>
* Whenever possible, event thread must be given. This is often the thread given focus by
* the debugger immediately upon stopping for the event. Parameters are not (yet) strictly
* specified, but it should include the stopped target, if that target is not already given
* by the event thread. It may optionally contain other useful information, such as an exit
* code, but no listener should depend on that information being given.
*
* <p>
* The best way to communicate to users what has happened is via the description. Almost
* every other result of an event is communicated by other means in the model, e.g., state
* changes, object creation, destruction. The description should contain as much information
* as possible to cue users as to why the other changes have occurred, and point them to
* relevant objects. For example, if trapped on a breakpoint, the description might contain
* the breakpoint's identifier. If the debugger prints a message for this event, that
* message is probably a sufficient description.
*
* @param object the event scope
* @param eventThread if applicable, the thread causing the event
* @param type the type of event
* @param description a human-readable description of the event
* @param parameters extra parameters for the event. TODO: Specify these for each type
*/
default void event(TargetEventScope object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
}
}
}

View file

@ -33,7 +33,8 @@ public interface TargetExecutionStateful extends TargetObject {
/**
* The object has been created, but it not yet alive
*
* This may apply, e.g., to a GDB "Inferior" which has no yet been used to launch or attach
* <p>
* This may apply, e.g., to a GDB "Inferior," which has no yet been used to launch or attach
* to a process.
*/
INACTIVE {
@ -55,6 +56,13 @@ public interface TargetExecutionStateful extends TargetObject {
/**
* The object is alive, but its execution state is unspecified
*
* <p>
* Implementations should use {@link #STOPPED} and {@link #RUNNING} whenever possible. For
* some objects, e.g., a process, this is conventionally determined by its parts, e.g.,
* threads: A process is running when <em>any</em> of its threads are running. It is stopped
* when <em>all</em> of its threads are stopped. For the clients' sakes, all models should
* implement these conventions internally.
*/
ALIVE {
@Override
@ -124,7 +132,7 @@ public interface TargetExecutionStateful extends TargetObject {
* <p>
* The object still exists but no longer represents something alive. This could be used for
* stale handles to objects which may still be queried (e.g., for a process exit code), or
* e.g., a GDB "Inferior" which could be re-used to launch or attach to another process.
* e.g., a GDB "Inferior," which could be re-used to launch or attach to another process.
*/
TERMINATED {
@Override
@ -173,18 +181,6 @@ public interface TargetExecutionStateful extends TargetObject {
@TargetAttributeType(name = STATE_ATTRIBUTE_NAME, required = true, hidden = true)
public default TargetExecutionState getExecutionState() {
return getTypedAttributeNowByName(STATE_ATTRIBUTE_NAME, TargetExecutionState.class,
TargetExecutionState.STOPPED);
}
public interface TargetExecutionStateListener extends TargetObjectListener {
/**
* The object has entered a different execution state
*
* @param object the object
* @param state the new state
*/
default void executionStateChanged(TargetExecutionStateful object,
TargetExecutionState state) {
}
TargetExecutionState.INACTIVE);
}
}

View file

@ -50,39 +50,25 @@ public interface TargetFocusScope extends TargetObject {
/**
* Get the focused object in this scope
*
* <p>
* Note that client UIs should be careful about event loops and user intuition when listening
* for changes of this attribute. The client should avoid calling
* {@link #requestFocus(TargetObject)} in response. Perhaps the simplest way is to only request
* focus when the selected object has actually changed. The debugger may "adjust" the focus. For
* example, when focusing a thread, the debugger may instead focus a particular frame in that
* thread (a successor). Or, when focusing a memory region, the debugger may only focus the
* owning process (an ancestor). The suggested strategy (a work in progress) is the "same level,
* same type" rule. It may be appropriate to highlight the actual focused object to cue the user
* in, but the user's selection should remain at the same level. If an ancestor or successor
* receives focus, leave the user's selection as is. If a sibling element or one of its
* successors receives focus, select that sibling. A similar rule applies to "cousin" elements,
* so long as they have the same type. In most other cases, it's appropriate to select the
* focused element. TODO: Implement this rule in {@link DebugModelConventions}
*
* @return a reference to the focused object or {@code null} if no object is focused.
*/
@TargetAttributeType(name = FOCUS_ATTRIBUTE_NAME, required = true, hidden = true)
default TargetObject getFocus() {
return getTypedAttributeNowByName(FOCUS_ATTRIBUTE_NAME, TargetObject.class, null);
}
public interface TargetFocusScopeListener extends TargetObjectListener {
/**
* Focused has changed within this scope
*
* <p>
* Note that client UIs should be careful about event loops and user intuition when
* receiving this event. The client should avoid calling
* {@link TargetFocusScope#requestFocus(TargetObject)} in response to this event. Perhaps
* the simplest way is to only request focus when the selected object has actually changed.
* Also, the debugger may "adjust" the focus. For example, when focusing a thread, the
* debugger may additionally focus a particular frame in that thread (a successor). Or, when
* focusing a memory region, the debugger may only focus the owning process (an ancestor).
* The suggested strategy (a work in progress) is the "same level, same type" rule. It may
* be appropriate to highlight the actual focused object to cue the user in, but the user's
* selection should remain at the same level. If an ancestor or successor receives focus,
* leave the user's selection as is. If a sibling element or one of its successors receives
* focus, select that sibling. A similar rule applies to "cousin" elements, so long as they
* have the same type. In most other cases, it's appropriate to select the focused element.
*
* <p>
* TODO: Implement this rule in {@link DebugModelConventions}
*
* @param object this scope
* @param focused the object receiving focus
*/
default void focusChanged(TargetFocusScope object, TargetObject focused) {
}
}
}

View file

@ -18,8 +18,6 @@ package ghidra.dbg.target;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetConsole.TargetTextConsoleListener;
import ghidra.dbg.target.schema.TargetAttributeType;
/**
@ -72,30 +70,4 @@ public interface TargetInterpreter extends TargetObject {
public default String getPrompt() {
return getTypedAttributeNowByName(PROMPT_ATTRIBUTE_NAME, String.class, ">");
}
public interface TargetInterpreterListener extends TargetTextConsoleListener {
/**
* {@inheritDoc}
*
* <p>
* This should only receive console output for non-captured commands. See
* {@link TargetInterpreter#executeCapture(String)}.
*/
@Override
default void consoleOutput(TargetObject console, Channel channel, String text) {
TargetTextConsoleListener.super.consoleOutput(console, channel, text);
}
/**
* The interpreter's prompt has changed
*
* <p>
* Any UI elements presenting the prompt should be updated immediately.
*
* @param interpreter the interpreter whose prompt changed
* @param prompt the new prompt
*/
default void promptChanged(TargetInterpreter interpreter, String prompt) {
}
}
}

View file

@ -19,9 +19,7 @@ import java.util.Map;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.error.DebuggerMemoryAccessException;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
/**
* The memory model of a target object
@ -90,38 +88,4 @@ public interface TargetMemory extends TargetObject {
public default CompletableFuture<? extends Map<String, ? extends TargetMemoryRegion>> getRegions() {
return fetchChildrenSupporting((Class) TargetMemoryRegion.class);
}
public interface TargetMemoryListener extends TargetObjectListener {
/**
* Memory was successfully read or written
*
* <p>
* If the implementation employs a cache, then it need only report reads or writes which
* updated that cache. However, that cache must be invalidated whenever any other event
* occurs which could change memory, e.g., the target stepping or running.
*
* <p>
* If the implementation can detect memory reads or writes <em>driven by the debugger</em>
* then it is also acceptable to call this method for those events. However, this method
* <em>must not</em> be called for memory changes <em>driven by the target</em>. In other
* words, this method should only be called for reads or writes requested by the user.
*
* @param memory this memory object
* @param address the starting address of the affected range
* @param data the new data for the affected range
*/
default void memoryUpdated(TargetMemory memory, Address address, byte[] data) {
}
/**
* An attempt to read memory failed
*
* @param memory the memory object
* @param range the range for the read which generated the error
* @param e the error
*/
default void memoryReadError(TargetMemory memory, AddressRange range,
DebuggerMemoryAccessException e) {
}
}
}

View file

@ -153,16 +153,17 @@ public interface TargetMethod extends TargetObject {
return (T) arguments.get(name);
}
if (required) {
throw new DebuggerIllegalArgumentException("Missing required parameter " + name);
throw new DebuggerIllegalArgumentException(
"Missing required parameter '" + name + "'");
}
return defaultValue;
}
@Override
public String toString() {
return String.format("<ParameterDescription " +
"name=%s type=%s default=%s required=%s " +
"display='%s' description='%s' choices=%s",
return String.format(
"<ParameterDescription " + "name=%s type=%s default=%s required=%s " +
"display='%s' description='%s' choices=%s",
name, type, defaultValue, required, display, description, choices);
}
}
@ -173,9 +174,8 @@ public interface TargetMethod extends TargetObject {
// Nothing
}
public static class ImmutableTargetParameterMap
extends AbstractNMap<String, ParameterDescription<?>>
implements TargetParameterMap {
public static class ImmutableTargetParameterMap extends
AbstractNMap<String, ParameterDescription<?>> implements TargetParameterMap {
public ImmutableTargetParameterMap(Map<String, ParameterDescription<?>> map) {
super(map);
@ -200,8 +200,7 @@ public interface TargetMethod extends TargetObject {
* @return a map of descriptions by name
*/
static TargetParameterMap makeParameters(Stream<ParameterDescription<?>> params) {
return TargetParameterMap.copyOf(
params.collect(Collectors.toMap(p -> p.name, p -> p)));
return TargetParameterMap.copyOf(params.collect(Collectors.toMap(p -> p.name, p -> p)));
}
/**
@ -210,8 +209,7 @@ public interface TargetMethod extends TargetObject {
* @param params the descriptions
* @return a map of descriptions by name
*/
static TargetParameterMap makeParameters(
Collection<ParameterDescription<?>> params) {
static TargetParameterMap makeParameters(Collection<ParameterDescription<?>> params) {
return makeParameters(params.stream());
}
@ -221,8 +219,7 @@ public interface TargetMethod extends TargetObject {
* @param params the descriptions
* @return a map of descriptions by name
*/
static TargetParameterMap makeParameters(
ParameterDescription<?>... params) {
static TargetParameterMap makeParameters(ParameterDescription<?>... params) {
return makeParameters(Stream.of(params));
}
@ -240,8 +237,7 @@ public interface TargetMethod extends TargetObject {
if (!parameters.keySet().containsAll(arguments.keySet())) {
Set<String> extraneous = new TreeSet<>(arguments.keySet());
extraneous.removeAll(parameters.keySet());
throw new DebuggerIllegalArgumentException(
"Extraneous parameters: " + extraneous);
throw new DebuggerIllegalArgumentException("Extraneous parameters: " + extraneous);
}
}
Map<String, Object> valid = new LinkedHashMap<>();
@ -290,8 +286,8 @@ public interface TargetMethod extends TargetObject {
* @return the parameter map
*/
static TargetParameterMap getParameters(TargetObject obj) {
return obj.getTypedAttributeNowByName(PARAMETERS_ATTRIBUTE_NAME,
TargetParameterMap.class, TargetParameterMap.of());
return obj.getTypedAttributeNowByName(PARAMETERS_ATTRIBUTE_NAME, TargetParameterMap.class,
TargetParameterMap.of());
}
/**
@ -299,11 +295,7 @@ public interface TargetMethod extends TargetObject {
*
* @return the name-description map of parameters
*/
@TargetAttributeType(
name = PARAMETERS_ATTRIBUTE_NAME,
required = true,
fixed = true,
hidden = true)
@TargetAttributeType(name = PARAMETERS_ATTRIBUTE_NAME, required = true, fixed = true, hidden = true)
default public TargetParameterMap getParameters() {
return getParameters(this);
}
@ -318,14 +310,9 @@ public interface TargetMethod extends TargetObject {
*
* @return the return type
*/
@TargetAttributeType(
name = RETURN_TYPE_ATTRIBUTE_NAME,
required = true,
fixed = true,
hidden = true)
@TargetAttributeType(name = RETURN_TYPE_ATTRIBUTE_NAME, required = true, fixed = true, hidden = true)
default public Class<?> getReturnType() {
return getTypedAttributeNowByName(RETURN_TYPE_ATTRIBUTE_NAME, Class.class,
Object.class);
return getTypedAttributeNowByName(RETURN_TYPE_ATTRIBUTE_NAME, Class.class, Object.class);
}
// TODO: Allow extra parameters, i.e., varargs?

View file

@ -19,6 +19,7 @@ import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.target.schema.TargetAttributeType;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.lifecycle.Experimental;
/**
@ -32,6 +33,12 @@ import ghidra.lifecycle.Experimental;
* TODO: Experiment with the idea of "synthetic modules" as presented by {@code dbgeng.dll}. Is
* there a similar idea in GDB? This could allow us to expose Ghidra's symbol table and types to the
* native debugger.
*
* <p>
* TODO: Rename this to {@code TargetModuleOperations}. Conventionally, it is a container of
* modules, but it doesn't technically have to be. If we don't eventually go forward with synthetic
* modules, then we could remove this interface altogether. A client searching for the module
* container should use {@link TargetObjectSchema#searchForCanonicalContainer(Class)}.
*/
@DebuggerTargetObjectIface("ModuleContainer")
public interface TargetModuleContainer extends TargetObject {

View file

@ -24,7 +24,6 @@ import ghidra.async.AsyncFence;
import ghidra.async.AsyncUtils;
import ghidra.dbg.*;
import ghidra.dbg.error.DebuggerModelTypeException;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.schema.*;
import ghidra.dbg.util.PathUtils;
import ghidra.dbg.util.PathUtils.PathComparator;
@ -161,16 +160,18 @@ public interface TargetObject extends Comparable<TargetObject> {
Set<Class<? extends TargetObject>> ALL_INTERFACES = Set.of(TargetAccessConditioned.class,
TargetAggregate.class, TargetAttachable.class, TargetAttacher.class,
TargetBreakpointContainer.class, TargetBreakpointSpec.class, TargetDataTypeMember.class,
TargetDataTypeNamespace.class, TargetDeletable.class, TargetDetachable.class,
TargetBreakpointLocation.class, TargetEnvironment.class, TargetEventScope.class,
TargetBreakpointLocation.class, TargetBreakpointLocationContainer.class,
TargetBreakpointSpec.class, TargetBreakpointSpecContainer.class, TargetConsole.class,
TargetDataTypeMember.class, TargetDataTypeNamespace.class, TargetDeletable.class,
TargetDetachable.class, TargetEnvironment.class, TargetEventScope.class,
TargetExecutionStateful.class, TargetFocusScope.class, TargetInterpreter.class,
TargetInterruptible.class, TargetKillable.class, TargetLauncher.class, TargetMethod.class,
TargetMemory.class, TargetMemoryRegion.class, TargetModule.class,
TargetInterruptible.class, TargetKillable.class, TargetLauncher.class, TargetMemory.class,
TargetMemoryRegion.class, TargetMethod.class, TargetModule.class,
TargetModuleContainer.class, TargetNamedDataType.class, TargetProcess.class,
TargetRegister.class, TargetRegisterBank.class, TargetRegisterContainer.class,
TargetResumable.class, TargetSection.class, TargetStack.class, TargetStackFrame.class,
TargetSteppable.class, TargetSymbol.class, TargetSymbolNamespace.class, TargetThread.class);
TargetResumable.class, TargetSection.class, TargetSectionContainer.class, TargetStack.class,
TargetStackFrame.class, TargetSteppable.class, TargetSymbol.class,
TargetSymbolNamespace.class, TargetThread.class, TargetTogglable.class);
Map<String, Class<? extends TargetObject>> INTERFACES_BY_NAME = initInterfacesByName();
/**
@ -256,7 +257,7 @@ public interface TargetObject extends Comparable<TargetObject> {
}
}
protected static Collection<String> getInterfaceNamesOf(Class<? extends TargetObject> cls) {
public static Collection<String> getInterfaceNamesOf(Class<? extends TargetObject> cls) {
return INTERFACE_NAMES_BY_CLASS.computeIfAbsent(cls, Protected::doGetInterfaceNamesOf);
}
@ -276,32 +277,6 @@ public interface TargetObject extends Comparable<TargetObject> {
}
}
enum TargetUpdateMode {
/**
* The object's elements are kept up to date via unsolicited push notifications / callbacks.
*
* <p>
* This is the default.
*/
UNSOLICITED,
/**
* The object's elements are only updated when requested.
*
* <p>
* The request may still generate push notifications / callbacks if others are listening
*/
SOLICITED,
/**
* The object's elements will not change.
*
* <p>
* This is a promise made by the model implementation. Once the {@code update_mode}
* attribute has this value, it should never be changed back. Note that other attributes of
* this object are still expected to be kept up to date, if they change.
*/
FIXED;
}
/**
* Check for target object equality
*
@ -603,10 +578,28 @@ public interface TargetObject extends Comparable<TargetObject> {
/**
* Get the cached elements of this object
*
* @see #getCallbackElements()
* @return the map of indices to element references
*/
public Map<String, ? extends TargetObject> getCachedElements();
/**
* Get the cached elements of this object synchronized with the callbacks
*
* <p>
* Whereas {@link #getCachedElements()} gets the map of elements <em>right now</em>, it's
* possible that view of elements is far ahead of the callback processing queue. This view is of
* the elements as the change callbacks have been processed so far. When accessing this from the
* {@link DebuggerModelListener#elementsChanged(TargetObject, Collection, Map)} callback, this
* map will have just had the given delta applied to it.
*
* <p>
* <b>WARNING:</b>The returned map must only be accessed by the callback thread.
*
* @return the map of indices to element references
*/
public Map<String, ? extends TargetObject> getCallbackElements();
/**
* Fetch all the elements of this object
*
@ -881,32 +874,6 @@ public interface TargetObject extends Comparable<TargetObject> {
return getTypedAttributeNowByName(KIND_ATTRIBUTE_NAME, String.class, getDisplay());
}
/**
* Get the element update mode for this object
*
* <p>
* The update mode informs the client's caching implementation. If set to
* {@link TargetUpdateMode#UNSOLICITED}, the client will assume its cache is kept up to date via
* listener callbacks, and may avoid querying for the object's elements. If set to
* {@link TargetUpdateMode#FIXED}, the client can optionally remove its listener for element
* changes but still assume its cache is up to date, since the object's elements are no longer
* changing. If set to {@link TargetUpdateMode#SOLICITED}, the client must re-validate its cache
* whenever the elements are requested. It is still recommended that the client listen for
* element changes, since the local cache may be updated (resulting in callbacks) when handling
* requests from another client.
*
* <p>
* IMPORTANT: Update mode does not apply to attributes. Except in rare circumstances, the model
* must keep an object's attributes up to date.
*
* @return the update mode
*/
@TargetAttributeType(name = UPDATE_MODE_ATTRIBUTE_NAME, hidden = true)
public default TargetUpdateMode getUpdateMode() {
return getTypedAttributeNowByName(UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.class,
TargetUpdateMode.UNSOLICITED);
}
/**
* A custom ordinal for positioning this item on screen
*
@ -976,10 +943,13 @@ public interface TargetObject extends Comparable<TargetObject> {
* Refresh the children of this object
*
* <p>
* This is necessary when {@link #getUpdateMode()} is {@link TargetUpdateMode#SOLICITED}. It is
* also useful when the user believes things are out of sync. This causes the model to update
* its attributes and/or elements. If either of the {@code refresh} parameters are set, the
* model should be aggressive in ensuring its caches are up to date.
* The default fetch implementations follow the prescription of
* {@link TargetObjectSchema#getElementResyncMode()} and
* {@link TargetObjectSchema#getAttributeResyncMode()}. The client may call this method when,
* for unknown reasons, the respective cache(s) are out of sync with the target debugger. Such
* circumstances indicate an implementation error, but this method may provide a means to
* recover. When a {@code refresh} parameter is set, the model should be aggressive in updating
* its respective cache(s).
*
* @param refreshAttributes ask the model to refresh attributes, querying the debugger if needed
* @param refreshElements as the model to refresh elements, querying the debugger if needed
@ -1032,10 +1002,28 @@ public interface TargetObject extends Comparable<TargetObject> {
/**
* Get the cached attributes of this object
*
* @see #getCallbackAttributes()
* @return the cached name-value map of attributes
*/
public Map<String, ?> getCachedAttributes();
/**
* Get the cached attributes of this object synchronized with the callbacks
*
* <p>
* Whereas {@link #getCachedAttributes()} gets the map of attributes <em>right now</em>, it's
* possible that view of attributes if far ahead of the callback processing queue. This view is
* of the attributes as the change callbacks have been processed so far. When accessing this
* from the {@link DebuggerModelListener#attributesChanged(TargetObject, Collection, Map)}
* callback, this map will have just had the given delta applied to it.
*
* <p>
* <b>WARNING:</b>The returned map must only be accessed by the callback thread.
*
* @return the cached name-value map of attributes
*/
public Map<String, ?> getCallbackAttributes();
/**
* Get the named attribute from the cache
*
@ -1106,7 +1094,7 @@ public interface TargetObject extends Comparable<TargetObject> {
* @see DebuggerObjectModel#addModelListener(DebuggerModelListener)
* @param l the listener
*/
public default void addListener(TargetObjectListener l) {
public default void addListener(DebuggerModelListener l) {
throw new UnsupportedOperationException();
}
@ -1118,107 +1106,7 @@ public interface TargetObject extends Comparable<TargetObject> {
*
* @param l the listener
*/
public default void removeListener(TargetObjectListener l) {
public default void removeListener(DebuggerModelListener l) {
throw new UnsupportedOperationException();
}
public interface TargetObjectListener {
/**
* The object was created
*
* <p>
* This can only be received by listening on the model. While the created object can now
* appear in other callbacks, it should not be used aside from those callbacks, until it is
* added to its parent. Until that time, the object may not adhere to the schema, since its
* children are still being initialized.
*
* @param object the newly-created object
*/
default void created(TargetObject object) {
}
/**
* The object is no longer valid
*
* <p>
* This should be the final callback ever issued for this object. Invalidation of an object
* implies invalidation of all its successors; nevertheless, the implementation MUST
* explicitly invoke this callback for those successors in preorder. Users need only listen
* for invalidation by installing a listener on the object of interest. However, a user must
* be able to ignore invalidation events on an object it has already removed and/or
* invalidated. For models that are managed by a client connection, disconnecting or
* otherwise terminating the session should invalidate the root, and thus every object must
* receive this callback.
*
* <p>
* If an invalidated object is replaced (i.e., a new object with the same path is added to
* the model), the implementation must be careful to issue all invalidations related to the
* removed object before the replacement is added, so that delayed invalidations are not
* mistakenly applied to the replacement or its successors.
*
* @param object the now-invalid object
* @param branch the root of the sub-tree being invalidated
* @param reason an informational, human-consumable reason, if applicable
*/
default void invalidated(TargetObject object, TargetObject branch, String reason) {
}
/**
* The object's display string has changed
*
* @param object the object
* @param display the new display string
*/
default void displayChanged(TargetObject object, String display) {
}
/**
* The object's elements changed
*
* @param parent the object whose children changed
* @param removed the list of removed children
* @param added a map of indices to new children references
*/
default void elementsChanged(TargetObject parent, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
}
/**
* The object's attributes changed
*
* <p>
* In the case of an object-valued attribute, changes to that object do not constitute a
* changed attribute. The attribute is considered changed only when that attribute is
* assigned to a completely different object.
*
* @param parent the object whose attributes changed
* @param removed the list of removed attributes
* @param added a map of names to new/changed attributes
*/
default void attributesChanged(TargetObject parent, Collection<String> removed,
Map<String, ?> added) {
}
/**
* The model has requested the user invalidate caches associated with this object
*
* <p>
* For objects with methods exposing contents which transcend elements and attributes (e.g.,
* memory contents), this callback requests that any caches associated with that content be
* invalidated. Most notably, this usually occurs when an object (e.g., thread) enters the
* {@link TargetExecutionState#RUNNING} state, to inform proxies that they should invalidate
* their memory and register caches. In most cases, users need not worry about this
* callback. Protocol implementations that use the model, however, should forward this
* request to the client implementation.
*
* <p>
* Note caches of elements and attributes are not affected by this callback. See
* {@link TargetObject#invalidateCaches()}.
*
* @param object the object whose caches must be invalidated
*/
default void invalidateCacheRequested(TargetObject object) {
}
}
}

View file

@ -68,6 +68,12 @@ public interface TargetRegister extends TargetObject {
return getTypedAttributeNowByName(LENGTH_ATTRIBUTE_NAME, Integer.class, 0);
}
/**
* Get the name of this register
*
* <p>
* TODO: Instead of overriding getIndex, we should introduce getRegisterName.
*/
@Override
public default String getIndex() {
return PathUtils.isIndex(getPath()) ? PathUtils.getIndex(getPath())

View file

@ -23,6 +23,7 @@ import java.util.stream.Collectors;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.error.DebuggerRegisterAccessException;
import ghidra.dbg.target.schema.TargetAttributeType;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.util.Msg;
/**
@ -36,14 +37,23 @@ import ghidra.util.Msg;
public interface TargetRegisterBank extends TargetObject {
String DESCRIPTIONS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "descriptions";
// TODO: Remove this stopgap once we implement register-value replay
String REGISTERVALS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "register_values";
/**
* Get the object describing the registers in this bank
*
* <p>
* TODO: {@link TargetRegisterContainer} ought to be removed. However, some models present a
* complex structure for their register banks and containers, splitting the set into, e.g.,
* User, Vector, etc. I suspect the simplest way for a client to accommodate this is to use
* {@link TargetObjectSchema#searchFor(Class, boolean)}, passing {@link TargetRegister}. The
* "canonical container" concept doesn't really work here, as that will yield each set, rather
* than the full descriptions container.
*
* @return a future which completes with object
*/
@TargetAttributeType(name = DESCRIPTIONS_ATTRIBUTE_NAME)
@SuppressWarnings("unchecked")
public default TargetRegisterContainer getDescriptions() {
return getTypedAttributeNowByName(DESCRIPTIONS_ATTRIBUTE_NAME,
TargetRegisterContainer.class, null);
@ -199,20 +209,4 @@ public interface TargetRegisterBank extends TargetObject {
return null;
});
}
public interface TargetRegisterBankListener extends TargetObjectListener {
/**
* Registers were successfully read or written
*
* <p>
* If the implementation employs a cache, then it need only report reads or writes which
* updated that cache. However, that cache must be invalidated whenever any other event
* occurs which could change register values, e.g., the target stepping or running.
*
* @param bank this register bank object
* @param updates a name-value map of updated registers
*/
default void registersUpdated(TargetRegisterBank bank, Map<String, byte[]> updates) {
}
}
}

View file

@ -20,9 +20,16 @@ import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.target.schema.TargetObjectSchema;
/**
* A container of register descriptions
*
* <p>
* TODO: Remove this. It really doesn't add anything that can't be discovered via the schema. A
* client searching for a register (description) container should use
* {@link TargetObjectSchema#searchForCanonicalContainer(Class)}, or discover the bank first, and
* ask for its descriptions.
*/
@DebuggerTargetObjectIface("RegisterContainer")
public interface TargetRegisterContainer extends TargetObject {
@ -39,7 +46,9 @@ public interface TargetRegisterContainer extends TargetObject {
* @implNote By default, this method collects all successor registers ordered by path.
* Overriding that behavior is not yet supported.
* @return the register descriptions
* @deprecated I don't think this has any actual utility.
*/
@Deprecated(forRemoval = true)
default CompletableFuture<? extends Collection<? extends TargetRegister>> getRegisters() {
return DebugModelConventions.collectSuccessors(this, TargetRegister.class);
}

View file

@ -0,0 +1,29 @@
/* ###
* 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.dbg.target;
import ghidra.dbg.DebuggerTargetObjectIface;
/**
* A place for breakpoint locations to reside
*
* <p>
* This is just a marker interface for finding where a target's sections are given.
*/
@DebuggerTargetObjectIface("SectionContainer")
public interface TargetSectionContainer extends TargetObject {
// Nothing here aside from a marker
}

View file

@ -27,6 +27,10 @@ import ghidra.dbg.DebuggerTargetObjectIface;
* <p>
* Conventionally, if the debugger can also unwind register values, then each frame should present a
* register bank. Otherwise, the same object presenting this stack should present the register bank.
*
* <p>
* TODO: Probably remove this. It serves only as a container of {@link TargetStackFrame}, which can
* be discovered using the schema.
*/
@DebuggerTargetObjectIface("Stack")
public interface TargetStack extends TargetObject {

View file

@ -0,0 +1,59 @@
/* ###
* 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.dbg.target;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerTargetObjectIface;
import ghidra.dbg.target.schema.TargetAttributeType;
/**
* An object which can be toggled
*/
@DebuggerTargetObjectIface("Togglable")
public interface TargetTogglable extends TargetObject {
String ENABLED_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "enabled";
/**
* Check if the object is enabled
*
* @return true if enabled, false otherwise
*/
@TargetAttributeType(name = ENABLED_ATTRIBUTE_NAME, required = true, hidden = true)
public default boolean isEnabled() {
return getTypedAttributeNowByName(ENABLED_ATTRIBUTE_NAME, Boolean.class, false);
}
/**
* Disable this object
*/
public CompletableFuture<Void> disable();
/**
* Enable this object
*/
public CompletableFuture<Void> enable();
/**
* Enable or disable this object
*
* @param enabled true to enable, false to disable
*/
public default CompletableFuture<Void> toggle(boolean enabled) {
return enabled ? enable() : disable();
}
}

View file

@ -133,7 +133,13 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
protected final Map<Class<? extends TargetObject>, TargetObjectSchema> schemasByClass =
new LinkedHashMap<>();
protected SchemaName nameFromAnnotatedClass(Class<? extends TargetObject> cls) {
/**
* Get the schema name for an annotated target object class
*
* @param cls the class
* @return the schema name
*/
public SchemaName nameFromAnnotatedClass(Class<? extends TargetObject> cls) {
synchronized (namesByClass) {
TargetObjectSchemaInfo info = cls.getAnnotation(TargetObjectSchemaInfo.class);
if (info == null) {
@ -203,75 +209,99 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
" annotation, or that the class was referenced by accident.");
}
return schemasByClass.computeIfAbsent(cls, c -> {
TargetObjectSchemaInfo info = cls.getAnnotation(TargetObjectSchemaInfo.class);
if (info == null) {
throw new IllegalArgumentException("Class " + cls + " is not annotated with @" +
TargetObjectSchemaInfo.class.getSimpleName());
}
SchemaBuilder builder = builder(name);
Set<Class<?>> allParents = ReflectionUtilities.getAllParents(cls);
for (Class<?> parent : allParents) {
DebuggerTargetObjectIface ifaceAnnot =
parent.getAnnotation(DebuggerTargetObjectIface.class);
if (ifaceAnnot != null) {
builder.addInterface(parent.asSubclass(TargetObject.class));
}
}
builder.setCanonicalContainer(info.canonicalContainer());
boolean sawDefaultElementType = false;
for (TargetElementType et : info.elements()) {
if (et.index().equals("")) {
sawDefaultElementType = true;
}
builder.addElementSchema(et.index(), nameFromClass(et.type()), et);
}
if (!sawDefaultElementType) {
Set<Class<? extends TargetObject>> bounds = getBoundsOfFetchElements(cls);
if (bounds.size() != 1) {
// TODO: Compile-time validation?
throw new IllegalArgumentException(
"Could not identify unique element class (" + bounds + ") for " + cls);
}
else {
Class<? extends TargetObject> bound = bounds.iterator().next();
SchemaName schemaName;
try {
schemaName = nameFromClass(bound);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Could not get schema name from bound " + bound + " of " + cls +
".fetchElements()",
e);
}
builder.setDefaultElementSchema(schemaName);
}
}
addPublicMethodsFromClass(builder, cls, cls);
for (Class<?> parent : allParents) {
if (TargetObject.class.isAssignableFrom(parent)) {
addPublicMethodsFromClass(builder, parent.asSubclass(TargetObject.class),
cls);
}
}
for (TargetAttributeType at : info.attributes()) {
AnnotatedAttributeSchema attrSchema = attributeSchemaFromAnnotation(at);
AttributeSchema exists = builder.getAttributeSchema(attrSchema.getName());
if (exists != null) {
attrSchema = attrSchema.lower((AnnotatedAttributeSchema) exists);
}
builder.replaceAttributeSchema(attrSchema, at);
}
SchemaBuilder builder = builderForClass(cls, name);
return builder.buildAndAdd();
});
}
}
/**
* Get a populated builder for an annotated target object class
*
* @param cls the class
* @return the builder
*/
public SchemaBuilder builderForClass(Class<? extends TargetObject> cls) {
return builderForClass(cls, nameFromAnnotatedClass(cls));
}
/**
* Get a populated builder for an annotated target object class
*
* @param cls the class
* @param name a custom name for the schema
* @return the builder
*/
public SchemaBuilder builderForClass(Class<? extends TargetObject> cls, SchemaName name) {
TargetObjectSchemaInfo info = cls.getAnnotation(TargetObjectSchemaInfo.class);
if (info == null) {
throw new IllegalArgumentException("Class " + cls + " is not annotated with @" +
TargetObjectSchemaInfo.class.getSimpleName());
}
SchemaBuilder builder = builder(name);
Set<Class<?>> allParents = ReflectionUtilities.getAllParents(cls);
for (Class<?> parent : allParents) {
DebuggerTargetObjectIface ifaceAnnot =
parent.getAnnotation(DebuggerTargetObjectIface.class);
if (ifaceAnnot != null) {
builder.addInterface(parent.asSubclass(TargetObject.class));
}
}
builder.setCanonicalContainer(info.canonicalContainer());
builder.setElementResyncMode(info.elementResync());
builder.setAttributeResyncMode(info.attributeResync());
boolean sawDefaultElementType = false;
for (TargetElementType et : info.elements()) {
if (et.index().equals("")) {
sawDefaultElementType = true;
}
builder.addElementSchema(et.index(), nameFromClass(et.type()), et);
}
if (!sawDefaultElementType) {
Set<Class<? extends TargetObject>> bounds = getBoundsOfFetchElements(cls);
if (bounds.size() != 1) {
// TODO: Compile-time validation?
throw new IllegalArgumentException(
"Could not identify unique element class (" + bounds + ") for " + cls);
}
else {
Class<? extends TargetObject> bound = bounds.iterator().next();
SchemaName schemaName;
try {
schemaName = nameFromClass(bound);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Could not get schema name from bound " + bound + " of " + cls +
".fetchElements()",
e);
}
builder.setDefaultElementSchema(schemaName);
}
}
addPublicMethodsFromClass(builder, cls, cls);
for (Class<?> parent : allParents) {
if (TargetObject.class.isAssignableFrom(parent)) {
addPublicMethodsFromClass(builder, parent.asSubclass(TargetObject.class),
cls);
}
}
for (TargetAttributeType at : info.attributes()) {
AnnotatedAttributeSchema attrSchema = attributeSchemaFromAnnotation(at);
AttributeSchema exists = builder.getAttributeSchema(attrSchema.getName());
if (exists != null) {
attrSchema = attrSchema.lower((AnnotatedAttributeSchema) exists);
}
builder.replaceAttributeSchema(attrSchema, at);
}
return builder;
}
protected String attributeNameFromBean(String beanName, boolean isBool) {
beanName = isBool ? StringUtils.removeStartIgnoreCase(beanName, "is")
: StringUtils.removeStartIgnoreCase(beanName, "get");
@ -346,6 +376,16 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
return true;
}
/**
* Get the schema for an annotated target object class
*
* <p>
* This will ensure all the schemas of the given class' dependencies are constructed and added
* to the context.
*
* @param cls the class
* @return the schema
*/
public TargetObjectSchema getSchemaForClass(Class<? extends TargetObject> cls) {
TargetObjectSchema schema = fromAnnotatedClass(cls);
fillDependencies();

View file

@ -124,24 +124,34 @@ public class DefaultTargetObjectSchema
private final Class<?> type;
private final Set<Class<? extends TargetObject>> interfaces;
private final boolean isCanonicalContainer;
private final Map<String, SchemaName> elementSchemas;
private final SchemaName defaultElementSchema;
private final ResyncMode elementResync;
private final Map<String, AttributeSchema> attributeSchemas;
private final AttributeSchema defaultAttributeSchema;
private final ResyncMode attributeResync;
DefaultTargetObjectSchema(SchemaContext context, SchemaName name, Class<?> type,
Set<Class<? extends TargetObject>> interfaces, boolean isCanonicalContainer,
Map<String, SchemaName> elementSchemas, SchemaName defaultElementSchema,
Map<String, AttributeSchema> attributeSchemas, AttributeSchema defaultAttributeSchema) {
ResyncMode elementResync,
Map<String, AttributeSchema> attributeSchemas, AttributeSchema defaultAttributeSchema,
ResyncMode attributeResync) {
this.context = context;
this.name = name;
this.type = type;
this.interfaces = Collections.unmodifiableSet(new LinkedHashSet<>(interfaces));
this.isCanonicalContainer = isCanonicalContainer;
this.elementSchemas = Collections.unmodifiableMap(new LinkedHashMap<>(elementSchemas));
this.defaultElementSchema = defaultElementSchema;
this.elementResync = elementResync;
this.attributeSchemas = Collections.unmodifiableMap(new LinkedHashMap<>(attributeSchemas));
this.defaultAttributeSchema = defaultAttributeSchema;
this.attributeResync = attributeResync;
}
@Override
@ -179,6 +189,11 @@ public class DefaultTargetObjectSchema
return defaultElementSchema;
}
@Override
public ResyncMode getElementResyncMode() {
return elementResync;
}
@Override
public Map<String, AttributeSchema> getAttributeSchemas() {
return attributeSchemas;
@ -189,6 +204,11 @@ public class DefaultTargetObjectSchema
return defaultAttributeSchema;
}
@Override
public ResyncMode getAttributeResyncMode() {
return attributeResync;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@ -209,11 +229,11 @@ public class DefaultTargetObjectSchema
sb.append(" ");
}
sb.append("]\n" + INDENT);
sb.append("elements = ");
sb.append("elements(resync " + elementResync + ") = ");
sb.append(elementSchemas);
sb.append(" default " + defaultElementSchema);
sb.append("\n" + INDENT);
sb.append("attributes = ");
sb.append("attributes(resync " + attributeResync + ") = ");
sb.append(attributeSchemas);
sb.append(" default " + defaultAttributeSchema);
sb.append("\n}");
@ -257,12 +277,18 @@ public class DefaultTargetObjectSchema
if (!Objects.equals(this.defaultElementSchema, that.defaultElementSchema)) {
return false;
}
if (!Objects.equals(this.elementResync, that.elementResync)) {
return false;
}
if (!Objects.equals(this.attributeSchemas, that.attributeSchemas)) {
return false;
}
if (!Objects.equals(this.defaultAttributeSchema, that.defaultAttributeSchema)) {
return false;
}
if (!Objects.equals(this.attributeResync, that.attributeResync)) {
return false;
}
return true;
}

View file

@ -20,13 +20,12 @@ import java.util.*;
import ghidra.dbg.attributes.TargetDataType;
import ghidra.dbg.attributes.TargetObjectList;
import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet;
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet;
import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetObject.TargetUpdateMode;
import ghidra.dbg.target.TargetSteppable.TargetStepKindSet;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathMatcher;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
@ -88,8 +87,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
SET_ATTACH_KIND("SET_ATTACH_KIND", TargetAttachKindSet.class), // TODO: Limited built-in generics
SET_BREAKPOINT_KIND("SET_BREAKPOINT_KIND", TargetBreakpointKindSet.class),
SET_STEP_KIND("SET_STEP_KIND", TargetStepKindSet.class),
EXECUTION_STATE("EXECUTION_STATE", TargetExecutionState.class),
UPDATE_MODE("UPDATE_MODE", TargetUpdateMode.class);
EXECUTION_STATE("EXECUTION_STATE", TargetExecutionState.class);
public static final class MinimalSchemaContext extends DefaultSchemaContext {
public static final SchemaContext INSTANCE = new MinimalSchemaContext();
@ -172,6 +170,11 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
return VOID.getName();
}
@Override
public ResyncMode getElementResyncMode() {
return TargetObjectSchema.DEFAULT_ELEMENT_RESYNC;
}
@Override
public Map<String, AttributeSchema> getAttributeSchemas() {
return Map.of();
@ -183,8 +186,22 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
}
@Override
public void searchFor(Set<PathPattern> result, List<String> prefix, boolean parentIsCanonical,
Class<? extends TargetObject> type, boolean requireCanonical) {
return;
public ResyncMode getAttributeResyncMode() {
return TargetObjectSchema.DEFAULT_ATTRIBUTE_RESYNC;
}
@Override
public PathMatcher searchFor(Class<? extends TargetObject> type, boolean requireCanonical) {
return new PathMatcher();
}
@Override
public List<String> searchForCanonicalContainer(Class<? extends TargetObject> type) {
return null;
}
@Override
public List<String> searchForSuitable(Class<? extends TargetObject> type, List<String> path) {
return null;
}
}

View file

@ -18,8 +18,7 @@ package ghidra.dbg.target.schema;
import java.util.*;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.dbg.target.schema.TargetObjectSchema.*;
public class SchemaBuilder {
private final DefaultSchemaContext context;
@ -28,10 +27,14 @@ public class SchemaBuilder {
private Class<?> type = TargetObject.class;
private Set<Class<? extends TargetObject>> interfaces = new LinkedHashSet<>();
private boolean isCanonicalContainer = false;
private Map<String, SchemaName> elementSchemas = new LinkedHashMap<>();
private SchemaName defaultElementSchema = EnumerableTargetObjectSchema.OBJECT.getName();
private ResyncMode elementResync = TargetObjectSchema.DEFAULT_ELEMENT_RESYNC;
private Map<String, AttributeSchema> attributeSchemas = new LinkedHashMap<>();
private AttributeSchema defaultAttributeSchema = AttributeSchema.DEFAULT_ANY;
private ResyncMode attributeResync = TargetObjectSchema.DEFAULT_ATTRIBUTE_RESYNC;
private Map<String, Object> elementOrigins = new LinkedHashMap<>();
private Map<String, Object> attributeOrigins = new LinkedHashMap<>();
@ -65,6 +68,11 @@ public class SchemaBuilder {
return this;
}
public SchemaBuilder removeInterface(Class<? extends TargetObject> iface) {
this.interfaces.remove(iface);
return this;
}
public SchemaBuilder setCanonicalContainer(boolean isCanonicalContainer) {
this.isCanonicalContainer = isCanonicalContainer;
return this;
@ -74,12 +82,6 @@ public class SchemaBuilder {
return isCanonicalContainer;
}
public SchemaBuilder setElementSchemas(Map<String, SchemaName> elementSchemas) {
this.elementSchemas.clear();
this.elementSchemas.putAll(elementSchemas);
return this;
}
/**
* Define the schema for a child element
*
@ -101,6 +103,15 @@ public class SchemaBuilder {
return this;
}
public SchemaBuilder removeElementSchema(String index) {
if (index.equals("")) {
return setDefaultElementSchema(EnumerableTargetObjectSchema.OBJECT.getName());
}
elementSchemas.remove(index);
elementOrigins.remove(index);
return this;
}
public Map<String, SchemaName> getElementSchemas() {
return Map.copyOf(elementSchemas);
}
@ -114,12 +125,15 @@ public class SchemaBuilder {
return defaultElementSchema;
}
public SchemaBuilder setAttributeSchemas(Map<String, AttributeSchema> attributeSchemas) {
this.attributeSchemas.clear();
this.attributeSchemas.putAll(attributeSchemas);
public SchemaBuilder setElementResyncMode(ResyncMode elementResync) {
this.elementResync = elementResync;
return this;
}
public ResyncMode getElementResyncMode() {
return elementResync;
}
/**
* Define the schema for a child attribute.
*
@ -144,6 +158,15 @@ public class SchemaBuilder {
return this;
}
public SchemaBuilder removeAttributeSchema(String name) {
if (name.equals("")) {
return setDefaultAttributeSchema(AttributeSchema.DEFAULT_ANY);
}
attributeSchemas.remove(name);
attributeOrigins.remove(name);
return this;
}
public Map<String, AttributeSchema> getAttributeSchemas() {
return Map.copyOf(attributeSchemas);
}
@ -170,6 +193,15 @@ public class SchemaBuilder {
return defaultAttributeSchema;
}
public SchemaBuilder setAttributeResyncMode(ResyncMode attributeResync) {
this.attributeResync = attributeResync;
return this;
}
public ResyncMode getAttributeResyncMode() {
return attributeResync;
}
public TargetObjectSchema buildAndAdd() {
TargetObjectSchema schema = build();
context.putSchema(schema);
@ -177,7 +209,9 @@ public class SchemaBuilder {
}
public TargetObjectSchema build() {
return new DefaultTargetObjectSchema(context, name, type, interfaces, isCanonicalContainer,
elementSchemas, defaultElementSchema, attributeSchemas, defaultAttributeSchema);
return new DefaultTargetObjectSchema(
context, name, type, interfaces, isCanonicalContainer,
elementSchemas, defaultElementSchema, elementResync,
attributeSchemas, defaultAttributeSchema, attributeResync);
}
}

View file

@ -17,15 +17,15 @@ package ghidra.dbg.target.schema;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import ghidra.dbg.agent.DefaultTargetObject;
import ghidra.dbg.target.TargetAggregate;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.util.*;
import ghidra.dbg.util.CollectionUtils.Delta;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils;
import ghidra.lifecycle.Internal;
import ghidra.util.Msg;
/**
@ -44,6 +44,9 @@ import ghidra.util.Msg;
* which matches any key. Similarly, the wild-card index is {@code []}.
*/
public interface TargetObjectSchema {
public static final ResyncMode DEFAULT_ELEMENT_RESYNC = ResyncMode.NEVER;
public static final ResyncMode DEFAULT_ATTRIBUTE_RESYNC = ResyncMode.NEVER;
/**
* An identifier for schemas within a context.
*
@ -99,6 +102,71 @@ public interface TargetObjectSchema {
}
}
/**
* A mode describing what "promise" a model makes when keeping elements or attributes up to date
*
* <p>
* Each object specifies a element sync mode, and an attribute sync mode. These describe when
* the client must call {@link TargetObject#resync(boolean, boolean)} to refresh/resync to
* ensure it has a fresh cache of elements and/or attributes. Note that any client requesting a
* resync will cause all clients to receive the updates.
*/
enum ResyncMode {
/**
* The object's elements are kept up to date via unsolicited push notifications / callbacks
*
* <p>
* The client should never have to call {@link TargetObject#resync()}. This is the default,
* and it is preferred for attributes. It is most appropriate for small-ish collections that
* change often and that the client is likely to need, e.g., the process, thread, and module
* lists. In general, if the native debugger or API offers callbacks for updating the
* collection, then this is the mode to use.
*/
NEVER {
@Override
public boolean shouldResync(CompletableFuture<Void> curRequest) {
return false;
}
},
/**
* The object must be explicitly synchronized once
*
* <p>
* This mode is appropriate for large collections, e.g., the symbols of a module. To push
* these without solicitation could be expensive, both for the model to retrieve them from
* the debugger, and for the client to process the collection. They should only be retrieved
* when asked, via {@link TargetObject#resync()}. Such collections are typically fixed, and
* so do not require later updates. Nevertheless, if the collection <em>does</em> change,
* then those updates must be pushed without further solicitation.
*/
ONCE {
@Override
public boolean shouldResync(CompletableFuture<Void> curRequest) {
return curRequest == null || curRequest.isCompletedExceptionally();
}
},
/**
* The object's elements are only updated when requested
*
* <p>
* This is the default for elements. It is appropriate for collections where the client
* doesn't necessarily need an up-to-date copy. Please note the higher likelihood that the
* client may make requests involving an object that has since become invalid. The model
* must be prepared to reject those requests gracefully. The most common example is the list
* of attachable processes: It should only be retrieved when requested, and there's no need
* to keep it up to date. If a process terminates, and the client later requests to attach
* to it, the request may be rejected.
*/
ALWAYS {
@Override
public boolean shouldResync(CompletableFuture<Void> curRequest) {
return true;
}
};
public abstract boolean shouldResync(CompletableFuture<Void> curRequest);
}
/**
* Schema descriptor for a child attribute.
*/
@ -258,6 +326,13 @@ public interface TargetObjectSchema {
return getDefaultElementSchema();
}
/**
* Get the re-synchronization mode for the object's elements
*
* @return the element re-synchronization mode
*/
ResyncMode getElementResyncMode();
/**
* Get the map of attribute names to named schemas
*
@ -299,6 +374,13 @@ public interface TargetObjectSchema {
return getDefaultAttributeSchema();
}
/**
* Get the re-synchronization mode for attributes
*
* @return the attribute re-synchronization mode
*/
ResyncMode getAttributeResyncMode();
/**
* Get the named schema for a child having the given key
*
@ -345,6 +427,13 @@ public interface TargetObjectSchema {
return childSchema.getSuccessorSchema(path.subList(1, path.size()));
}
/**
* Do the same as {@link #searchFor(Class, List, boolean)} with an empty prefix
*/
default PathMatcher searchFor(Class<? extends TargetObject> type, boolean requireCanonical) {
return searchFor(type, List.of(), requireCanonical);
}
/**
* Find (sub) path patterns that match objects implementing a given interface
*
@ -353,44 +442,290 @@ public interface TargetObjectSchema {
* successor implementing the interface.
*
* @param type the sub-type of {@link TargetObject} to search for
* @param prefix the prefix for each relative path pattern
* @param requireCanonical only return patterns matching a canonical location for the type
* @return a set of patterns where such objects could be found
*/
default Set<PathPattern> searchFor(Class<? extends TargetObject> type,
default PathMatcher searchFor(Class<? extends TargetObject> type, List<String> prefix,
boolean requireCanonical) {
if (type == TargetObject.class) {
throw new IllegalArgumentException("Must provide a specific interface");
}
Set<PathPattern> result = new LinkedHashSet<>();
searchFor(result, List.of(), false, type, requireCanonical);
PathMatcher result = new PathMatcher();
Private.searchFor(this, result, prefix, true, type, requireCanonical, new HashSet<>());
return result;
}
@Internal // TODO: Make a separate internal interface?
default void searchFor(Set<PathPattern> result, List<String> prefix, boolean parentIsCanonical,
Class<? extends TargetObject> type, boolean requireCanonical) {
if (getInterfaces().contains(type) && parentIsCanonical) {
result.add(new PathPattern(prefix));
class Private {
private abstract static class BreadthFirst<T extends SearchEntry> {
Set<T> allOnLevel = new HashSet<>();
public BreadthFirst(Set<T> seed) {
allOnLevel.addAll(seed);
}
public void expandAttributes(Set<T> nextLevel, T ent) {
SchemaContext ctx = ent.schema.getContext();
for (AttributeSchema as : ent.schema.getAttributeSchemas().values()) {
try {
SchemaName schema = as.getSchema();
TargetObjectSchema child = ctx.getSchema(schema);
expandAttribute(nextLevel, ent, child,
PathUtils.extend(ent.path, as.getName()));
}
catch (NullPointerException npe) {
Msg.error(this, "Null schema for " + as);
}
}
}
public void expandDefaultAttribute(Set<T> nextLevel, T ent) {
SchemaContext ctx = ent.schema.getContext();
AttributeSchema das = ent.schema.getDefaultAttributeSchema();
TargetObjectSchema child = ctx.getSchema(das.getSchema());
expandAttribute(nextLevel, ent, child, PathUtils.extend(ent.path, das.getName()));
}
public void expandElements(Set<T> nextLevel, T ent) {
SchemaContext ctx = ent.schema.getContext();
for (Map.Entry<String, SchemaName> elemEnt : ent.schema.getElementSchemas()
.entrySet()) {
TargetObjectSchema child = ctx.getSchema(elemEnt.getValue());
expandElement(nextLevel, ent, child,
PathUtils.index(ent.path, elemEnt.getKey()));
}
}
public void expandDefaultElement(Set<T> nextLevel, T ent) {
SchemaContext ctx = ent.schema.getContext();
TargetObjectSchema child = ctx.getSchema(ent.schema.getDefaultElementSchema());
expandElement(nextLevel, ent, child, PathUtils.index(ent.path, ""));
}
public void nextLevel() {
Set<T> nextLevel = new HashSet<>();
for (T ent : allOnLevel) {
if (!descend(ent)) {
continue;
}
expandAttributes(nextLevel, ent);
expandDefaultAttribute(nextLevel, ent);
expandElements(nextLevel, ent);
expandDefaultElement(nextLevel, ent);
}
allOnLevel = nextLevel;
}
public boolean descend(T ent) {
return true;
}
public void expandAttribute(Set<T> nextLevel, T ent, TargetObjectSchema schema,
List<String> path) {
}
public void expandElement(Set<T> nextLevel, T ent, TargetObjectSchema schema,
List<String> path) {
}
}
for (Entry<String, SchemaName> ent : getElementSchemas().entrySet()) {
List<String> extended = PathUtils.index(prefix, ent.getKey());
TargetObjectSchema elemSchema = getContext().getSchema(ent.getValue());
elemSchema.searchFor(result, extended, isCanonicalContainer(), type, requireCanonical);
}
List<String> deExtended = PathUtils.extend(prefix, "[]");
TargetObjectSchema deSchema = getContext().getSchema(getDefaultElementSchema());
deSchema.searchFor(result, deExtended, isCanonicalContainer(), type, requireCanonical);
private static class SearchEntry {
final List<String> path;
final TargetObjectSchema schema;
for (Entry<String, AttributeSchema> ent : getAttributeSchemas().entrySet()) {
List<String> extended = PathUtils.extend(prefix, ent.getKey());
TargetObjectSchema attrSchema = getContext().getSchema(ent.getValue().getSchema());
attrSchema.searchFor(result, extended, isCanonicalContainer(), type, requireCanonical);
public SearchEntry(List<String> path, TargetObjectSchema schema) {
this.path = path;
this.schema = schema;
}
}
List<String> daExtended = PathUtils.extend(prefix, "");
TargetObjectSchema daSchema =
getContext().getSchema(getDefaultAttributeSchema().getSchema());
daSchema.searchFor(result, daExtended, isCanonicalContainer(), type, requireCanonical);
private static class CanonicalSearchEntry extends SearchEntry {
final boolean parentIsCanonical;
public CanonicalSearchEntry(List<String> path, boolean parentIsCanonical,
TargetObjectSchema schema) {
super(path, schema);
this.parentIsCanonical = parentIsCanonical;
}
}
private static void searchFor(TargetObjectSchema sch, PathMatcher result,
List<String> prefix, boolean parentIsCanonical, Class<? extends TargetObject> type,
boolean requireCanonical, Set<TargetObjectSchema> visited) {
if (!visited.add(sch)) {
return;
}
if (sch.getInterfaces().contains(type) && parentIsCanonical) {
result.addPattern(prefix);
}
SchemaContext ctx = sch.getContext();
boolean isCanonical = sch.isCanonicalContainer();
for (Entry<String, SchemaName> ent : sch.getElementSchemas().entrySet()) {
List<String> extended = PathUtils.index(prefix, ent.getKey());
TargetObjectSchema elemSchema = ctx.getSchema(ent.getValue());
searchFor(elemSchema, result, extended, isCanonical, type, requireCanonical,
visited);
}
List<String> deExtended = PathUtils.extend(prefix, "[]");
TargetObjectSchema deSchema = ctx.getSchema(sch.getDefaultElementSchema());
searchFor(deSchema, result, deExtended, isCanonical, type, requireCanonical, visited);
for (Entry<String, AttributeSchema> ent : sch.getAttributeSchemas().entrySet()) {
List<String> extended = PathUtils.extend(prefix, ent.getKey());
TargetObjectSchema attrSchema = ctx.getSchema(ent.getValue().getSchema());
searchFor(attrSchema, result, extended, parentIsCanonical, type, requireCanonical,
visited);
}
List<String> daExtended = PathUtils.extend(prefix, "");
TargetObjectSchema daSchema =
ctx.getSchema(sch.getDefaultAttributeSchema().getSchema());
searchFor(daSchema, result, daExtended, parentIsCanonical, type, requireCanonical,
visited);
visited.remove(sch);
}
static List<String> searchForSuitableInAggregate(TargetObjectSchema seed,
Class<? extends TargetObject> type) {
Set<SearchEntry> init = Set.of(new SearchEntry(List.of(), seed));
BreadthFirst<SearchEntry> breadth = new BreadthFirst<>(init) {
final Set<TargetObjectSchema> visited = new HashSet<>();
@Override
public boolean descend(SearchEntry ent) {
return ent.schema.getInterfaces().contains(TargetAggregate.class);
}
@Override
public void expandAttribute(Set<SearchEntry> nextLevel, SearchEntry ent,
TargetObjectSchema schema, List<String> path) {
if (visited.add(schema)) {
nextLevel.add(new SearchEntry(path, schema));
}
}
@Override
public void expandDefaultAttribute(Set<SearchEntry> nextLevel, SearchEntry ent) {
}
@Override
public void expandElements(Set<SearchEntry> nextLevel, SearchEntry ent) {
}
@Override
public void expandDefaultElement(Set<SearchEntry> nextLevel, SearchEntry ent) {
}
};
while (!breadth.allOnLevel.isEmpty()) {
Set<SearchEntry> found = breadth.allOnLevel.stream()
.filter(ent -> ent.schema.getInterfaces().contains(type))
.collect(Collectors.toSet());
if (!found.isEmpty()) {
if (found.size() == 1) {
return found.iterator().next().path;
}
return null;
}
breadth.nextLevel();
}
return null;
}
}
/**
* Find the (sub) path to the canonical container for objects implementing a given interface
*
* <p>
* If more than one container is found having the shortest path, then {@code null} is returned.
*
* @param type the sub-type of {@link TargetObject} to search for
* @return the single path to that container
*/
default List<String> searchForCanonicalContainer(Class<? extends TargetObject> type) {
if (type == TargetObject.class) {
throw new IllegalArgumentException("Must provide a specific interface");
}
SchemaContext ctx = getContext();
Set<TargetObjectSchema> visited = new HashSet<>();
Set<TargetObjectSchema> visitedAsElement = new HashSet<>();
Set<Private.CanonicalSearchEntry> allOnLevel = new HashSet<>();
allOnLevel.add(new Private.CanonicalSearchEntry(List.of(), false, this));
while (!allOnLevel.isEmpty()) {
List<String> found = null;
for (Private.CanonicalSearchEntry ent : allOnLevel) {
if (ent.schema.getInterfaces().contains(type) && ent.parentIsCanonical) {
// Check for final being index is in parentIsCanonical.
if (found != null) {
return null; // Non-unique answer
}
found = PathUtils.parent(ent.path);
}
}
if (found != null) {
return List.copyOf(found); // Unique shortest answer
}
Set<Private.CanonicalSearchEntry> nextLevel = new HashSet<>();
for (Private.CanonicalSearchEntry ent : allOnLevel) {
if (PathPattern.isWildcard(PathUtils.getKey(ent.path))) {
continue;
}
for (Map.Entry<String, AttributeSchema> attrEnt : ent.schema.getAttributeSchemas()
.entrySet()) {
TargetObjectSchema attrSchema = ctx.getSchema(attrEnt.getValue().getSchema());
if (TargetObject.class.isAssignableFrom(attrSchema.getType()) &&
visited.add(attrSchema)) {
nextLevel.add(new Private.CanonicalSearchEntry(
PathUtils.extend(ent.path, attrEnt.getKey()), false, // If child is not element, this is not is canonical container
attrSchema));
}
}
for (Map.Entry<String, SchemaName> elemEnt : ent.schema.getElementSchemas()
.entrySet()) {
TargetObjectSchema elemSchema = ctx.getSchema(elemEnt.getValue());
visited.add(elemSchema); // Add but do not condition
if (visitedAsElement.add(elemSchema)) {
nextLevel.add(new Private.CanonicalSearchEntry(
PathUtils.index(ent.path, elemEnt.getKey()),
ent.schema.isCanonicalContainer(), elemSchema));
}
}
TargetObjectSchema deSchema = ctx.getSchema(ent.schema.getDefaultElementSchema());
visited.add(deSchema);
if (visitedAsElement.add(deSchema)) {
nextLevel.add(new Private.CanonicalSearchEntry(PathUtils.index(ent.path, ""),
ent.schema.isCanonicalContainer(), deSchema));
}
}
allOnLevel = nextLevel;
}
// We exhausted the reachable schemas
return null;
}
default List<String> searchForSuitable(Class<? extends TargetObject> type, List<String> path) {
for (; path != null; path = PathUtils.parent(path)) {
TargetObjectSchema schema = getSuccessorSchema(path);
if (schema.getInterfaces().contains(type)) {
return path;
}
List<String> inAgg = Private.searchForSuitableInAggregate(schema, type);
if (inAgg != null) {
return PathUtils.extend(path, inAgg);
}
}
return null;
}
default List<String> searchForAncestor(Class<? extends TargetObject> type, List<String> path) {
for (; path != null; path = PathUtils.parent(path)) {
TargetObjectSchema schema = getSuccessorSchema(path);
if (schema.getInterfaces().contains(type)) {
return path;
}
}
return null;
}
/**
@ -433,8 +768,8 @@ public interface TargetObjectSchema {
String path =
key == null ? null : PathUtils.toString(PathUtils.extend(parentPath, key));
String msg = path == null
? "Value " + value + " does not conform to required type " +
getType() + " of schema " + this
? "Value " + value + " does not conform to required type " + getType() +
" of schema " + this
: "Value " + value + " for " + path + " does not conform to required type " +
getType() + " of schema " + this;
Msg.error(this, msg);
@ -445,8 +780,8 @@ public interface TargetObjectSchema {
for (Class<? extends TargetObject> iface : getInterfaces()) {
if (!iface.isAssignableFrom(cls)) {
// TODO: Should this throw an exception, eventually?
String msg = "Value " + value + " does not implement required interface " +
iface + " of schema " + this;
String msg = "Value " + value + " does not implement required interface " + iface +
" of schema " + this;
Msg.error(this, msg);
if (strict) {
throw new AssertionError(msg);
@ -465,8 +800,7 @@ public interface TargetObjectSchema {
*/
default void validateRequiredAttributes(TargetObject object, boolean strict) {
Set<String> present = object.getCachedAttributes().keySet();
Set<String> missing = getAttributeSchemas()
.values()
Set<String> missing = getAttributeSchemas().values()
.stream()
.filter(AttributeSchema::isRequired)
.map(AttributeSchema::getName)
@ -553,8 +887,7 @@ public interface TargetObjectSchema {
* @param delta the delta, before or after the fact
*/
default void validateElementDelta(List<String> parentPath,
Delta<?, ? extends TargetObject> delta,
boolean strict) {
Delta<?, ? extends TargetObject> delta, boolean strict) {
for (Map.Entry<String, ? extends TargetObject> ent : delta.added.entrySet()) {
TargetObject element = ent.getValue();
TargetObjectSchema schema = getContext().getSchema(getElementSchema(ent.getKey()));

View file

@ -17,6 +17,8 @@ package ghidra.dbg.target.schema;
import java.lang.annotation.*;
import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetObjectSchemaInfo {
@ -26,5 +28,9 @@ public @interface TargetObjectSchemaInfo {
TargetElementType[] elements() default {};
ResyncMode elementResync() default ResyncMode.NEVER;
TargetAttributeType[] attributes() default {};
ResyncMode attributeResync() default ResyncMode.NEVER;
}

View file

@ -24,8 +24,7 @@ import org.jdom.input.SAXBuilder;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.dbg.target.schema.TargetObjectSchema.*;
import ghidra.util.Msg;
import ghidra.util.xml.XmlUtilities;
@ -88,6 +87,10 @@ public class XmlSchemaContext extends DefaultSchemaContext {
if (schema.isCanonicalContainer()) {
XmlUtilities.setStringAttr(result, "canonical", "yes");
}
XmlUtilities.setStringAttr(result, "elementResync",
schema.getElementResyncMode().name());
XmlUtilities.setStringAttr(result, "attributeResync",
schema.getAttributeResyncMode().name());
for (Map.Entry<String, SchemaName> ent : schema.getElementSchemas().entrySet()) {
Element elemElem = new Element("element");
@ -162,6 +165,10 @@ public class XmlSchemaContext extends DefaultSchemaContext {
}
builder.setCanonicalContainer(parseBoolean(schemaElem, "canonical"));
builder.setElementResyncMode(
ResyncMode.valueOf(schemaElem.getAttributeValue("elementResync")));
builder.setAttributeResyncMode(
ResyncMode.valueOf(schemaElem.getAttributeValue("attributeResync")));
for (Element elemElem : XmlUtilities.getChildren(schemaElem, "element")) {
SchemaName schema = name(elemElem.getAttributeValue("schema"));

View file

@ -346,7 +346,8 @@ public enum CollectionUtils {
return removed.isEmpty() && added.isEmpty();
}
public Delta<T, U> apply(Map<String, T> mutable, BiPredicate<Object, Object> equals) {
public Delta<T, U> apply(Map<String, T> mutable,
BiPredicate<Object, Object> equals) {
return apply(mutable, removed.keySet(), added, equals);
}

View file

@ -0,0 +1,283 @@
/* ###
* 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.dbg.util;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import ghidra.async.AsyncFence;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerModelClosedReason;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.error.DebuggerMemoryAccessException;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
import ghidra.util.Msg;
/**
* A mechanism for re-ordering model callbacks
*
* <p>
* When this is added to the model, {@code replay} must be true, or behavior of the mechanism is
* undefined.
*/
public class DebuggerCallbackReorderer implements DebuggerModelListener {
private class ObjectRecord {
private final TargetObject obj;
private final CompletableFuture<TargetObject> addedToParent = new CompletableFuture<>();
private final CompletableFuture<TargetObject> complete;
ObjectRecord(TargetObject obj) {
this.obj = obj;
TargetObject parent = obj.getParent();
ObjectRecord parentRecord = parent == null ? null : records.get(parent);
if (parentRecord == null) {
complete = addedToParent.thenApply(this::completed);
}
else {
complete = parentRecord.complete.thenCompose(__ -> addedToParent)
.thenApply(this::completed);
}
}
TargetObject completed(TargetObject obj) {
records.remove(obj);
// NB. We should already be on the clientExecutor
Map<String, ?> attributes = obj.getCallbackAttributes();
if (!attributes.isEmpty()) {
defensive(() -> listener.attributesChanged(obj, List.of(), attributes),
"attributesChanged(r)");
}
Map<String, ? extends TargetObject> elements = obj.getCallbackElements();
if (!elements.isEmpty()) {
defensive(() -> listener.elementsChanged(obj, List.of(), elements),
"elementsChanged(r)");
}
return obj;
}
void added() {
if (!addedToParent.isDone()) {
addedToParent.complete(obj);
}
}
void removed() {
if (!addedToParent.isDone()) {
addedToParent.cancel(false);
}
}
}
private final DebuggerModelListener listener;
private final Map<TargetObject, ObjectRecord> records = new HashMap<>();
private CompletableFuture<Void> lastEvent = AsyncUtils.NIL;
public DebuggerCallbackReorderer(DebuggerModelListener listener) {
this.listener = listener;
}
private void defensive(Runnable r, String cb) {
try {
r.run();
}
catch (Throwable t) {
Msg.error(this, "Listener " + listener + " caused exception processing " + cb, t);
}
}
@Override
public void catastrophic(Throwable t) {
listener.catastrophic(t);
}
@Override
public void modelClosed(DebuggerModelClosedReason reason) {
listener.modelClosed(reason);
}
@Override
public void modelOpened() {
listener.modelOpened();
}
@Override
public void modelStateChanged() {
listener.modelStateChanged();
}
@Override
public void created(TargetObject object) {
//System.err.println("created object='" + object.getJoinedPath(".") + "'");
records.put(object, new ObjectRecord(object));
defensive(() -> listener.created(object), "created");
}
@Override
public void invalidated(TargetObject object, TargetObject branch, String reason) {
ObjectRecord remove = records.remove(object);
if (remove != null) {
remove.removed();
}
defensive(() -> listener.invalidated(object, branch, reason), "invalidated");
}
@Override
public void rootAdded(TargetObject root) {
defensive(() -> listener.rootAdded(root), "rootAdded");
records.get(root).added();
}
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
//System.err.println("attributesChanged object=" + object.getJoinedPath(".") + ",removed=" +
// removed + ",added=" + added);
ObjectRecord record = records.get(object);
if (record == null) {
defensive(() -> listener.attributesChanged(object, removed, added),
"attributesChanged");
}
// Removed taken care of via invalidation
for (Entry<String, ?> ent : added.entrySet()) {
//System.err.println(" " + ent.getKey());
Object val = ent.getValue();
if (val instanceof TargetObject) {
TargetObject obj = (TargetObject) val;
if (!PathUtils.isLink(object.getPath(), ent.getKey(), obj.getPath())) {
ObjectRecord rec = records.get(obj);
rec.added();
}
}
}
}
@Override
public void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
//System.err.println("elementsChanged object=" + object.getJoinedPath(".") + ",removed=" +
// removed + ",added=" + added);
ObjectRecord record = records.get(object);
if (record == null) {
defensive(() -> listener.elementsChanged(object, removed, added), "elementsChanged");
}
// Removed taken care of via invalidation
for (Entry<String, ? extends TargetObject> ent : added.entrySet()) {
//System.err.println(" " + ent.getKey());
TargetObject obj = ent.getValue();
if (!PathUtils.isElementLink(object.getPath(), ent.getKey(), obj.getPath())) {
ObjectRecord rec = records.get(obj);
if (rec != null) {
rec.added();
}
}
}
}
private synchronized void orderedOnObjects(Collection<TargetObject> objects, Runnable r,
String cb) {
AsyncFence fence = new AsyncFence();
fence.include(lastEvent);
for (TargetObject obj : objects) {
ObjectRecord record = records.get(obj);
if (record != null) {
fence.include(record.complete);
}
}
lastEvent = fence.ready().thenAccept(__ -> {
defensive(r, cb);
}).exceptionally(ex -> {
Msg.error(this, "Callback " + cb + " dropped for error in dependency", ex);
return null;
});
}
@Override
public void breakpointHit(TargetObject container, TargetObject trapped, TargetStackFrame frame,
TargetBreakpointSpec spec, TargetBreakpointLocation breakpoint) {
List<TargetObject> args = frame == null
? List.of(container, trapped, spec, breakpoint)
: List.of(container, trapped, frame, spec, breakpoint);
orderedOnObjects(args, () -> {
listener.breakpointHit(container, trapped, frame, spec, breakpoint);
}, "breakpointHit");
}
@Override
public void consoleOutput(TargetObject console, Channel channel, byte[] data) {
orderedOnObjects(List.of(console), () -> {
listener.consoleOutput(console, channel, data);
}, "consoleOutput");
}
private Collection<TargetObject> gatherObjects(Collection<?>... collections) {
Set<TargetObject> objs = new HashSet<>();
for (Collection<?> col : collections) {
for (Object val : col) {
if (val instanceof TargetObject) {
objs.add((TargetObject) val);
}
}
}
return objs;
}
@Override
public void event(TargetObject object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
List<TargetObject> objs = eventThread == null
? List.of(object)
: List.of(object, eventThread);
orderedOnObjects(gatherObjects(objs, parameters), () -> {
listener.event(object, eventThread, type, description, parameters);
}, "event(" + type + ") " + description);
}
@Override
public void invalidateCacheRequested(TargetObject object) {
orderedOnObjects(List.of(object), () -> {
listener.invalidateCacheRequested(object);
}, "invalidateCacheRequested");
}
@Override
public void memoryReadError(TargetObject memory, AddressRange range,
DebuggerMemoryAccessException e) {
orderedOnObjects(List.of(memory), () -> {
listener.memoryReadError(memory, range, e);
}, "invalidateCacheRequested");
}
@Override
public void memoryUpdated(TargetObject memory, Address address, byte[] data) {
orderedOnObjects(List.of(memory), () -> {
listener.memoryUpdated(memory, address, data);
}, "invalidateCacheRequested");
}
@Override
public void registersUpdated(TargetObject bank, Map<String, byte[]> updates) {
orderedOnObjects(List.of(bank), () -> {
listener.registersUpdated(bank, updates);
}, "invalidateCacheRequested");
}
}

View file

@ -18,13 +18,26 @@ package ghidra.dbg.util;
import java.util.*;
import java.util.function.Predicate;
import org.apache.commons.lang3.StringUtils;
public class PathMatcher implements PathPredicates {
protected static final Set<String> WILD_SINGLETON = Set.of("");
protected final Set<PathPattern> patterns = new HashSet<>();
public void addPattern(List<String> pattern) {
patterns.add(new PathPattern(pattern));
}
public void addPattern(PathPattern pattern) {
patterns.add(pattern);
}
@Override
public String toString() {
return String.format("<PathMatcher\n %s\n>", StringUtils.join(patterns, "\n "));
}
/**
* TODO: We could probably do a lot better, esp. for many patterns, by using a trie.
*/
@ -43,12 +56,66 @@ public class PathMatcher implements PathPredicates {
}
@Override
public boolean successorCouldMatch(List<String> path) {
return anyPattern(p -> p.successorCouldMatch(path));
public boolean successorCouldMatch(List<String> path, boolean strict) {
return anyPattern(p -> p.successorCouldMatch(path, strict));
}
@Override
public boolean ancestorMatches(List<String> path) {
return anyPattern(p -> p.ancestorMatches(path));
public boolean ancestorMatches(List<String> path, boolean strict) {
return anyPattern(p -> p.ancestorMatches(path, strict));
}
@Override
public List<String> getSingletonPath() {
if (patterns.size() != 1) {
return null;
}
return patterns.iterator().next().getSingletonPath();
}
@Override
public PathPattern getSingletonPattern() {
if (patterns.size() != 1) {
return null;
}
return patterns.iterator().next();
}
@Override
public Set<String> getNextNames(List<String> path) {
Set<String> result = new HashSet<>();
for (PathPattern pattern : patterns) {
result.addAll(pattern.getNextNames(path));
if (result.contains("")) {
return WILD_SINGLETON;
}
}
return result;
}
@Override
public Set<String> getNextIndices(List<String> path) {
Set<String> result = new HashSet<>();
for (PathPattern pattern : patterns) {
result.addAll(pattern.getNextIndices(path));
if (result.contains("")) {
return WILD_SINGLETON;
}
}
return result;
}
@Override
public boolean isEmpty() {
return patterns.isEmpty();
}
@Override
public PathMatcher applyIndices(List<String> indices) {
PathMatcher result = new PathMatcher();
for (PathPattern pat : patterns) {
result.addPattern(pat.applyIndices(indices));
}
return result;
}
}

View file

@ -15,8 +15,7 @@
*/
package ghidra.dbg.util;
import java.util.List;
import java.util.Objects;
import java.util.*;
public class PathPattern implements PathPredicates {
private final List<String> pattern;
@ -44,6 +43,11 @@ public class PathPattern implements PathPredicates {
this.pattern = List.copyOf(pattern);
}
@Override
public String toString() {
return String.format("<PathPattern %s>", PathUtils.toString(pattern));
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PathPattern)) {
@ -58,6 +62,10 @@ public class PathPattern implements PathPredicates {
return pattern.hashCode();
}
public static boolean isWildcard(String pat) {
return "[]".equals(pat) || "".equals(pat);
}
public static boolean keyMatches(String pat, String key) {
if (key.equals(pat)) {
return true;
@ -65,7 +73,7 @@ public class PathPattern implements PathPredicates {
if ("[]".equals(pat) && PathUtils.isIndex(key)) {
return true;
}
if ("".equals(pat) && PathUtils.isName(pat)) {
if ("".equals(pat) && PathUtils.isName(key)) {
return true;
}
return false;
@ -89,18 +97,101 @@ public class PathPattern implements PathPredicates {
}
@Override
public boolean successorCouldMatch(List<String> path) {
public boolean successorCouldMatch(List<String> path, boolean strict) {
if (path.size() > pattern.size()) {
return false;
}
if (strict && path.size() == pattern.size()) {
return false;
}
return matchesUpTo(path, path.size());
}
@Override
public boolean ancestorMatches(List<String> path) {
public boolean ancestorMatches(List<String> path, boolean strict) {
if (path.size() < pattern.size()) {
return false;
}
if (strict && path.size() == pattern.size()) {
return false;
}
return matchesUpTo(path, pattern.size());
}
protected static boolean containsWildcards(List<String> pattern) {
for (String pat : pattern) {
if (isWildcard(pat)) {
return true;
}
}
return false;
}
@Override
public List<String> getSingletonPath() {
if (containsWildcards(pattern)) {
return null;
}
return pattern;
}
public int countWildcards() {
return (int) pattern.stream().filter(k -> isWildcard(k)).count();
}
@Override
public PathPattern getSingletonPattern() {
return this;
}
@Override
public Set<String> getNextNames(List<String> path) {
if (path.size() >= pattern.size()) {
return Set.of();
}
String pat = pattern.get(path.size());
if (PathUtils.isName(pat)) {
return Set.of(pat);
}
return Set.of();
}
@Override
public Set<String> getNextIndices(List<String> path) {
if (path.size() >= pattern.size()) {
return Set.of();
}
String pat = pattern.get(path.size());
if (PathUtils.isIndex(pat)) {
return Set.of(PathUtils.parseIndex(pat));
}
return Set.of();
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public PathPattern applyIndices(List<String> indices) {
List<String> result = new ArrayList<>(pattern.size());
Iterator<String> it = indices.iterator();
for (String pat : pattern) {
if (it.hasNext() && isWildcard(pat)) {
String index = it.next();
if (PathUtils.isIndex(pat)) {
result.add(PathUtils.makeKey(index));
}
else {
// NB. Rare for attribute wildcards, but just in case
result.add(index);
}
}
else {
result.add(pat);
}
}
return new PathPattern(result);
}
}

View file

@ -15,7 +15,12 @@
*/
package ghidra.dbg.util;
import java.util.List;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import ghidra.async.AsyncFence;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.util.PathUtils.PathComparator;
public interface PathPredicates {
/**
@ -29,6 +34,7 @@ public interface PathPredicates {
/**
* Check if the given path <em>could</em> have a matching successor
*
* <p>
* This essentially checks if the given path is a viable prefix to the matcher.
*
* @implNote this method could become impractical for culling queries if we allow too
@ -38,15 +44,235 @@ public interface PathPredicates {
*
*
* @param path the path (prefix) to check
* @param strict true to exclude the case where {@link #matches(List)} would return true
* @return true if a successor could match, false otherwise
*/
boolean successorCouldMatch(List<String> path);
boolean successorCouldMatch(List<String> path, boolean strict);
/**
* Check if the given path has an ancestor that matches
*
* @param path the path to check
* @param strict true to exclude the case where {@link #matches(List)} would return true
* @return true if an ancestor matches, false otherwise
*/
boolean ancestorMatches(List<String> path);
boolean ancestorMatches(List<String> path, boolean strict);
/**
* Assuming a successor of path could match, get the patterns for the next possible key
*
* <p>
* If the pattern could accept a name next, get all patterns describing those names
*
* @param path the ancestor path
* @return a set of patterns
*/
Set<String> getNextNames(List<String> path);
/**
* Assuming a successor of path could match, get the patterns for the next possible index
*
* <p>
* If the pattern could accept an index next, get all patterns describing those indices
*
* @param path the ancestor path
* @return a set of patterns, without brack@Override ets ({@code [])
*/
Set<String> getNextIndices(List<String> path);
/**
* If this predicate is known to match only one path, i.e., no wildcards, get that path
*
* @return the singleton path, or {@code null}
*/
List<String> getSingletonPath();
/**
* If this predicate consists of a single pattern, get that pattern
*
* @return the singleton pattern, or {@code null}
*/
PathPattern getSingletonPattern();
static boolean anyMatches(Set<String> pats, String key) {
for (String pat : pats) {
if ("".equals(pat)) {
return true;
}
if (key.equals(pat)) {
return true;
}
}
return false;
}
default NavigableMap<List<String>, ?> getCachedValues(TargetObject seed) {
return getCachedValues(List.of(), seed);
}
default NavigableMap<List<String>, ?> getCachedValues(List<String> path, Object val) {
NavigableMap<List<String>, Object> result = new TreeMap<>(PathComparator.KEYED);
getCachedValues(result, path, val);
return result;
}
default void getCachedValues(Map<List<String>, Object> result, List<String> path, Object val) {
if (matches(path)) {
result.put(path, val);
}
if (val instanceof TargetObject && successorCouldMatch(path, true)) {
TargetObject cur = (TargetObject) val;
Set<String> nextNames = getNextNames(path);
if (!nextNames.isEmpty()) {
for (Map.Entry<String, ?> ent : cur.getCachedAttributes().entrySet()) {
Object value = ent.getValue();
String name = ent.getKey();
if (!anyMatches(nextNames, name)) {
continue;
}
getCachedValues(result, PathUtils.extend(path, name), value);
}
}
Set<String> nextIndices = getNextIndices(path);
if (!nextIndices.isEmpty()) {
for (Map.Entry<String, ?> ent : cur.getCachedElements().entrySet()) {
Object obj = ent.getValue();
String index = ent.getKey();
if (!anyMatches(nextIndices, index)) {
continue;
}
getCachedValues(result, PathUtils.index(path, index), obj);
}
}
}
}
default NavigableMap<List<String>, TargetObject> getCachedSuccessors(TargetObject seed) {
NavigableMap<List<String>, TargetObject> result = new TreeMap<>(PathComparator.KEYED);
getCachedSuccessors(result, List.of(), seed);
return result;
}
default void getCachedSuccessors(Map<List<String>, TargetObject> result,
List<String> path, TargetObject cur) {
if (matches(path)) {
result.put(path, cur);
}
if (successorCouldMatch(path, true)) {
Set<String> nextNames = getNextNames(path);
if (!nextNames.isEmpty()) {
for (Map.Entry<String, ?> ent : cur.getCachedAttributes().entrySet()) {
Object value = ent.getValue();
if (!(value instanceof TargetObject)) {
continue;
}
String name = ent.getKey();
if (!anyMatches(nextNames, name)) {
continue;
}
TargetObject obj = (TargetObject) value;
getCachedSuccessors(result, PathUtils.extend(path, name), obj);
}
}
Set<String> nextIndices = getNextIndices(path);
if (!nextIndices.isEmpty()) {
for (Map.Entry<String, ? extends TargetObject> ent : cur.getCachedElements()
.entrySet()) {
TargetObject obj = ent.getValue();
String index = ent.getKey();
if (!anyMatches(nextIndices, index)) {
continue;
}
getCachedSuccessors(result, PathUtils.index(path, index), obj);
}
}
}
}
default CompletableFuture<NavigableMap<List<String>, TargetObject>> fetchSuccessors(
TargetObject seed) {
NavigableMap<List<String>, TargetObject> result = new TreeMap<>(PathComparator.KEYED);
return fetchSuccessors(result, List.of(), seed).thenApply(__ -> result);
}
default CompletableFuture<Void> fetchSuccessors(Map<List<String>, TargetObject> result,
List<String> path, TargetObject cur) {
AsyncFence fence = new AsyncFence();
if (matches(path)) {
synchronized (result) {
result.put(path, cur);
}
}
if (successorCouldMatch(path, true)) {
Set<String> nextNames = getNextNames(path);
if (!nextNames.isEmpty()) {
fence.include(cur.fetchAttributes().thenCompose(attrs -> {
AsyncFence aFence = new AsyncFence();
for (Map.Entry<String, ?> ent : attrs.entrySet()) {
Object value = ent.getValue();
if (!(value instanceof TargetObject)) {
continue;
}
String name = ent.getKey();
if (!anyMatches(nextNames, name)) {
continue;
}
TargetObject obj = (TargetObject) value;
aFence.include(
fetchSuccessors(result, PathUtils.extend(path, name), obj));
}
return aFence.ready();
}));
}
Set<String> nextIndices = getNextIndices(path);
if (!nextIndices.isEmpty()) {
fence.include(cur.fetchElements().thenCompose(elems -> {
AsyncFence eFence = new AsyncFence();
for (Map.Entry<String, ? extends TargetObject> ent : elems.entrySet()) {
TargetObject obj = ent.getValue();
String index = ent.getKey();
if (!anyMatches(nextIndices, index)) {
continue;
}
eFence.include(
fetchSuccessors(result, PathUtils.index(path, index), obj));
}
return eFence.ready();
}));
}
}
return fence.ready();
}
/**
* Substitute wildcards from left to right for the given list of indices
*
* <p>
* Takes each pattern and substitutes its wildcards for the given indices, starting from the
* left and working right. This object is unmodified, and the result is returned.
*
* <p>
* If there are fewer wildcards in a pattern than given, only the left-most indices are taken.
* If there are fewer indices than wildcards in a pattern, then the right-most wildcards are
* left in the resulting pattern. Note while rare, attribute wildcards are substituted, too.
*
* @param indices the indices to substitute
* @return the pattern or matcher with the applied substitutions
*/
PathPredicates applyIndices(List<String> indices);
default PathPredicates applyIndices(String... indices) {
return applyIndices(List.of(indices));
}
/**
* Test if any patterns are contained here
*
* <p>
* Note that the presence of a pattern does not guarantee the presence of a matching object.
* However, the absence of any pattern does guarantee no object can match.
*
* @return
*/
boolean isEmpty();
}

View file

@ -0,0 +1,56 @@
/* ###
* 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.dbg;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import ghidra.async.AsyncReference;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
import ghidra.dbg.agent.DefaultTargetModelRoot;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.testutil.DebuggerModelTestUtils;
import ghidra.program.model.address.AddressFactory;
public class AnnotatedDebuggerAttributeListenerTest implements DebuggerModelTestUtils {
@Test
public void testAnnotatedListener() throws Throwable {
AbstractDebuggerObjectModel model = new AbstractDebuggerObjectModel() {
@Override
public AddressFactory getAddressFactory() {
return null;
}
};
DefaultTargetModelRoot obj = new DefaultTargetModelRoot(model, "Test");
AsyncReference<String, Void> display = new AsyncReference<>();
DebuggerModelListener l = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) {
@AttributeCallback("_test")
private void testChanged(TargetObject object, String disp) {
display.set(disp, null);
}
};
obj.addListener(l);
obj.changeAttributes(List.of(), Map.ofEntries(Map.entry("_test", "Testing")), "Because");
waitOn(display.waitValue("Testing"));
obj.changeAttributes(List.of("_test"), Map.of(), "Because");
waitOn(display.waitValue(null));
}
}

View file

@ -38,8 +38,8 @@ public class DebugModelConventionsTest {
mb.createTestModel();
mb.createTestProcessesAndThreads();
TargetBreakpointContainer bpts = DebugModelConventions
.findSuitable(TargetBreakpointContainer.class, mb.testProcess1)
TargetBreakpointSpecContainer bpts = DebugModelConventions
.findSuitable(TargetBreakpointSpecContainer.class, mb.testProcess1)
.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
assertEquals(mb.testProcess1.breaks, bpts);
}
@ -49,8 +49,8 @@ public class DebugModelConventionsTest {
mb.createTestModel();
mb.createTestProcessesAndThreads();
TargetBreakpointContainer bpts = DebugModelConventions
.findSuitable(TargetBreakpointContainer.class, mb.testProcess1.threads)
TargetBreakpointSpecContainer bpts = DebugModelConventions
.findSuitable(TargetBreakpointSpecContainer.class, mb.testProcess1.threads)
.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
assertEquals(mb.testProcess1.breaks, bpts);
}

View file

@ -15,7 +15,7 @@
*/
package ghidra.dbg.agent;
import static ghidra.lifecycle.Unfinished.TODO;
import static ghidra.lifecycle.Unfinished.*;
import static org.junit.Assert.*;
import java.util.*;
@ -30,11 +30,10 @@ import ghidra.async.AsyncTestUtils;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetRegisterBank;
import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener;
import ghidra.dbg.util.*;
import ghidra.dbg.util.AttributesChangedListener.AttributesChangedInvocation;
import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation;
import ghidra.dbg.util.InvalidatedListener.InvalidatedInvocation;
import ghidra.dbg.testutil.*;
import ghidra.dbg.testutil.AttributesChangedListener.AttributesChangedInvocation;
import ghidra.dbg.testutil.ElementsChangedListener.ElementsChangedInvocation;
import ghidra.dbg.testutil.InvalidatedListener.InvalidatedInvocation;
import ghidra.program.model.address.AddressFactory;
import ghidra.program.model.address.AddressSpace;
@ -278,7 +277,7 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
}
@Override
public void registersUpdated(TargetRegisterBank bank, Map<String, byte[]> updates) {
public void registersUpdated(TargetObject bank, Map<String, byte[]> updates) {
record.add(new ImmutablePair<>("registersUpdated", bank));
}
}
@ -291,19 +290,16 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A");
FakeTargetRegisterBank fakeA1rb = new FakeTargetRegisterBank(model, fakeA, "[1]");
fakeA1rb.listeners.fire(TargetRegisterBankListener.class)
.registersUpdated(fakeA1rb, Map.of());
fakeA1rb.listeners.fire.registersUpdated(fakeA1rb, Map.of());
fakeA.setElements(List.of(fakeA1rb), "Init");
model.root.setAttributes(List.of(fakeA), Map.of(), "Init");
waitOn(model.clientExecutor);
assertEquals(List.of(
new ImmutablePair<>("created", fakeA),
assertEquals(List.of(new ImmutablePair<>("created", fakeA),
new ImmutablePair<>("created", fakeA1rb),
new ImmutablePair<>("registersUpdated", fakeA1rb),
new ImmutablePair<>("addedElem", fakeA1rb),
new ImmutablePair<>("addedAttr", fakeA)),
new ImmutablePair<>("addedElem", fakeA1rb), new ImmutablePair<>("addedAttr", fakeA)),
listener.record);
}
@ -312,8 +308,7 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A");
FakeTargetRegisterBank fakeA1rb = new FakeTargetRegisterBank(model, fakeA, "[1]");
fakeA1rb.listeners.fire(TargetRegisterBankListener.class)
.registersUpdated(fakeA1rb, Map.of());
fakeA1rb.listeners.fire.registersUpdated(fakeA1rb, Map.of());
fakeA.setElements(List.of(fakeA1rb), "Init");
model.root.setAttributes(List.of(fakeA), Map.of(), "Init");
EventRecordingListener listener = new EventRecordingListener();
@ -321,12 +316,9 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
waitOn(model.clientExecutor);
assertEquals(List.of(
new ImmutablePair<>("created", model.root),
new ImmutablePair<>("created", fakeA),
new ImmutablePair<>("created", fakeA1rb),
new ImmutablePair<>("addedElem", fakeA1rb),
new ImmutablePair<>("addedAttr", fakeA)),
assertEquals(List.of(new ImmutablePair<>("created", model.root),
new ImmutablePair<>("created", fakeA), new ImmutablePair<>("created", fakeA1rb),
new ImmutablePair<>("addedElem", fakeA1rb), new ImmutablePair<>("addedAttr", fakeA)),
listener.record);
}
}

View file

@ -54,10 +54,10 @@ public abstract class AbstractTestTargetRegisterBank<P extends TestTargetObject>
}
result.put(n, v);
}
return regs.getModel().future(result).thenApply(__ -> {
listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, result);
return model.gateFuture(regs.getModel().future(result).thenApply(__ -> {
listeners.fire.registersUpdated(this, result);
return result;
}).thenCompose(model::gateFuture);
}));
}
protected CompletableFuture<Void> writeRegs(Map<String, byte[]> values,
@ -80,9 +80,9 @@ public abstract class AbstractTestTargetRegisterBank<P extends TestTargetObject>
}
}
future.thenAccept(__ -> {
listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, updates);
}).thenCompose(model::gateFuture);
return future;
listeners.fire.registersUpdated(this, updates);
});
return model.gateFuture(future);
}
public void setFromBank(AbstractTestTargetRegisterBank<?> bank) {

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.dbg.model;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
import ghidra.dbg.agent.SpiTargetObject;
import ghidra.program.model.address.*;
public class EmptyDebuggerObjectModel extends AbstractDebuggerObjectModel {
protected final AddressSpace ram = new GenericAddressSpace("ram", 64, AddressSpace.TYPE_RAM, 0);
protected final AddressFactory factory = new DefaultAddressFactory(new AddressSpace[] { ram });
@Override
public AddressFactory getAddressFactory() {
return factory;
}
public Address addr(long off) {
return ram.getAddress(off);
}
public AddressRange range(long min, long max) {
return new AddressRangeImpl(addr(min), addr(max));
}
@Override
public void addModelRoot(SpiTargetObject root) {
super.addModelRoot(root);
}
}

View file

@ -15,14 +15,17 @@
*/
package ghidra.dbg.model;
import java.io.IOException;
import java.util.concurrent.*;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
import org.jdom.JDOMException;
import ghidra.dbg.target.TargetObject;
import ghidra.program.model.address.*;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.target.schema.XmlSchemaContext;
// TODO: Refactor with other Fake and Test model stuff.
public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel {
public class TestDebuggerObjectModel extends EmptyDebuggerObjectModel {
public static final String TEST_MODEL_STRING = "Test Model";
protected static final int DELAY_MILLIS = 250;
@ -34,9 +37,19 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel {
ASYNC, DELAYED;
}
protected final AddressSpace ram =
new GenericAddressSpace("ram", 64, AddressSpace.TYPE_RAM, 0);
protected final AddressFactory factory = new DefaultAddressFactory(new AddressSpace[] { ram });
public static final XmlSchemaContext SCHEMA_CTX;
public static final TargetObjectSchema ROOT_SCHEMA;
static {
try {
SCHEMA_CTX = XmlSchemaContext.deserialize(
EmptyDebuggerObjectModel.class.getResourceAsStream("test_schema.xml"));
ROOT_SCHEMA = SCHEMA_CTX.getSchema(SCHEMA_CTX.name("Test"));
}
catch (IOException | JDOMException e) {
throw new AssertionError(e);
}
}
public final TestTargetSession session;
protected int invalidateCachesCount;
@ -50,25 +63,25 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel {
}
public TestDebuggerObjectModel(String rootHint) {
this.session = new TestTargetSession(this, rootHint);
this.session = new TestTargetSession(this, rootHint, ROOT_SCHEMA);
addModelRoot(session);
}
@Override
public TargetObjectSchema getRootSchema() {
return ROOT_SCHEMA;
}
@Override
public String toString() {
return TEST_MODEL_STRING;
}
@Override
@Override // TODO: Give test writer control of addModelRoot
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
return future(session);
}
@Override
public AddressFactory getAddressFactory() {
return factory;
}
@Override
public CompletableFuture<Void> close() {
session.invalidateSubtree(session, "Model closed");
@ -87,14 +100,6 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel {
return session.requestFocus(obj);
}
public Address addr(long off) {
return ram.getAddress(off);
}
public AddressRange range(long min, long max) {
return new AddressRangeImpl(addr(min), addr(max));
}
@Override
public synchronized void invalidateAllLocalCaches() {
invalidateCachesCount++;

View file

@ -18,10 +18,8 @@ package ghidra.dbg.model;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.attributes.TargetObjectList;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet;
import ghidra.dbg.util.CollectionUtils.Delta;
import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet;
import ghidra.dbg.util.PathUtils;
import ghidra.program.model.address.Address;
@ -36,7 +34,6 @@ public class TestTargetBreakpoint
changeAttributes(List.of(), Map.of(
SPEC_ATTRIBUTE_NAME, this,
ADDRESS_ATTRIBUTE_NAME, address,
AFFECTS_ATTRIBUTE_NAME, TargetObjectList.of(parent.getParent()),
ENABLED_ATTRIBUTE_NAME, true,
EXPRESSION_ATTRIBUTE_NAME, address.toString(),
KINDS_ATTRIBUTE_NAME, TargetBreakpointKindSet.copyOf(kinds),
@ -56,23 +53,17 @@ public class TestTargetBreakpoint
@Override
public CompletableFuture<Void> disable() {
Delta<?, ?> delta = changeAttributes(List.of(), Map.of(
changeAttributes(List.of(), Map.of(
ENABLED_ATTRIBUTE_NAME, false //
), "Disabled Breakpoint");
if (delta.added.containsKey(ENABLED_ATTRIBUTE_NAME)) {
listeners.fire(TargetBreakpointSpecListener.class).breakpointToggled(this, false);
}
return getModel().future(null);
}
@Override
public CompletableFuture<Void> enable() {
Delta<?, ?> delta = changeAttributes(List.of(), Map.of(
changeAttributes(List.of(), Map.of(
ENABLED_ATTRIBUTE_NAME, true //
), "Enabled Breakpoint");
if (delta.added.containsKey(ENABLED_ATTRIBUTE_NAME)) {
listeners.fire(TargetBreakpointSpecListener.class).breakpointToggled(this, true);
}
return getModel().future(null);
}

View file

@ -19,7 +19,7 @@ import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import ghidra.dbg.target.TargetBreakpointContainer;
import ghidra.dbg.target.TargetBreakpointSpecContainer;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.program.model.address.AddressRange;
@ -31,7 +31,7 @@ import ghidra.program.model.address.AddressRange;
public class TestTargetBreakpointContainer
extends DefaultTestTargetObject<TestTargetBreakpoint, TestTargetProcess>
implements TargetBreakpointContainer {
implements TargetBreakpointSpecContainer {
protected static final TargetBreakpointKindSet ALL_KINDS =
TargetBreakpointKindSet.of(TargetBreakpointKind.values());

View file

@ -66,18 +66,16 @@ public class TestTargetInterpreter
changeAttributes(List.of(), Map.of(
DISPLAY_ATTRIBUTE_NAME, display //
), "Display changed");
listeners.fire.displayChanged(this, display);
}
public void setPrompt(String prompt) {
changeAttributes(List.of(), Map.of(
PROMPT_ATTRIBUTE_NAME, prompt //
), "Prompt changed");
listeners.fire(TargetInterpreterListener.class).promptChanged(this, prompt);
}
public void output(Channel channel, String line) {
listeners.fire(TargetInterpreterListener.class).consoleOutput(this, channel, line + "\n");
listeners.fire.consoleOutput(this, channel, line + "\n");
}
public void clearCalls() {

View file

@ -47,7 +47,7 @@ public class TestTargetMemory
memory.getData(address.getOffset(), data);
CompletableFuture<byte[]> future = getModel().future(data);
future.thenAccept(__ -> {
listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, data);
listeners.fire.memoryUpdated(this, address, data);
});
return future;
}
@ -62,7 +62,7 @@ public class TestTargetMemory
setMemory(address, data);
CompletableFuture<Void> future = getModel().future(null);
future.thenAccept(__ -> {
listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, data);
listeners.fire.memoryUpdated(this, address, data);
});
return future;
}
@ -78,7 +78,6 @@ public class TestTargetMemory
changeAttributes(List.of(), Map.ofEntries(
Map.entry(ACCESSIBLE_ATTRIBUTE_NAME, accessible)),
"Set Test Memory Accessibility");
listeners.fire(TargetAccessibilityListener.class).accessibilityChanged(this, accessible);
return old;
}
}

View file

@ -23,6 +23,8 @@ import ghidra.async.AsyncUtils;
import ghidra.dbg.agent.DefaultTargetModelRoot;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema;
public class TestTargetSession extends DefaultTargetModelRoot
implements TestTargetObject, TargetFocusScope, TargetEventScope, TargetLauncher {
@ -33,18 +35,20 @@ public class TestTargetSession extends DefaultTargetModelRoot
public final TestMimickJavaLauncher mimickJavaLauncher;
public TestTargetSession(TestDebuggerObjectModel model, String rootHint) {
super(model, rootHint);
this(model, rootHint, EnumerableTargetObjectSchema.OBJECT);
}
public TestTargetSession(TestDebuggerObjectModel model, String rootHint,
TargetObjectSchema schema) {
super(model, rootHint, schema);
environment = new TestTargetEnvironment(this);
processes = new TestTargetProcessContainer(this);
interpreter = new TestTargetInterpreter(this);
mimickJavaLauncher = new TestMimickJavaLauncher(this);
changeAttributes(List.of(), List.of(
environment,
processes,
interpreter,
mimickJavaLauncher),
Map.of(), "Initialized");
changeAttributes(List.of(),
List.of(environment, processes, interpreter, mimickJavaLauncher), Map.of(),
"Initialized");
}
public TestTargetProcess addProcess(int pid) {
@ -58,19 +62,16 @@ public class TestTargetSession extends DefaultTargetModelRoot
@Override
public CompletableFuture<Void> requestFocus(TargetObject obj) {
return getModel().future(null).thenAccept(__ -> {
changeAttributes(List.of(), List.of(), Map.of(
FOCUS_ATTRIBUTE_NAME, obj //
return model.gateFuture(getModel().future(null).thenAccept(__ -> {
changeAttributes(List.of(), List.of(), Map.of(FOCUS_ATTRIBUTE_NAME, obj //
), "Focus requested");
listeners.fire(TargetFocusScopeListener.class).focusChanged(this, obj);
}).thenCompose(model::gateFuture);
}));
}
public void simulateStep(TestTargetThread eventThread) {
eventThread.setState(TargetExecutionState.RUNNING);
listeners.fire(TargetEventScopeListener.class)
.event(this, eventThread, TargetEventType.STEP_COMPLETED,
"Test thread completed a step", List.of());
listeners.fire.event(this, eventThread, TargetEventType.STEP_COMPLETED,
"Test thread completed a step", List.of());
eventThread.setState(TargetExecutionState.STOPPED);
}

View file

@ -58,8 +58,5 @@ public class TestTargetThread
Delta<?, ?> delta = changeAttributes(List.of(), List.of(), Map.of(
STATE_ATTRIBUTE_NAME, state //
), "Changed state");
if (delta.added.containsKey(STATE_ATTRIBUTE_NAME)) {
listeners.fire(TargetExecutionStateListener.class).executionStateChanged(this, state);
}
}
}

View file

@ -25,6 +25,7 @@ import org.junit.Test;
import ghidra.dbg.agent.*;
import ghidra.dbg.target.*;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
public class AnnotatedTargetObjectSchemaTest {
@ -40,8 +41,6 @@ public class AnnotatedTargetObjectSchemaTest {
EnumerableTargetObjectSchema.STRING.getName(), false, false, true), null);
builder.addAttributeSchema(new DefaultAttributeSchema("_kind",
EnumerableTargetObjectSchema.STRING.getName(), false, true, true), null);
builder.addAttributeSchema(new DefaultAttributeSchema("_update_mode",
EnumerableTargetObjectSchema.UPDATE_MODE.getName(), false, false, true), null);
builder.addAttributeSchema(new DefaultAttributeSchema("_order",
EnumerableTargetObjectSchema.INT.getName(), false, false, true), null);
builder.addAttributeSchema(new DefaultAttributeSchema("_modified",
@ -235,6 +234,29 @@ public class AnnotatedTargetObjectSchemaTest {
assertEquals("TestAnnotatedTargetRootWithListedAttrs", schema.getName().toString());
}
@TargetObjectSchemaInfo(elementResync = ResyncMode.ONCE, attributeResync = ResyncMode.ALWAYS)
static class TestAnnotatedTargetRootWithResyncModes extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithResyncModes(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
}
@Test
public void testAnnotatedRootWithResyuncModes() {
AnnotatedSchemaContext ctx = new AnnotatedSchemaContext();
TargetObjectSchema schema =
ctx.getSchemaForClass(TestAnnotatedTargetRootWithResyncModes.class);
TargetObjectSchema exp = addBasicAttributes(ctx.builder(schema.getName()))
.addInterface(TargetAggregate.class)
.setElementResyncMode(ResyncMode.ONCE)
.setAttributeResyncMode(ResyncMode.ALWAYS)
.build();
assertEquals(exp, schema);
}
static class NotAPrimitive {
}

View file

@ -27,7 +27,6 @@ import org.junit.Test;
import ghidra.dbg.agent.*;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetObject.TargetUpdateMode;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
@ -268,8 +267,6 @@ public class TargetObjectSchemaValidationTest {
ValidatedObject createRepleteValidatedObject() {
TargetObjectSchema schema = ctx.builder(new SchemaName("test"))
.addAttributeSchema(new DefaultAttributeSchema("_update_mode",
EnumerableTargetObjectSchema.UPDATE_MODE.getName(), true, false, false), null)
.addAttributeSchema(new DefaultAttributeSchema("_display",
EnumerableTargetObjectSchema.STRING.getName(), true, false, false), null)
.addAttributeSchema(new DefaultAttributeSchema("int",
@ -287,12 +284,10 @@ public class TargetObjectSchemaValidationTest {
ValidatedObject obj = createRepleteValidatedObject();
obj.setAttributes(List.of(), Map.of(
"_display", "Hello",
"_update_mode", TargetUpdateMode.SOLICITED,
"int", 5),
"Test");
obj.setAttributes(List.of(), Map.of(
"_display", "World",
"_update_mode", TargetUpdateMode.FIXED,
"int", 6),
"Test");
}
@ -309,7 +304,6 @@ public class TargetObjectSchemaValidationTest {
ValidatedObject obj = createRepleteValidatedObject();
obj.setAttributes(List.of(), Map.of(
"_display", "World",
"_update_mode", TargetUpdateMode.UNSOLICITED,
"int", 7.0),
"Test");
}

View file

@ -25,13 +25,12 @@ import org.junit.Test;
import ghidra.dbg.target.TargetInterpreter;
import ghidra.dbg.target.TargetProcess;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
import ghidra.dbg.target.schema.TargetObjectSchema.*;
public class XmlTargetObjectSchemaTest {
protected static final String SCHEMA_XML = "" +
"<context>\n" +
" <schema name=\"root\" canonical=\"yes\">\n" +
" <schema name=\"root\" canonical=\"yes\" elementResync=\"NEVER\" attributeResync=\"ONCE\">\n" +
" <interface name=\"Process\" />\n" +
" <interface name=\"Interpreter\" />\n" +
" <element index=\"reserved\" schema=\"VOID\" />\n" +
@ -40,7 +39,7 @@ public class XmlTargetObjectSchemaTest {
" <attribute name=\"some_object\" schema=\"OBJECT\" required=\"yes\" fixed=\"yes\" hidden=\"yes\" />\n" +
" <attribute schema=\"ANY\" hidden=\"yes\" />\n" +
" </schema>\n" +
" <schema name=\"down1\">\n" +
" <schema name=\"down1\" elementResync=\"ALWAYS\" attributeResync=\"ALWAYS\">\n" +
" <element schema=\"OBJECT\" />\n" +
" <attribute schema=\"VOID\" fixed=\"yes\" hidden=\"yes\" />\n" +
" </schema>\n" +
@ -55,13 +54,17 @@ public class XmlTargetObjectSchemaTest {
.setCanonicalContainer(true)
.addElementSchema("reserved", EnumerableTargetObjectSchema.VOID.getName(), null)
.addElementSchema("", NAME_DOWN1, null)
.setElementResyncMode(ResyncMode.NEVER)
.addAttributeSchema(new DefaultAttributeSchema("some_int",
EnumerableTargetObjectSchema.INT.getName(), false, false, false), null)
.addAttributeSchema(new DefaultAttributeSchema("some_object",
EnumerableTargetObjectSchema.OBJECT.getName(), true, true, true), null)
.setAttributeResyncMode(ResyncMode.ONCE)
.buildAndAdd();
protected static final TargetObjectSchema SCHEMA_DOWN1 = CTX.builder(NAME_DOWN1)
.setElementResyncMode(ResyncMode.ALWAYS)
.setDefaultAttributeSchema(AttributeSchema.DEFAULT_VOID)
.setAttributeResyncMode(ResyncMode.ALWAYS)
.buildAndAdd();
@Test

View file

@ -0,0 +1,309 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import ghidra.dbg.AnnotatedDebuggerAttributeListener;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
import ghidra.dbg.agent.DefaultTargetModelRoot;
import ghidra.dbg.error.DebuggerIllegalArgumentException;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
import ghidra.dbg.testutil.ElementTrackingListener;
import ghidra.program.model.address.AddressFactory;
public abstract class AbstractDebuggerModelAttacherTest extends AbstractDebuggerModelTest
implements RequiresAttachSpecimen {
public List<String> getExpectedAttacherPath() {
return null;
}
public List<String> getExpectedAttachableContainerPath() {
return null;
}
public List<String> getExpectedProcessesContainerPath() {
return null;
}
public abstract TargetParameterMap getExpectedAttachParameters();
public abstract void assertEnvironment(TargetEnvironment environment);
@Test
public void testAttacherIsWhereExpected() throws Throwable {
List<String> expectedAttacherPath = getExpectedAttacherPath();
assumeNotNull(expectedAttacherPath);
m.build();
TargetAttacher attacher = findAttacher();
assertEquals(expectedAttacherPath, attacher.getPath());
}
@Test
public void testProcessContainerIsWhereExpected() throws Throwable {
List<String> expectedProcessContainerPath = getExpectedProcessesContainerPath();
assumeNotNull(expectedProcessContainerPath);
m.build();
TargetObject container = findProcessContainer();
assertEquals(expectedProcessContainerPath, container.getPath());
}
@Test
public void testAttachableContainerIsWhereExpected() throws Throwable {
List<String> expectedAttachableContainerPath = getExpectedAttachableContainerPath();
assumeNotNull(expectedAttachableContainerPath);
m.build();
TargetObject container = findAttachableContainer();
assertEquals(expectedAttachableContainerPath, container.getPath());
}
protected void runTestListAttachable(TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
waitAcc(container);
Collection<TargetAttachable> attachables = fetchAttachables(container);
assertNotNull(getAttachable(attachables, specimen, dummy, this));
}
@Test
public void testListAttachable() throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assumeTrue(m.hasAttachableContainer());
m.build();
dummy = specimen.runDummy();
TargetObject container = findAttachableContainer();
runTestListAttachable(container);
}
// TODO: Attacher parameters, when we go that way.
protected void runTestAttachByPid(TargetAttacher attacher) throws Throwable {
waitAcc(attacher);
waitOn(attacher.attach(dummy.pid));
}
@Test
public void testAttachByPid() throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
m.build();
dummy = specimen.runDummy();
var listener = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) {
CompletableFuture<Void> observedCreated = new CompletableFuture<>();
@AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)
public void stateChanged(TargetObject object, TargetExecutionState state) {
// We're only expecting one process, so this should be fine
TargetProcess process = DebugModelConventions.liveProcessOrNull(object);
if (process == null) {
return;
}
try {
TargetEnvironment env = findEnvironment(process.getPath());
assertEnvironment(env);
observedCreated.complete(null);
}
catch (Throwable e) {
observedCreated.completeExceptionally(e);
}
}
};
// NB. I've intentionally omitted the reorderer here. The model should get it right.
m.getModel().addModelListener(listener);
TargetAttacher attacher = m.find(TargetAttacher.class, List.of());
runTestAttachByPid(attacher);
waitOn(listener.observedCreated);
}
protected void runTestAttachByObj(TargetAttacher attacher, TargetObject container)
throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
Collection<TargetAttachable> attachables = fetchAttachables(container);
TargetAttachable target = getAttachable(attachables, specimen, dummy, this);
waitAcc(attacher);
waitOn(attacher.attach(target));
}
@Test
public void testAttachByObj() throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assumeTrue(m.hasAttachableContainer());
m.build();
dummy = specimen.runDummy();
TargetAttacher attacher = findAttacher();
TargetObject container = findAttachableContainer();
runTestAttachByObj(attacher, container);
}
protected static class BogusObjectModel extends AbstractDebuggerObjectModel {
@Override
public AddressFactory getAddressFactory() {
return null;
}
}
protected static class BogusTargetAttachable extends DefaultTargetModelRoot
implements TargetAttachable {
public BogusTargetAttachable(AbstractDebuggerObjectModel model) {
super(model, "Bogus");
}
}
protected void runTestAttachByObjBogusThrowsException(TargetAttacher attacher)
throws Throwable {
waitAcc(attacher);
BogusObjectModel bogusModel = new BogusObjectModel();
TargetAttachable bogusAttachable = new BogusTargetAttachable(bogusModel);
waitOn(attacher.attach(bogusAttachable));
}
@Test(expected = DebuggerIllegalArgumentException.class)
public void testAttachByObjBogusThrowsException() throws Throwable {
m.build();
TargetAttacher attacher = m.find(TargetAttacher.class, List.of());
runTestAttachByObjBogusThrowsException(attacher);
}
protected void runTestAttachByPidThenDetach(TargetAttacher attacher, TargetObject container)
throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assertNull(getProcessRunning(container, specimen, this));
runTestAttachByPid(attacher);
runTestDetach(container, specimen);
assertTrue(dummy.process.isAlive());
}
@Test
public void testAttachByPidThenDetach() throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assumeTrue(m.hasDetachableProcesses());
m.build();
dummy = specimen.runDummy();
TargetAttacher attacher = findAttacher();
TargetObject container = findProcessContainer();
runTestAttachByPidThenDetach(attacher, container);
}
protected void runTestAttachByPidThenKill(TargetAttacher attacher, TargetObject container)
throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assertNull(getProcessRunning(container, specimen, this));
runTestAttachByPid(attacher);
runTestKill(container, specimen);
retryVoid(() -> assertFalse(dummy.process.isAlive()), List.of(AssertionError.class));
}
@Test
public void testAttachByPidThenKill() throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assumeTrue(m.hasKillableProcesses());
m.build();
dummy = specimen.runDummy();
TargetAttacher attacher = findAttacher();
TargetObject container = findProcessContainer();
runTestAttachByPidThenKill(attacher, container);
}
protected void runTestAttachByPidThenResumeInterrupt(TargetAttacher attacher,
TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assertNull(getProcessRunning(container, specimen, this));
runTestAttachByPid(attacher);
runTestResumeInterruptMany(container, specimen, 3);
assertTrue(dummy.process.isAlive());
}
@Test
public void testAttachByPidThenResumeInterrupt() throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assumeTrue(m.hasResumableProcesses());
m.build();
dummy = specimen.runDummy();
TargetAttacher attacher = findAttacher();
TargetObject container = findProcessContainer();
runTestAttachByPidThenResumeInterrupt(attacher, container);
}
protected void runTestAttachShowsInProcessContainer(TargetAttacher attacher,
TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assertNull(getProcessRunning(container, specimen, this));
runTestAttachByPid(attacher);
retryForProcessRunning(container, specimen, this);
}
@Test
public void testAttachShowsInProcessContainer() throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assumeTrue(m.hasProcessContainer());
m.build();
dummy = specimen.runDummy();
TargetAttacher attacher = findAttacher();
TargetObject container = findProcessContainer();
runTestAttachShowsInProcessContainer(attacher, container);
}
protected void runTestAttachShowsInProcessContainerViaListener(TargetAttacher attacher,
TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
ElementTrackingListener<? extends TargetProcess> procListener =
new ElementTrackingListener<>(TargetProcess.class);
container.addListener(procListener);
// NB. Have to express interest, otherwise model is not obligated to invoke listener
Collection<TargetProcess> procsBefore = fetchProcesses(container);
procListener.putAll(container.getCachedElements());
assertNull(getProcessRunning(procsBefore, specimen, this));
runTestAttachByPid(attacher);
retryVoid(() -> {
// Cannot fetch elements. rely only on listener.
assertNotNull(getProcessRunning(procListener.elements.values(), specimen, this));
}, List.of(AssertionError.class));
}
@Test
public void testAttachShowsInProcessContainerViaListener() throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assumeTrue(m.hasProcessContainer());
m.build();
dummy = specimen.runDummy();
TargetAttacher attacher = findAttacher();
TargetObject container = findProcessContainer();
runTestAttachShowsInProcessContainerViaListener(attacher, container);
}
}

View file

@ -0,0 +1,358 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.junit.Test;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.util.DebuggerCallbackReorderer;
import ghidra.program.model.address.AddressRange;
import ghidra.program.model.address.AddressRangeImpl;
import ghidra.util.Msg;
/**
* Tests the functionality of breakpoints
*
* <p>
* Note that this test does not check for nuances regarding specification vs.
* location, as it is meant to generalize across models for interests of the UI
* only. As such, we only test that we can set breakpoints at given addresses
* and that a location manifests there, regardless of the intervening
* mechanisms. We also test some basic operations on the breakpoint (location)
* itself. Models which have separate specifications from locations, or for
* which you want to test non-address specifications will need to add their own
* tests, tailored to the semantics of that model's breakpoint specifications.
*
* <p>
* TODO: Enable, disable (if supported), delete (if supported), manipulation via
* CLI is synced
*/
public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebuggerModelTest
implements RequiresTarget {
/**
* Get the expected (absolute) path of the target's breakpoint container
*
* @param targetPath the path of the target
* @return the expected path, or {@code null} for no assertion
*/
public List<String> getExpectedBreakpointContainerPath(List<String> targetPath) {
return null;
}
public abstract TargetBreakpointKindSet getExpectedSupportedKinds();
public abstract AddressRange getSuitableRangeForBreakpoint(TargetObject target,
TargetBreakpointKind kind) throws Throwable;
public boolean isSupportsTogglableLocations() {
return false;
}
public boolean isSupportsDeletableLocations() {
return false;
}
@Test
public void testBreakpointContainerIsWhereExpected() throws Throwable {
m.build();
TargetObject target = obtainTarget();
List<String> expectedBreakpointContainerPath =
getExpectedBreakpointContainerPath(target.getPath());
assumeNotNull(expectedBreakpointContainerPath);
TargetBreakpointSpecContainer container =
m.suitable(TargetBreakpointSpecContainer.class, target.getPath());
assertEquals(expectedBreakpointContainerPath, container.getPath());
}
@Test
public void testBreakpointContainerSupportsExpectedKinds() throws Throwable {
m.build();
TargetObject target = obtainTarget();
TargetBreakpointSpecContainer container =
m.suitable(TargetBreakpointSpecContainer.class, target.getPath());
waitOn(container.fetchAttributes());
assertEquals(getExpectedSupportedKinds(), container.getSupportedBreakpointKinds());
}
@Test
public void testBreakpointsSupportTogglableAsExpected() throws Throwable {
m.build();
for (TargetObjectSchema schema : m.getModel()
.getRootSchema()
.getContext()
.getAllSchemas()) {
Set<Class<? extends TargetObject>> ifs = schema.getInterfaces();
if (ifs.contains(TargetBreakpointLocation.class)) {
boolean supportsTogglableLocations = ifs.contains(TargetTogglable.class) &&
!ifs.contains(TargetBreakpointSpec.class);
assertEquals(isSupportsTogglableLocations(), supportsTogglableLocations);
}
}
}
@Test
public void testBreakpointLocationsSupportDeletableAsExpected() throws Throwable {
m.build();
for (TargetObjectSchema schema : m.getModel()
.getRootSchema()
.getContext()
.getAllSchemas()) {
Set<Class<? extends TargetObject>> ifs = schema.getInterfaces();
if (ifs.contains(TargetBreakpointLocation.class)) {
boolean supportsDeletableLocations = ifs.contains(TargetDeletable.class) &&
!ifs.contains(TargetBreakpointSpec.class);
assertEquals(isSupportsDeletableLocations(), supportsDeletableLocations);
}
}
}
protected TargetBreakpointLocation assertAtLeastOneLocCovers(
Collection<? extends TargetBreakpointLocation> locs, AddressRange range,
TargetBreakpointKind kind) throws Throwable {
for (TargetBreakpointLocation l : locs) {
TargetBreakpointSpec spec = l.getSpecification();
if (spec == null) { // Mid construction?
continue;
}
if (l.getAddress() == null || l.getLength() == null) {
continue;
}
AddressRange actualRange = new AddressRangeImpl(l.getAddress(), l.getLength());
if (!actualRange.contains(range.getMinAddress()) ||
!actualRange.contains(range.getMaxAddress())) {
continue;
}
if (spec.getKinds() == null) {
continue;
}
if (!spec.getKinds().contains(kind)) {
continue;
}
return l;
}
fail("No location covers expected breakpoint");
return null;
}
protected void runTestPlaceBreakpoint(TargetBreakpointKind kind) throws Throwable {
assumeTrue(getExpectedSupportedKinds().contains(kind));
m.build();
var monitor = new DebuggerModelListener() {
DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this);
@Override
public void created(TargetObject object) {
if (!object.getJoinedPath(".").contains("reak")) {
return;
}
Msg.debug(this, "CREATED " + object.getJoinedPath("."));
}
protected String logDisp(Object val) {
if (val == null) {
return "<null>"; // Should never happen
}
if (val instanceof TargetObject) {
TargetObject obj = (TargetObject) val;
return "obj-" + obj.getJoinedPath(".");
}
return val.toString();
}
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
if (!object.getJoinedPath(".").contains("reak")) {
return;
}
Msg.debug(this,
"ATTRIBUTES: object=" + object.getJoinedPath(".") + ",removed=" + removed);
for (Entry<String, ?> ent : added.entrySet()) {
Msg.debug(this,
" ATTR_added: " + ent.getKey() + "=" + logDisp(ent.getValue()));
}
}
@Override
public void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
if (!object.getJoinedPath(".").contains("reak")) {
return;
}
Msg.debug(this,
"ELEMENTS: object=" + object.getJoinedPath(".") + ",removed=" + removed);
for (Entry<String, ?> ent : added.entrySet()) {
Msg.debug(this,
" ELEM_added: " + ent.getKey() + "=" + logDisp(ent.getValue()));
}
}
};
m.getModel().addModelListener(monitor.reorderer, true);
TargetObject target = obtainTarget();
TargetBreakpointSpecContainer container = findBreakpointSpecContainer(target.getPath());
AddressRange range = getSuitableRangeForBreakpoint(target, kind);
waitOn(container.placeBreakpoint(range, Set.of(kind)));
retryVoid(() -> {
Collection<? extends TargetBreakpointLocation> found =
m.findAll(TargetBreakpointLocation.class, target.getPath()).values();
assertAtLeastOneLocCovers(found, range, kind);
}, List.of(AssertionError.class));
}
@Test
public void testPlaceSoftwareBreakpoint() throws Throwable {
runTestPlaceBreakpoint(TargetBreakpointKind.SW_EXECUTE);
}
@Test
public void testPlaceHardwareBreakpoint() throws Throwable {
runTestPlaceBreakpoint(TargetBreakpointKind.HW_EXECUTE);
}
@Test
public void testPlaceReadBreakpoint() throws Throwable {
runTestPlaceBreakpoint(TargetBreakpointKind.READ);
}
@Test
public void testPlaceWriteBreakpoint() throws Throwable {
runTestPlaceBreakpoint(TargetBreakpointKind.WRITE);
}
protected Set<TargetBreakpointLocation> createLocations() throws Throwable {
// TODO: Test with multiple targets?
TargetObject target = obtainTarget();
TargetBreakpointSpecContainer container = findBreakpointSpecContainer(target.getPath());
assertNotNull("No breakpoint spec container", container);
Set<TargetBreakpointLocation> locs = new HashSet<>();
for (TargetBreakpointKind kind : getExpectedSupportedKinds()) {
AddressRange range = getSuitableRangeForBreakpoint(target, kind);
waitOn(container.placeBreakpoint(range, Set.of(kind)));
locs.add(retry(() -> {
Collection<? extends TargetBreakpointLocation> found =
m.findAll(TargetBreakpointLocation.class, target.getPath()).values();
return assertAtLeastOneLocCovers(found, range, kind);
}, List.of(AssertionError.class)));
}
Msg.debug(this, "Have locations: " +
locs.stream().map(l -> l.getJoinedPath(".")).collect(Collectors.toSet()));
return locs;
}
protected void runToggleTest(Set<TargetTogglable> set) throws Throwable {
List<TargetTogglable> order = new ArrayList<>(set);
Collections.shuffle(order);
// Disable each
for (TargetTogglable t : order) {
waitOn(t.disable());
retryVoid(() -> {
assertFalse(t.isEnabled());
}, List.of(AssertionError.class));
}
// Repeat it for fun. Should have no effect
for (TargetTogglable t : order) {
waitOn(t.disable());
retryVoid(() -> {
assertFalse(t.isEnabled());
}, List.of(AssertionError.class));
}
// Enable each
for (TargetTogglable t : order) {
waitOn(t.enable());
retryVoid(() -> {
assertTrue(t.isEnabled());
}, List.of(AssertionError.class));
}
// Repeat it for fun. Should have no effect
for (TargetTogglable t : order) {
waitOn(t.enable());
retryVoid(() -> {
assertTrue(t.isEnabled());
}, List.of(AssertionError.class));
}
}
@Test
public void testToggleBreakpoints() throws Throwable {
m.build();
Set<TargetBreakpointLocation> locs = createLocations();
runToggleTest(locs.stream()
.map(l -> l.getSpecification().as(TargetTogglable.class))
.collect(Collectors.toSet()));
}
@Test
public void testToggleBreakpointLocations() throws Throwable {
assumeTrue(isSupportsTogglableLocations());
m.build();
Set<TargetBreakpointLocation> locs = createLocations();
runToggleTest(
locs.stream().map(l -> l.as(TargetTogglable.class)).collect(Collectors.toSet()));
}
protected void runDeleteTest(Set<TargetDeletable> set) throws Throwable {
List<TargetDeletable> order = new ArrayList<>(set);
Collections.shuffle(order);
// Disable each
for (TargetDeletable d : order) {
waitOn(d.delete());
retryVoid(() -> {
assertFalse(d.isValid());
}, List.of(AssertionError.class));
}
}
@Test
public void testDeleteBreakpoints() throws Throwable {
m.build();
Set<TargetBreakpointLocation> locs = createLocations();
runDeleteTest(locs.stream()
.map(l -> l.getSpecification().as(TargetDeletable.class))
.collect(Collectors.toSet()));
}
@Test
public void testDeleteBreakpointLocations() throws Throwable {
assumeTrue(isSupportsDeletableLocations());
m.build();
Set<TargetBreakpointLocation> locs = createLocations();
runDeleteTest(
locs.stream().map(l -> l.as(TargetDeletable.class)).collect(Collectors.toSet()));
}
}

View file

@ -0,0 +1,28 @@
/* ###
* 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.dbg.test;
import java.util.Map;
import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen;
public abstract class AbstractDebuggerModelEnvironmentTest {
protected void doTestLaunchEnvironment(DebuggerTestSpecimen specimen,
Map<String, String> expectedEnvironment) {
// TODO: Check that the environment is as expected before PROCESS_CREATED is emitted
// For models without event scope, before TargetProcess gets added.
}
}

View file

@ -0,0 +1,93 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import ghidra.dbg.DebugModelConventions.AsyncAccess;
import ghidra.dbg.error.DebuggerModelTerminatingException;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.util.Msg;
public abstract class AbstractDebuggerModelFactoryTest extends AbstractDebuggerModelTest {
protected abstract Map<String, Object> getFailingFactoryOptions();
@Test
public void testBuildAndClose() throws Throwable {
m.build();
assertNotNull(m.getModel());
}
@Test
public void testBuildFailingOptionsErr() throws Throwable {
for (Map.Entry<String, Object> bad : getFailingFactoryOptions().entrySet()) {
Map<String, Object> options = new HashMap<>(m.getFactoryOptions());
options.put(bad.getKey(), bad.getValue());
try {
m.buildModel(options);
fail();
}
catch (Exception ex) {
if (!DebuggerModelTerminatingException.isIgnorable(ex)) {
throw ex;
}
// Pass
}
}
}
@Test
public void testPing() throws Throwable {
m.build();
waitOn(m.getModel().ping("Hello, Ghidra Async Debugging!"));
}
@Test
public void testWaitRootAccess() throws Throwable {
m.build();
TargetObject root = m.getRoot();
AsyncAccess access = access(root);
waitAcc(access);
}
@Test
public void testHasNonEnumerableRootSchema() throws Throwable {
m.build();
TargetObjectSchema rootSchema = m.getModel().getRootSchema();
Msg.info(this, rootSchema.getContext());
assertFalse(rootSchema instanceof EnumerableTargetObjectSchema);
}
@Test
public void testNonExistentPathGivesNull() throws Throwable {
m.build();
TargetObject root = m.getRoot();
waitAcc(root);
TargetObject noExist = waitOn(root.fetchSuccessor(m.getBogusPath()));
assertNull(noExist);
}
}

View file

@ -0,0 +1,115 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import ghidra.dbg.target.TargetFocusScope;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.util.PathUtils;
public abstract class AbstractDebuggerModelFocusTest extends AbstractDebuggerModelTest {
/**
* Get (possibly generate) things for this focus test to try out
*
* @throws Throwable if anything goes wrong
*/
protected abstract Set<TargetObject> getFocusableThings() throws Throwable;
/**
* Governs whether assertions permit the actual object to be a successor of the expected object
*
* @return true to permit successors, false to require exact
*/
protected boolean permitSuccessor() {
return true;
}
protected void assertSuccessorOrExact(TargetObject expected, TargetObject actual) {
assertNotNull(actual);
if (permitSuccessor()) {
assertTrue("Expected successor of '" + expected.getJoinedPath(".") +
"' got '" + actual.getJoinedPath(".") + "'",
PathUtils.isAncestor(expected.getPath(), actual.getPath()));
}
else {
assertSame(expected, actual);
}
}
/**
* If the default focus is one of the focusable things (after generation), assert its path
*
* @return the path of the expected default focus, or {@code null} for no assertion
*/
protected List<String> getExpectedDefaultFocus() {
return null;
}
@Test
public void testDefaultFocusIsAsExpected() throws Throwable {
List<String> expectedDefaultFocus = getExpectedDefaultFocus();
assumeNotNull(expectedDefaultFocus);
m.build();
TargetFocusScope scope = findFocusScope();
Set<TargetObject> focusable = getFocusableThings();
// The default must be one of the focusable objects
assertTrue(focusable.stream().anyMatch(f -> f.getPath().equals(expectedDefaultFocus)));
retryVoid(() -> {
assertEquals(expectedDefaultFocus, scope.getFocus().getPath());
}, List.of(AssertionError.class));
}
@Test
public void testFocusEachOnce() throws Throwable {
m.build();
TargetFocusScope scope = findFocusScope();
Set<TargetObject> focusable = getFocusableThings();
for (TargetObject obj : focusable) {
waitOn(scope.requestFocus(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, scope.getFocus());
}, List.of(AssertionError.class));
}
}
@Test
public void testFocusEachTwice() throws Throwable {
m.build();
TargetFocusScope scope = findFocusScope();
Set<TargetObject> focusable = getFocusableThings();
for (TargetObject obj : focusable) {
waitOn(scope.requestFocus(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, scope.getFocus());
}, List.of(AssertionError.class));
waitOn(scope.requestFocus(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, scope.getFocus());
}, List.of(AssertionError.class));
}
}
}

View file

@ -0,0 +1,196 @@
/* ###
* 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.dbg.test;
import static ghidra.lifecycle.Unfinished.*;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Ignore;
import org.junit.Test;
import ghidra.async.AsyncReference;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.error.DebuggerModelTerminatingException;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetInterpreter;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.testutil.CatchOffThread;
import ghidra.util.Msg;
public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebuggerModelTest
implements RequiresAttachSpecimen, RequiresLaunchSpecimen {
public List<String> getExpectedInterpreterPath() {
return null;
}
protected abstract String getEchoCommand(String msg);
protected abstract String getQuitCommand();
/**
* Get the CLI command to attach to the {@link #dummy} process
*
* @return the command
*/
protected abstract String getAttachCommand();
@Test
public void testInterpreterIsWhereExpected() throws Throwable {
List<String> expectedInterpreterPath = getExpectedInterpreterPath();
assumeNotNull(expectedInterpreterPath);
m.build();
TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of());
assertEquals(expectedInterpreterPath, interpreter.getPath());
}
protected void runTestExecute(TargetInterpreter interpreter, String cmd) throws Throwable {
AsyncReference<String, Void> lastOut = new AsyncReference<>();
DebuggerModelListener l = new DebuggerModelListener() {
@Override
public void consoleOutput(TargetObject interpreter, Channel channel, byte[] out) {
String str = new String(out);
Msg.debug(this, "Got " + channel + " output: " + str);
for (String line : str.split("\n")) {
lastOut.set(line.trim(), null);
}
}
};
interpreter.addListener(l);
waitAcc(interpreter);
waitOn(interpreter.execute(cmd));
waitOn(lastOut.waitValue("test"));
}
@Test
public void testExecute() throws Throwable {
String cmd = getEchoCommand("test");
assumeNotNull(cmd);
m.build();
TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of());
runTestExecute(interpreter, cmd);
}
protected void runTestExecuteCapture(TargetInterpreter interpreter, String cmd)
throws Throwable {
waitAcc(interpreter);
try (CatchOffThread off = new CatchOffThread()) {
DebuggerModelListener l = new DebuggerModelListener() {
@Override
public void consoleOutput(TargetObject interpreter, Channel channel, byte[] out) {
String str = new String(out);
Msg.debug(this, "Got " + channel + " output: " + str);
if (!str.contains("test")) {
return;
}
off.catching(() -> fail("Unexpected output:" + str));
}
};
interpreter.addListener(l);
waitAcc(interpreter);
String out = waitOn(interpreter.executeCapture(cmd));
// Not the greatest, but allow extra lines
List<String> lines =
Stream.of(out.split("\n")).map(s -> s.trim()).collect(Collectors.toList());
assertTrue(lines.contains("test"));
}
}
@Test
public void testExecuteCapture() throws Throwable {
String cmd = getEchoCommand("test");
assumeNotNull(cmd);
m.build();
TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of());
runTestExecuteCapture(interpreter, cmd);
}
@Test(expected = DebuggerModelTerminatingException.class)
public void testExecuteQuit() throws Throwable {
String cmd = getQuitCommand();
assumeNotNull(cmd);
m.build();
TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of());
runTestExecute(interpreter, cmd);
}
@Test
@Ignore
public void testFocusIsSynced() throws Throwable {
TODO();
}
@Test
@Ignore
public void testBreakpointsAreSynced() throws Throwable {
TODO();
// TODO: Place different kinds
// TODO: Enable/disable
// TODO: Delete (spec vs. loc?)
}
protected void runTestLaunchViaInterpreterShowsInProcessContainer(TargetInterpreter interpreter,
TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getLaunchSpecimen();
assertNull(getProcessRunning(container, specimen, this));
for (String line : specimen.getLaunchScript()) {
waitOn(interpreter.execute(line));
}
retryForProcessRunning(container, specimen, this);
}
@Test
public void testLaunchViaInterpreterShowsInProcessContainer() throws Throwable {
assumeTrue(m.hasProcessContainer());
m.build();
TargetInterpreter interpreter = findInterpreter();
TargetObject container = findProcessContainer();
assertNotNull("No process container", container);
runTestLaunchViaInterpreterShowsInProcessContainer(interpreter, container);
}
protected void runTestAttachViaInterpreterShowsInProcessContainer(TargetInterpreter interpreter,
TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assertNull(getProcessRunning(container, specimen, this));
String cmd = getAttachCommand();
waitOn(interpreter.execute(cmd));
retryForProcessRunning(container, specimen, this);
}
@Test
public void testAttachViaInterpreterShowsInProcessContainer() throws Throwable {
DebuggerTestSpecimen specimen = getAttachSpecimen();
assumeTrue(m.hasProcessContainer());
m.build();
dummy = specimen.runDummy();
TargetInterpreter interpreter = findInterpreter();
TargetObject container = findProcessContainer();
assertNotNull("No process container", container);
runTestAttachViaInterpreterShowsInProcessContainer(interpreter, container);
}
}

View file

@ -0,0 +1,224 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import ghidra.dbg.AnnotatedDebuggerAttributeListener;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
import ghidra.dbg.testutil.ElementTrackingListener;
public abstract class AbstractDebuggerModelLauncherTest extends AbstractDebuggerModelTest
implements RequiresLaunchSpecimen {
public List<String> getExpectedLauncherPath() {
return null;
}
public List<String> getExpectedProcessesContainerPath() {
return null;
}
public abstract TargetParameterMap getExpectedLauncherParameters();
public abstract void assertEnvironment(TargetEnvironment environment);
@Test
public void testLauncherIsWhereExpected() throws Throwable {
List<String> expectedLauncherPath = getExpectedLauncherPath();
assumeNotNull(expectedLauncherPath);
m.build();
TargetLauncher launcher = findLauncher();
assertEquals(expectedLauncherPath, launcher.getPath());
}
@Test
public void testProcessContainerIsWhereExpected() throws Throwable {
List<String> expectedProcessContainerPath = getExpectedProcessesContainerPath();
assumeNotNull(expectedProcessContainerPath);
m.build();
TargetObject container = findProcessContainer();
assertEquals(expectedProcessContainerPath, container.getPath());
}
protected void runTestLaunchParameters(TargetLauncher launcher,
TargetParameterMap expectedParameters) throws Throwable {
waitAcc(launcher);
waitOn(launcher.fetchAttributes());
assertEquals(expectedParameters, launcher.getParameters());
}
@Test
public void testLaunchParameters() throws Throwable {
TargetParameterMap expectedParameters = getExpectedLauncherParameters();
assumeNotNull(expectedParameters);
m.build();
TargetLauncher launcher = findLauncher();
runTestLaunchParameters(launcher, expectedParameters);
}
protected void runTestLaunch(TargetLauncher launcher) throws Throwable {
DebuggerTestSpecimen specimen = getLaunchSpecimen();
waitAcc(launcher);
waitOn(launcher.launch(specimen.getLauncherArgs()));
}
@Test
public void testLaunch() throws Throwable {
m.build();
var listener = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) {
CompletableFuture<Void> observedCreated = new CompletableFuture<>();
@AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)
public void stateChanged(TargetObject object, TargetExecutionState state) {
// We're only expecting one process, so this should be fine
TargetProcess process = DebugModelConventions.liveProcessOrNull(object);
if (process == null) {
return;
}
try {
TargetEnvironment env = findEnvironment(process.getPath());
assertEnvironment(env);
observedCreated.complete(null);
}
catch (Throwable e) {
observedCreated.completeExceptionally(e);
}
}
};
// NB. I've intentionally omitted the reorderer here. The model should get it right.
m.getModel().addModelListener(listener);
TargetLauncher launcher = findLauncher();
runTestLaunch(launcher);
waitOn(listener.observedCreated);
}
protected void runTestLaunchThenDetach(TargetLauncher launcher,
TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getLaunchSpecimen();
assertNull(getProcessRunning(container, specimen, this));
runTestLaunch(launcher);
runTestDetach(container, specimen);
}
@Test
public void testLaunchThenDetach() throws Throwable {
assumeTrue(m.hasDetachableProcesses());
m.build();
TargetLauncher launcher = findLauncher();
TargetObject container = findProcessContainer();
runTestLaunchThenDetach(launcher, container);
}
protected void runTestLaunchThenKill(TargetLauncher launcher,
TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getLaunchSpecimen();
assertNull(getProcessRunning(container, specimen, this));
runTestLaunch(launcher);
runTestKill(container, specimen);
}
@Test
public void testLaunchThenKill() throws Throwable {
assumeTrue(m.hasKillableProcesses());
m.build();
TargetLauncher launcher = findLauncher();
TargetObject container = findProcessContainer();
runTestLaunchThenKill(launcher, container);
}
protected void runTestLaunchThenResume(TargetLauncher launcher,
TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getLaunchSpecimen();
assertNull(getProcessRunning(container, specimen, this));
runTestLaunch(launcher);
runTestResumeTerminates(container, specimen);
}
@Test
public void testLaunchThenResume() throws Throwable {
assumeTrue(m.hasKillableProcesses());
m.build();
TargetLauncher launcher = findLauncher();
TargetObject container = findProcessContainer();
runTestLaunchThenResume(launcher, container);
}
protected void runTestLaunchShowsInProcessContainer(TargetLauncher launcher,
TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getLaunchSpecimen();
assertNull(getProcessRunning(container, specimen, this));
runTestLaunch(launcher);
retryForProcessRunning(container, specimen, this);
}
@Test
public void testLaunchShowsInProcessContainer() throws Throwable {
assumeTrue(m.hasProcessContainer());
m.build();
TargetLauncher launcher = findLauncher();
TargetObject container = findProcessContainer();
runTestLaunchShowsInProcessContainer(launcher, container);
}
protected void runTestLaunchShowsInProcessContainerViaListener(
TargetLauncher launcher, TargetObject container) throws Throwable {
DebuggerTestSpecimen specimen = getLaunchSpecimen();
ElementTrackingListener<? extends TargetProcess> procListener =
new ElementTrackingListener<>(TargetProcess.class);
container.addListener(procListener);
// NB. Have to express interest, otherwise model is not obligated to invoke listener
Collection<TargetProcess> procsBefore = fetchProcesses(container);
procListener.putAll(container.getCachedElements());
assertNull(getProcessRunning(procsBefore, specimen, this));
runTestLaunch(launcher);
retryVoid(() -> {
// Cannot fetch elements. rely only on listener.
assertNotNull(getProcessRunning(procListener.elements.values(), specimen, this));
}, List.of(AssertionError.class));
}
@Test
public void testLaunchShowsInProcessContainerViaListener() throws Throwable {
assumeTrue(m.hasProcessContainer());
m.build();
TargetLauncher launcher = findLauncher();
TargetObject container = findProcessContainer();
runTestLaunchShowsInProcessContainerViaListener(launcher, container);
}
}

View file

@ -0,0 +1,32 @@
/* ###
* 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.dbg.test;
/**
* TODO: We need more tests to verify that commands affect the specified thing.
* Some models seem to just use the "current," which is <em>usually</em> correct
* in practice because of focus syncing, but it may not be, esp., if the user is
* scripting. In particular, when it comes to actions on processes and threads:
*
* <ul>
* <li>Process.kill</li>
* <li>Process.detach</li>
* <li>Thread.step</li>
* </ul>
*/
public class AbstractDebuggerModelMultiprocessTest {
}

View file

@ -0,0 +1,225 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import java.math.BigInteger;
import java.util.*;
import java.util.Map.Entry;
import org.junit.Test;
import ghidra.dbg.target.*;
import ghidra.dbg.target.schema.TargetObjectSchema;
/**
* Tests the functionality of a register bank
*
* <p>
* Note that multiple sub-cases of this test can be generated to separate
* testing various types of registers. E.g., one for user registers, one for
* control registers, another for vector registers, etc. The model developer
* should be thorough, and decide how best to break the tests down, depending on
* the mechanism the model uses to read and/or write to each set. Even if
* different cases are not used, the total register set should exercise all the
* various register types.
*/
public abstract class AbstractDebuggerModelRegistersTest extends AbstractDebuggerModelTest
implements RequiresTarget {
/**
* Get the expected (absolute) path of the target's (inner-most) register
* bank
*
* @param threadPath the path of the target (usually a thread)
* @return the expected path, or {@code null} for no assertion
*/
public List<String> getExpectedRegisterBankPath(List<String> threadPath) {
return null;
}
/**
* This has been a popular convention, and may soon become required
*
* <p>
* Background: Technically, the descriptions (register container) can be
* higher up the model tree, e.g., to apply to an entire process, rather
* than to specific threads. Of course, this might imply all threads have
* the same set of registers. That assumption seems intuitive, but on some
* platforms, e.g., dbgeng with WoW64, some threads may only have the 32-bit
* registers available. Even then, a process could present two register
* containers, one for the 64-bit and one for the 32-bit registers,
* assigning the bank's {@link TargetRegisterBank#getDescriptions()}
* attribute accordingly.
*
* <p>
* However, none of that really matters if you choose the
* banks-are-containers convention. The primary motivation for doing this is
* to present register values as attributes in the tree. This makes them
* accessible from the "Objects" window of the UI, which is significant,
* because using the "Registers" window requires the target be recorded into
* a trace. Thus, if this test detects that the model's
* {@link TargetRegisterBank}s are also {@link TargetRegisterContainer}s,
* then this method must return true, and it will further verify the
* {@link TargetObject#getValue()} attribute of each register object
* correctly reports the same values as
* {@link TargetRegisterBank#readRegistersNamed(Collection)}. TODO:
* Currently the value is given as a string, encoding the value in base-16.
* I'd rather it were a byte array.
*
* @return true if the convention is expected and should be tested, false if
* not
*/
public boolean isRegisterBankAlsoContainer() {
return true;
}
/**
* Get the values to write to the registers
*
* <p>
* This collection is used for validation in other tests. The descriptions
* are validated to have lengths consistent with the written values, and the
* read values are expected to have lengths equal to the written values.
*
* @return the name-value map to write, and use for validation
*/
public abstract Map<String, byte[]> getRegisterWrites();
/**
* This various slightly from the usual find pattern, since we attempt to
* find any thread first
*
* @param seedPath the path to the target or thread
* @return the bank, or {@code null} if one cannot be uniquely identified in
* a thread, or the target
* @throws Throwable if anything goes wrong
*/
protected TargetRegisterBank findRegisterBank(List<String> seedPath) throws Throwable {
return m.findWithIndex(TargetRegisterBank.class, "0", seedPath);
}
@Test
public void testRegisterBankIsWhereExpected() throws Throwable {
m.build();
TargetObject target = maybeSubstituteThread(obtainTarget());
List<String> expectedRegisterBankPath =
getExpectedRegisterBankPath(target.getPath());
assumeNotNull(expectedRegisterBankPath);
TargetRegisterBank bank = findRegisterBank(target.getPath());
assertEquals(expectedRegisterBankPath, bank.getPath());
}
@Test
public void testBanksAreContainersConventionIsAsExpected() throws Throwable {
m.build();
boolean banksAreContainers = true;
for (TargetObjectSchema schema : m.getModel()
.getRootSchema()
.getContext()
.getAllSchemas()) {
if (schema.getInterfaces().contains(TargetRegisterBank.class)) {
banksAreContainers &=
schema.getInterfaces().contains(TargetRegisterContainer.class);
}
}
assertEquals(isRegisterBankAlsoContainer(), banksAreContainers);
}
@Test
public void testRegistersHaveExpectedSizes() throws Throwable {
m.build();
TargetObject target = maybeSubstituteThread(obtainTarget());
TargetRegisterBank bank = m.findWithIndex(TargetRegisterBank.class, "0", target.getPath());
TargetObject descriptions = bank.getDescriptions();
for (Entry<String, byte[]> ent : getRegisterWrites().entrySet()) {
String regName = ent.getKey();
TargetRegister reg =
m.findWithIndex(TargetRegister.class, regName, descriptions.getPath());
assertEquals(ent.getValue().length, (reg.getBitLength() + 7) / 8);
}
}
// TODO: Test cases for writing to non-existing registers (by name)
@Test
public void testReadRegisters() throws Throwable {
m.build();
TargetObject target = maybeSubstituteThread(obtainTarget());
TargetRegisterBank bank = m.findWithIndex(TargetRegisterBank.class, "0", target.getPath());
Map<String, byte[]> exp = getRegisterWrites();
Map<String, byte[]> read = waitOn(bank.readRegistersNamed(exp.keySet()));
assertEquals("Not all registers were read, or extras were read", exp.keySet(),
read.keySet());
// NB. The specimen is not expected to control the register values. Just validate lengths
for (String name : exp.keySet()) {
assertEquals(exp.get(name).length, read.get(name).length);
}
if (!isRegisterBankAlsoContainer()) {
return; // pass
}
for (String name : exp.keySet()) {
expectRegisterObjectValue(bank, name, read.get(name));
}
}
protected void expectRegisterObjectValue(TargetRegisterBank bank, String name, byte[] value)
throws Throwable {
retryVoid(() -> {
TargetRegister reg = m.findWithIndex(TargetRegister.class, name, bank.getPath());
assertNotNull(reg);
String actualHex = (String) reg.getValue();
assertNotNull(actualHex);
assertEquals(new BigInteger(1, value), new BigInteger(actualHex, 16));
}, List.of(AssertionError.class));
}
@Test
public void testWriteRegisters() throws Throwable {
m.build();
TargetObject target = maybeSubstituteThread(obtainTarget());
TargetRegisterBank bank = m.findWithIndex(TargetRegisterBank.class, "0", target.getPath());
Map<String, byte[]> write = getRegisterWrites();
waitOn(bank.writeRegistersNamed(write));
// NB. This only really tests the cache, if applicable. A scenario checks for efficacy.
Map<String, byte[]> read = waitOn(bank.readRegistersNamed(write.keySet()));
assertEquals("Not all registers were read, or extras were read", write.keySet(),
read.keySet());
for (String name : write.keySet()) {
assertArrayEquals(write.get(name), read.get(name));
}
if (!isRegisterBankAlsoContainer()) {
return; // pass
}
for (String name : write.keySet()) {
expectRegisterObjectValue(bank, name, read.get(name));
}
}
}

View file

@ -0,0 +1,158 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import java.lang.invoke.MethodHandles;
import java.util.*;
import org.junit.Test;
import ghidra.dbg.AnnotatedDebuggerAttributeListener;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebugModelConventions.AsyncState;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.util.Msg;
/**
* A scenario which tests a single process with two threads
*/
public abstract class AbstractDebuggerModelScenarioCloneExitTest extends AbstractDebuggerModelTest {
/**
* Time to wait to observe the child from the clone before resuming again
*/
public static final long WAIT_FOR_CHILD_MS = 1000;
/**
* This specimen must clone or similar, and then both parent and child must exit immediately
*
* <p>
* They may optionally print information, but they cannot spin, sleep, or otherwise hang around.
*
* @return the specimen
*/
protected abstract DebuggerTestSpecimen getSpecimen();
/**
* Perform whatever preparation is necessary to ensure the child will remain attached and be
* trapped upon its being clone from its parent
*
* <p>
* If this cannot be done without a handle to the parent process, override
* {@link #postLaunch(TargetObject, TargetProcess)} instead.
*
* @param launcher the launcher
* @throws Throwable if anything goes wrong
*/
protected void preLaunch(TargetLauncher launcher) throws Throwable {
}
/**
* Perform whatever preparation is necessary to ensure the child will remain attached and be
* trapped upon its being cloned from its parent
*
* <p>
* For most debuggers, no special setup is necessary.
*
* @param process the process trapped at launch -- typically at {@code main()}.
* @throws Throwable if anything goes wrong
*/
protected void postLaunch(TargetProcess process) throws Throwable {
}
/**
* Get a breakpoint expression that will trap both threads post-clone
*
* @return the expression
*/
protected abstract String getBreakpointExpression();
/**
* Test the following scenario:
*
* <ol>
* <li>Obtain a launcher and use it to start the specimen</li>
* <li>Place a breakpoint on the new process</li>
* <li>Resume the process until it is TERMINATED</li>
* <li>Verify exactly two unique threads were trapped by the breakpoint</li>
* </ol>
*
* <p>
* Note because some platforms, notably Windows, may produce additional threads before executing
* {@code main}, we cannot simply count THREAD_CREATED events. We mitigate this by using a
* breakpoint which should only trap threads executing user code. Note that we do not verify
* which thread is trapped first, since we do not control thread scheduling.
*/
@Test
public void testScenario() throws Throwable {
DebuggerTestSpecimen specimen = getSpecimen();
m.build();
List<TargetObject> trapped = new ArrayList<>();
var monitor = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) {
// For model developer diagnostics
@AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)
private void stateChanged(TargetObject obj, TargetExecutionState state) {
Msg.debug(this, obj.getJoinedPath(".") + " is now " + state);
}
@Override
public void breakpointHit(TargetObject container, TargetObject thread,
TargetStackFrame frame, TargetBreakpointSpec spec,
TargetBreakpointLocation breakpoint) {
Msg.debug(this, thread.getJoinedPath(".") + " trapped by " +
breakpoint.getJoinedPath(".") + " (" + spec.getExpression() + ")");
if (spec.getExpression().equals(getBreakpointExpression())) {
Msg.debug(this, " Counted");
trapped.add(thread);
}
}
};
m.getModel().addModelListener(monitor);
TargetLauncher launcher = findLauncher();
preLaunch(launcher);
Msg.debug(this, "Launching " + specimen);
waitOn(launcher.launch(specimen.getLauncherArgs()));
Msg.debug(this, " Done launching");
TargetObject processContainer = findProcessContainer();
assertNotNull("No process container", processContainer);
TargetProcess process = retryForProcessRunning(processContainer, specimen, this);
postLaunch(process);
TargetBreakpointSpecContainer breakpointContainer =
findBreakpointSpecContainer(process.getPath());
Msg.debug(this, "Placing breakpoint");
waitOn(breakpointContainer.placeBreakpoint(getBreakpointExpression(),
Set.of(TargetBreakpointKind.SW_EXECUTE)));
assertTrue(DebugModelConventions.isProcessAlive(process));
AsyncState state =
new AsyncState(m.suitable(TargetExecutionStateful.class, process.getPath()));
for (int i = 1; DebugModelConventions.isProcessAlive(process); i++) {
Msg.debug(this, "(" + i + ") Resuming process until terminated");
resume(process);
Msg.debug(this, " Done " + i);
waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING));
}
assertEquals(2, trapped.size());
assertEquals(2, Set.copyOf(trapped).size());
}
}

View file

@ -0,0 +1,258 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import ghidra.dbg.AnnotatedDebuggerAttributeListener;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebugModelConventions.AsyncState;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.util.Msg;
/**
* A scenario which tests multiple processes -- a child forked from a parent. The debugger must
* become attached to both. If the debugger does not support attaching to the child, while remaining
* attached to the parent as well, then this scenario cannot be applied to test it.
*/
public abstract class AbstractDebuggerModelScenarioForkExitTest extends AbstractDebuggerModelTest {
/**
* Time to wait to observe the child from the fork before resuming again
*/
public static final long WAIT_FOR_CHILD_MS = 1000;
/**
* This specimen must fork or similar, and then both parent and child must exit immediately
*
* <p>
* They may optionally print information, but they cannot spin, sleep, or otherwise hang around.
* For platforms without {@code fork()}, e.g., Windows, the nearest equivalent behavior should
* be performed, e.g., roughly {@code CreateProcess("SameSpecimen.exe /child")}.
*
* @return the specimen
*/
protected abstract DebuggerTestSpecimen getSpecimen();
/**
* Perform whatever preparation is necessary to ensure the child will remain attached and be
* trapped upon its being forked from its parent
*
* <p>
* If this cannot be done without a handle to the parent process, override
* {@link #postLaunch(TargetObject, TargetProcess)} instead.
*
* @param launcher the launcher
* @throws Throwable if anything goes wrong
*/
protected void preLaunch(TargetLauncher launcher) throws Throwable {
}
/**
* Perform whatever preparation is necessary to ensure the child will remain attached and be
* trapped upon its being forked from its parent
*
* @param parentProcess the parent process
* @throws Throwable if anything goes wrong
*/
protected void postLaunch(TargetProcess parentProcess) throws Throwable {
}
protected void postFork(TargetProcess parentProcess, TargetProcess childProcess)
throws Throwable {
}
/**
* Get a breakpoint expression that will trap the parent post-fork
*
* <p>
* Ideally, this same expression will trap the child post-fork as well. Note this test presumes
* the child will be trapped by the debugger upon fork. See
* {@link #getChildBreakpointExpression()}
*
* @return the expression
*/
protected abstract String getParentBreakpointExpression();
/**
* Get a breakpoint expression that will trap the child post-fork
*
* <p>
* If breakpoints are not passed from parent to child, the test will need to set a breakpoint to
* trap the child. Override this with a suitable expression, if needed.
*
* @return the expression
*/
protected String getChildBreakpointExpression() {
return null;
}
/**
* This is invoked for both the launch of the specimen and upon fork
*
* <p>
* Because one is forked from the other, we should expect to see the same environment attributes
* for both processes. That said, if a tester <em>does</em> need to distinguish, and please
* think carefully about whether or not you should, you can examine the environment's path to
* determine which process it applies to.
*
* @param environment the environment at the time the process became alive
*/
public abstract void assertEnvironment(TargetEnvironment environment);
/**
* Test the following scenario:
*
* <ol>
* <li>Obtain a launcher and use it to start the specimen</li>
* <li>Place a breakpoint on the new (parent) process</li>
* <li>Resume the process until the fork is observed, generating the child process</li>
* <li>Verify both processes are ALIVE</li>
* <li>Resume the parent process until it is TERMINATED</li>
* <li>Verify the child process is still ALIVE</li>
* <li>Resume the child process until it is TERMIANTED</li>
* </ol>
*/
@Test
public void testScenario() throws Throwable {
DebuggerTestSpecimen specimen = getSpecimen();
m.build();
var stateMonitor = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) {
Set<TargetProcess> observed = new HashSet<>();
CompletableFuture<TargetProcess> observedParent = new CompletableFuture<>();
CompletableFuture<TargetProcess> observedChild = new CompletableFuture<>();
List<CompletableFuture<TargetProcess>> futures =
List.of(observedParent, observedChild);
@AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)
private void stateChanged(TargetObject obj, TargetExecutionState state) {
Msg.debug(this, "STATE: " + obj.getJoinedPath(".") + " is now " + state);
TargetProcess process = DebugModelConventions.liveProcessOrNull(obj);
if (process == null) {
return;
}
CompletableFuture<TargetProcess> f = futures.get(observed.size());
if (observed.add(process)) {
try {
TargetEnvironment env = findEnvironment(process.getPath());
assertEnvironment(env);
f.complete(process);
}
catch (Throwable e) {
f.completeExceptionally(e);
}
}
}
@Override
public void event(TargetObject object, TargetThread eventThread,
TargetEventType type, String description, List<Object> parameters) {
Msg.debug(this, "EVENT: " + object.getJoinedPath(".") + " emitted " + type +
"(desc=" + description + ",params=" + parameters + ")");
}
};
m.getModel().addModelListener(stateMonitor);
TargetLauncher launcher = findLauncher();
preLaunch(launcher);
Msg.debug(this, "Launching " + specimen);
waitOn(launcher.launch(specimen.getLauncherArgs()));
Msg.debug(this, " Done launching");
TargetObject processContainer = findProcessContainer();
assertNotNull("No process container", processContainer);
TargetProcess parentProcess = waitOn(stateMonitor.observedParent);
Msg.debug(this, "Parent is " + parentProcess.getJoinedPath("."));
postLaunch(parentProcess);
AsyncState parentState =
new AsyncState(m.suitable(TargetExecutionStateful.class, parentProcess.getPath()));
waitOn(parentState.waitValue(TargetExecutionState.STOPPED));
placeBreakpoint("parent", parentProcess, getParentBreakpointExpression());
TargetProcess childProcess = null;
for (int i = 1; childProcess == null; i++) {
Msg.debug(this, "(" + i + ") Resuming until fork");
resume(parentProcess);
Msg.debug(this, " Done " + i);
waitAcc(access(parentProcess));
try {
childProcess = retryForOtherProcessRunning(processContainer, specimen, this,
p -> p != parentProcess, WAIT_FOR_CHILD_MS);
}
catch (AssertionError e) {
// Try resuming again
}
}
Msg.debug(this, "Child is " + childProcess.getJoinedPath("."));
assertNotSame(parentProcess, childProcess);
assertNotEquals(parentProcess, childProcess);
assertSame(childProcess, waitOn(stateMonitor.observedChild));
assertTrue(DebugModelConventions.isProcessAlive(parentProcess));
AsyncState childState =
new AsyncState(m.suitable(TargetExecutionStateful.class, childProcess.getPath()));
waitOn(parentState.waitUntil(s -> s == TargetExecutionState.STOPPED));
postFork(parentProcess, childProcess);
placeChildBreakpoint(childProcess);
for (int i = 1; DebugModelConventions.isProcessAlive(parentProcess); i++) {
Msg.debug(this, "(" + i + ") Resuming parent until terminated");
resume(parentProcess);
Msg.debug(this, " Done " + i);
TargetExecutionState state =
waitOn(parentState.waitUntil(s -> s != TargetExecutionState.RUNNING));
Msg.debug(this, "Parent state after resume-wait-not-running: " + state);
Msg.debug(this, " And Child: " + childState.get());
}
assertTrue(DebugModelConventions.isProcessAlive(childProcess));
waitOn(childState.waitUntil(s -> s == TargetExecutionState.STOPPED));
for (int i = 1; DebugModelConventions.isProcessAlive(childProcess); i++) {
Msg.debug(this, "Resuming child until terminated");
resume(childProcess);
Msg.debug(this, " Done " + i);
waitOn(childState.waitUntil(s -> s != TargetExecutionState.RUNNING));
}
}
protected void placeBreakpoint(String who, TargetProcess process, String expression)
throws Throwable {
TargetBreakpointSpecContainer container =
findBreakpointSpecContainer(process.getPath());
Msg.debug(this, "Placing breakpoint (on " + who + ")");
waitOn(container.placeBreakpoint(expression, Set.of(TargetBreakpointKind.SW_EXECUTE)));
}
protected void placeChildBreakpoint(TargetProcess childProcess) throws Throwable {
String expression = getChildBreakpointExpression();
if (expression != null) {
placeBreakpoint("child", childProcess, expression);
}
}
}

View file

@ -0,0 +1,172 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertTrue;
import java.lang.invoke.MethodHandles;
import java.util.Objects;
import org.junit.Assert;
import org.junit.Test;
import ghidra.dbg.AnnotatedDebuggerAttributeListener;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebugModelConventions.AsyncState;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.program.model.address.Address;
import ghidra.util.Msg;
/**
* A scenario that verifies memory writes affect the target
*/
public abstract class AbstractDebuggerModelScenarioMemoryTest extends AbstractDebuggerModelTest {
/**
* This specimen must perform some observable action, which can be affected by a memory write
*
* <p>
* A common example is to exit with a code read from memory. It may also be useful for debugging
* purposes to print a message from the same memory.
*
* @return the specimen
*/
protected abstract DebuggerTestSpecimen getSpecimen();
/**
* Get the destination address to write to
*
* <p>
* The most reliable way to do this is to ensure an easily identifiable symbol for the
* destination address is exported, then use the debugging API to obtain its address.
*
* @param process the process running the specimen
* @return the destination address
* @throws Throwable if anything goes wrong
*/
protected abstract Address getAddressToWrite(TargetProcess process) throws Throwable;
/**
* Get the bytes to write
*
* <p>
* It's probably best to use a string encoded in the platform's preferred format.
*
* @return the bytes
*/
protected abstract byte[] getBytesToWrite();
/**
* Get the expected bytes after read
*
* <p>
* This should be the same as {@link #getBytesToWrite()}, but preferably includes some
* additional bytes after, so that memory reads can be verified to come from the actual target,
* and not just from a cached write. A common scenario is to partially write over a string, then
* read the entire string, verifying both the overwritten part and the remainder.
*
* @return the bytes
*/
protected abstract byte[] getExpectedBytes();
/**
* Perform whatever preparation is necessary to observe the expected effect
*
* @param process the process trapped at launch -- typically at {@code main()}.
* @throws Throwable if anything goes wrong
*/
protected void postLaunch(TargetProcess process) throws Throwable {
}
/**
* Verify, using {@link Assert}, that the target exhibited the effect of the memory write
*
* <p>
* Note that the given process may be invalid, depending on the model's implementation. The
* tester should know how the model under test behaves. If the object is invalid, it's possible
* its attributes were updated immediately preceding invalidation with observable information,
* but this is usually not the case. The better approach is to devise an effect that can be
* observed in an event callback. To install such a listener, override
* {@link #postLaunch(TargetProcess)} and record the relevant information to be validated here.
* Do not place assertions in the event callback, since the failures they could produce will not
* be recorded as test failures. If the effect can be observed in multiple ways, it is best to
* verify all of them.
*
* @param process the target process, which may no longer be valid
* @throws Throwable if anything goes wrong or an assertion fails
*/
protected abstract void verifyExpectedEffect(TargetProcess process) throws Throwable;
/**
* Test the following scenario:
*
* <ol>
* <li>Obtain a launcher and use it to start the specimen</li>
* <li>Overwrite bytes at a designated address in memory</li>
* <li>Read those bytes and verify they were modified</li>
* <li>Resume the process until it is TERMINATED</li>
* <li>Verify some effect, usually the exit code</li>
* </ol>
*/
@Test
public void testScenario() throws Throwable {
DebuggerTestSpecimen specimen = getSpecimen();
m.build();
// For model developer diagnostics
var stateMonitor = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) {
@AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)
private void stateChanged(TargetObject obj, TargetExecutionState state) {
Msg.debug(this, obj.getJoinedPath(".") + " is now " + state);
}
};
m.getModel().addModelListener(stateMonitor);
TargetLauncher launcher = findLauncher();
Msg.debug(this, "Launching " + specimen);
waitOn(launcher.launch(specimen.getLauncherArgs()));
Msg.debug(this, " Done launching");
TargetObject processContainer = findProcessContainer();
TargetProcess process = retryForProcessRunning(processContainer, specimen, this);
postLaunch(process);
Address address = Objects.requireNonNull(getAddressToWrite(process));
byte[] data = Objects.requireNonNull(getBytesToWrite());
TargetMemory memory = Objects.requireNonNull(findMemory(process.getPath()));
Msg.debug(this, "Writing memory");
waitOn(memory.writeMemory(address, data));
Msg.debug(this, " Done");
byte[] expected = getExpectedBytes();
byte[] read = waitOn(memory.readMemory(address, expected.length));
assertArrayEquals(expected, read);
assertTrue(DebugModelConventions.isProcessAlive(process));
AsyncState state =
new AsyncState(m.suitable(TargetExecutionStateful.class, process.getPath()));
for (int i = 1; DebugModelConventions.isProcessAlive(process); i++) {
Msg.debug(this, "(" + i + ") Resuming process until terminated");
resume(process);
Msg.debug(this, " Done " + i);
waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING));
Msg.debug(this, "Parent state after resume-wait-not-running: " + state);
}
verifyExpectedEffect(process);
}
}

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.dbg.test;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import org.junit.Assert;
import org.junit.Test;
import ghidra.dbg.AnnotatedDebuggerAttributeListener;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebugModelConventions.AsyncState;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.util.Msg;
public abstract class AbstractDebuggerModelScenarioRegistersTest extends AbstractDebuggerModelTest {
/**
* This specimen must have an observable behavior which can be effected by writing a register
*
* <p>
* The simplest is probably to exit with a code from a known register. This can probably be best
* accomplished by having main call {@code exit(some_func(0));} where {@code some_func} has a
* known calling convention and simply returns its parameter. This way, the test can break on
* {@code some_func} and write to the register for that first parameter. On architectures where
* the standard calling convention passes parameters via memory, you may be able to select an
* alternative that uses registers, or you may have to use inline/pure assembly.
*
* @return the specimen
*/
protected abstract DebuggerTestSpecimen getSpecimen();
/**
* Perform any work needed after the specimen has been launched
*
* @param process the process running the specimen
* @throws Throwable if anything goes wrong
*/
protected void postLaunch(TargetProcess process) throws Throwable {
}
/**
* Get the expression to break when the register write should be made
*
* <p>
* More than likely, this should just be the symbol for that function.
*
* @return the expression
*/
protected abstract String getBreakpointExpression();
/**
* Get the registers and values to write to achieve the desired effect
*
* @return the name-value map of registers to write
*/
protected abstract Map<String, byte[]> getRegisterWrites();
/**
* Verify, using {@link Assert}, that the target exhibited the effect of the register write
*
* <p>
* Note that the given process may be invalid, depending on the model's implementation. The
* tester should know how the model under test behaves. If the object is invalid, it's possible
* its attributes were updated immediately preceding invalidation with observable information,
* but this is usually not the case. The better approach is to devise an effect that can be
* observed in an event callback. To install such a listener, override
* {@link #postLaunch(TargetProcess)} and record the relevant information to be validated here.
* Do not place assertions in the event callback, since the failures they could produce will not
* be recorded as test failures. If the effect can be observed in multiple ways, it is best to
* verify all of them.
*
* @param process the target process, which may no longer be valid
* @throws Throwable if anything goes wrong or an assertion fails
*/
protected abstract void verifyExpectedEffect(TargetProcess process) throws Throwable;
/**
* Test the following scenario
*
* <ol>
* <li>Obtain a launcher and use it to start the specimen</li>
* <li>Place a breakpoint</li>
* <li>Continue until a thread hits the breakpoint</li>
* <li>Write that thread's registers</li>
* <li>Resume the process until it is TERMINATED</li>
* <li>Verify some effect, usually the exit code</li>
* </ol>
*/
@Test
public void testScenario() throws Throwable {
DebuggerTestSpecimen specimen = getSpecimen();
m.build();
// For model developer diagnostics
var bpMonitor = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) {
CompletableFuture<TargetObject> trapped = new CompletableFuture<>();
@AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)
private void stateChanged(TargetObject obj, TargetExecutionState state) {
Msg.debug(this, obj.getJoinedPath(".") + " is now " + state);
}
@Override
public void breakpointHit(TargetObject container, TargetObject trapped,
TargetStackFrame frame, TargetBreakpointSpec spec,
TargetBreakpointLocation breakpoint) {
Msg.debug(this, "TRAPPED by " + spec);
if (getBreakpointExpression().equals(spec.getExpression())) {
this.trapped.complete(trapped);
Msg.debug(this, " Counted");
}
}
};
m.getModel().addModelListener(bpMonitor);
TargetLauncher launcher = findLauncher();
Msg.debug(this, "Launching " + specimen);
waitOn(launcher.launch(specimen.getLauncherArgs()));
Msg.debug(this, " Done launching");
TargetObject processContainer = findProcessContainer();
assertNotNull(processContainer);
TargetProcess process = retryForProcessRunning(processContainer, specimen, this);
postLaunch(process);
TargetBreakpointSpecContainer breakpointContainer =
findBreakpointSpecContainer(process.getPath());
Msg.debug(this, "Placing breakpoint");
waitOn(breakpointContainer.placeBreakpoint(getBreakpointExpression(),
Set.of(TargetBreakpointKind.SW_EXECUTE)));
assertTrue(DebugModelConventions.isProcessAlive(process));
AsyncState state =
new AsyncState(m.suitable(TargetExecutionStateful.class, process.getPath()));
/**
* NB. If an assert(isAlive) is failing, check that breakpointHit() is emitted before
* attributeChanged(state=STOPPED)
*/
for (int i = 1; !bpMonitor.trapped.isDone(); i++) {
assertTrue(state.get().isAlive());
Msg.debug(this, "(" + i + ") Resuming process until breakpoint hit");
resume(process);
Msg.debug(this, " Done " + i);
waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING));
}
assertTrue(state.get().isAlive());
TargetObject target = waitOn(bpMonitor.trapped);
Map<String, byte[]> toWrite = getRegisterWrites();
TargetRegisterBank bank = Objects
.requireNonNull(m.findWithIndex(TargetRegisterBank.class, "0", target.getPath()));
Msg.debug(this, "Writing registers: " + toWrite.keySet());
waitOn(bank.writeRegistersNamed(toWrite));
Msg.debug(this, " Done");
assertTrue(DebugModelConventions.isProcessAlive(process));
for (int i = 1; DebugModelConventions.isProcessAlive(process); i++) {
Msg.debug(this, "(" + i + ") Resuming process until terminated");
resume(process);
Msg.debug(this, " Done " + i);
waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING));
Msg.debug(this, "Parent state after resume-wait-not-running: " + state);
}
verifyExpectedEffect(process);
}
}

View file

@ -0,0 +1,176 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import ghidra.dbg.AnnotatedDebuggerAttributeListener;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebugModelConventions.AsyncState;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.util.PathMatcher;
import ghidra.dbg.util.PathPattern;
import ghidra.program.model.address.Address;
import ghidra.util.Msg;
public abstract class AbstractDebuggerModelScenarioStackTest extends AbstractDebuggerModelTest {
/**
* This specimen must create a stack easily recognizable by examination of 4 frames' PCs
*
* <p>
* This is accomplished by writing 4 functions where each calls the next, the innermost
* function's symbol providing an easily-placed breakpoint. When the breakpoint is hit, frame 0
* should be at the entry of the innermost function, and the pc for each frame after that should
* be within the body of its respective following function.
*
* @return the specimen
*/
protected abstract DebuggerTestSpecimen getSpecimen();
/**
* Perform any work needed after the specimen has been launched
*
* @param process the process running the specimen
* @throws Throwable if anything goes wrong
*/
protected void postLaunch(TargetProcess process) throws Throwable {
}
/**
* Get the expression to break at the innermost recognizable function
*
* <p>
* More than likely, this should just be the symbol for that function.
*
* @return the expression
*/
protected abstract String getBreakpointExpression();
/**
* Examine the address of the given frame and verify it is where expected
*
* <p>
* Note if this validation needs access to the process, it should at least record where that
* process is by overriding {@link #postLaunch(TargetProcess)}. Ideally, it can perform all of
* the necessary lookups, e.g., to record symbol values, there instead of here.
*
* @param index the index
* @param pc the program counter
*/
protected abstract void validateFramePC(int index, Address pc);
/**
* Test the following scenario:
*
* <ol>
* <li>Obtain a launcher and use it to start the specimen</li>
* <li>Place the breakpoint on the new process</li>
* <li>Resume the process until the breakpoint is hit</li>
* <li>Read the stack and verify the PC for each frame</li>
* <li>Resume the process until it is TERMINATED</li>
* </ol>
*/
@Test
public void testScenario() throws Throwable {
DebuggerTestSpecimen specimen = getSpecimen();
m.build();
var bpMonitor = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) {
boolean hit = false;
@AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)
private void stateChanged(TargetObject object, TargetExecutionState state) {
Msg.debug(this, "STATE " + object.getJoinedPath(".") + " is now " + state);
}
@Override
public void breakpointHit(TargetObject container, TargetObject trapped,
TargetStackFrame frame, TargetBreakpointSpec spec,
TargetBreakpointLocation breakpoint) {
Msg.debug(this, "TRAPPED by " + spec);
if (getBreakpointExpression().equals(spec.getExpression())) {
hit = true;
Msg.debug(this, " Counted");
}
}
};
m.getModel().addModelListener(bpMonitor);
Msg.debug(this, "Launching " + specimen);
TargetLauncher launcher = findLauncher();
waitOn(launcher.launch(specimen.getLauncherArgs()));
Msg.debug(this, " Done launching");
TargetObject processContainer = findProcessContainer();
TargetProcess process = retryForProcessRunning(processContainer, specimen, this);
postLaunch(process);
TargetBreakpointSpecContainer breakpointContainer =
findBreakpointSpecContainer(process.getPath());
Msg.debug(this, "Placing breakpoint");
waitOn(breakpointContainer.placeBreakpoint(getBreakpointExpression(),
Set.of(TargetBreakpointKind.SW_EXECUTE)));
assertTrue(DebugModelConventions.isProcessAlive(process));
AsyncState state =
new AsyncState(m.suitable(TargetExecutionStateful.class, process.getPath()));
/**
* NB. If an assert(isAlive) is failing, check that breakpointHit() is emitted before
* attributeChanged(state=STOPPED)
*/
for (int i = 1; !bpMonitor.hit; i++) {
assertTrue(state.get().isAlive());
Msg.debug(this, "(" + i + ") Resuming process until breakpoint hit");
resume(process);
Msg.debug(this, " Done " + i);
waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING));
}
assertTrue(state.get().isAlive());
TargetStack stack = findStack(process.getPath());
PathMatcher matcher = stack.getSchema().searchFor(TargetStackFrame.class, true);
PathPattern pattern = matcher.getSingletonPattern();
assertNotNull("Frames are not clearly indexable", pattern);
assertEquals("Frames are not clearly indexable", 1, pattern.countWildcards());
// Sort by path should present them innermost to outermost
List<TargetStackFrame> frames = retry(() -> {
List<TargetStackFrame> result =
List.copyOf(m.findAll(TargetStackFrame.class, stack.getPath()).values());
assertTrue("Fewer than 4 frames", result.size() > 4);
return result;
}, List.of(AssertionError.class));
for (int i = 0; i < 4; i++) {
TargetStackFrame f = frames.get(i);
validateFramePC(i, f.getProgramCounter());
}
for (int i = 1; DebugModelConventions.isProcessAlive(process); i++) {
Msg.debug(this, "(" + i + ") Resuming process until terminated");
resume(process);
Msg.debug(this, " Done " + i);
waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING));
}
}
}

View file

@ -0,0 +1,231 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import ghidra.async.AsyncDebouncer;
import ghidra.async.AsyncTimer;
import ghidra.dbg.DebugModelConventions.AsyncState;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
/**
* Tests the functionality of a single-stepping a target
*
* <p>
* Note that multiple sub-cases of this test can be generated in order to test each steppable object
* in the model. Take care when selecting which object (usually a thread) to put under test. If, for
* example, the selected thread is performing a blocking system call, then the tests will almost
* certainly time out.
*/
public abstract class AbstractDebuggerModelSteppableTest extends AbstractDebuggerModelTest
implements RequiresTarget {
/**
* Get the expected (absolute) path of the steppable under test
*
* @param threadPath the path of the target (usually a thread)
* @return the expected path, or {@code null} for no assertion
*/
public List<String> getExpectedSteppablePath(List<String> threadPath) {
return null;
}
@Test
public void testSteppableIsWhereExpected() throws Throwable {
m.build();
TargetObject target = maybeSubstituteThread(obtainTarget());
List<String> expectedSteppablePath = getExpectedSteppablePath(target.getPath());
assumeNotNull(expectedSteppablePath);
TargetSteppable steppable = findSteppable(target.getPath());
assertEquals(expectedSteppablePath, steppable.getPath());
}
/**
* An arbitrary number
*
* <p>
* Should be enough to prove that stepping works consistently, but not so high that the test
* drags on. Definitely 2 or greater :)
*
* @return the number of steps in the test
*/
protected int getStepCount() {
return 5;
}
/**
* This just steps the target some number of times and verifies the execution state between
*
* @throws Throwable if anything goes wrong
*/
@Test
public void testStep() throws Throwable {
m.build();
TargetObject target = maybeSubstituteThread(obtainTarget());
AsyncState state =
new AsyncState(m.suitable(TargetExecutionStateful.class, target.getPath()));
TargetSteppable steppable = findSteppable(target.getPath());
for (int i = 0; i < getStepCount(); i++) {
waitOn(steppable.step());
TargetExecutionState st =
waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING));
assertTrue("Target terminated while stepping", st.isAlive());
}
}
// TODO: Test other kinds of steps
// TODO: Test expected step kinds (or expected parameters)
// TODO: Once there, use the generic method invocation style
/**
* The window of silence necessary to assume no more callbacks will occur
*
* @return the window in milliseconds
*/
protected long getDebounceWindowMs() {
return 1000;
}
enum CallbackType {
EVENT_RUNNING,
EVENT_STOPPED,
REGS_UPDATED,
CACHE_INVALIDATED;
}
/**
* Test event order for single stepping
*
* <p>
* This tests that the {@link DebuggerModelListener#registersUpdated(TargetObject, Map)}
* callback occurs "last" following a step. While other callbacks may intervene, the order ought
* to be {@code event(RUNNING)}, {@code event(STOPPED)}, {@code registersUpdated()}. An
* {@code event()} cannot follow, as that would cause the snap to advance, making registers
* appear stale . Worse, if {@code registersUpdated} precedes {@code STOPPED}, the recorder will
* write values into the snap previous to the one it ought. This principle applies to all
* {@code event()}s, but is easiest to test for single-stepping. We also check that the
* registers (cached) are not invalidated after they are updated for the step. Note that
* {@code STOPPED} can be substituted for any event which implies the target is stopped.
*
* @throws Throwable if anything goes wrong
*/
@Test
public void testStepEventOrder() throws Throwable {
m.build();
var listener = new DebuggerModelListener() {
List<CallbackType> callbacks = new ArrayList<>();
List<String> log = new ArrayList<>();
AsyncDebouncer<Void> debouncer =
new AsyncDebouncer<Void>(AsyncTimer.DEFAULT_TIMER, getDebounceWindowMs());
@Override
public void event(TargetObject object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
synchronized (callbacks) {
if (type == TargetEventType.RUNNING) {
callbacks.add(CallbackType.EVENT_RUNNING);
}
else if (type.impliesStop) {
callbacks.add(CallbackType.EVENT_STOPPED);
}
log.add("event(" + type + "): " + description);
}
debouncer.contact(null);
}
@Override
public void registersUpdated(TargetObject bank, Map<String, byte[]> updates) {
synchronized (callbacks) {
callbacks.add(CallbackType.REGS_UPDATED);
log.add("registersUpdated()");
}
debouncer.contact(null);
}
@Override
public void invalidateCacheRequested(TargetObject object) {
synchronized (callbacks) {
callbacks.add(CallbackType.CACHE_INVALIDATED);
log.add("invalidateCacheRequested()");
}
debouncer.contact(null);
}
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
debouncer.contact(null);
}
@Override
public void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
debouncer.contact(null);
}
};
m.getModel().addModelListener(listener);
CompletableFuture<Void> settledBefore = listener.debouncer.settled();
TargetObject target = maybeSubstituteThread(obtainTarget());
AsyncState state =
new AsyncState(m.suitable(TargetExecutionStateful.class, target.getPath()));
TargetSteppable steppable = findSteppable(target.getPath());
waitOnNoValidate(settledBefore);
synchronized (listener.callbacks) {
listener.callbacks.clear();
listener.log.add("CLEARED callbacks");
}
CompletableFuture<Void> settledAfter = listener.debouncer.settled();
waitOn(steppable.step());
TargetExecutionState st =
waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING));
assertTrue("Target terminated while stepping", st.isAlive());
waitOnNoValidate(settledAfter);
List<CallbackType> callbacks;
synchronized (listener.callbacks) {
callbacks = List.copyOf(listener.callbacks);
}
int stoppedIdx = callbacks.indexOf(CallbackType.EVENT_STOPPED);
assertNotEquals(-1, stoppedIdx);
List<CallbackType> follows = callbacks.subList(stoppedIdx + 1, callbacks.size());
assertFalse("Observed multiple event(STOPPED/OTHER) callbacks for one step",
follows.contains(CallbackType.EVENT_STOPPED));
int regsUpdatedIdx = callbacks.indexOf(CallbackType.REGS_UPDATED);
assertNotEquals("Did not observe a registersUpdated() callback", -1, regsUpdatedIdx);
assertTrue("registersUpdated() must follow event(STOPPED/OTHER)",
regsUpdatedIdx > stoppedIdx);
int invalidatedIdx = follows.indexOf(CallbackType.CACHE_INVALIDATED);
assertTrue("Observed an invalidateCacheRequest() after registersUpdated()",
invalidatedIdx < regsUpdatedIdx); // absent or precedes
}
}

View file

@ -0,0 +1,345 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import org.junit.After;
import org.junit.Before;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebugModelConventions.AsyncState;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.testutil.*;
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
import ghidra.util.Msg;
/**
*
* <ul>
* <li>TODO: ensure order: created(Thread), event(THREAD_CREATED), created(RegisterBank) ?</li>
* <li>TODO: ensure registersUpdated(RegisterBank) immediately upon created(RegisterBank) ?</li>
* </ul>
*/
public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadlessIntegrationTest
implements TestDebuggerModelProvider, DebuggerModelTestUtils {
protected DummyProc dummy;
public ModelHost m;
/**
* The default seed path to use when searching for a type of object
*
* @return the seed path
*/
protected List<String> seedPath() {
return List.of();
}
protected TargetObject findAttachableContainer() throws Throwable {
return m.findContainer(TargetAttachable.class, seedPath());
}
protected TargetAttacher findAttacher() throws Throwable {
return m.find(TargetAttacher.class, seedPath());
}
protected TargetFocusScope findFocusScope() throws Throwable {
return m.find(TargetFocusScope.class, seedPath());
}
protected TargetInterpreter findInterpreter() throws Throwable {
return m.find(TargetInterpreter.class, seedPath());
}
/**
* Get the launcher under test
*
* <p>
* This can be overridden to force a different launcher under the test.
*
* @return the launcher
* @throws Throwable if anything goes wrong
*/
protected TargetLauncher findLauncher() throws Throwable {
return m.find(TargetLauncher.class, seedPath());
}
/**
* Get the process container under test
*
* <p>
* This can be overridden to force a different object under the test.
*
* @return the process container
* @throws Throwable if anything goes wrong
*/
protected TargetObject findProcessContainer() throws Throwable {
return m.findContainer(TargetProcess.class, seedPath());
}
/**
* Get the breakpoint container of a target under test
*
* @param seedPath the path to the target
* @return the breakpoint container
* @throws Throwable if anything goes wrong
*/
protected TargetBreakpointSpecContainer findBreakpointSpecContainer(List<String> seedPath)
throws Throwable {
return m.suitable(TargetBreakpointSpecContainer.class, seedPath);
}
protected TargetEnvironment findEnvironment(List<String> seedPath) throws Throwable {
return m.suitable(TargetEnvironment.class, seedPath);
}
/**
* Get the steppable object of a target under test
*
* @param seedPath the path to the target
* @return the steppable object
* @throws Throwable if anything goes wrong
*/
protected TargetSteppable findSteppable(List<String> seedPath) throws Throwable {
return m.find(TargetSteppable.class, seedPath);
}
/**
* Get the memory of a target under test
*
* @param seedPath the path to the target
* @return the memory
* @throws Throwable if anything goes wrong
*/
protected TargetMemory findMemory(List<String> seedPath) throws Throwable {
return m.find(TargetMemory.class, seedPath);
}
/**
* Find any thread to put under test
*
* @param seedPath the path to the target process
* @return the thread
* @throws Throwable if anything goes wrong
*/
protected TargetThread findAnyThread(List<String> seedPath) throws Throwable {
return m.findAny(TargetThread.class, seedPath);
}
// TODO: Seems TargetStack is just a container for TargetStackFrame
// This could be replaced by findContainer(TargetStackFrame)
protected TargetStack findStack(List<String> seedPath) throws Throwable {
return m.find(TargetStack.class, seedPath);
}
protected TargetRegisterBank findAnyRegisterBank(List<String> seedPath) throws Throwable {
return m.findAny(TargetRegisterBank.class, seedPath);
}
protected TargetStackFrame findAnyStackFrame(List<String> seedPath) throws Throwable {
return m.findAny(TargetStackFrame.class, seedPath);
}
protected TargetObject maybeSubstituteThread(TargetObject target) throws Throwable {
TargetThread thread = findAnyThread(target.getPath());
return thread == null ? target : thread;
}
@Override
public void validateCompletionThread() {
m.validateCompletionThread();
}
@Before
public void setUpDebuggerModelTest() throws Throwable {
m = modelHost();
}
@After
public void tearDownDebuggerModelTest() throws Throwable {
/**
* NB. Model has to be closed before dummy. If dummy is suspended by a debugger, terminating
* it, even forcibly, may fail.
*/
if (m != null) {
m.close();
}
if (dummy != null) {
dummy.close();
}
}
public interface DebuggerTestSpecimen {
/**
* Run the specimen outside the debugger
*
* <p>
* This is really only applicable to processes which are going to run/wait indefinitely,
* since this is likely used in tests involving attach.
*
* @return a handle to the process
* @throws Throwable if anything goes wrong
*/
DummyProc runDummy() throws Throwable;
/**
* Get the arguments to launch this specimen using the model's launcher
*
* @return the arguments
*/
Map<String, Object> getLauncherArgs();
/**
* Get the script to launch this specimen via the interpreter
*/
List<String> getLaunchScript();
/**
* Check if this specimen is the image for the given process
*
* @param process the process to examine
* @param test the test case
* @return true if the specimen is the image, false otherwise
* @throws Throwable if anything goes wrong
*/
boolean isRunningIn(TargetProcess process, AbstractDebuggerModelTest test) throws Throwable;
/**
* Check if this specimen is the image for the given attachable process
*
* <p>
* The actual check is usually done by the OS-assigned PID.
*
* @param dummy the dummy process whose image is known to be this specimen
* @param attachable the attachable process presented by the model
* @param test the test case
* @return true if the attachable process represents the given dummy
* @throws Throwable if anything goes wrong
*/
boolean isAttachable(DummyProc dummy, TargetAttachable attachable,
AbstractDebuggerModelTest test) throws Throwable;
}
/**
* Set a software breakpoint and resume until it is hit
*
* @param bpExpression the expression for the breakpoint
* @param target the target to resume
* @return the object which is actually trapped, often a thread
* @throws Throwable if anything goes wrong
*/
protected TargetObject trapAt(String bpExpression, TargetObject target) throws Throwable {
var listener = new DebuggerModelListener() {
CompletableFuture<TargetObject> trapped = new CompletableFuture<>();
@Override
public void event(TargetObject object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
Msg.debug(this, "EVENT " + type + " '" + description + "'");
}
@Override
public void breakpointHit(TargetObject container, TargetObject trapped,
TargetStackFrame frame, TargetBreakpointSpec spec,
TargetBreakpointLocation breakpoint) {
if (bpExpression.equals(spec.getExpression())) {
this.trapped.complete(trapped);
}
}
};
target.getModel().addModelListener(listener);
TargetBreakpointSpecContainer breakpoints = findBreakpointSpecContainer(target.getPath());
waitOn(breakpoints.placeBreakpoint(bpExpression, Set.of(TargetBreakpointKind.SW_EXECUTE)));
AsyncState state =
new AsyncState(m.suitable(TargetExecutionStateful.class, target.getPath()));
while (!listener.trapped.isDone()) {
resume(target);
TargetExecutionState st =
waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING));
assertTrue("Target terminated before it was trapped", st.isAlive());
}
target.getModel().removeModelListener(listener);
return waitOn(listener.trapped);
}
protected void runTestDetach(TargetObject container, DebuggerTestSpecimen specimen)
throws Throwable {
TargetProcess process = retryForProcessRunning(container, specimen, this);
TargetDetachable detachable = m.suitable(TargetDetachable.class, process.getPath());
waitAcc(detachable);
waitOn(detachable.detach());
assertFalse(DebugModelConventions.isProcessAlive(process));
}
protected void runTestKill(TargetObject container, DebuggerTestSpecimen specimen)
throws Throwable {
TargetProcess process = retryForProcessRunning(container, specimen, this);
TargetKillable killable = m.suitable(TargetKillable.class, process.getPath());
waitAcc(killable);
waitOn(killable.kill());
assertFalse(DebugModelConventions.isProcessAlive(process));
}
protected void runTestResumeTerminates(TargetObject container, DebuggerTestSpecimen specimen)
throws Throwable {
TargetProcess process = retryForProcessRunning(container, specimen, this);
TargetResumable resumable = m.suitable(TargetResumable.class, process.getPath());
AsyncState state =
new AsyncState(m.suitable(TargetExecutionStateful.class, process.getPath()));
TargetExecutionState st = waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING));
assertTrue(st.isAlive());
waitOn(resumable.resume());
retryVoid(() -> assertFalse(DebugModelConventions.isProcessAlive(process)),
List.of(AssertionError.class));
}
protected void runTestResumeInterruptMany(TargetObject container, DebuggerTestSpecimen specimen,
int repetitions) throws Throwable {
TargetProcess process = retryForProcessRunning(container, specimen, this);
TargetResumable resumable = m.suitable(TargetResumable.class, process.getPath());
TargetInterruptible interruptible =
m.suitable(TargetInterruptible.class, process.getPath());
TargetExecutionStateful stateful =
m.suitable(TargetExecutionStateful.class, process.getPath());
for (int i = 0; i < repetitions; i++) {
waitAcc(resumable);
waitOn(resumable.resume());
if (stateful != null) {
retryVoid(() -> {
assertEquals(TargetExecutionState.RUNNING, stateful.getExecutionState());
}, List.of(AssertionError.class));
}
// NB. Never have to wait to interrupt. (Hmmmm, do we believe this?)
waitOn(interruptible.interrupt());
if (stateful != null) {
retryVoid(() -> {
assertEquals(TargetExecutionState.STOPPED, stateful.getExecutionState());
}, List.of(AssertionError.class));
}
}
waitOn(container.getModel().ping("Are you still there?"));
}
}

View file

@ -0,0 +1,245 @@
/* ###
* 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.dbg.test;
import java.util.*;
import java.util.Map.Entry;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.testutil.*;
import ghidra.dbg.testutil.TestDebuggerModelProvider.ModelHost;
import ghidra.dbg.util.*;
import ghidra.dbg.util.ConfigurableFactory.Property;
import ghidra.dbg.util.PathUtils.PathComparator;
public abstract class AbstractModelHost implements ModelHost, DebuggerModelTestUtils {
protected DebuggerObjectModel model;
public CallbackValidator callbackValidator;
public EventValidator eventValidator;
public TargetObjectAddedWaiter waiter;
public DebuggerConsole console;
protected boolean validateCallbacks = true;
protected boolean validateEvents = true;
protected boolean provideConsole = true;
@Override
public DebuggerObjectModel buildModel(Map<String, Object> options) throws Throwable {
DebuggerModelFactory factory = getModelFactory();
for (Map.Entry<String, Object> opt : options.entrySet()) {
@SuppressWarnings("unchecked")
Property<Object> property =
(Property<Object>) factory.getOptions().get(opt.getKey());
property.setValue(opt.getValue());
}
DebuggerObjectModel model = waitOn(factory.build());
if (validateCallbacks) {
callbackValidator = new CallbackValidator(model);
}
if (validateEvents) {
eventValidator = new EventValidator(model);
}
if (provideConsole) {
console = new DebuggerConsole(model);
}
waiter = new TargetObjectAddedWaiter(model);
return model;
}
@Override
public AbstractModelHost build() throws Throwable {
model = buildModel(getFactoryOptions());
return this;
}
@Override
public DebuggerObjectModel getModel() {
return model;
}
@Override
public void validateCompletionThread() {
if (callbackValidator != null) {
callbackValidator.validateCompletionThread();
}
}
@Override
public TargetObject getRoot() throws Throwable {
// Nothing waits on root unless they call this. Cannot use getModelRoot()
return waitOn(model.fetchModelRoot());
}
@Override
public void close() throws Exception {
if (model != null) {
try {
waitOn(model.close());
}
catch (Exception e) {
throw e;
}
catch (Throwable e) {
throw new AssertionError(e);
}
}
if (callbackValidator != null) {
callbackValidator.close();
}
if (eventValidator != null) {
eventValidator.close();
}
if (console != null) {
console.close();
}
if (waiter != null) {
waiter.close();
}
}
public abstract DebuggerModelFactory getModelFactory();
@Override
public List<String> getBogusPath() {
return PathUtils.parse("THIS.PATH[SHOULD].NEVER[EXIST]");
}
@Override
public boolean hasDetachableProcesses() {
return true;
}
@Override
public boolean hasInterruptibleProcesses() {
return true;
}
@Override
public boolean hasKillableProcesses() {
return true;
}
@Override
public boolean hasResumableProcesses() {
return true;
}
@Override
public boolean hasAttachableContainer() {
return true;
}
@Override
public boolean hasAttacher() {
return true;
}
@Override
public boolean hasEventScope() {
return true;
}
@Override
public boolean hasLauncher() {
return true;
}
@Override
public boolean hasProcessContainer() {
return true;
}
@Override
public TargetObjectAddedWaiter getAddedWaiter() {
return waiter;
}
@Override
public <T extends TargetObject> T find(Class<T> cls, List<String> seedPath) throws Throwable {
PathMatcher matcher =
model.getRootSchema().getSuccessorSchema(seedPath).searchFor(cls, seedPath, true);
if (matcher.isEmpty()) {
return null;
}
return cls.cast(assertUniqueShortest(waitOn(waiter.waitAtLeastOne(matcher))));
}
@Override
public <T extends TargetObject> T findWithIndex(Class<T> cls, String index,
List<String> seedPath) throws Throwable {
Objects.requireNonNull(index); // Use find if no index is expected
PathPredicates matcher = model.getRootSchema()
.getSuccessorSchema(seedPath)
.searchFor(cls, seedPath, true)
.applyIndices(index);
if (matcher.isEmpty()) {
return null;
}
return cls.cast(waitOn(waiter.wait(matcher.getSingletonPath())));
}
@Override
public <T extends TargetObject> T findAny(Class<T> cls, List<String> seedPath)
throws Throwable {
PathMatcher matcher =
model.getRootSchema().getSuccessorSchema(seedPath).searchFor(cls, seedPath, true);
if (matcher.isEmpty()) {
return null;
}
return cls.cast(waitOn(waiter.waitAtLeastOne(matcher)).firstEntry().getValue());
}
@Override
public <T extends TargetObject> NavigableMap<List<String>, T> findAll(Class<T> cls,
List<String> seedPath) throws Throwable {
PathMatcher matcher =
model.getRootSchema().getSuccessorSchema(seedPath).searchFor(cls, seedPath, false);
if (matcher.isEmpty()) {
return new TreeMap<>();
}
// NB. Outside of testing, an "unsafe" cast of the map should be fine.
// During testing, we should expend the energy to verify the heap.
NavigableMap<List<String>, T> result = new TreeMap<>(PathComparator.KEYED);
for (Entry<List<String>, ?> ent : waitOn(waiter.waitAtLeastOne(matcher)).entrySet()) {
result.put(ent.getKey(), cls.cast(ent.getValue()));
}
return result;
}
@Override
public TargetObject findContainer(Class<? extends TargetObject> cls, List<String> seedPath)
throws Throwable {
List<String> foundSub =
model.getRootSchema().getSuccessorSchema(seedPath).searchForCanonicalContainer(cls);
if (foundSub == null) {
return null;
}
List<String> path = PathUtils.extend(seedPath, foundSub);
return (TargetObject) waitOn(waiter.wait(path));
}
@Override
public <T extends TargetObject> T suitable(Class<T> cls, List<String> seedPath)
throws Throwable {
List<String> path = model.getRootSchema().searchForSuitable(cls, seedPath);
if (path == null) {
return null;
}
return cls.cast(waitOn(waiter.wait(path)));
}
}

View file

@ -0,0 +1,40 @@
/* ###
* 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.dbg.test;
import ghidra.dbg.target.TargetAttacher;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen;
import ghidra.dbg.testutil.DummyProc;
public interface ProvidesTargetViaAttachSpecimen extends RequiresTarget, RequiresAttachSpecimen {
void setDummy(DummyProc dummy);
AbstractDebuggerModelTest getTest();
@Override
default TargetObject obtainTarget() throws Throwable {
TargetAttacher attacher = getTest().findAttacher();
TargetObject container = getTest().findProcessContainer();
DebuggerTestSpecimen specimen = getAttachSpecimen();
waitAcc(attacher);
DummyProc dummy = specimen.runDummy();
setDummy(dummy);
attacher.attach(dummy.pid);
return retryForProcessRunning(container, specimen, getTest());
}
}

View file

@ -0,0 +1,44 @@
/* ###
* 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.dbg.test;
import static org.junit.Assert.*;
import ghidra.dbg.target.TargetLauncher;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen;
public interface ProvidesTargetViaLaunchSpecimen extends RequiresTarget, RequiresLaunchSpecimen {
/**
* Probably just {@code return this;}
*
* @return the test
*/
AbstractDebuggerModelTest getTest();
@Override
default TargetObject obtainTarget() throws Throwable {
TargetLauncher launcher = getTest().findLauncher();
assertNotNull("No launcher found", launcher);
TargetObject container = getTest().findProcessContainer();
assertNotNull("No process container found", container);
DebuggerTestSpecimen specimen = getLaunchSpecimen();
waitAcc(launcher);
launcher.launch(specimen.getLauncherArgs());
return retryForProcessRunning(container, specimen, getTest());
}
}

View file

@ -0,0 +1,32 @@
/* ###
* 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.dbg.test;
import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen;
public interface RequiresAttachSpecimen {
/**
* Get the specimen to use for attach tests
*
* <p>
* This specimen should live indefinitely. It should only terminate when the debugger or
* operating system kills it. A good example on UNIX is "{@code dd}".
*
* @return the specimen
*/
DebuggerTestSpecimen getAttachSpecimen();
}

View file

@ -0,0 +1,33 @@
/* ###
* 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.dbg.test;
import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen;
public interface RequiresLaunchSpecimen {
/**
* Get the specimen to use for launch tests
*
* <p>
* This specimen should live for only a short period of time. When left to execute freely, it
* should immediately terminate on its own without error. A good example on UNIX is
* "{@code echo Hello, World!}"
*
* @return the specimen
*/
DebuggerTestSpecimen getLaunchSpecimen();
}

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.dbg.test;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetProcess;
import ghidra.dbg.testutil.DebuggerModelTestUtils;
public interface RequiresTarget extends DebuggerModelTestUtils {
/**
* Perform whatever minimal setup is necessary to obtain a target suitable for testing
*
* <p>
* For user-mode debugging this is almost certainly a {@link TargetProcess}.
*
* @return the target
* @throws Throwable if anything goes wrong
*/
TargetObject obtainTarget() throws Throwable;
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.dbg.util;
package ghidra.dbg.testutil;
import java.util.LinkedList;

View file

@ -0,0 +1,21 @@
/* ###
* 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.dbg.testutil;
public class AddressValidator {
// TODO: Ensure 'Memory' exists before any address/range is in any callback / attribute
// TODO: Should it also verify it is covered by a known region?
}

View file

@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.dbg.util;
package ghidra.dbg.testutil;
import java.util.Collection;
import java.util.Map;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetObject.TargetObjectListener;
import ghidra.dbg.util.AttributesChangedListener.AttributesChangedInvocation;
import ghidra.dbg.testutil.AttributesChangedListener.AttributesChangedInvocation;
public class AttributesChangedListener extends
AbstractInvocationListener<AttributesChangedInvocation> implements TargetObjectListener {
AbstractInvocationListener<AttributesChangedInvocation> implements DebuggerModelListener {
public static class AttributesChangedInvocation {
public final TargetObject parent;
public final Collection<String> removed;

View file

@ -0,0 +1,377 @@
/* ###
* 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.dbg.testutil;
import static org.junit.Assert.*;
import java.util.*;
import java.util.Map.Entry;
import ghidra.dbg.*;
import ghidra.dbg.agent.AbstractTargetObject;
import ghidra.dbg.attributes.TargetObjectList;
import ghidra.dbg.attributes.TargetStringList;
import ghidra.dbg.error.DebuggerMemoryAccessException;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetAttacher.TargetAttachKind;
import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
import ghidra.dbg.target.TargetSteppable.TargetStepKindSet;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
import ghidra.util.Msg;
import ghidra.util.NumericUtilities;
public class CallbackValidator implements DebuggerModelListener, AutoCloseable {
public CatchOffThread off = new CatchOffThread();
public final DebuggerObjectModel model;
public Thread thread = null;
public Set<TargetObject> valid = new HashSet<>();
// Knobs
// TODO: Make these methods instead?
public boolean log = false;
public boolean requireSameThread = true;
public boolean requireSameModel = true;
public boolean requireValid = true;
public boolean requireProxy = true;
public Set<Class<?>> allowedTypes = Set.of( // When not a TargetObject
Boolean.class, boolean.class, Byte.class, byte.class, byte[].class, Character.class,
char.class, Double.class, double.class, Float.class, float.class, Integer.class, int.class,
Long.class, long.class, String.class, TargetStringList.class, Address.class,
AddressRange.class, TargetAttachKind.class, TargetAttachKindSet.class,
TargetBreakpointKind.class, TargetBreakpointKindSet.class, TargetStepKind.class,
TargetStepKindSet.class, TargetExecutionState.class, TargetEventType.class,
TargetParameterMap.class, TargetObjectList.class);
public CallbackValidator(DebuggerObjectModel model) {
this.model = model;
model.addModelListener(this, true);
}
public void validateCallbackThread(String callback) {
if (thread == null) {
thread = Thread.currentThread();
}
if (requireSameThread) {
assertEquals("Callback " + callback + " came from an unexpected thread", thread,
Thread.currentThread());
}
}
public void validateCompletionThread() {
if (thread == null) {
thread = Thread.currentThread();
}
if (requireSameThread) {
if (thread != Thread.currentThread()) {
throw new AssertionError("Completion came from an unexpected thread expected:" +
thread + " but got " + Thread.currentThread());
}
assertEquals("Completion came from an unexpected thread", thread,
Thread.currentThread());
}
}
public void validateObjectModel(String callback, TargetObject obj) {
if (requireSameModel) {
assertEquals("Callback " + callback + " included foreign object " + obj, model,
obj.getModel());
}
}
public void validateObjectProxy(String callback, TargetObject obj) {
if (requireProxy && obj instanceof AbstractTargetObject<?>) {
AbstractTargetObject<?> ato = (AbstractTargetObject<?>) obj;
assertEquals(
"Non-proxy object " + obj.getJoinedPath(".") + "leaked into callback " + callback,
obj, ato.getProxy());
}
}
public void validateObjectValid(String callback, TargetObject obj) {
if (requireValid) {
assertTrue("Object " + obj.getJoinedPath(".") + " invalid during callback " + callback,
valid.contains(obj));
}
}
public void validateObjectInvalid(String callback, TargetObject obj) {
if (requireValid) {
assertFalse("Object " + obj.getJoinedPath(".") + " valid during callback " + callback +
", but should have been invalid", valid.contains(obj));
}
}
public void validateInvalidObject(String callback, TargetObject obj) {
assertNotNull(obj);
validateObjectModel(callback, obj);
validateObjectProxy(callback, obj);
validateObjectInvalid(callback, obj);
}
public void validateObject(String callback, TargetObject obj) {
assertNotNull(obj);
validateObjectModel(callback, obj);
validateObjectProxy(callback, obj);
validateObjectValid(callback, obj);
}
public void validateObjectOptional(String callback, TargetObject obj) {
if (obj != null) {
validateObject(callback, obj);
}
}
public void validateObjects(String callback, Collection<? extends TargetObject> objs) {
for (TargetObject obj : objs) {
validateObject(callback, obj);
}
}
public void validateObjectsInMap(String callback, Map<String, ?> map) {
for (Entry<String, ?> ent : map.entrySet()) {
Object obj = ent.getValue();
validateObjectOrAllowedType(callback + "(key=" + ent.getKey() + ")", obj);
}
}
public void validateObjectOrAllowedType(String callback, Object obj) {
if (obj instanceof TargetObject) {
validateObject(callback, (TargetObject) obj);
return;
}
for (Class<?> cls : allowedTypes) {
if (cls.isInstance(obj)) {
return;
}
}
fail(
"Invalid object type in callback " + callback + " " + obj + "(" + obj.getClass() + ")");
}
public void validateObjectsInCollection(String callback, Collection<?> objs) {
for (Object obj : objs) {
validateObjectOrAllowedType(callback, obj);
}
}
@Override
public synchronized void catastrophic(Throwable t) {
if (log) {
Msg.info(this, "catastrophic(t=" + t + ")");
}
off.catching(() -> {
throw new AssertionError("Catastrophic error", t);
});
}
@Override
public void modelClosed(DebuggerModelClosedReason reason) {
if (log) {
Msg.info(this, "modelClosed(reason=" + reason + ")");
}
}
@Override
public synchronized void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
if (log) {
Msg.info(this, "elementsChanged(object=" + object + ",removed=" + removed + ",added=" +
added + ")");
}
off.catching(() -> {
validateCallbackThread("elementsChanged");
validateObject("elementsChanged.object", object);
validateObjectsInMap("elementsChanged.added(object=" + object.getJoinedPath(".") + ")",
added);
});
}
@Override
public synchronized void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
if (log) {
Msg.info(this, "attributesChanged(object=" + object + ",removed=" + removed +
",added=" + added + ")");
}
off.catching(() -> {
validateCallbackThread("attributesChanged");
validateObject("attributesChanged.object", object);
validateObjectsInMap(
"attributesChanged.added(object=" + object.getJoinedPath(".") + ")", added);
});
}
@Override
public synchronized void breakpointHit(TargetObject container, TargetObject trapped,
TargetStackFrame frame, TargetBreakpointSpec spec,
TargetBreakpointLocation breakpoint) {
if (log) {
Msg.info(this, "breakpointHit(container=" + container + ",trapped=" + trapped +
",frame=" + frame + ",spec=" + spec + ",breakpoint=" + breakpoint + ")");
}
off.catching(() -> {
validateCallbackThread("breakpointHit");
validateObject("breakpointHit.container", container);
validateObject("breakpointHit.trapped", trapped);
validateObjectOptional("breakpointHit.frame", frame);
validateObject("breakpointHit.spec", spec);
validateObject("breakpointHit.breakpoint", breakpoint);
});
}
@Override
public synchronized void consoleOutput(TargetObject console, Channel channel, byte[] data) {
if (log) {
Msg.info(this, "consoleOutput(console=" + console + ",channel=" + channel + ",data=" +
new String(data) + ")");
}
off.catching(() -> {
validateCallbackThread("consoleOutput");
validateObject("consoleOutput", console);
assertNotNull(data);
});
}
@Override
public synchronized void created(TargetObject object) {
if (log) {
Msg.info(this, "created(object=" + object + ")");
}
valid.add(object);
off.catching(() -> {
validateCallbackThread("created");
validateObject("created", object);
});
}
@Override
public synchronized void event(TargetObject object, TargetThread eventThread,
TargetEventType type, String description, List<Object> parameters) {
if (log) {
Msg.info(this, "event(object=" + object + ",eventThread=" + eventThread + ",type=" +
type + ",description=" + description + ",parameters=" + parameters + ")");
}
off.catching(() -> {
validateCallbackThread("event");
validateObject("event", object);
if (type == TargetEventType.THREAD_CREATED || type == TargetEventType.THREAD_EXITED) {
validateObject("event", eventThread);
}
else {
validateObjectOptional("event.eventThread", eventThread);
}
assertNotNull(type);
assertNotNull(description);
validateObjectsInCollection("event.parameters", parameters);
});
}
@Override
public synchronized void invalidateCacheRequested(TargetObject object) {
if (log) {
Msg.info(this, "invalidateCacheRequested(object=" + object + ")");
}
off.catching(() -> {
validateCallbackThread("invalidateCacheRequested");
validateObject("invalidateCacheRequested", object);
});
}
@Override
public synchronized void invalidated(TargetObject object, TargetObject branch, String reason) {
if (log) {
Msg.info(this,
"invalidated(object=" + object + ",branch=" + branch + ",reason=" + reason + ")");
}
off.catching(() -> {
validateCallbackThread("invalidated");
validateObject("invalidated", object);
valid.remove(object);
validateInvalidObject("invalidated", branch); // pre-ordered callbacks
assertNotNull(reason);
});
}
@Override
public synchronized void memoryReadError(TargetObject memory, AddressRange range,
DebuggerMemoryAccessException e) {
if (log) {
Msg.info(this,
"memoryReadError(memory=" + memory + ",range=" + range + ",e=" + e + ")");
}
off.catching(() -> {
validateCallbackThread("memoryReadError");
validateObject("memoryReadError", memory);
assertNotNull(range);
throw new AssertionError("Memory read error", e);
});
}
@Override
public void memoryUpdated(TargetObject memory, Address address, byte[] data) {
if (log) {
Msg.info(this, "memoryUpdated(memory=" + memory + ",address=" + address + ",data=" +
NumericUtilities.convertBytesToString(data) + ")");
}
off.catching(() -> {
validateCallbackThread("memoryUpdated");
validateObject("memoryUpdated", memory);
assertNotNull(address);
// TODO: Validate address for regions
assertNotNull(data);
});
}
@Override
public void registersUpdated(TargetObject bank, Map<String, byte[]> updates) {
if (log) {
Msg.info(this, "registersUpdated(bank=" + bank + ",updates=" +
DebuggerModelTestUtils.hexlify(updates) + ")");
}
off.catching(() -> {
validateCallbackThread("registersUpdated");
validateObject("registersUpdated", bank);
assertNotNull(updates);
// TODO: Validate names to descriptions, including lengths of values
});
}
@Override
public void rootAdded(TargetObject root) {
if (log) {
Msg.info(this, "rootAdded(root=" + root + ")");
}
off.catching(() -> {
validateCallbackThread("rootAdded");
validateObject("rootAdded", root);
});
}
@Override
public synchronized void close() throws Exception {
off.close();
}
}

View file

@ -0,0 +1,46 @@
/* ###
* 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.dbg.testutil;
import ghidra.util.Msg;
public class CatchOffThread implements AutoCloseable {
protected Throwable caught;
public void catching(Runnable runnable) {
try {
runnable.run();
}
catch (Throwable e) {
if (caught == null) {
caught = e;
}
Msg.error(this, "Off-thread exception: " + e);
}
}
@Override
public void close() throws Exception {
if (caught != null) {
if (caught instanceof Exception) {
throw (Exception) caught;
}
else {
throw new AssertionError("Off-thread exception", caught);
}
}
}
}

View file

@ -0,0 +1,84 @@
/* ###
* 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.dbg.testutil;
import java.io.*;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetInterpreter;
import ghidra.dbg.target.TargetObject;
public class DebuggerConsole extends Thread implements DebuggerModelListener, AutoCloseable {
private final DebuggerObjectModel model;
private final BufferedReader reader;
private TargetInterpreter interpreter;
private boolean closed = false;
public DebuggerConsole(DebuggerObjectModel model) {
this.model = model;
this.reader = new BufferedReader(new InputStreamReader(System.in));
model.addModelListener(this);
setDaemon(true);
start();
}
@Override
public void consoleOutput(TargetObject console, Channel channel, byte[] data) {
if (console instanceof TargetInterpreter) {
if (interpreter == null) {
System.out.println("Found interpreter: " + console);
interpreter = (TargetInterpreter) console;
}
}
String text = new String(data);
System.out.println(text);
}
@Override
public void run() {
try {
while (!closed) {
String line = reader.readLine();
if (interpreter == null) {
System.err.println("Have not found interpreter, yet");
continue;
}
interpreter.execute(line).whenComplete((__, ex) -> {
if (ex != null) {
System.err.println("Command error: " + ex.getMessage());
}
else {
System.out.println("Command finished");
}
});
}
}
catch (IOException e) {
System.err.println("IOException on console: " + e);
}
}
@Override
public void close() throws Exception {
model.removeModelListener(this);
closed = true;
interrupt();
}
}

View file

@ -0,0 +1,209 @@
/* ###
* 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.dbg.testutil;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import ghidra.async.AsyncReference;
import ghidra.async.AsyncTestUtils;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebugModelConventions.AsyncAccess;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
import ghidra.dbg.test.AbstractDebuggerModelTest;
import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen;
import ghidra.util.NumericUtilities;
public interface DebuggerModelTestUtils extends AsyncTestUtils {
default byte[] arr(String hex) {
return NumericUtilities.convertStringToBytes(hex);
}
/**
* Performs the cast, after verifying the schema, too
*
* @param <T> the type of the object
* @param type the class of the object
* @param obj the object to cast
* @return the same object
*/
default <T extends TargetObject> T cast(Class<T> type, TargetObject obj) {
assertTrue(obj.getSchema().getInterfaces().contains(type));
return type.cast(obj);
}
default <T extends TargetObject> T ancestor(Class<T> type, TargetObject seed) throws Throwable {
return DebugModelConventions.ancestor(type, seed);
}
default AsyncAccess access(TargetObject obj) throws Throwable {
return new AsyncAccess(
ancestor(TargetAccessConditioned.class, Objects.requireNonNull(obj)));
}
default void waitAcc(TargetObject obj) throws Throwable {
AsyncAccess acc = access(obj);
waitAcc(acc);
acc.dispose();
}
default void waitAcc(AsyncReference<Boolean, ?> access) throws Throwable {
waitOn(access.waitValue(true));
}
default void cli(TargetObject interpreter, String cmd) throws Throwable {
TargetInterpreter as = interpreter.as(TargetInterpreter.class);
waitOn(as.execute(cmd));
}
default String captureCli(TargetObject interpreter, String cmd) throws Throwable {
TargetInterpreter as = interpreter.as(TargetInterpreter.class);
return waitOn(as.executeCapture(cmd));
}
default void launch(TargetObject launcher, Map<String, ?> args) throws Throwable {
TargetLauncher as = launcher.as(TargetLauncher.class);
waitOn(as.launch(args));
}
default void resume(TargetObject resumable) throws Throwable {
TargetResumable as = resumable.as(TargetResumable.class);
waitOn(as.resume());
}
default void step(TargetObject steppable, TargetStepKind kind) throws Throwable {
TargetSteppable as = steppable.as(TargetSteppable.class);
waitOn(as.step(kind));
}
default TargetObject getFocus(TargetObject scope) {
TargetFocusScope as = scope.as(TargetFocusScope.class);
return as.getFocus();
}
default void focus(TargetObject scope, TargetObject focus) throws Throwable {
TargetFocusScope as = scope.as(TargetFocusScope.class);
waitOn(as.requestFocus(focus));
}
static Map<String, String> hexlify(Map<String, byte[]> map) {
return map.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey,
e -> NumericUtilities.convertBytesToString(e.getValue())));
}
/**
* Assert that there is a single reference with shortest path, and get it
*
* @param <T> the type of object reference
* @param refs the map of paths to references, <em>sorted shortest-key-first</em>
* @return the value of the entry with shortest key
*/
default <T> T assertUniqueShortest(NavigableMap<List<String>, T> refs) {
assertTrue(refs.size() >= 1);
Iterator<Entry<List<String>, T>> rit = refs.entrySet().iterator();
Entry<List<String>, T> shortest = rit.next();
if (!rit.hasNext()) {
return shortest.getValue();
}
Entry<List<String>, T> next = rit.next();
assertTrue("Shortest is not unique: " + refs,
next.getKey().size() > shortest.getKey().size());
return shortest.getValue();
}
default TargetAttachable getAttachable(Collection<? extends TargetAttachable> attachables,
DebuggerTestSpecimen specimen, DummyProc dummy, AbstractDebuggerModelTest test) {
return attachables.stream().filter(a -> {
try {
return specimen.isAttachable(dummy, a, test);
}
catch (Throwable e) {
throw new AssertionError(e);
}
}).findFirst().orElse(null);
}
default TargetProcess getProcessRunning(Collection<? extends TargetProcess> processes,
DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test) {
return getProcessRunning(processes, specimen, test, p -> true);
}
default TargetProcess getProcessRunning(Collection<? extends TargetProcess> processes,
DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test,
Predicate<TargetProcess> predicate) {
return processes.stream().filter(p -> {
try {
return predicate.test(p) && specimen.isRunningIn(p, test);
}
catch (Throwable e) {
throw new AssertionError(e);
}
}).findFirst().orElse(null);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
default Collection<TargetProcess> fetchProcesses(TargetObject container)
throws Throwable {
return (Collection) waitOn(container.fetchElements(true)).values();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
default Collection<TargetAttachable> fetchAttachables(TargetObject container)
throws Throwable {
return (Collection) waitOn(container.fetchElements(true)).values();
}
default TargetProcess getProcessRunning(TargetObject container,
DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test) throws Throwable {
return getProcessRunning(container, specimen, test, p -> true);
}
default TargetProcess getProcessRunning(TargetObject container,
DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test,
Predicate<TargetProcess> predicate) throws Throwable {
return getProcessRunning(fetchProcesses(container), specimen, test, predicate);
}
default TargetProcess retryForProcessRunning(TargetObject container,
DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test) throws Throwable {
return retry(() -> {
TargetProcess process = getProcessRunning(container, specimen, test);
assertNotNull(process);
return process;
}, List.of(AssertionError.class));
}
default TargetProcess retryForOtherProcessRunning(TargetObject container,
DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test,
Predicate<TargetProcess> predicate, long timeoutMs)
throws Throwable {
return retry(timeoutMs, () -> {
TargetProcess process = getProcessRunning(container, specimen, test, predicate);
assertNotNull(process);
return process;
}, List.of(AssertionError.class));
}
}

View file

@ -16,14 +16,13 @@
package ghidra.dbg.testutil;
import java.io.*;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import ghidra.framework.Application;
public class DummyProc implements AutoCloseable {
final Process process;
public final Process process;
public final long pid;
public static String which(String cmd) {
@ -33,40 +32,37 @@ public class DummyProc implements AutoCloseable {
catch (Exception e) {
// fallback to system
}
if (new File(cmd).canExecute()) {
return cmd;
}
String line;
try {
Process exec = new ProcessBuilder("which", cmd).start();
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");
Process exec = new ProcessBuilder(isWindows ? "where" : "which", cmd).start();
exec.waitFor();
BufferedReader reader =
new BufferedReader(new InputStreamReader(exec.getInputStream()));
return reader.readLine().trim();
line = reader.readLine();
}
catch (Exception e) {
throw new RuntimeException(e);
}
if (line == null) {
throw new RuntimeException("Cannot find " + cmd);
}
return line.trim();
}
public static DummyProc run(String... args) throws NoSuchFieldException, SecurityException,
IllegalArgumentException, IllegalAccessException, IOException {
public static DummyProc run(String... args) throws IOException {
DummyProc proc = new DummyProc(args);
return proc;
}
DummyProc(String... args) throws IOException, NoSuchFieldException, SecurityException,
IllegalArgumentException, IllegalAccessException {
DummyProc(String... args) throws IOException {
args[0] = which(args[0]);
process = new ProcessBuilder(args).start();
@SuppressWarnings("hiding")
long pid = -1;
try {
Field pidFld = process.getClass().getDeclaredField("pid");
pidFld.setAccessible(true);
pid = pidFld.getLong(process);
}
catch (NoSuchFieldException | SecurityException e) {
throw new AssertionError("Could not get pid for DummyProc", e);
}
this.pid = pid;
pid = process.pid();
}
@Override

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.dbg.util;
package ghidra.dbg.testutil;
import java.util.*;
@ -21,11 +21,11 @@ import org.apache.commons.lang3.reflect.TypeUtils;
import org.apache.commons.lang3.reflect.Typed;
import ghidra.async.AsyncReference;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetObject.TargetObjectListener;
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
public class ElementTrackingListener<T extends TargetObject> implements TargetObjectListener {
public class ElementTrackingListener<T extends TargetObject> implements DebuggerModelListener {
public final Class<T> valType;
public final Map<String, T> elements = new TreeMap<>(TargetObjectKeyComparator.ELEMENT);
public final AsyncReference<Integer, Void> size = new AsyncReference<>();
@ -40,11 +40,6 @@ public class ElementTrackingListener<T extends TargetObject> implements TargetOb
this((Class) TypeUtils.getRawType(valType.getType(), null));
}
@Override
public void displayChanged(TargetObject object, String display) {
// Don't care
}
public synchronized AsyncReference<T, Void> refElement(String index) {
T elem = elements.get(index);
AsyncReference<T, Void> ref = new AsyncReference<>(elem);
@ -70,10 +65,4 @@ public class ElementTrackingListener<T extends TargetObject> implements TargetOb
}
size.set(elements.size(), null);
}
@Override
public void attributesChanged(TargetObject parent, Collection<String> removed,
Map<String, ?> added) {
// Don't care
}
}

View file

@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.dbg.util;
package ghidra.dbg.testutil;
import java.util.Collection;
import java.util.Map;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetObject.TargetObjectListener;
import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation;
import ghidra.dbg.testutil.ElementsChangedListener.ElementsChangedInvocation;
public class ElementsChangedListener extends AbstractInvocationListener<ElementsChangedInvocation>
implements TargetObjectListener {
implements DebuggerModelListener {
public static class ElementsChangedInvocation {
public final TargetObject parent;
public final Collection<String> removed;

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.dbg.testutil;
import java.util.*;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetThread;
public class EventSequenceListener implements DebuggerModelListener {
public static class EventRecord {
public final TargetObject object;
public final TargetThread eventThread;
public final TargetEventType type;
public final String description;
public final List<Object> parameters;
public EventRecord(TargetObject object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
this.object = object;
this.eventThread = eventThread;
this.type = type;
this.description = description;
this.parameters = parameters;
}
@Override
public String toString() {
return String.format("<EventRecord obj=%s thread=%s type=%s desc='%s' params=%s",
object, eventThread, type, description, parameters);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof EventRecord)) {
return false;
}
EventRecord that = (EventRecord) obj;
if (!Objects.equals(this.object, that.object)) {
return false;
}
if (!Objects.equals(this.eventThread, that.eventThread)) {
return false;
}
if (!Objects.equals(this.type, that.type)) {
return false;
}
if (!Objects.equals(this.description, that.description)) {
return false;
}
if (!Objects.equals(this.parameters, that.parameters)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return Objects.hash(object, eventThread, type, description, parameters);
}
}
public final List<EventRecord> events = new ArrayList<>();
@Override
public void event(TargetObject object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
events.add(new EventRecord(object, eventThread, type, description, parameters));
}
}

View file

@ -0,0 +1,303 @@
/* ###
* 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.dbg.testutil;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import java.util.*;
import java.util.function.Function;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.util.Msg;
public class EventValidator
implements DebuggerModelListener, AutoCloseable, DebuggerModelTestUtils {
public CatchOffThread off = new CatchOffThread();
interface Observation {
String getEvent();
TargetObject getObject();
Observation inParameter(String event);
default Observation inAncestor(String event, TargetObject successor) {
return inParameter("Parent of " + successor.getJoinedPath(".") + " in " + event);
}
Observation inCreated(String event);
Observation inDestroyed(String event);
}
static abstract class AbstractObservation implements Observation {
private final String event;
private final TargetObject object;
public AbstractObservation(String event, TargetObject object) {
this.event = event;
this.object = object;
}
@Override
public String getEvent() {
return event;
}
@Override
public TargetObject getObject() {
return object;
}
protected String getPath() {
return object.getJoinedPath(".");
}
}
static class NoObservation extends AbstractObservation {
public NoObservation(TargetObject object) {
super("[none]", object);
}
@Override
public Observation inParameter(String event) {
return new UseObservation(event, getObject());
}
@Override
public Observation inCreated(String event) {
return new CreatedObservation(event, getObject());
}
@Override
public Observation inDestroyed(String event) {
return new DestroyedObservation(event, getObject());
}
}
static class UseObservation extends AbstractObservation {
public UseObservation(String event, TargetObject object) {
super(event, object);
}
@Override
public Observation inParameter(String event) {
return this;
}
@Override
public Observation inCreated(String event) {
throw new AssertionError(
"Observed " + getEvent() + " for " + getPath() + " before " + event);
}
@Override
public Observation inDestroyed(String event) {
return new DestroyedObservation(event, getObject());
}
}
static class CreatedObservation extends AbstractObservation {
public CreatedObservation(String event, TargetObject object) {
super(event, object);
}
@Override
public Observation inParameter(String event) {
return this;
}
@Override
public Observation inCreated(String event) {
throw new AssertionError("Observed double-" + event + " of " + getPath());
}
@Override
public Observation inDestroyed(String event) {
return new DestroyedObservation(event, getObject());
}
}
static class DestroyedObservation extends AbstractObservation {
public DestroyedObservation(String event, TargetObject object) {
super(event, object);
}
@Override
public Observation inParameter(String event) {
throw new AssertionError(
"Observed " + event + " of " + getPath() + " after " + getEvent());
}
@Override
public Observation inCreated(String event) {
return new CreatedObservation(event, getObject());
}
@Override
public Observation inDestroyed(String event) {
throw new AssertionError("Observed double-" + event + " of " + getPath());
}
}
public final DebuggerObjectModel model;
public Map<TargetProcess, Observation> processes = new HashMap<>();
public Map<TargetThread, Observation> threads = new HashMap<>();
public Map<TargetModule, Observation> modules = new HashMap<>();
// Knobs
public boolean log = false;
public EventValidator(DebuggerObjectModel model) {
this.model = model;
model.addModelListener(this);
}
@Override
public void invalidated(TargetObject object, TargetObject branch, String reason) {
if (log) {
Msg.info(this,
"invalidated(object=" + object + ",branch=" + branch + ",reason=" + reason + ")");
}
processes.remove(object);
threads.remove(object);
modules.remove(object);
}
@Override
public synchronized void event(TargetObject object, TargetThread eventThread,
TargetEventType type, String description, List<Object> parameters) {
if (log) {
Msg.info(this,
"event(object=" + object + ",eventThread=" + eventThread + ",type=" + type +
",description=" + description + ",parameters=" + parameters + ")");
}
off.catching(() -> {
switch (type) {
case PROCESS_CREATED:
validateCreated(type.name(), TargetProcess.class, processes, parameters);
break;
case PROCESS_EXITED:
validateDestroyed(type.name(), TargetProcess.class, processes, parameters);
break;
case THREAD_CREATED:
validateCreated(type.name(), TargetThread.class, threads, parameters);
break;
case THREAD_EXITED:
validateDestroyed(type.name(), TargetThread.class, threads, parameters);
break;
case MODULE_LOADED:
validateCreated(type.name(), TargetModule.class, modules, parameters);
break;
case MODULE_UNLOADED:
validateDestroyed(type.name(), TargetModule.class, modules, parameters);
break;
case STOPPED:
case RUNNING:
case BREAKPOINT_HIT:
case STEP_COMPLETED:
case EXCEPTION:
case SIGNAL:
validateParameters(type.name(), parameters);
break;
default:
fail("Unexpected event type");
}
});
}
protected <T extends TargetObject> void observe(Map<T, Observation> map, T object,
Function<Observation, Observation> func) {
map.compute(object, (__, observation) -> {
if (observation == null) {
observation = new NoObservation(object);
}
return func.apply(observation);
});
}
protected void validateParameters(String event, List<Object> objects) {
for (Object obj : objects) {
if (obj instanceof TargetProcess) {
observe(processes, (TargetProcess) obj, o -> o.inParameter(event));
}
if (obj instanceof TargetThread) {
observe(threads, (TargetThread) obj, o -> o.inParameter(event));
}
if (obj instanceof TargetModule) {
observe(modules, (TargetModule) obj, o -> o.inParameter(event));
}
}
}
protected void validateAncestors(String event, TargetObject object) {
TargetObject ancestor = object;
while (null != (ancestor = ancestor.getParent())) { // Yes, pre-step to parent
if (ancestor instanceof TargetProcess) {
observe(processes, (TargetProcess) ancestor, o -> o.inAncestor(event, object));
}
if (ancestor instanceof TargetThread) {
observe(threads, (TargetThread) ancestor, o -> o.inAncestor(event, object));
}
if (ancestor instanceof TargetModule) {
observe(modules, (TargetModule) ancestor, o -> o.inAncestor(event, object));
}
}
}
protected <T> T doGetFirstAs(Class<T> cls, List<Object> objects) {
if (objects.isEmpty()) {
return null;
}
Object first = objects.get(0);
if (!cls.isInstance(first)) {
return null;
}
return cls.cast(first);
}
protected <T> T getFirstAs(String event, Class<T> cls, List<Object> objects) {
T result = doGetFirstAs(cls, objects);
assertNotNull("The first parameter of " + event + " must be a " + cls.getSimpleName(),
result);
return result;
}
protected <T extends TargetObject> void validateCreated(String event, Class<T> cls,
Map<T, Observation> map, List<Object> objects) {
T t = getFirstAs(event, cls, objects);
observe(map, t, o -> o.inCreated(event));
validateAncestors(event, t);
validateParameters(event, objects.subList(1, objects.size()));
}
protected <T extends TargetObject> void validateDestroyed(String event, Class<T> cls,
Map<T, Observation> map, List<Object> objects) {
T t = getFirstAs(event, cls, objects);
observe(map, t, o -> o.inDestroyed(event));
validateAncestors(event, t);
validateParameters(event, objects.subList(1, objects.size()));
}
@Override
public synchronized void close() throws Exception {
off.close();
}
}

View file

@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.dbg.util;
package ghidra.dbg.testutil;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetObject.TargetObjectListener;
import ghidra.dbg.util.InvalidatedListener.InvalidatedInvocation;
import ghidra.dbg.testutil.InvalidatedListener.InvalidatedInvocation;
public class InvalidatedListener extends
AbstractInvocationListener<InvalidatedInvocation> implements TargetObjectListener {
public class InvalidatedListener extends AbstractInvocationListener<InvalidatedInvocation>
implements DebuggerModelListener {
public static class InvalidatedInvocation {
public final TargetObject object;
public final TargetObject branch;

View file

@ -0,0 +1,20 @@
/* ###
* 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.dbg.testutil;
public class RegisterUpdateValidator {
// Ensure updates are for registers that exist in the descriptions
}

View file

@ -0,0 +1,124 @@
/* ###
* 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.dbg.testutil;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.util.*;
public class TargetObjectAddedWaiter
implements DebuggerModelListener, DebuggerModelTestUtils, AutoCloseable {
private final DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this);
private final Map<List<String>, CompletableFuture<Object>> pathBacklog = new HashMap<>();
private final Map<PathPredicates, CompletableFuture<NavigableMap<List<String>, ?>>> predBacklog =
new HashMap<>();
private final DebuggerObjectModel model;
public TargetObjectAddedWaiter(DebuggerObjectModel model) {
this.model = model;
model.addModelListener(reorderer, true);
}
@Override
public void close() throws Exception {
model.removeModelListener(reorderer);
}
protected void retryBacklogs() {
synchronized (predBacklog) {
// NB. getModelRoot() can be non-null before rootAdded. Use fetch.getNow instead.
TargetObject root = model.fetchModelRoot().getNow(null);
if (root != null) {
for (Iterator<Entry<PathPredicates, CompletableFuture<NavigableMap<List<String>, ?>>>> it =
predBacklog.entrySet().iterator(); it.hasNext();) {
Entry<PathPredicates, CompletableFuture<NavigableMap<List<String>, ?>>> ent =
it.next();
NavigableMap<List<String>, ?> values = ent.getKey().getCachedValues(root);
if (!values.isEmpty()) {
// NB. This is completed with a lock, but tests should just use waitOn
ent.getValue().complete(values);
it.remove();
}
}
}
}
}
@Override
public void rootAdded(TargetObject root) {
retryBacklogs();
}
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
for (Entry<String, ?> ent : added.entrySet()) {
List<String> attrPath = PathUtils.extend(object.getPath(), ent.getKey());
CompletableFuture<Object> cf = pathBacklog.remove(attrPath);
if (cf != null) {
cf.complete(ent.getValue());
}
}
retryBacklogs();
}
@Override
public void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
for (Entry<String, ?> ent : added.entrySet()) {
List<String> elemPath = PathUtils.index(object.getPath(), ent.getKey());
CompletableFuture<Object> cf = pathBacklog.remove(elemPath);
if (cf != null) {
cf.complete(ent.getValue());
}
}
retryBacklogs();
}
public CompletableFuture<?> wait(List<String> path) {
Objects.requireNonNull(path);
synchronized (pathBacklog) {
Object val = model.getModelValue(path);
if (val != null) {
return CompletableFuture.completedFuture(val);
}
CompletableFuture<Object> promise = new CompletableFuture<>();
pathBacklog.put(path, promise);
return promise;
}
}
public CompletableFuture<NavigableMap<List<String>, ?>> waitAtLeastOne(
PathPredicates predicates) {
synchronized (predBacklog) {
TargetObject root = model.getModelRoot();
if (root != null) {
NavigableMap<List<String>, ?> result = predicates.getCachedValues(root);
if (!result.isEmpty()) {
return CompletableFuture.completedFuture(result);
}
}
CompletableFuture<NavigableMap<List<String>, ?>> promise = new CompletableFuture<>();
predBacklog.put(predicates, promise);
return promise;
}
}
}

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