mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 02:39:44 +02:00
GP-0: Fixed DebuggerModelServiceTest. Quite a few changes.
This commit is contained in:
parent
9f8c6290f4
commit
734507ce12
12 changed files with 223 additions and 31 deletions
|
@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture;
|
||||||
/**
|
/**
|
||||||
* The interface for receiving input callbacks via {@code IDebugInputCallbacks} or a newer variant.
|
* The interface for receiving input callbacks via {@code IDebugInputCallbacks} or a newer variant.
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* Note: The wrapper implementation will select the appropriate native interface version.
|
* Note: The wrapper implementation will select the appropriate native interface version.
|
||||||
*/
|
*/
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
|
|
|
@ -346,7 +346,7 @@ public class DefaultTraceRecorder implements TraceRecorder {
|
||||||
|
|
||||||
protected void invalidate() {
|
protected void invalidate() {
|
||||||
valid = false;
|
valid = false;
|
||||||
//listenerForRecord.dispose();
|
objectManager.disposeModelListeners();
|
||||||
trace.release(this);
|
trace.release(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,9 +30,11 @@ public class PermanentTransactionExecutor {
|
||||||
|
|
||||||
private final TransactionCoalescer txc;
|
private final TransactionCoalescer txc;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
|
private final UndoableDomainObject obj;
|
||||||
|
|
||||||
public PermanentTransactionExecutor(UndoableDomainObject obj, String name,
|
public PermanentTransactionExecutor(UndoableDomainObject obj, String name,
|
||||||
Function<ThreadFactory, Executor> executorFactory, int delayMs) {
|
Function<ThreadFactory, Executor> executorFactory, int delayMs) {
|
||||||
|
this.obj = obj;
|
||||||
txc = new DefaultTransactionCoalescer<>(obj, RecorderPermanentTransaction::start, delayMs);
|
txc = new DefaultTransactionCoalescer<>(obj, RecorderPermanentTransaction::start, delayMs);
|
||||||
this.executor = executorFactory.apply(
|
this.executor = executorFactory.apply(
|
||||||
new BasicThreadFactory.Builder().namingPattern(name + "-thread-%d").build());
|
new BasicThreadFactory.Builder().namingPattern(name + "-thread-%d").build());
|
||||||
|
@ -40,6 +42,9 @@ public class PermanentTransactionExecutor {
|
||||||
|
|
||||||
public void execute(String description, Runnable runnable) {
|
public void execute(String description, Runnable runnable) {
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
|
if (obj.isClosed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try (CoalescedTx tx = txc.start(description)) {
|
try (CoalescedTx tx = txc.start(description)) {
|
||||||
runnable.run();
|
runnable.run();
|
||||||
}
|
}
|
||||||
|
|
|
@ -266,4 +266,9 @@ public class TraceEventListener extends AnnotatedDebuggerAttributeListener {
|
||||||
return recorder.getThreadMap();
|
return recorder.getThreadMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
target.getModel().removeModelListener(reorderer);
|
||||||
|
reorderer.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,6 +209,11 @@ public class TraceObjectListener implements DebuggerModelListener {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
target.getModel().removeModelListener(reorderer);
|
||||||
|
reorderer.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
private CompletableFuture<List<TargetObject>> findDependenciesTop(TargetObject added) {
|
private CompletableFuture<List<TargetObject>> findDependenciesTop(TargetObject added) {
|
||||||
List<TargetObject> result = new ArrayList<>();
|
List<TargetObject> result = new ArrayList<>();
|
||||||
|
|
|
@ -648,4 +648,9 @@ public class TraceObjectManager {
|
||||||
objects.remove(path);
|
objects.remove(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void disposeModelListeners() {
|
||||||
|
eventListener.dispose();
|
||||||
|
objectListener.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -183,6 +183,7 @@ public interface TraceRecorder {
|
||||||
/**
|
/**
|
||||||
* Check if recording is active and the given view is at the present
|
* Check if recording is active and the given view is at the present
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* To be at the present means the view's trace and snap matches the recorder's trace and snap.
|
* To be at the present means the view's trace and snap matches the recorder's trace and snap.
|
||||||
* The recorder must also be actively recording. Otherwise, this returns {@code false}.
|
* The recorder must also be actively recording. Otherwise, this returns {@code false}.
|
||||||
*
|
*
|
||||||
|
|
|
@ -46,6 +46,7 @@ import mockit.VerificationsInOrder;
|
||||||
/**
|
/**
|
||||||
* TODO: Cover the error cases, and cases where {@code null} is expected
|
* TODO: Cover the error cases, and cases where {@code null} is expected
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* TODO: Cover cases where multiple recorders are present
|
* TODO: Cover cases where multiple recorders are present
|
||||||
*/
|
*/
|
||||||
public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITest
|
public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITest
|
||||||
|
@ -277,7 +278,9 @@ public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITes
|
||||||
CollectionChangeDelegateWrapper<TraceRecorder> wrapper =
|
CollectionChangeDelegateWrapper<TraceRecorder> wrapper =
|
||||||
new CollectionChangeDelegateWrapper<>(recorderChangeListener);
|
new CollectionChangeDelegateWrapper<>(recorderChangeListener);
|
||||||
modelService.addTraceRecordersChangedListener(wrapper);
|
modelService.addTraceRecordersChangedListener(wrapper);
|
||||||
|
Trace trace = recorder.getTrace();
|
||||||
recorder.stopRecording();
|
recorder.stopRecording();
|
||||||
|
waitForDomainObject(trace);
|
||||||
|
|
||||||
new VerificationsInOrder() {
|
new VerificationsInOrder() {
|
||||||
{
|
{
|
||||||
|
|
|
@ -48,7 +48,10 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
ObjectRecord(TargetObject obj) {
|
ObjectRecord(TargetObject obj) {
|
||||||
this.obj = obj;
|
this.obj = obj;
|
||||||
TargetObject parent = obj.getParent();
|
TargetObject parent = obj.getParent();
|
||||||
ObjectRecord parentRecord = parent == null ? null : records.get(parent);
|
ObjectRecord parentRecord;
|
||||||
|
synchronized (records) {
|
||||||
|
parentRecord = parent == null ? null : records.get(parent);
|
||||||
|
}
|
||||||
if (parentRecord == null) {
|
if (parentRecord == null) {
|
||||||
complete = addedToParent.thenApply(this::completed);
|
complete = addedToParent.thenApply(this::completed);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +62,9 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
TargetObject completed(TargetObject obj) {
|
TargetObject completed(TargetObject obj) {
|
||||||
|
synchronized (records) {
|
||||||
records.remove(obj);
|
records.remove(obj);
|
||||||
|
}
|
||||||
// NB. We should already be on the clientExecutor
|
// NB. We should already be on the clientExecutor
|
||||||
Map<String, ?> attributes = obj.getCallbackAttributes();
|
Map<String, ?> attributes = obj.getCallbackAttributes();
|
||||||
if (!attributes.isEmpty()) {
|
if (!attributes.isEmpty()) {
|
||||||
|
@ -85,6 +90,11 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
addedToParent.cancel(false);
|
addedToParent.cancel(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
addedToParent.cancel(false);
|
||||||
|
complete.cancel(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final DebuggerModelListener listener;
|
private final DebuggerModelListener listener;
|
||||||
|
@ -92,6 +102,8 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
private final Map<TargetObject, ObjectRecord> records = new HashMap<>();
|
private final Map<TargetObject, ObjectRecord> records = new HashMap<>();
|
||||||
private CompletableFuture<Void> lastEvent = AsyncUtils.NIL;
|
private CompletableFuture<Void> lastEvent = AsyncUtils.NIL;
|
||||||
|
|
||||||
|
private volatile boolean disposed = false;
|
||||||
|
|
||||||
public DebuggerCallbackReorderer(DebuggerModelListener listener) {
|
public DebuggerCallbackReorderer(DebuggerModelListener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
@ -107,34 +119,57 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void catastrophic(Throwable t) {
|
public void catastrophic(Throwable t) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
listener.catastrophic(t);
|
listener.catastrophic(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void modelClosed(DebuggerModelClosedReason reason) {
|
public void modelClosed(DebuggerModelClosedReason reason) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
listener.modelClosed(reason);
|
listener.modelClosed(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void modelOpened() {
|
public void modelOpened() {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
listener.modelOpened();
|
listener.modelOpened();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void modelStateChanged() {
|
public void modelStateChanged() {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
listener.modelStateChanged();
|
listener.modelStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void created(TargetObject object) {
|
public void created(TargetObject object) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
//System.err.println("created object='" + object.getJoinedPath(".") + "'");
|
//System.err.println("created object='" + object.getJoinedPath(".") + "'");
|
||||||
|
synchronized (records) {
|
||||||
records.put(object, new ObjectRecord(object));
|
records.put(object, new ObjectRecord(object));
|
||||||
|
}
|
||||||
defensive(() -> listener.created(object), "created");
|
defensive(() -> listener.created(object), "created");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||||
ObjectRecord remove = records.remove(object);
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ObjectRecord remove;
|
||||||
|
synchronized (records) {
|
||||||
|
remove = records.remove(object);
|
||||||
|
}
|
||||||
if (remove != null) {
|
if (remove != null) {
|
||||||
remove.removed();
|
remove.removed();
|
||||||
}
|
}
|
||||||
|
@ -143,16 +178,27 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void rootAdded(TargetObject root) {
|
public void rootAdded(TargetObject root) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
defensive(() -> listener.rootAdded(root), "rootAdded");
|
defensive(() -> listener.rootAdded(root), "rootAdded");
|
||||||
|
synchronized (records) {
|
||||||
records.get(root).added();
|
records.get(root).added();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void attributesChanged(TargetObject object, Collection<String> removed,
|
public void attributesChanged(TargetObject object, Collection<String> removed,
|
||||||
Map<String, ?> added) {
|
Map<String, ?> added) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
//System.err.println("attributesChanged object=" + object.getJoinedPath(".") + ",removed=" +
|
//System.err.println("attributesChanged object=" + object.getJoinedPath(".") + ",removed=" +
|
||||||
// removed + ",added=" + added);
|
// removed + ",added=" + added);
|
||||||
ObjectRecord record = records.get(object);
|
ObjectRecord record;
|
||||||
|
synchronized (records) {
|
||||||
|
record = records.get(object);
|
||||||
|
}
|
||||||
if (record == null) {
|
if (record == null) {
|
||||||
defensive(() -> listener.attributesChanged(object, removed, added),
|
defensive(() -> listener.attributesChanged(object, removed, added),
|
||||||
"attributesChanged");
|
"attributesChanged");
|
||||||
|
@ -164,7 +210,10 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
if (val instanceof TargetObject) {
|
if (val instanceof TargetObject) {
|
||||||
TargetObject obj = (TargetObject) val;
|
TargetObject obj = (TargetObject) val;
|
||||||
if (!PathUtils.isLink(object.getPath(), ent.getKey(), obj.getPath())) {
|
if (!PathUtils.isLink(object.getPath(), ent.getKey(), obj.getPath())) {
|
||||||
ObjectRecord rec = records.get(obj);
|
ObjectRecord rec;
|
||||||
|
synchronized (records) {
|
||||||
|
rec = records.get(obj);
|
||||||
|
}
|
||||||
if (rec != null) {
|
if (rec != null) {
|
||||||
rec.added();
|
rec.added();
|
||||||
}
|
}
|
||||||
|
@ -176,9 +225,15 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
@Override
|
@Override
|
||||||
public void elementsChanged(TargetObject object, Collection<String> removed,
|
public void elementsChanged(TargetObject object, Collection<String> removed,
|
||||||
Map<String, ? extends TargetObject> added) {
|
Map<String, ? extends TargetObject> added) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
//System.err.println("elementsChanged object=" + object.getJoinedPath(".") + ",removed=" +
|
//System.err.println("elementsChanged object=" + object.getJoinedPath(".") + ",removed=" +
|
||||||
// removed + ",added=" + added);
|
// removed + ",added=" + added);
|
||||||
ObjectRecord record = records.get(object);
|
ObjectRecord record;
|
||||||
|
synchronized (records) {
|
||||||
|
record = records.get(object);
|
||||||
|
}
|
||||||
if (record == null) {
|
if (record == null) {
|
||||||
defensive(() -> listener.elementsChanged(object, removed, added), "elementsChanged");
|
defensive(() -> listener.elementsChanged(object, removed, added), "elementsChanged");
|
||||||
}
|
}
|
||||||
|
@ -187,7 +242,10 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
//System.err.println(" " + ent.getKey());
|
//System.err.println(" " + ent.getKey());
|
||||||
TargetObject obj = ent.getValue();
|
TargetObject obj = ent.getValue();
|
||||||
if (!PathUtils.isElementLink(object.getPath(), ent.getKey(), obj.getPath())) {
|
if (!PathUtils.isElementLink(object.getPath(), ent.getKey(), obj.getPath())) {
|
||||||
ObjectRecord rec = records.get(obj);
|
ObjectRecord rec;
|
||||||
|
synchronized (records) {
|
||||||
|
rec = records.get(obj);
|
||||||
|
}
|
||||||
if (rec != null) {
|
if (rec != null) {
|
||||||
rec.added();
|
rec.added();
|
||||||
}
|
}
|
||||||
|
@ -195,16 +253,17 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void orderedOnObjects(Collection<TargetObject> objects, Runnable r,
|
private void orderedOnObjects(Collection<TargetObject> objects, Runnable r, String cb) {
|
||||||
String cb) {
|
|
||||||
AsyncFence fence = new AsyncFence();
|
AsyncFence fence = new AsyncFence();
|
||||||
fence.include(lastEvent);
|
fence.include(lastEvent);
|
||||||
|
synchronized (records) {
|
||||||
for (TargetObject obj : objects) {
|
for (TargetObject obj : objects) {
|
||||||
ObjectRecord record = records.get(obj);
|
ObjectRecord record = records.get(obj);
|
||||||
if (record != null) {
|
if (record != null) {
|
||||||
fence.include(record.complete);
|
fence.include(record.complete);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
lastEvent = fence.ready().thenAccept(__ -> {
|
lastEvent = fence.ready().thenAccept(__ -> {
|
||||||
defensive(r, cb);
|
defensive(r, cb);
|
||||||
}).exceptionally(ex -> {
|
}).exceptionally(ex -> {
|
||||||
|
@ -216,6 +275,9 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
@Override
|
@Override
|
||||||
public void breakpointHit(TargetObject container, TargetObject trapped, TargetStackFrame frame,
|
public void breakpointHit(TargetObject container, TargetObject trapped, TargetStackFrame frame,
|
||||||
TargetBreakpointSpec spec, TargetBreakpointLocation breakpoint) {
|
TargetBreakpointSpec spec, TargetBreakpointLocation breakpoint) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
List<TargetObject> args = frame == null
|
List<TargetObject> args = frame == null
|
||||||
? List.of(container, trapped, spec, breakpoint)
|
? List.of(container, trapped, spec, breakpoint)
|
||||||
: List.of(container, trapped, frame, spec, breakpoint);
|
: List.of(container, trapped, frame, spec, breakpoint);
|
||||||
|
@ -226,6 +288,9 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void consoleOutput(TargetObject console, Channel channel, byte[] data) {
|
public void consoleOutput(TargetObject console, Channel channel, byte[] data) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
orderedOnObjects(List.of(console), () -> {
|
orderedOnObjects(List.of(console), () -> {
|
||||||
listener.consoleOutput(console, channel, data);
|
listener.consoleOutput(console, channel, data);
|
||||||
}, "consoleOutput");
|
}, "consoleOutput");
|
||||||
|
@ -246,6 +311,9 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
@Override
|
@Override
|
||||||
public void event(TargetObject object, TargetThread eventThread, TargetEventType type,
|
public void event(TargetObject object, TargetThread eventThread, TargetEventType type,
|
||||||
String description, List<Object> parameters) {
|
String description, List<Object> parameters) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
List<TargetObject> objs = eventThread == null
|
List<TargetObject> objs = eventThread == null
|
||||||
? List.of(object)
|
? List.of(object)
|
||||||
: List.of(object, eventThread);
|
: List.of(object, eventThread);
|
||||||
|
@ -256,6 +324,9 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidateCacheRequested(TargetObject object) {
|
public void invalidateCacheRequested(TargetObject object) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
orderedOnObjects(List.of(object), () -> {
|
orderedOnObjects(List.of(object), () -> {
|
||||||
listener.invalidateCacheRequested(object);
|
listener.invalidateCacheRequested(object);
|
||||||
}, "invalidateCacheRequested");
|
}, "invalidateCacheRequested");
|
||||||
|
@ -264,6 +335,9 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
@Override
|
@Override
|
||||||
public void memoryReadError(TargetObject memory, AddressRange range,
|
public void memoryReadError(TargetObject memory, AddressRange range,
|
||||||
DebuggerMemoryAccessException e) {
|
DebuggerMemoryAccessException e) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
orderedOnObjects(List.of(memory), () -> {
|
orderedOnObjects(List.of(memory), () -> {
|
||||||
listener.memoryReadError(memory, range, e);
|
listener.memoryReadError(memory, range, e);
|
||||||
}, "invalidateCacheRequested");
|
}, "invalidateCacheRequested");
|
||||||
|
@ -271,6 +345,9 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void memoryUpdated(TargetObject memory, Address address, byte[] data) {
|
public void memoryUpdated(TargetObject memory, Address address, byte[] data) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
orderedOnObjects(List.of(memory), () -> {
|
orderedOnObjects(List.of(memory), () -> {
|
||||||
listener.memoryUpdated(memory, address, data);
|
listener.memoryUpdated(memory, address, data);
|
||||||
}, "invalidateCacheRequested");
|
}, "invalidateCacheRequested");
|
||||||
|
@ -278,8 +355,23 @@ public class DebuggerCallbackReorderer implements DebuggerModelListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registersUpdated(TargetObject bank, Map<String, byte[]> updates) {
|
public void registersUpdated(TargetObject bank, Map<String, byte[]> updates) {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
orderedOnObjects(List.of(bank), () -> {
|
orderedOnObjects(List.of(bank), () -> {
|
||||||
listener.registersUpdated(bank, updates);
|
listener.registersUpdated(bank, updates);
|
||||||
}, "invalidateCacheRequested");
|
}, "invalidateCacheRequested");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
disposed = true;
|
||||||
|
Set<ObjectRecord> volRecs;
|
||||||
|
synchronized (records) {
|
||||||
|
volRecs = Set.copyOf(records.values());
|
||||||
|
records.clear();
|
||||||
|
}
|
||||||
|
for (ObjectRecord rec : volRecs) {
|
||||||
|
rec.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
import com.google.common.collect.*;
|
import com.google.common.collect.*;
|
||||||
|
|
||||||
|
import ghidra.framework.model.DomainObjectClosedListener;
|
||||||
|
import ghidra.framework.model.DomainObjectException;
|
||||||
import ghidra.program.model.address.*;
|
import ghidra.program.model.address.*;
|
||||||
import ghidra.trace.model.Trace;
|
import ghidra.trace.model.Trace;
|
||||||
import ghidra.trace.model.Trace.TraceSnapshotChangeType;
|
import ghidra.trace.model.Trace.TraceSnapshotChangeType;
|
||||||
|
@ -29,6 +31,7 @@ import ghidra.trace.model.program.TraceProgramView;
|
||||||
import ghidra.trace.model.time.*;
|
import ghidra.trace.model.time.*;
|
||||||
import ghidra.util.*;
|
import ghidra.util.*;
|
||||||
import ghidra.util.datastruct.ListenerSet;
|
import ghidra.util.datastruct.ListenerSet;
|
||||||
|
import ghidra.util.exception.ClosedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes and tracks the "viewport" resulting from forking patterns encoded in snapshot schedules
|
* Computes and tracks the "viewport" resulting from forking patterns encoded in snapshot schedules
|
||||||
|
@ -46,7 +49,8 @@ import ghidra.util.datastruct.ListenerSet;
|
||||||
* optimization.
|
* optimization.
|
||||||
*/
|
*/
|
||||||
public class DefaultTraceTimeViewport implements TraceTimeViewport {
|
public class DefaultTraceTimeViewport implements TraceTimeViewport {
|
||||||
protected class ForSnapshotsListener extends TraceDomainObjectListener {
|
protected class ForSnapshotsListener extends TraceDomainObjectListener
|
||||||
|
implements DomainObjectClosedListener {
|
||||||
{
|
{
|
||||||
listenFor(TraceSnapshotChangeType.ADDED, this::snapshotAdded);
|
listenFor(TraceSnapshotChangeType.ADDED, this::snapshotAdded);
|
||||||
listenFor(TraceSnapshotChangeType.CHANGED, this::snapshotChanged);
|
listenFor(TraceSnapshotChangeType.CHANGED, this::snapshotChanged);
|
||||||
|
@ -80,8 +84,14 @@ public class DefaultTraceTimeViewport implements TraceTimeViewport {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void domainObjectClosed() {
|
||||||
|
trace.removeListener(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected final Trace trace;
|
||||||
protected final TraceTimeManager timeManager;
|
protected final TraceTimeManager timeManager;
|
||||||
protected final List<Range<Long>> ordered = new ArrayList<>();
|
protected final List<Range<Long>> ordered = new ArrayList<>();
|
||||||
protected final RangeSet<Long> spanSet = TreeRangeSet.create();
|
protected final RangeSet<Long> spanSet = TreeRangeSet.create();
|
||||||
|
@ -91,7 +101,9 @@ public class DefaultTraceTimeViewport implements TraceTimeViewport {
|
||||||
protected long snap;
|
protected long snap;
|
||||||
|
|
||||||
public DefaultTraceTimeViewport(Trace trace) {
|
public DefaultTraceTimeViewport(Trace trace) {
|
||||||
|
this.trace = trace;
|
||||||
this.timeManager = trace.getTimeManager();
|
this.timeManager = trace.getTimeManager();
|
||||||
|
trace.addCloseListener(listener);
|
||||||
trace.addListener(listener);
|
trace.addListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,17 +118,16 @@ public class ListenerMap<K, P, V extends P> {
|
||||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||||
//Msg.debug(this, "Queuing invocation: " + method.getName() + " @" +
|
//Msg.debug(this, "Queuing invocation: " + method.getName() + " @" +
|
||||||
// System.identityHashCode(executor));
|
// System.identityHashCode(executor));
|
||||||
|
// Listener adds/removes need to take immediate effect, even with queued events
|
||||||
|
executor.execute(() -> {
|
||||||
Collection<V> listenersVolatile;
|
Collection<V> listenersVolatile;
|
||||||
Set<ListenerMap<?, ? extends P, ?>> chainedVolatile;
|
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
listenersVolatile = map.values();
|
listenersVolatile = map.values();
|
||||||
chainedVolatile = chained;
|
|
||||||
}
|
}
|
||||||
for (V l : listenersVolatile) {
|
for (V l : listenersVolatile) {
|
||||||
if (!ext.isAssignableFrom(l.getClass())) {
|
if (!ext.isAssignableFrom(l.getClass())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
executor.execute(() -> {
|
|
||||||
//Msg.debug(this,
|
//Msg.debug(this,
|
||||||
// "Invoking: " + method.getName() + " @" + System.identityHashCode(executor));
|
// "Invoking: " + method.getName() + " @" + System.identityHashCode(executor));
|
||||||
try {
|
try {
|
||||||
|
@ -141,9 +140,13 @@ public class ListenerMap<K, P, V extends P> {
|
||||||
catch (Throwable e) {
|
catch (Throwable e) {
|
||||||
reportError(l, e);
|
reportError(l, e);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
for (ListenerMap<?, ? extends P, ?> c : chained) {
|
});
|
||||||
|
Set<ListenerMap<?, ? extends P, ?>> chainedVolatile;
|
||||||
|
synchronized (lock) {
|
||||||
|
chainedVolatile = chained;
|
||||||
|
}
|
||||||
|
for (ListenerMap<?, ? extends P, ?> c : chainedVolatile) {
|
||||||
// Invocation will check if assignable
|
// Invocation will check if assignable
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
T l = ((ListenerMap<?, P, ?>) c).fire(ext);
|
T l = ((ListenerMap<?, P, ?>) c).fire(ext);
|
||||||
|
|
|
@ -16,13 +16,14 @@
|
||||||
package ghidra.util.datastruct;
|
package ghidra.util.datastruct;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import ghidra.util.datastruct.ListenerMap;
|
|
||||||
|
|
||||||
public class ListenerMapTest {
|
public class ListenerMapTest {
|
||||||
public interface DummyListener {
|
public interface DummyListener {
|
||||||
void event(String e);
|
void event(String e);
|
||||||
|
@ -65,6 +66,65 @@ public class ListenerMapTest {
|
||||||
assertEquals("EventC", ar3.get());
|
assertEquals("EventC", ar3.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void waitEvents(Executor executor) throws Throwable {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
}, executor).get(1000, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddsRemovesImmediatelyEffective() throws Throwable {
|
||||||
|
Executor executor = Executors.newSingleThreadExecutor();
|
||||||
|
CompletableFuture.runAsync(() -> Thread.currentThread().setName("ExecutorThread"), executor)
|
||||||
|
.get();
|
||||||
|
ListenerMap<String, DummyListener, DummyListener> listeners =
|
||||||
|
new ListenerMap<>(DummyListener.class, executor);
|
||||||
|
|
||||||
|
Map<String, CompletableFuture<?>> stalls = Map.ofEntries(
|
||||||
|
Map.entry("StallA", new CompletableFuture<>()),
|
||||||
|
Map.entry("StallB", new CompletableFuture<>()),
|
||||||
|
Map.entry("StallD", new CompletableFuture<>()));
|
||||||
|
AtomicReference<String> ar1 = new AtomicReference<>();
|
||||||
|
DummyListener l1 = s -> {
|
||||||
|
CompletableFuture<?> stall = stalls.get(s);
|
||||||
|
if (stall != null) {
|
||||||
|
try {
|
||||||
|
stall.get();
|
||||||
|
}
|
||||||
|
catch (InterruptedException | ExecutionException e) {
|
||||||
|
// Nothing I really can do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ar1.set(s);
|
||||||
|
};
|
||||||
|
AtomicReference<String> ar2 = new AtomicReference<>();
|
||||||
|
DummyListener l2 = ar2::set;
|
||||||
|
|
||||||
|
listeners.put("Key1", l1);
|
||||||
|
ar1.set("None");
|
||||||
|
listeners.fire.event("StallA");
|
||||||
|
assertEquals("None", ar1.get());
|
||||||
|
stalls.get("StallA").complete(null);
|
||||||
|
waitEvents(executor);
|
||||||
|
assertEquals("StallA", ar1.get());
|
||||||
|
|
||||||
|
// NB. It's the the fire timeline that matters, but the completion timeline
|
||||||
|
listeners.fire.event("StallB");
|
||||||
|
listeners.fire.event("EventC");
|
||||||
|
listeners.put("Key2", l2);
|
||||||
|
stalls.get("StallB").complete(null);
|
||||||
|
waitEvents(executor);
|
||||||
|
assertEquals("EventC", ar1.get());
|
||||||
|
assertEquals("EventC", ar2.get());
|
||||||
|
|
||||||
|
listeners.fire.event("StallD");
|
||||||
|
listeners.fire.event("EventE");
|
||||||
|
listeners.remove("Key2");
|
||||||
|
stalls.get("StallD").complete(null);
|
||||||
|
waitEvents(executor);
|
||||||
|
assertEquals("EventE", ar1.get());
|
||||||
|
assertNotEquals("EventE", ar2.get());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testContinuesOnError() {
|
public void testContinuesOnError() {
|
||||||
ListenerMap<String, DummyListener, DummyListener> listeners =
|
ListenerMap<String, DummyListener, DummyListener> listeners =
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue