mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 19:42:36 +02:00
Merge remote-tracking branch 'origin/GP-2752_Dan_removePerTargetObjectListeners--SQUASHED'
This commit is contained in:
commit
b9a6bfdcd3
78 changed files with 384 additions and 1261 deletions
|
@ -15,9 +15,7 @@
|
|||
*/
|
||||
package ghidra.dbg;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -457,281 +455,6 @@ public enum DebugModelConventions {
|
|||
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 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
|
||||
*
|
||||
* @param removed the removed object
|
||||
*/
|
||||
protected abstract void objectRemoved(TargetObject removed);
|
||||
|
||||
/**
|
||||
* An object has been added to the sub-tree
|
||||
*
|
||||
* @param added the added object
|
||||
*/
|
||||
protected abstract void objectAdded(TargetObject added);
|
||||
|
||||
/**
|
||||
* Decide whether a sub-tree (of the sub-tree) should be tracked
|
||||
*
|
||||
* @param obj the root of the sub-tree to consider
|
||||
* @return false to ignore, true to track
|
||||
*/
|
||||
protected abstract boolean checkDescend(TargetObject obj);
|
||||
|
||||
@Override
|
||||
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||
runNotInSwing(this, () -> doInvalidated(object, reason), "invalidated");
|
||||
}
|
||||
|
||||
private void doInvalidated(TargetObject object, String reason) {
|
||||
List<TargetObject> removed = new ArrayList<>();
|
||||
synchronized (objects) {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* NOTE: Can't use iteration, because subtrees will also remove stuff, causing
|
||||
* ConcurrentModificationException, even if removal is via the iterator...
|
||||
*/
|
||||
List<String> path = object.getPath();
|
||||
while (true) {
|
||||
Entry<List<String>, TargetObject> ent = objects.ceilingEntry(path);
|
||||
if (ent == null || !PathUtils.isAncestor(path, ent.getKey())) {
|
||||
break;
|
||||
}
|
||||
objects.remove(ent.getKey());
|
||||
TargetObject succ = ent.getValue();
|
||||
succ.removeListener(this);
|
||||
removed.add(succ);
|
||||
}
|
||||
}
|
||||
for (TargetObject r : removed) {
|
||||
objectRemovedSafe(r);
|
||||
}
|
||||
}
|
||||
|
||||
private void objectRemovedSafe(TargetObject removed) {
|
||||
try {
|
||||
objectRemoved(removed);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
Msg.error(this, "Error in callback", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void objectAddedSafe(TargetObject obj) {
|
||||
try {
|
||||
objectAdded(obj);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
Msg.error(this, "Error in callback", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void considerObj(TargetObject obj) {
|
||||
if (!checkDescend(obj)) {
|
||||
return;
|
||||
}
|
||||
CompletableFuture.runAsync(() -> addListenerAndConsiderSuccessors(obj))
|
||||
.exceptionally(ex -> {
|
||||
Msg.error(this, "Could add to object: " + obj, ex);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void considerElements(TargetObject parent,
|
||||
Map<String, ? extends TargetObject> elements) {
|
||||
synchronized (objects) {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
if (!objects.containsKey(parent.getPath())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (TargetObject e : elements.values()) {
|
||||
considerObj(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void considerAttributes(TargetObject obj, Map<String, ?> attributes) {
|
||||
synchronized (objects) {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
if (!objects.containsKey(obj.getPath())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, ?> ent : attributes.entrySet()) {
|
||||
String name = ent.getKey();
|
||||
Object val = ent.getValue();
|
||||
if (!(val instanceof TargetObject)) {
|
||||
continue;
|
||||
}
|
||||
TargetObject a = (TargetObject) val;
|
||||
if (PathUtils.isLink(obj.getPath(), name, a.getPath())) {
|
||||
continue;
|
||||
}
|
||||
considerObj(a);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a specified object, without initially adding the sub-tree
|
||||
*
|
||||
* <p>
|
||||
* Note that {@link #checkDescend(TargetObject)} must also exclude the sub-tree, otherwise
|
||||
* children added later will be tracked.
|
||||
*
|
||||
* @param obj the object to track
|
||||
* @return true if the object was not already being listened to
|
||||
*/
|
||||
public boolean addListener(TargetObject obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
obj.addListener(this);
|
||||
synchronized (objects) {
|
||||
if (objects.put(obj.getPath(), obj) == obj) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
objectAddedSafe(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a specified sub-tree to this listener
|
||||
*
|
||||
* @param obj
|
||||
* @return true if the object was not already being listened to
|
||||
*/
|
||||
public boolean addListenerAndConsiderSuccessors(TargetObject obj) {
|
||||
boolean result = addListener(obj);
|
||||
if (result && checkDescend(obj)) {
|
||||
obj.fetchElements()
|
||||
.thenAcceptAsync(elems -> considerElements(obj, elems))
|
||||
.exceptionally(ex -> {
|
||||
Msg.error(this, "Could not fetch elements of obj: " + obj, ex);
|
||||
return null;
|
||||
});
|
||||
obj.fetchAttributes()
|
||||
.thenAcceptAsync(attrs -> considerAttributes(obj, attrs))
|
||||
.exceptionally(ex -> {
|
||||
Msg.error(this, "Could not fetch attributes of obj: " + obj, ex);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void elementsChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ? extends TargetObject> added) {
|
||||
runNotInSwing(this, () -> doElementsChanged(parent, removed, added), "elementsChanged");
|
||||
}
|
||||
|
||||
private void doElementsChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ? extends TargetObject> added) {
|
||||
if (checkDescend(parent)) {
|
||||
considerElements(parent, added);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attributesChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ?> added) {
|
||||
runNotInSwing(this, () -> doAttributesChanged(parent, removed, added),
|
||||
"attributesChanged");
|
||||
}
|
||||
|
||||
private void doAttributesChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ?> added) {
|
||||
if (checkDescend(parent)) {
|
||||
considerAttributes(parent, added);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of this sub-tree tracker/listener
|
||||
*
|
||||
* <p>
|
||||
* This uninstalls the listener from every tracked object and clears its collection of
|
||||
* tracked objects.
|
||||
*/
|
||||
public void dispose() {
|
||||
synchronized (objects) {
|
||||
disposed = true;
|
||||
for (Iterator<TargetObject> it = objects.values().iterator(); it.hasNext();) {
|
||||
TargetObject obj = it.next();
|
||||
obj.removeListener(this);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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();
|
||||
}
|
||||
|
||||
@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;
|
||||
// Check that all requests have been issued (fence is ready)
|
||||
if (listeners != null) {
|
||||
set(getAllAccessibility(), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected final List<ListenerForAccess> listeners;
|
||||
protected final AsyncFence initFence = new AsyncFence();
|
||||
|
||||
public AllRequiredAccess(Collection<? extends TargetAccessConditioned> allReq) {
|
||||
Msg.debug(this, "Listening for access on: " + allReq);
|
||||
listeners = allReq.stream().map(ListenerForAccess::new).collect(Collectors.toList());
|
||||
set(getAllAccessibility(), null);
|
||||
}
|
||||
|
||||
public boolean getAllAccessibility() {
|
||||
return listeners.stream().allMatch(l -> l.accessible);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AsyncAttribute<T> extends AsyncReference<T, Void>
|
||||
implements DebuggerModelListener {
|
||||
private final TargetObject obj;
|
||||
|
@ -741,7 +464,7 @@ public enum DebugModelConventions {
|
|||
public AsyncAttribute(TargetObject obj, String name) {
|
||||
this.name = name;
|
||||
this.obj = obj;
|
||||
obj.addListener(this);
|
||||
obj.getModel().addModelListener(this);
|
||||
set((T) obj.getCachedAttribute(name), null);
|
||||
obj.fetchAttribute(name).exceptionally(ex -> {
|
||||
Msg.error(this, "Could not get initial value of " + name + " for " + obj, ex);
|
||||
|
@ -753,6 +476,9 @@ public enum DebugModelConventions {
|
|||
@SuppressWarnings("unchecked")
|
||||
public void attributesChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ?> added) {
|
||||
if (parent != obj) {
|
||||
return;
|
||||
}
|
||||
if (added.containsKey(name)) {
|
||||
set((T) added.get(name), null);
|
||||
}
|
||||
|
@ -768,7 +494,7 @@ public enum DebugModelConventions {
|
|||
@Override
|
||||
public void dispose(Throwable reason) {
|
||||
super.dispose(reason);
|
||||
obj.removeListener(this);
|
||||
obj.getModel().removeModelListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -784,34 +510,6 @@ public enum DebugModelConventions {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain an object which tracks accessibility for a given target object.
|
||||
*
|
||||
* <p>
|
||||
* Recall that for an object to be considered accessible, it and its ancestors must all be
|
||||
* accessible. Objects without the {@link TargetAccessConditioned} interface, are assumed
|
||||
* accessible.
|
||||
*
|
||||
* <p>
|
||||
* <b>Caution:</b> The returned {@link AllRequiredAccess} object has the only strong references
|
||||
* to the listeners. If you intend to wait for access, e.g., by calling
|
||||
* {@link AsyncReference#waitValue(Object)}, you must ensure a strong reference to this object
|
||||
* is maintained for the duration of the wait. If not, it could be garbage collected, and you
|
||||
* will never get a callback.
|
||||
*
|
||||
* @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);
|
||||
return collectAncestors.thenApply(AllRequiredAccess::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request activation of the given object in its nearest active scope
|
||||
*
|
||||
|
|
|
@ -25,9 +25,6 @@ import ghidra.dbg.target.TargetObject;
|
|||
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.lifecycle.Internal;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.datastruct.ListenerSet;
|
||||
|
||||
/**
|
||||
* An abstract implementation of {@link TargetObject}
|
||||
|
@ -63,15 +60,10 @@ public abstract class AbstractTargetObject<P extends TargetObject> implements Sp
|
|||
|
||||
protected volatile boolean valid = true;
|
||||
|
||||
// TODO: Remove these, and just do invocations on model's 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<>(DebuggerModelListener.class, model.clientExecutor);
|
||||
this.model = model;
|
||||
listeners.addChained(model.listeners);
|
||||
this.parent = parent;
|
||||
if (parent == null) {
|
||||
this.path = key == null ? List.of() : List.of(key);
|
||||
|
@ -103,7 +95,7 @@ public abstract class AbstractTargetObject<P extends TargetObject> implements Sp
|
|||
assert proxy != null;
|
||||
synchronized (model.lock) {
|
||||
model.objectCreated(proxy);
|
||||
listeners.fire.created(proxy);
|
||||
broadcast().created(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,19 +185,6 @@ public abstract class AbstractTargetObject<P extends TargetObject> implements Sp
|
|||
return valid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(DebuggerModelListener l) {
|
||||
if (!valid) {
|
||||
throw new IllegalStateException("Object is no longer valid: " + getProxy());
|
||||
}
|
||||
listeners.add(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(DebuggerModelListener l) {
|
||||
listeners.remove(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDebuggerObjectModel getModel() {
|
||||
return model;
|
||||
|
@ -252,14 +231,7 @@ public abstract class AbstractTargetObject<P extends TargetObject> implements Sp
|
|||
}
|
||||
valid = false;
|
||||
model.objectInvalidated(getProxy());
|
||||
listeners.fire.invalidated(getProxy(), branch, reason);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
listeners.clear();
|
||||
listeners.clearChained();
|
||||
}, model.clientExecutor).exceptionally(ex -> {
|
||||
Msg.error(this, "Error emptying invalidated object's listener set: ", ex);
|
||||
return null;
|
||||
});
|
||||
broadcast().invalidated(getProxy(), branch, reason);
|
||||
}
|
||||
|
||||
protected void doInvalidateElements(Map<String, ?> elems, String reason) {
|
||||
|
@ -330,19 +302,8 @@ public abstract class AbstractTargetObject<P extends TargetObject> implements Sp
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@Override
|
||||
public DebuggerModelListener broadcast() {
|
||||
return model.listeners.fire;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,32 +131,6 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
parent.getSchema().getChildSchema(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this object is being observed
|
||||
*
|
||||
* <p>
|
||||
* TODO: It'd be nice if we could know what is being observed: attributes, elements, console
|
||||
* output, etc. In other words, the sub-types and overrides of the listeners.
|
||||
*
|
||||
* <p>
|
||||
* Note, if an implementation chooses to cull requests because no one is listening, it should
|
||||
* take care to re-synchronize when a listener is added. The implementor will need to override
|
||||
* {@link #addListener(TargetObjectListener)}.
|
||||
*
|
||||
* @implNote The recommended pattern on the client side for keeping a synchronized cache is to
|
||||
* add a listener, and then retrieve the current elements. Thus, it is acceptable to
|
||||
* neglect invoking the callback on the new listener during re-synchronization.
|
||||
* However, more testing is needed to verify this doesn't cause problems when network
|
||||
* messaging is involved.
|
||||
*
|
||||
* @return true if there is at least one listener on this object
|
||||
* @deprecated Since the addition of model listeners, everything is always observed
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
protected boolean isObserved() {
|
||||
return !listeners.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> resync(boolean refreshAttributes, boolean refreshElements) {
|
||||
return CompletableFuture.allOf(fetchAttributes(refreshAttributes),
|
||||
|
@ -303,7 +277,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
doInvalidateElements(delta.removed, reason);
|
||||
if (!delta.isEmpty()) {
|
||||
updateCallbackElements(delta);
|
||||
listeners.fire.elementsChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||
broadcast().elementsChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
|
@ -351,7 +325,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
doInvalidateElements(delta.removed, reason);
|
||||
if (!delta.isEmpty()) {
|
||||
updateCallbackElements(delta);
|
||||
listeners.fire.elementsChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||
broadcast().elementsChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
|
@ -493,7 +467,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);
|
||||
broadcast().attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
|
@ -558,7 +532,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
doInvalidateAttributes(delta.removed, reason);
|
||||
if (!delta.isEmpty()/* && !reason.equals("Default")*/) {
|
||||
updateCallbackAttributes(delta);
|
||||
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||
broadcast().attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package ghidra.dbg.agent;
|
||||
|
||||
import ghidra.dbg.DebuggerModelListener;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
|
||||
public interface SpiTargetObject extends TargetObject, InvalidatableTargetObjectIf {
|
||||
|
@ -35,4 +36,6 @@ public interface SpiTargetObject extends TargetObject, InvalidatableTargetObject
|
|||
default SpiTargetObject getDelegate() {
|
||||
return this;
|
||||
}
|
||||
|
||||
DebuggerModelListener broadcast();
|
||||
}
|
||||
|
|
|
@ -1084,41 +1084,4 @@ public interface TargetObject extends Comparable<TargetObject> {
|
|||
public default CompletableFuture<Void> invalidateCaches() {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for object events
|
||||
*
|
||||
* <p>
|
||||
* The client must maintain a strong reference to the listener. To allow stale listeners to be
|
||||
* garbage collected, the implementation should use weak or soft references. That said, the
|
||||
* client should not rely on the implementation to garbage collect its listeners. All unneeded
|
||||
* listeners should be removed using {@link #removeListener(TargetObjectListener)}. The
|
||||
* exception is when an object is invalidated. The client may safely neglect removing any
|
||||
* listeners it registered with that object. If the object does not keep listeners, i.e., it
|
||||
* produces no events, this method may do nothing.
|
||||
*
|
||||
* <p>
|
||||
* Clients ought to listen on the model instead of specific objects, especially since an object
|
||||
* may emit events immediately after its creation, but before the client has a chance to add a
|
||||
* listener. Worse yet, the object could be invalidated before the client can retrieve its
|
||||
* children. Listening on the model ensures the reception of a complete log of events.
|
||||
*
|
||||
* @see DebuggerObjectModel#addModelListener(DebuggerModelListener)
|
||||
* @param l the listener
|
||||
*/
|
||||
public default void addListener(DebuggerModelListener l) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener
|
||||
*
|
||||
* <p>
|
||||
* If the given listener is not registered with this object, this method does nothing.
|
||||
*
|
||||
* @param l the listener
|
||||
*/
|
||||
public default void removeListener(DebuggerModelListener l) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
package ghidra.dbg;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
|
@ -45,8 +44,17 @@ public class AnnotatedDebuggerAttributeListenerTest implements DebuggerModelTest
|
|||
private void testChanged(TargetObject object, String disp) {
|
||||
display.set(disp, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attributesChanged(TargetObject object, Collection<String> removed,
|
||||
Map<String, ?> added) {
|
||||
if (object != obj) {
|
||||
return;
|
||||
}
|
||||
super.attributesChanged(object, removed, added);
|
||||
}
|
||||
};
|
||||
obj.addListener(l);
|
||||
obj.getModel().addModelListener(l);
|
||||
obj.changeAttributes(List.of(), Map.ofEntries(Map.entry("_test", "Testing")), "Because");
|
||||
waitOn(display.waitValue("Testing"));
|
||||
|
||||
|
|
|
@ -309,7 +309,7 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
|||
|
||||
FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A");
|
||||
FakeTargetRegisterBank fakeA1rb = new FakeTargetRegisterBank(model, fakeA, "[1]");
|
||||
fakeA1rb.listeners.fire.registersUpdated(fakeA1rb, Map.of());
|
||||
fakeA1rb.broadcast().registersUpdated(fakeA1rb, Map.of());
|
||||
fakeA.setElements(List.of(fakeA1rb), "Init");
|
||||
model.root.setAttributes(List.of(fakeA), Map.of(), "Init");
|
||||
|
||||
|
@ -329,7 +329,7 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
|||
|
||||
FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A");
|
||||
FakeTargetRegisterBank fakeA1rb = new FakeTargetRegisterBank(model, fakeA, "[1]");
|
||||
fakeA1rb.listeners.fire.registersUpdated(fakeA1rb, Map.of());
|
||||
fakeA1rb.broadcast().registersUpdated(fakeA1rb, Map.of());
|
||||
fakeA.setElements(List.of(fakeA1rb), "Init");
|
||||
model.root.setAttributes(List.of(fakeA), Map.of(), "Init");
|
||||
EventRecordingListener listener = new EventRecordingListener();
|
||||
|
|
|
@ -64,7 +64,7 @@ public abstract class AbstractTestTargetRegisterBank<P extends TestTargetObject>
|
|||
}
|
||||
populateObjectValues(result, "Read registers");
|
||||
return model.gateFuture(descs.getModel().future(result).thenApply(__ -> {
|
||||
listeners.fire.registersUpdated(this, result);
|
||||
broadcast().registersUpdated(this, result);
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ public abstract class AbstractTestTargetRegisterBank<P extends TestTargetObject>
|
|||
}
|
||||
populateObjectValues(updates, "Write registers");
|
||||
future.thenAccept(__ -> {
|
||||
listeners.fire.registersUpdated(this, updates);
|
||||
broadcast().registersUpdated(this, updates);
|
||||
});
|
||||
return model.gateFuture(future);
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ public class TestTargetInterpreter
|
|||
}
|
||||
|
||||
public void output(Channel channel, String line) {
|
||||
listeners.fire.consoleOutput(this, channel, line + "\n");
|
||||
broadcast().consoleOutput(this, channel, line + "\n");
|
||||
}
|
||||
|
||||
public void clearCalls() {
|
||||
|
|
|
@ -52,7 +52,7 @@ public class TestTargetMemory
|
|||
getMemory(address, data);
|
||||
CompletableFuture<byte[]> future = getModel().future(data);
|
||||
future.thenAccept(__ -> {
|
||||
listeners.fire.memoryUpdated(this, address, data);
|
||||
broadcast().memoryUpdated(this, address, data);
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ public class TestTargetMemory
|
|||
setMemory(address, data);
|
||||
CompletableFuture<Void> future = getModel().future(null);
|
||||
future.thenAccept(__ -> {
|
||||
listeners.fire.memoryUpdated(this, address, data);
|
||||
broadcast().memoryUpdated(this, address, data);
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ public class TestTargetSession extends DefaultTargetModelRoot
|
|||
|
||||
public void simulateStep(TestTargetThread eventThread) {
|
||||
eventThread.setState(TargetExecutionState.RUNNING);
|
||||
listeners.fire.event(this, eventThread, TargetEventType.STEP_COMPLETED,
|
||||
broadcast().event(this, eventThread, TargetEventType.STEP_COMPLETED,
|
||||
"Test thread completed a step", List.of());
|
||||
eventThread.setState(TargetExecutionState.STOPPED);
|
||||
}
|
||||
|
|
|
@ -119,7 +119,10 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
|
|||
AsyncReference<String, Void> lastOut = new AsyncReference<>();
|
||||
DebuggerModelListener l = new DebuggerModelListener() {
|
||||
@Override
|
||||
public void consoleOutput(TargetObject interpreter, Channel channel, byte[] out) {
|
||||
public void consoleOutput(TargetObject object, Channel channel, byte[] out) {
|
||||
if (object != interpreter) {
|
||||
return;
|
||||
}
|
||||
String str = new String(out);
|
||||
Msg.debug(this, "Got " + channel + " output: " + str);
|
||||
for (String line : str.split("\n")) {
|
||||
|
@ -127,7 +130,7 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
|
|||
}
|
||||
}
|
||||
};
|
||||
interpreter.addListener(l);
|
||||
interpreter.getModel().addModelListener(l);
|
||||
waitAcc(interpreter);
|
||||
waitOn(interpreter.execute(cmd));
|
||||
waitOn(lastOut.waitValue("test"));
|
||||
|
@ -150,7 +153,10 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
|
|||
try (CatchOffThread off = new CatchOffThread()) {
|
||||
DebuggerModelListener l = new DebuggerModelListener() {
|
||||
@Override
|
||||
public void consoleOutput(TargetObject interpreter, Channel channel, byte[] out) {
|
||||
public void consoleOutput(TargetObject object, Channel channel, byte[] out) {
|
||||
if (object != interpreter) {
|
||||
return;
|
||||
}
|
||||
String str = new String(out);
|
||||
Msg.debug(this, "Got " + channel + " output: " + str);
|
||||
if (!str.contains("test")) {
|
||||
|
@ -159,7 +165,7 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
|
|||
off.catching(() -> fail("Unexpected output:" + str));
|
||||
}
|
||||
};
|
||||
interpreter.addListener(l);
|
||||
interpreter.getModel().addModelListener(l);
|
||||
waitAcc(interpreter);
|
||||
String out = waitOn(interpreter.executeCapture(cmd));
|
||||
// Not the greatest, but allow extra lines
|
||||
|
|
|
@ -262,7 +262,7 @@ public class DebuggerCallbackReordererTest implements DebuggerModelTestUtils {
|
|||
* child of [r1], to guarantee registersUpdated has happened
|
||||
*/
|
||||
toA.changeElements(List.of(), List.of(toA1), "Test");
|
||||
toA.getListeners().fire.registersUpdated(toA, Map.of("r1", new byte[4]));
|
||||
toA.broadcast().registersUpdated(toA, Map.of("r1", new byte[4]));
|
||||
root.changeAttributes(List.of(), List.of(toA), Map.of(), "Test");
|
||||
/**
|
||||
* CFs may get queued in depth, so add root here to ensure registersUpdated comes before
|
||||
|
@ -321,12 +321,14 @@ public class DebuggerCallbackReordererTest implements DebuggerModelTestUtils {
|
|||
FakeTargetRoot root = new FakeTargetRoot(model, "Root", model.getRootSchema());
|
||||
FakeTargetObject processes = new FakeTargetObject(model, root, "Processes");
|
||||
FakeTargetProcess proc1 = new FakeTargetProcess(model, processes, "[1]");
|
||||
root.getListeners().fire.event(root, null, TargetEventType.PROCESS_CREATED,
|
||||
"Process 1 created", List.of(proc1));
|
||||
root.broadcast()
|
||||
.event(root, null, TargetEventType.PROCESS_CREATED, "Process 1 created",
|
||||
List.of(proc1));
|
||||
FakeTargetObject p1threads = new FakeTargetObject(model, proc1, "Threads");
|
||||
FakeTargetThread thread1 = new FakeTargetThread(model, p1threads, "[1]");
|
||||
root.getListeners().fire.event(root, thread1, TargetEventType.THREAD_CREATED,
|
||||
"Thread 1 created", List.of());
|
||||
root.broadcast()
|
||||
.event(root, thread1, TargetEventType.THREAD_CREATED, "Thread 1 created",
|
||||
List.of());
|
||||
|
||||
p1threads.changeElements(List.of(), List.of(thread1), "Test");
|
||||
proc1.changeAttributes(List.of(), List.of(p1threads), Map.of(), "Test");
|
||||
|
@ -366,15 +368,18 @@ public class DebuggerCallbackReordererTest implements DebuggerModelTestUtils {
|
|||
FakeTargetRoot root = new FakeTargetRoot(model, "Root", model.getRootSchema());
|
||||
FakeTargetObject processes = new FakeTargetObject(model, root, "Processes");
|
||||
FakeTargetProcess proc1 = new FakeTargetProcess(model, processes, "[1]");
|
||||
root.getListeners().fire.event(root, null, TargetEventType.PROCESS_CREATED,
|
||||
"Process 1 created", List.of(proc1));
|
||||
root.broadcast()
|
||||
.event(root, null, TargetEventType.PROCESS_CREATED, "Process 1 created",
|
||||
List.of(proc1));
|
||||
FakeTargetObject p1threads = new FakeTargetObject(model, proc1, "Threads");
|
||||
FakeTargetThread thread1 = new FakeTargetThread(model, p1threads, "[1]");
|
||||
root.getListeners().fire.event(root, thread1, TargetEventType.THREAD_CREATED,
|
||||
"Thread 1 created", List.of());
|
||||
root.broadcast()
|
||||
.event(root, thread1, TargetEventType.THREAD_CREATED, "Thread 1 created",
|
||||
List.of());
|
||||
FakeTargetThread thread2 = new FakeTargetThread(model, p1threads, "[2]");
|
||||
root.getListeners().fire.event(root, thread1, TargetEventType.THREAD_CREATED,
|
||||
"Thread 2 created", List.of());
|
||||
root.broadcast()
|
||||
.event(root, thread1, TargetEventType.THREAD_CREATED, "Thread 2 created",
|
||||
List.of());
|
||||
|
||||
p1threads.changeElements(List.of(), List.of(thread2), "Test");
|
||||
proc1.changeAttributes(List.of(), List.of(p1threads), Map.of(), "Test");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue