Merge remote-tracking branch 'origin/GP-2752_Dan_removePerTargetObjectListeners--SQUASHED'

This commit is contained in:
Ryan Kurtz 2022-11-12 01:36:31 -05:00
commit b9a6bfdcd3
78 changed files with 384 additions and 1261 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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