GP-704: Converting models to a push-centric comm pattern.

This commit is contained in:
Dan 2021-02-23 10:57:51 -05:00
parent dd37995833
commit 5bb6f95a84
95 changed files with 2348 additions and 1635 deletions

View file

@ -21,7 +21,7 @@ import ghidra.dbg.target.TargetEnvironment;
public interface DbgModelTargetEnvironment<T extends TargetEnvironment<T>>
extends DbgModelTargetObject, TargetEnvironment<T> {
public void refresh();
public void refreshInternal();
@Override
public default String getArchitecture() {

View file

@ -20,7 +20,7 @@ import ghidra.dbg.target.TargetEnvironment;
public interface DbgModelTargetEnvironmentEx
extends DbgModelTargetObject, TargetEnvironment<DbgModelTargetEnvironmentEx> {
public void refresh();
public void refreshInternal();
/*
@Override

View file

@ -23,11 +23,13 @@ import agent.dbgeng.dbgeng.DebugClient.DebugStatus;
import agent.dbgeng.manager.impl.DbgManagerImpl;
import agent.dbgeng.model.AbstractDbgModel;
import ghidra.dbg.agent.InvalidatableTargetObjectIf;
import ghidra.dbg.agent.SpiTargetObject;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetObject.TargetObjectListener;
import ghidra.dbg.util.CollectionUtils.Delta;
import ghidra.util.datastruct.ListenerSet;
public interface DbgModelTargetObject extends TargetObject, InvalidatableTargetObjectIf {
public interface DbgModelTargetObject extends SpiTargetObject, InvalidatableTargetObjectIf {
@Override
public AbstractDbgModel getModel();

View file

@ -60,6 +60,7 @@ public class DbgModelImpl extends AbstractDbgModel {
s.add();
DbgModelTargetSessionContainer sessions = root.sessions;
this.session = (DbgModelTargetSessionImpl) sessions.getTargetSession(s);
addModelRoot(root);
}
@Override
@ -110,7 +111,7 @@ public class DbgModelImpl extends AbstractDbgModel {
public CompletableFuture<Void> close() {
try {
terminate();
return CompletableFuture.completedFuture(null);
return super.close();
}
catch (Throwable t) {
return CompletableFuture.failedFuture(t);

View file

@ -56,8 +56,8 @@ public class DbgModelTargetObjectImpl extends DefaultTargetObject<TargetObject,
}
@Override
protected void doInvalidate(String reason) {
super.doInvalidate(reason);
protected void doInvalidate(TargetObject branch, String reason) {
super.doInvalidate(branch, reason);
getManager().removeStateListener(accessListener);
}

View file

@ -30,12 +30,27 @@ import ghidra.dbg.target.*;
import ghidra.dbg.target.schema.*;
import ghidra.dbg.util.PathUtils;
@TargetObjectSchemaInfo(name = "Debugger", elements = { //
@TargetObjectSchemaInfo(
name = "Debugger",
elements = { //
@TargetElementType(type = Void.class) //
}, attributes = { //
@TargetAttributeType(name = "Available", type = DbgModelTargetAvailableContainerImpl.class, required = true, fixed = true), //
@TargetAttributeType(name = "Connectors", type = DbgModelTargetConnectorContainerImpl.class, required = true, fixed = true), //
@TargetAttributeType(name = "Sessions", type = DbgModelTargetSessionContainerImpl.class, required = true, fixed = true), //
},
attributes = { //
@TargetAttributeType(
name = "Available",
type = DbgModelTargetAvailableContainerImpl.class,
required = true,
fixed = true), //
@TargetAttributeType(
name = "Connectors",
type = DbgModelTargetConnectorContainerImpl.class,
required = true,
fixed = true), //
@TargetAttributeType(
name = "Sessions",
type = DbgModelTargetSessionContainerImpl.class,
required = true,
fixed = true), //
@TargetAttributeType(type = Void.class) //
})
public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot
@ -136,12 +151,6 @@ public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot
), reason.desc());
}
//@Override
public void refresh() {
// TODO ???
System.err.println("root:refresh");
}
@Override
public TargetAccessibility getAccessibility() {
return accessibility;

View file

@ -23,10 +23,16 @@ import agent.dbgeng.model.iface2.DbgModelTargetSession;
import agent.dbgeng.model.iface2.DbgModelTargetSessionAttributes;
import ghidra.dbg.target.schema.*;
@TargetObjectSchemaInfo(name = "SessionAttributes", elements = { //
@TargetObjectSchemaInfo(
name = "SessionAttributes",
elements = { //
@TargetElementType(type = Void.class) //
}, attributes = { //
@TargetAttributeType(name = "Machine", type = DbgModelTargetSessionAttributesMachineImpl.class, fixed = true), //
},
attributes = { //
@TargetAttributeType(
name = "Machine",
type = DbgModelTargetSessionAttributesMachineImpl.class,
fixed = true), //
@TargetAttributeType(type = Void.class) //
})
public class DbgModelTargetSessionAttributesImpl extends DbgModelTargetObjectImpl
@ -75,8 +81,8 @@ public class DbgModelTargetSessionAttributesImpl extends DbgModelTargetObjectImp
*/
@Override
public void refresh() {
machineAttributes.refresh();
public void refreshInternal() {
machineAttributes.refreshInternal();
}
}

View file

@ -60,15 +60,15 @@ public class DbgModelTargetSessionAttributesMachineImpl extends DbgModelTargetOb
@Override
public void sessionAdded(DbgSession session, DbgCause cause) {
refresh();
refreshInternal();
}
@Override
public void processAdded(DbgProcess process, DbgCause cause) {
refresh();
refreshInternal();
}
public void refresh() {
public void refreshInternal() {
DebugControl control = getManager().getControl();
int processorType = control.getActualProcessorType();
if (processorType < 0) {

View file

@ -16,20 +16,27 @@
package agent.dbgmodel.model.impl;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.jdom.JDOMException;
import agent.dbgeng.manager.impl.DbgManagerImpl;
import agent.dbgeng.model.AbstractDbgModel;
import agent.dbgeng.model.iface2.DbgModelTargetObject;
import agent.dbgeng.model.iface2.DbgModelTargetSession;
import agent.dbgmodel.manager.DbgManager2Impl;
import ghidra.dbg.agent.AbstractTargetObject;
import ghidra.dbg.agent.AbstractTargetObject.ProxyFactory;
import ghidra.dbg.agent.SpiTargetObject;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.target.schema.XmlSchemaContext;
import ghidra.program.model.address.*;
import utilities.util.ProxyUtilities;
public class DbgModel2Impl extends AbstractDbgModel {
public class DbgModel2Impl extends AbstractDbgModel
implements ProxyFactory<List<Class<? extends TargetObject>>> {
// TODO: Need some minimal memory modeling per architecture on the model/agent side.
// The model must convert to and from Ghidra's address space names
protected static final String SPACE_NAME = "ram";
@ -66,6 +73,15 @@ public class DbgModel2Impl extends AbstractDbgModel {
//System.out.println(XmlSchemaContext.serialize(SCHEMA_CTX));
this.root = new DbgModel2TargetRootImpl(this, ROOT_SCHEMA);
this.completedRoot = CompletableFuture.completedFuture(root);
addModelRoot(root);
}
@Override
public SpiTargetObject createProxy(AbstractTargetObject<?> delegate,
List<Class<? extends TargetObject>> mixins) {
mixins.add(DbgModel2TargetProxy.class);
return ProxyUtilities.composeOnDelegate(DbgModelTargetObject.class,
(DbgModelTargetObject) delegate, mixins, DelegateDbgModel2TargetObject.LOOKUP);
}
@Override
@ -116,7 +132,7 @@ public class DbgModel2Impl extends AbstractDbgModel {
public CompletableFuture<Void> close() {
try {
terminate();
return CompletableFuture.completedFuture(null);
return super.close();
}
catch (Throwable t) {
return CompletableFuture.failedFuture(t);

View file

@ -59,8 +59,6 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
protected String DBG_PROMPT = "(kd2)"; // Used by DbgModelTargetEnvironment
protected boolean fireAttributesChanged = false;
protected static String indexObject(ModelObject obj) {
return obj.getSearchKey();
}
@ -84,6 +82,12 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
super(model, parent, name, typeHint, schema);
}
public <I> DbgModel2TargetObjectImpl(ProxyFactory<I> proxyFactory, I proxyInfo,
AbstractDbgModel model, TargetObject parent, String name,
String typeHint) {
super(proxyFactory, proxyInfo, model, parent, name, typeHint);
}
@Override
public DbgModel2Impl getModel() {
return (DbgModel2Impl) super.getModel();
@ -127,7 +131,6 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
@Override
public CompletableFuture<Void> requestAttributes(boolean refresh) {
fireAttributesChanged = true;
Map<String, Object> nmap = new HashMap<>();
return requestNativeAttributes().thenCompose(map -> {
synchronized (attributes) {
@ -418,13 +421,10 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
}
doInvalidateAttributes(delta.removed, reason);
if (parent == null && !delta.isEmpty()) {
if (!delta.isEmpty()) {
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
return delta;
}
if (fireAttributesChanged && !delta.isEmpty()) {
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
}
return delta;
}
@ -439,14 +439,9 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
}
doInvalidateAttributes(delta.removed, reason);
if (fireAttributesChanged && !delta.isEmpty()) {
if (!delta.isEmpty()) {
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
}
return delta;
}
@Override
protected boolean enforcesStrictSchema() {
return false;
}
}

View file

@ -109,7 +109,6 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot
}
if (doFire) {
this.focus = sel;
fireAttributesChanged = true;
changeAttributes(List.of(), List.of(), Map.of( //
TargetFocusScope.FOCUS_ATTRIBUTE_NAME, focus //
), "Focus changed");
@ -492,12 +491,6 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot
});
}
//@Override
public void refresh() {
// TODO ???
System.err.println("root:refresh");
}
@Override
public TargetAccessibility getAccessibility() {
return accessibility;

View file

@ -28,13 +28,11 @@ import agent.dbgeng.model.iface2.*;
import agent.dbgmodel.dbgmodel.main.ModelObject;
import agent.dbgmodel.jna.dbgmodel.DbgModelNative.ModelObjectKind;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
import ghidra.dbg.util.PathUtils;
import ghidra.util.datastruct.ListenerSet;
import utilities.util.ProxyUtilities;
public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl implements //
DbgModelTargetAccessConditioned<DelegateDbgModel2TargetObject>, //
@ -160,7 +158,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
mixins.add(mixin);
}
}
return new DelegateDbgModel2TargetObject(model, parent, key, object, mixins).proxy;
return new DelegateDbgModel2TargetObject(model, parent, key, object, mixins).getProxy();
}
protected static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
@ -170,8 +168,6 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
protected final ProxyState state;
protected final Cleanable cleanable;
private final DbgModelTargetObject proxy;
private boolean breakpointEnabled;
private final ListenerSet<TargetBreakpointAction> breakpointActions =
new ListenerSet<>(TargetBreakpointAction.class) {
@ -189,15 +185,12 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
public DelegateDbgModel2TargetObject(DbgModel2Impl model, DbgModelTargetObject parent,
String key, ModelObject modelObject, List<Class<? extends TargetObject>> mixins) {
super(model, parent.getProxy(), key, getHintForObject(modelObject));
super(model, mixins, model, parent.getProxy(), key, getHintForObject(modelObject));
this.state = new ProxyState(model, modelObject);
this.cleanable = CLEANER.register(this, state);
getManager().addStateListener(accessListener);
mixins.add(DbgModel2TargetProxy.class);
this.proxy =
ProxyUtilities.composeOnDelegate(DbgModelTargetObject.class, this, mixins, LOOKUP);
if (proxy instanceof DbgEventsListener) {
model.getManager().addEventsListener((DbgEventsListener) proxy);
}
@ -216,11 +209,6 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
return delegate;
}
@Override
public <T extends TypedTargetObject<T>> T as(Class<T> iface) {
return DebuggerObjectModel.requireIface(iface, proxy, getPath());
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public CompletableFuture<? extends DelegateDbgModel2TargetObject> fetch() {
@ -228,8 +216,8 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
}
@Override
public TargetObject getProxy() {
return proxy;
public DbgModelTargetObject getProxy() {
return (DbgModelTargetObject) proxy;
}
@SuppressWarnings("unchecked")
@ -311,7 +299,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
return;
}
if (proxy instanceof DbgModelTargetRegister || proxy instanceof DbgModelTargetStackFrame) {
DbgThread thread = proxy.getParentThread().getThread();
DbgThread thread = getProxy().getParentThread().getThread();
if (thread.equals(getManager().getEventThread())) {
requestAttributes(true);
}
@ -354,6 +342,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
}
}
@Override
public DelegateDbgModel2TargetObject getDelegate() {
return this;
}
@ -388,6 +377,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
this.breakpointEnabled = enabled;
}
@Override
public ListenerSet<TargetBreakpointAction> getActions() {
return breakpointActions;
}

View file

@ -70,6 +70,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
this.completedSession = CompletableFuture.completedFuture(session);
gdb.addStateListener(gdbExitListener);
addModelRoot(session);
}
@Override
@ -136,7 +137,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
public void terminate() throws IOException {
listeners.fire.modelClosed(DebuggerModelClosedReason.NORMAL);
session.invalidateSubtree("GDB is terminating");
session.invalidateSubtree(session, "GDB is terminating");
gdb.terminate();
}
@ -154,7 +155,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
public CompletableFuture<Void> close() {
try {
terminate();
return AsyncUtils.NIL;
return super.close();
}
catch (Throwable t) {
return CompletableFuture.failedFuture(t);

View file

@ -59,7 +59,7 @@ public class GdbModelTargetEnvironment
VISIBLE_ENDIAN_ATTRIBUTE_NAME, endian,
UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED),
"Initialized");
refresh();
refreshInternal();
}
protected CompletableFuture<Void> refreshArchitecture() {
@ -156,7 +156,7 @@ public class GdbModelTargetEnvironment
});
}
protected CompletableFuture<Void> refresh() {
protected CompletableFuture<Void> refreshInternal() {
AsyncFence fence = new AsyncFence();
fence.include(refreshArchitecture());
fence.include(refreshOS());

View file

@ -220,9 +220,9 @@ public class GdbModelTargetInferior
protected CompletableFuture<Void> inferiorStarted(Long pid) {
AsyncFence fence = new AsyncFence();
fence.include(modules.refresh());
fence.include(registers.refresh());
fence.include(environment.refresh());
fence.include(modules.refreshInternal());
fence.include(registers.resync());
fence.include(environment.refreshInternal());
return fence.ready().thenAccept(__ -> {
if (pid != null) {
changeAttributes(List.of(), Map.of( //

View file

@ -29,12 +29,14 @@ import ghidra.dbg.target.TargetExecutionStateful;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.schema.TargetAttributeType;
import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
import ghidra.util.Msg;
import ghidra.util.datastruct.WeakValueHashMap;
@TargetObjectSchemaInfo(name = "InferiorContainer", attributes = {
@TargetObjectSchemaInfo(
name = "InferiorContainer",
attributes = {
@TargetAttributeType(type = Void.class)
}, canonicalContainer = true)
},
canonicalContainer = true)
public class GdbModelTargetInferiorContainer
extends DefaultTargetObject<GdbModelTargetInferior, GdbModelTargetSession>
implements GdbEventsListenerAdapter {
@ -70,7 +72,7 @@ public class GdbModelTargetInferiorContainer
" started " + inf.getExecutable() + " pid=" + inf.getPid(),
List.of(inferior));
}).exceptionally(ex -> {
Msg.error(this, "Could not notify inferior started", ex);
impl.reportError(this, "Could not notify inferior started", ex);
return null;
});
}

View file

@ -32,9 +32,12 @@ import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
import ghidra.lifecycle.Internal;
import ghidra.util.Msg;
@TargetObjectSchemaInfo(name = "ModuleContainer", attributes = {
@TargetObjectSchemaInfo(
name = "ModuleContainer",
attributes = {
@TargetAttributeType(type = Void.class)
}, canonicalContainer = true)
},
canonicalContainer = true)
public class GdbModelTargetModuleContainer
extends DefaultTargetObject<GdbModelTargetModule, GdbModelTargetInferior>
implements TargetModuleContainer<GdbModelTargetModuleContainer> {
@ -118,7 +121,7 @@ public class GdbModelTargetModuleContainer
return modulesByName.get(name);
}
public CompletableFuture<?> refresh() {
public CompletableFuture<?> refreshInternal() {
if (!isObserved()) {
return AsyncUtils.NIL;
}

View file

@ -33,9 +33,12 @@ import ghidra.dbg.target.schema.*;
import ghidra.dbg.util.PathUtils;
import ghidra.util.Msg;
@TargetObjectSchemaInfo(name = "Session", elements = {
@TargetObjectSchemaInfo(
name = "Session",
elements = {
@TargetElementType(type = Void.class)
}, attributes = {
},
attributes = {
@TargetAttributeType(type = Void.class)
})
public class GdbModelTargetSession extends DefaultTargetModelRoot implements //
@ -91,12 +94,18 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements //
return inferiors;
}
@TargetAttributeType(name = GdbModelTargetAvailableContainer.NAME, required = true, fixed = true)
@TargetAttributeType(
name = GdbModelTargetAvailableContainer.NAME,
required = true,
fixed = true)
public GdbModelTargetAvailableContainer getAvailable() {
return available;
}
@TargetAttributeType(name = GdbModelTargetBreakpointContainer.NAME, required = true, fixed = true)
@TargetAttributeType(
name = GdbModelTargetBreakpointContainer.NAME,
required = true,
fixed = true)
public GdbModelTargetBreakpointContainer getBreakpoints() {
return breakpoints;
}
@ -135,7 +144,6 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements //
throw new AssertionError();
}
listeners.fire(TargetInterpreterListener.class).consoleOutput(this, dbgChannel, out);
}
@Override

View file

@ -435,9 +435,9 @@ public abstract class AbstractModelForGdbTest
AllTargetObjectListenerAdapter l = new AllTargetObjectListenerAdapter() {
@Override
public void consoleOutput(TargetObject interpreter, Channel channel,
String out) {
byte[] out) {
Msg.debug(this, "Got " + channel + " output: " + out);
lastOut.set(out, null);
lastOut.set(new String(out), null);
}
};

View file

@ -110,7 +110,7 @@ public class GadpForGdbTest extends AbstractModelForGdbTest {
catch (AssertionError e) {
assertEquals(
"Client implementation sent an invalid request: " +
"BAD_REQUEST: Unrecognized request: ERROR_REQUEST",
"EC_BAD_REQUEST: Unrecognized request: ERROR_REQUEST",
e.getMessage());
}
}

View file

@ -18,27 +18,24 @@ package ghidra.dbg.gadp.client;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.ref.Cleaner.Cleanable;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.agent.DefaultTargetObject;
import ghidra.dbg.gadp.GadpRegistry;
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.protocol.Gadp.EventNotification.EvtCase;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.memory.CachedMemory;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetAccessConditioned;
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibility;
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.util.CollectionUtils.Delta;
import ghidra.dbg.util.PathUtils;
import ghidra.program.model.address.AddressSpace;
import ghidra.util.Msg;
import ghidra.util.datastruct.ListenerSet;
@ -47,7 +44,9 @@ import utilities.util.ProxyUtilities;
/**
* This class is meant to be used as a delegate to a composed proxy
*/
public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
public class DelegateGadpClientTargetObject
extends DefaultTargetObject<GadpClientTargetObject, GadpClientTargetObject>
implements GadpClientTargetObject {
protected abstract static class GadpHandlerMap<A extends Annotation, K> {
protected final Class<A> annotationType;
protected final Class<?>[] paramClasses;
@ -148,75 +147,38 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
}
}
protected static class ProxyState implements Runnable {
protected final GadpClient client;
protected final List<String> path;
protected boolean valid = true;
public ProxyState(GadpClient client, List<String> path) {
this.client = client;
this.path = path;
}
@Override
public void run() {
if (!valid) {
return;
}
client.unsubscribe(path).exceptionally(e -> {
Msg.error(this, "Could not unsubscribe from " + path + ": " + e);
return null;
});
}
}
protected static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
protected static final Map<Set<Class<? extends TargetObject>>, GadpEventHandlerMap> EVENT_HANDLER_MAPS_BY_COMPOSITION =
new HashMap<>();
protected static final Map<Set<Class<? extends TargetObject>>, GadpAttributeChangeCallbackMap> ATTRIBUTE_CHANGE_CALLBACKS_MAPS_BY_COMPOSITION =
new HashMap<>();
protected static GadpClientTargetObject makeModelProxy(GadpClient client, List<String> path,
String typeHint, List<String> ifaceNames) {
protected static GadpClientTargetObject makeModelProxy(GadpClient client,
GadpClientTargetObject parent, String key, String typeHint, List<String> ifaceNames) {
List<Class<? extends TargetObject>> ifaces = TargetObject.getInterfacesByName(ifaceNames);
List<Class<? extends TargetObject>> mixins = GadpRegistry.getMixins(ifaces);
return new DelegateGadpClientTargetObject(client, path, typeHint, ifaceNames, ifaces,
mixins).proxy;
TargetObjectSchema schema =
parent == null ? client.getRootSchema() : parent.getSchema().getChildSchema(key);
return new DelegateGadpClientTargetObject(client, parent, key, typeHint, schema, ifaceNames,
ifaces, mixins).getProxy();
}
protected final ProxyState state;
protected final int hash;
protected final Cleanable cleanable;
private final GadpClientTargetObject proxy;
private TargetObjectSchema schema; // lazily evaluated
private final String typeHint;
private final GadpClient client;
private final List<String> ifaceNames;
private final List<Class<? extends TargetObject>> ifaces;
private final GadpEventHandlerMap eventHandlers;
private final GadpAttributeChangeCallbackMap attributeChangeCallbacks;
protected final ListenerSet<TargetObjectListener> listeners;
// TODO: Use path element comparators?
protected final Map<String, TargetObjectRef> elements = new TreeMap<>();
// TODO: Use path element comparators?
protected final Map<String, Object> attributes = new TreeMap<>();
protected Map<AddressSpace, CachedMemory> memCache = null; // Becomes active if this is a TargetMemory
protected Map<String, byte[]> regCache = null; // Becomes active if this is a TargtRegisterBank
protected ListenerSet<TargetBreakpointAction> actions = null; // Becomes active is this is a TargetBreakpointSpec
public DelegateGadpClientTargetObject(GadpClient client, List<String> path, String typeHint,
List<String> ifaceNames, List<Class<? extends TargetObject>> ifaces,
public DelegateGadpClientTargetObject(GadpClient client, GadpClientTargetObject parent,
String key, String typeHint, TargetObjectSchema schema, List<String> ifaceNames,
List<Class<? extends TargetObject>> ifaces,
List<Class<? extends TargetObject>> mixins) {
this.listeners = new ListenerSet<>(TargetObjectListener.class, client.getClientExecutor());
this.state = new ProxyState(client, path);
this.hash = computeHashCode();
this.cleanable = GadpClient.CLEANER.register(this, state);
this.proxy = ProxyUtilities.composeOnDelegate(GadpClientTargetObject.class,
this, mixins, MethodHandles.lookup());
this.typeHint = typeHint;
super(client, mixins, client, parent, key, typeHint, schema);
this.client = client;
this.ifaceNames = ifaceNames;
this.ifaces = ifaces;
@ -229,48 +191,14 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
GadpAttributeChangeCallbackMap::new);
}
@Override
public boolean equals(Object obj) {
return doEquals(obj);
}
@Override
public int hashCode() {
return hash;
}
@Override
public String toString() {
return "<GADP TargetObject: '" + PathUtils.toString(getPath()) + "' via " +
getModel().description + ">";
}
@Override
public GadpClient getModel() {
return state.client;
return client;
}
@Override
public List<String> getProtocolID() {
return state.path;
}
@Override
public List<String> getPath() {
return state.path;
}
@Override
public TargetObjectSchema getSchema() {
if (schema == null) {
schema = getModel().getRootSchema().getSuccessorSchema(getPath());
}
return schema;
}
@Override
public String getTypeHint() {
return typeHint;
public GadpClientTargetObject getProxy() {
return (GadpClientTargetObject) super.getProxy();
}
@Override
@ -279,104 +207,32 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
}
@Override
public Collection<Class<? extends TargetObject>> getInterfaces() {
public Collection<? extends Class<? extends TargetObject>> getInterfaces() {
return ifaces;
}
@Override
public boolean isValid() {
return state.valid;
}
@Override
public Map<String, TargetObjectRef> getCachedElements() {
synchronized (this.elements) {
return Map.copyOf(elements);
}
}
@Override
public Map<String, ?> getCachedAttributes() {
synchronized (this.attributes) {
return Map.copyOf(attributes);
}
}
@Override
public Object getCachedAttribute(String name) {
synchronized (attributes) {
return attributes.get(name);
}
}
protected void putCachedProxy(String key, GadpClientTargetObject proxy) {
if (PathUtils.isIndex(key)) {
synchronized (elements) {
elements.put(PathUtils.parseIndex(key), proxy);
}
}
else {
synchronized (attributes) {
attributes.put(key, proxy);
}
}
}
protected Optional<Object> cachedChild(String key) {
/**
* TODO: Object metadata which indicates whether the attributes/elements support
* subscription (push notifications). Otherwise, if the parent is cached, GADP will assume
* the server is sending updates. If the model actually requires pulling, the GADP client
* will not know, and will instead use its (likely stale) cache.
*/
assert key != null;
if (PathUtils.isIndex(key)) {
/**
* NOTE: I do not need to check the subscription level. The level has to do with
* including object info. Having OBJECT level is sufficient to have up-to-date keys.
*/
synchronized (elements) {
return Optional.ofNullable(elements.get(PathUtils.parseIndex(key)));
}
}
assert PathUtils.isName(key);
synchronized (attributes) {
return Optional.ofNullable(attributes.get(key));
}
}
/**
* {@inheritDoc}
*
* The delegate has to override defaults which introspect on, or otherwise would leak "this".
* "this" is the delegate; we must instead operate on the proxy.
*/
@Override
public <T extends TypedTargetObject<T>> T as(Class<T> iface) {
return DebuggerObjectModel.requireIface(iface, proxy, state.path);
}
@Override
public CompletableFuture<? extends TargetObject> fetch() {
return CompletableFuture.completedFuture(proxy);
return CompletableFuture.completedFuture(getProxy());
}
@Override
public CompletableFuture<?> fetchAttribute(String name) {
if (!PathUtils.isInvocation(name)) {
return GadpClientTargetObject.super.fetchAttribute(name);
}
return state.client.fetchModelValue(PathUtils.extend(state.path, name));
public CompletableFuture<Void> resync(boolean attributes, boolean elements) {
return client.sendChecked(Gadp.ResyncRequest.newBuilder()
.setPath(GadpValueUtils.makePath(path))
.setAttributes(attributes)
.setElements(elements),
Gadp.ResyncReply.getDefaultInstance()).thenApply(rep -> null);
}
@Override
public void addListener(TargetObjectListener l) {
listeners.add(l);
protected CompletableFuture<Void> requestAttributes(boolean refresh) {
return resync(refresh, false);
}
@Override
public void removeListener(TargetObjectListener l) {
listeners.remove(l);
protected CompletableFuture<Void> requestElements(boolean refresh) {
return resync(false, refresh);
}
@Override
@ -384,34 +240,25 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
return this;
}
public void updateWithInfo(Gadp.ModelObjectInfo info) {
Map<String, TargetObjectRef> elements =
GadpValueUtils.getElementMap(this, info.getElementIndexList());
Map<String, Object> attributes =
GadpValueUtils.getAttributeMap(this, info.getAttributeList());
Delta<TargetObjectRef, TargetObjectRef> deltaE = setElements(elements);
Delta<Object, Object> deltaA = setAttributes(attributes);
fireElementsChanged(deltaE);
fireAttributesChanged(deltaA);
}
public void updateWithDelta(Gadp.ModelObjectDelta delta) {
Map<String, TargetObjectRef> elementsAdded =
GadpValueUtils.getElementMap(this, delta.getIndexAddedList());
public void updateWithDeltas(Gadp.ModelObjectDelta deltaE, Gadp.ModelObjectDelta deltaA) {
Map<String, GadpClientTargetObject> elementsAdded =
GadpValueUtils.getElementMap(this, deltaE.getAddedList());
Map<String, Object> attributesAdded =
GadpValueUtils.getAttributeMap(this, delta.getAttributeAddedList());
GadpValueUtils.getAttributeMap(this, deltaA.getAddedList());
Delta<TargetObjectRef, TargetObjectRef> deltaE =
updateElements(Delta.create(delta.getIndexRemovedList(), elementsAdded));
Delta<Object, Object> deltaA =
updateAttributes(Delta.create(delta.getAttributeRemovedList(), attributesAdded));
fireElementsChanged(deltaE);
fireAttributesChanged(deltaA);
changeElements(deltaE.getRemovedList(), List.of(), elementsAdded, "Updated");
Delta<?, ?> attrDelta =
changeAttributes(deltaA.getRemovedList(), attributesAdded, "Updated");
for (String name : attrDelta.getKeysRemoved()) {
handleAttributeChange(name, null);
}
for (Map.Entry<String, ?> a : attrDelta.added.entrySet()) {
handleAttributeChange(a.getKey(), a.getValue());
}
}
protected void handleEvent(Gadp.EventNotification notify) {
eventHandlers.handle(proxy, notify.getEvtCase(), notify);
eventHandlers.handle(getProxy(), notify.getEvtCase(), notify);
}
/**
@ -437,61 +284,11 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
* @param value the new value of the attribute
*/
protected void handleAttributeChange(String name, Object value) {
attributeChangeCallbacks.handle(proxy, name, value);
}
protected <U extends TargetObjectRef> Delta<TargetObjectRef, U> updateElements(
Delta<TargetObjectRef, U> delta) {
synchronized (this.elements) {
return delta.apply(elements);
}
}
protected <U extends TargetObjectRef> Delta<TargetObjectRef, U> setElements(
Map<String, U> elements) {
synchronized (this.elements) {
return Delta.computeAndSet(this.elements, elements, Delta.SAME);
}
}
protected <U> Delta<Object, U> updateAttributes(Delta<Object, U> delta) {
synchronized (this.attributes) {
return delta.apply(attributes, Delta.EQUAL);
}
}
protected <U> Delta<Object, U> setAttributes(Map<String, U> attributes) {
synchronized (this.attributes) {
return Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL);
}
}
protected void fireElementsChanged(Delta<?, ? extends TargetObjectRef> delta) {
if (!delta.isEmpty()) {
listeners.fire.elementsChanged(proxy, delta.getKeysRemoved(), delta.added);
}
}
protected void fireAttributesChanged(Delta<?, ?> delta) {
if (!delta.isEmpty()) {
listeners.fire.attributesChanged(proxy, delta.getKeysRemoved(), delta.added);
for (Map.Entry<String, ?> a : delta.added.entrySet()) {
handleAttributeChange(a.getKey(), a.getValue());
}
}
}
protected void doInvalidateSubtree(String reason) {
state.client.invalidateSubtree(state.path, reason);
}
protected void doInvalidate(String reason) {
state.valid = false;
listeners.fire.invalidated(proxy, reason);
attributeChangeCallbacks.handle(getProxy(), name, value);
}
protected void assertValid() {
if (!state.valid) {
if (!valid) {
throw new IllegalStateException("Object is no longer valid: " + toString());
}
}
@ -505,13 +302,13 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
public synchronized CompletableFuture<Void> invalidateCaches() {
assertValid();
doClearCaches();
return state.client.sendChecked(Gadp.CacheInvalidateRequest.newBuilder()
.setPath(GadpValueUtils.makePath(state.path)),
return client.sendChecked(Gadp.CacheInvalidateRequest.newBuilder()
.setPath(GadpValueUtils.makePath(path)),
Gadp.CacheInvalidateReply.getDefaultInstance()).thenApply(rep -> null);
}
protected synchronized CachedMemory getMemoryCache(AddressSpace space) {
GadpClientTargetMemory memory = (GadpClientTargetMemory) proxy;
GadpClientTargetMemory memory = (GadpClientTargetMemory) getProxy();
if (memCache == null) {
memCache = new HashMap<>();
}
@ -552,4 +349,10 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
}
return actions;
}
@Override
protected void doInvalidate(TargetObject branch, String reason) {
client.removeProxy(path, reason);
super.doInvalidate(branch, reason);
}
}

View file

@ -17,13 +17,11 @@ package ghidra.dbg.gadp.client;
import java.io.EOFException;
import java.io.IOException;
import java.lang.ref.Cleaner;
import java.nio.channels.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.jdom.JDOMException;
@ -33,29 +31,29 @@ import com.google.protobuf.Message;
import com.google.protobuf.ProtocolStringList;
import ghidra.async.*;
import ghidra.dbg.*;
import ghidra.dbg.DebuggerModelClosedReason;
import ghidra.dbg.agent.*;
import ghidra.dbg.agent.AbstractTargetObject.ProxyFactory;
import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.error.*;
import ghidra.dbg.gadp.GadpVersion;
import ghidra.dbg.gadp.error.*;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.protocol.Gadp.*;
import ghidra.dbg.gadp.util.*;
import ghidra.dbg.gadp.protocol.Gadp.ObjectCreatedEvent;
import ghidra.dbg.gadp.protocol.Gadp.RootMessage;
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
import ghidra.dbg.gadp.util.ProtobufOneofByTypeHelper;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetObject.TargetUpdateMode;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.target.schema.XmlSchemaContext;
import ghidra.dbg.util.PathUtils;
import ghidra.dbg.util.PathUtils.PathComparator;
import ghidra.lifecycle.Internal;
import ghidra.program.model.address.*;
import ghidra.util.*;
import ghidra.util.datastruct.ListenerSet;
import ghidra.util.datastruct.WeakValueTreeMap;
import ghidra.util.exception.DuplicateNameException;
import utilities.util.ProxyUtilities;
public class GadpClient implements DebuggerObjectModel {
protected static final Cleaner CLEANER = Cleaner.create();
public class GadpClient extends AbstractDebuggerObjectModel
implements ProxyFactory<List<Class<? extends TargetObject>>> {
protected static final int WARN_OUTSTANDING_REQUESTS = 10000;
// TODO: More sophisticated cache management
@ -264,6 +262,7 @@ public class GadpClient implements DebuggerObjectModel {
}
protected final String description;
protected final AsynchronousByteChannel byteChannel;
protected final AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> messageChannel;
protected AsyncReference<ChannelState, DebuggerModelClosedReason> channelState =
@ -272,44 +271,32 @@ public class GadpClient implements DebuggerObjectModel {
protected XmlSchemaContext schemaContext;
protected TargetObjectSchema rootSchema;
protected final Executor clientExecutor = Executors.newSingleThreadExecutor();
protected final ListenerSet<DebuggerModelListener> listenersClient =
new ListenerSet<>(DebuggerModelListener.class, clientExecutor);
protected final TriConsumer<ChannelState, ChannelState, DebuggerModelClosedReason> listenerForChannelState =
this::channelStateChanged;
protected final MessagePairingCache messageMatcher = new MessagePairingCache();
protected final AtomicInteger sequencer = new AtomicInteger();
protected final NavigableMap<List<String>, GadpClientTargetObject> modelProxies =
new WeakValueTreeMap<>(PathComparator.KEYED);
/**
* Forget all values and rely on getCachedValue instead. This lazy map will just de-dup pending
* requests. Once received, the behavior depends on what we know about the parent object. If
* nothing is known about the parent object, we assume we cannot cache.
*/
protected final AsyncLazyMap<List<String>, Object> valueRequests =
new AsyncLazyMap<>(new HashMap<>(), p -> doRequestValue(p, false));
protected final AsyncLazyMap<List<String>, GadpClientTargetObject> attrsRequests =
new AsyncLazyMap<>(new HashMap<>(), p -> doRequestAttributes(p, false));
protected final AsyncLazyMap<List<String>, GadpClientTargetObject> elemsRequests =
new AsyncLazyMap<>(new HashMap<>(), p -> doRequestElements(p, false));
protected final Map<List<String>, GadpClientTargetObject> modelProxies = new HashMap<>();
protected final GadpAddressFactory factory = new GadpAddressFactory();
{
channelState.addChangeListener(listenerForChannelState);
valueRequests.forgetValues((p, v) -> true);
elemsRequests.forgetValues(this::forgetElementsRequests);
attrsRequests.forgetValues(this::forgetAttributesRequests);
}
public GadpClient(String description, AsynchronousByteChannel channel) {
this.description = description;
this.byteChannel = channel;
this.messageChannel = createMessageChannel(channel);
}
@Override
public SpiTargetObject createProxy(AbstractTargetObject<?> delegate,
List<Class<? extends TargetObject>> mixins) {
return ProxyUtilities.composeOnDelegate(GadpClientTargetObject.class,
(GadpClientTargetObject) delegate, mixins, GadpClientTargetObject.LOOKUP);
}
protected AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> createMessageChannel(
AsynchronousByteChannel channel) {
return new AsyncProtobufMessageChannel<>(channel);
@ -323,16 +310,16 @@ public class GadpClient implements DebuggerObjectModel {
protected void channelStateChanged(ChannelState old, ChannelState set,
DebuggerModelClosedReason reason) {
if (old == ChannelState.NEGOTIATING && set == ChannelState.ACTIVE) {
listenersClient.fire.modelOpened();
listeners.fire.modelOpened();
}
else if (old == ChannelState.ACTIVE && set == ChannelState.CLOSED) {
listenersClient.fire.modelClosed(reason);
listeners.fire.modelClosed(reason);
List<GadpClientTargetObject> copy;
synchronized (modelProxies) {
synchronized (lock) {
copy = List.copyOf(modelProxies.values());
}
for (GadpClientTargetObject proxy : copy) {
proxy.getDelegate().doInvalidate("GADP Client disconnected");
proxy.getDelegate().doInvalidate(root, "GADP Client disconnected");
}
}
}
@ -369,7 +356,10 @@ public class GadpClient implements DebuggerObjectModel {
protected <M extends Message> CompletableFuture<M> sendChecked(Message.Builder req,
M exampleRep) {
return sendCommand(req).thenApply(msg -> require(exampleRep, checkError(msg)));
return sendCommand(req)
.thenApply(msg -> require(exampleRep, checkError(msg)));
//.thenCompose(msg -> flushEvents().thenApply(__ -> msg));
// messageMatcher.fulfill happens on clientExecutor already
}
protected void receiveLoop() {
@ -380,9 +370,7 @@ public class GadpClient implements DebuggerObjectModel {
}
messageChannel.read(Gadp.RootMessage::parseFrom).handle(loop::consume);
}, TypeSpec.cls(Gadp.RootMessage.class), (msg, loop) -> {
loop.repeat(); // All async loop to continue while we process
try {
CompletableFuture.runAsync(() -> {
Gadp.EventNotification notify =
MSG_HELPER.expect(msg, Gadp.EventNotification.getDefaultInstance());
if (notify != null) {
@ -391,10 +379,11 @@ public class GadpClient implements DebuggerObjectModel {
else {
messageMatcher.fulfill(msg.getSequence(), msg);
}
}
catch (Throwable e) {
Msg.error(this, "Error processing message: " + msg, e);
}
}, clientExecutor).exceptionally(ex -> {
Msg.error(this, "Error processing message: ", ex);
return null;
});
loop.repeat(); // All async loop to continue while we process
}).exceptionally(exc -> {
exc = AsyncUtils.unwrapThrowable(exc);
if (exc instanceof NotYetConnectedException) {
@ -409,6 +398,9 @@ public class GadpClient implements DebuggerObjectModel {
else if (exc instanceof CancelledKeyException) {
Msg.error(this, "Channel key is cancelled. Probably closed");
}
else if (exc instanceof RejectedExecutionException) {
Msg.trace(this, "Ignoring rejection", exc);
}
else {
Msg.error(this, "Receive failed for an unknown reason", exc);
}
@ -418,19 +410,31 @@ public class GadpClient implements DebuggerObjectModel {
}
protected void processNotification(Gadp.EventNotification notify) {
if (!byteChannel.isOpen()) {
return;
}
ProtocolStringList path = notify.getPath().getEList();
GadpClientTargetObject obj = getCachedProxy(path);
if (obj == null) {
if (!hasPendingRequest(path)) {
/**
* For pending, I guess we just miss the event. NB: If it was a model event, then
* the pending subscribe reply ought to already reflect the update we're ignoring
* here.
*/
Msg.error(this, "Server sent notification for non-cached object: " + notify);
//Msg.debug(this, "Processing notification: " + path + " " + notify.getEvtCase());
if (notify.hasObjectCreatedEvent()) {
notify.getObjectCreatedEvent();
// AbstractTargetObject invokes created event
createProxy(path, notify.getObjectCreatedEvent());
return;
}
if (notify.hasRootAddedEvent()) {
if (!path.isEmpty()) {
Msg.warn(this, "Server gave non-root path for root-added event: " +
PathUtils.toString(path));
}
synchronized (lock) {
addModelRoot(getProxy(List.of(), true));
}
return;
}
GadpClientTargetObject obj = getProxy(path, true);
if (obj == null) {
return; // Error already logged
}
obj.getDelegate().handleEvent(notify);
}
@ -439,16 +443,6 @@ public class GadpClient implements DebuggerObjectModel {
return description + " via GADP (" + channelState.get().name().toLowerCase() + ")";
}
@Override
public void addModelListener(DebuggerModelListener listener) {
listenersClient.add(listener);
}
@Override
public void removeModelListener(DebuggerModelListener listener) {
listenersClient.remove(listener);
}
public CompletableFuture<Void> connect() {
Gadp.ConnectRequest.Builder req = GadpVersion.makeRequest();
if (channelState.get() != ChannelState.INACTIVE) {
@ -493,8 +487,13 @@ public class GadpClient implements DebuggerObjectModel {
public CompletableFuture<Void> close() {
try {
messageChannel.close();
CompletableFuture.runAsync(() -> {
channelState.set(ChannelState.CLOSED, DebuggerModelClosedReason.normal());
return AsyncUtils.NIL;
}, clientExecutor).exceptionally(ex -> {
Msg.error("Problem upon firing channel state change", ex);
return null;
});
return super.close();
}
catch (IOException e) {
return CompletableFuture.failedFuture(e);
@ -528,292 +527,47 @@ public class GadpClient implements DebuggerObjectModel {
});
}
protected GadpClientTargetObject getCachedProxy(List<String> path) {
synchronized (modelProxies) {
protected GadpClientTargetObject getProxy(List<String> path, boolean internal) {
synchronized (lock) {
GadpClientTargetObject proxy = modelProxies.get(path);
if (proxy == null && internal) {
Msg.error(this,
"Server referred to non-existent object at path: " + PathUtils.toString(path));
}
return proxy;
}
}
protected GadpClientTargetObject createProxy(List<String> path, ObjectCreatedEvent evt) {
synchronized (lock) {
if (modelProxies.containsKey(path)) {
Msg.error(this, "Agent announced creation of an already-existing object: " +
PathUtils.toString(path));
return modelProxies.get(path);
}
}
protected GadpClientTargetObject removeCachedProxy(List<String> path) {
synchronized (modelProxies) {
return modelProxies.remove(path);
}
}
/**
* Check for a cached value
*
* This first checks if the given path is a cached object and returns it if so. Otherwise, it
* checks if the parent is cached and, if so, examines its cached children.
*
* Note, if {@code null} is returned, the cache has no knowledge of the given path; the server
* must be queried. If the returned optional has no value, the cache knows the path does not
* exist.
*
* @param path the path
* @return an optional value, or {@code null} if not cached
*/
protected Optional<Object> getCachedValue(List<String> path) {
GadpClientTargetObject proxy = getCachedProxy(path);
if (proxy != null) {
return Optional.of(proxy);
}
List<String> parentPath = PathUtils.parent(path);
GadpClientTargetObject parent;
if (parentPath == null) {
return null;
parent = null;
}
GadpClientTargetObject parent = getCachedProxy(parentPath);
else {
parent = getProxy(parentPath, true);
if (parent == null) {
return null;
Msg.error(this, "Got object's created event before its parent's: " +
PathUtils.toString(path));
}
Optional<Object> val = parent.getDelegate().cachedChild(PathUtils.getKey(path));
if (val == null) {
return null;
}
if (val.isEmpty()) {
if (PathUtils.isInvocation(PathUtils.getKey(path))) {
return null;
}
return val;
}
Object v = val.get();
/**
* NOTE: val should not be a TargetObject, otherwise it should have hit via
* getCachedProxy(path). If this is a TargetObject, it's because the proxy was created
* between then and the call to cachedChild -- possible due to a race condition. In that
* case, it seems harmless to just return it anyway.
*/
if (v instanceof TargetObject) {
return val;
}
if (!(v instanceof TargetObjectRef)) {
return val;
}
TargetObjectRef r = (TargetObjectRef) v;
if (path.equals(r.getPath())) {
// An TargetObject is expected, but we only have the placeholder cached
return null;
}
// else a link, which we are not required to fetch
return val;
}
protected void cacheInParent(List<String> path, GadpClientTargetObject proxy) {
List<String> parentPath = PathUtils.parent(path);
if (parentPath == null) {
return;
}
GadpClientTargetObject parent = modelProxies.get(parentPath);
if (parent == null) {
return;
}
parent.getDelegate().putCachedProxy(PathUtils.getKey(path), proxy);
}
@Internal
public GadpClientTargetObject getProxyForInfo(ModelObjectInfo info) {
synchronized (modelProxies) {
return modelProxies.computeIfAbsent(info.getPath().getEList(), path -> {
GadpClientTargetObject proxy = DelegateGadpClientTargetObject.makeModelProxy(this,
path, info.getTypeHint(), info.getInterfaceList());
cacheInParent(path, proxy);
parent, PathUtils.getKey(path), evt.getTypeHint(), evt.getInterfaceList());
modelProxies.put(path, proxy);
return proxy;
});
}
}
@Internal
public TargetObjectRef getProxyOrStub(List<String> path) {
GadpClientTargetObject cached = getCachedProxy(path);
if (cached != null) {
return cached;
protected void removeProxy(List<String> path, String reason) {
synchronized (lock) {
modelProxies.remove(path);
}
return new GadpClientTargetObjectStub(this, path);
}
protected CompletableFuture<Void> unsubscribe(List<String> path) {
AsyncFence fence = new AsyncFence();
cleanRequests(path);
fence.include(sendChecked(
Gadp.SubscribeRequest.newBuilder()
.setPath(GadpValueUtils.makePath(path))
.setSubscribe(false),
Gadp.SubscribeReply.getDefaultInstance()));
return fence.ready();
}
protected void invalidateSubtree(List<String> path, String reason) {
List<List<String>> pathsToInvalidate = new ArrayList<>();
List<GadpClientTargetObject> proxiesToInvalidate;
synchronized (modelProxies) {
// keySet is the only one which isn't a copy.
// TODO: Would rather iterate entries when AbstractWeakValueMap is fixed
for (List<String> succPath : modelProxies.tailMap(path, true).keySet()) {
if (!PathUtils.isAncestor(path, succPath)) {
break;
}
pathsToInvalidate.add(succPath);
}
proxiesToInvalidate =
pathsToInvalidate.stream().map(modelProxies::remove).collect(Collectors.toList());
}
for (GadpClientTargetObject proxy : proxiesToInvalidate) {
proxy.getDelegate().doInvalidate(reason);
}
}
public boolean hasPendingRequest(List<String> path) {
return valueRequests.containsKey(path) || attrsRequests.containsKey(path) ||
elemsRequests.containsKey(path);
}
public boolean hasPendingAttributes(List<String> path) {
return attrsRequests.containsKey(path);
}
public boolean hasPendingElements(List<String> path) {
return elemsRequests.containsKey(path);
}
protected void cleanRequests(List<String> path) {
valueRequests.forget(path);
attrsRequests.forget(path);
elemsRequests.forget(path);
}
@Override
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
return fetchModelObject(List.of());
}
protected CompletableFuture<Object> doRequestValueWithObjectInfo(
List<String> path, boolean refresh,
boolean fetchElements, boolean refreshElements,
boolean fetchAttributes, boolean refreshAttributes) {
CompletableFuture<SubscribeReply> reply = sendChecked(Gadp.SubscribeRequest.newBuilder()
.setPath(GadpValueUtils.makePath(path))
.setSubscribe(true)
.setRefresh(refresh)
.setFetchElements(fetchElements)
.setRefreshElements(refreshElements)
.setFetchAttributes(fetchAttributes)
.setRefreshAttributes(refreshAttributes),
Gadp.SubscribeReply.getDefaultInstance());
return reply.thenApplyAsync(rep -> { // Async to avoid processing info with a lock
Gadp.Value value = rep.getValue();
if (value.getSpecCase() == Gadp.Value.SpecCase.OBJECT_STUB) {
Msg.error(this, "Server responded to object request with a stub!");
return null;
}
if (value.getSpecCase() != Gadp.Value.SpecCase.OBJECT_INFO) {
return GadpValueUtils.getValue(this, path, value);
}
Gadp.ModelObjectInfo info = value.getObjectInfo();
GadpClientTargetObject proxy = getProxyForInfo(info);
proxy.getDelegate().updateWithInfo(info);
return proxy;
}, AsyncUtils.FRAMEWORK_EXECUTOR);
}
protected CompletableFuture<GadpClientTargetObject> doRequestElements(List<String> path,
boolean refresh) {
return doRequestValueWithObjectInfo(path, false, true, refresh, false, false)
.thenApply(GadpClient::targetObjectOrNull);
}
protected CompletableFuture<GadpClientTargetObject> doRequestAttributes(List<String> path,
boolean refresh) {
return doRequestValueWithObjectInfo(path, false, false, false, true, refresh)
.thenApply(GadpClient::targetObjectOrNull);
}
protected boolean forgetElementsRequests(List<String> path, GadpClientTargetObject proxy) {
return proxy == null || !proxy.isValid() ||
proxy.getUpdateMode() == TargetUpdateMode.SOLICITED;
}
protected CompletableFuture<GadpClientTargetObject> checkProcessedElemsReply(List<String> path,
boolean refresh) {
if (refresh) { // NB: the map pre-tests the forget condition, too
elemsRequests.forget(path);
return elemsRequests.get(path, p -> doRequestElements(p, refresh));
}
return elemsRequests.get(path);
}
@Override
public CompletableFuture<? extends Map<String, ? extends TargetObjectRef>> fetchObjectElements(
List<String> path, boolean refresh) {
CompletableFuture<GadpClientTargetObject> processedReply =
checkProcessedElemsReply(path, refresh);
return processedReply.thenApply(proxy -> {
if (proxy == null) { // The path doesn't exist, so return null, per the docs
return null;
}
return proxy.getCachedElements();
}).exceptionally(GadpClient::nullForNotExist);
}
protected boolean forgetAttributesRequests(List<String> path, GadpClientTargetObject proxy) {
return proxy == null || !proxy.isValid();
}
protected CompletableFuture<GadpClientTargetObject> checkProcessedAttrsReply(List<String> path,
boolean refresh) {
if (refresh) {
attrsRequests.forget(path);
return attrsRequests.get(path, p -> doRequestAttributes(p, refresh));
}
return attrsRequests.get(path);
}
@Override
public CompletableFuture<? extends Map<String, ?>> fetchObjectAttributes(List<String> path,
boolean refresh) {
CompletableFuture<GadpClientTargetObject> processedReply =
checkProcessedAttrsReply(path, refresh);
return processedReply.thenApply(proxy -> {
if (proxy == null) { // The path doesn't exist, so return null, per the docs
return null;
}
return proxy.getCachedAttributes();
}).exceptionally(GadpClient::nullForNotExist);
}
@Override
public CompletableFuture<?> fetchModelValue(List<String> path, boolean refresh) {
/**
* NB. Not sure there's value in checking for element/attribute requests on the parent. May
* cull some requests, but the logic could get complicated. E.g., if we wait for it to
* complete, and it comes back a TargetObjectRef, well, we have to fetch anyway. For
* attributes, we'd also have to consider the case where it's absent, because it's a method
* invocation.
*/
if (!refresh) {
Optional<Object> cached = getCachedValue(path);
if (cached != null) {
Object val = cached.orElse(null);
if (!(val instanceof TargetObjectRef)) {
return CompletableFuture.completedFuture(val);
}
TargetObjectRef ref = (TargetObjectRef) val;
TargetObject obj = GadpValueUtils.getTargetObjectNonLink(path, ref);
if (obj != null) {
return CompletableFuture.completedFuture(val);
}
}
}
return valueRequests.get(path, p -> doRequestValue(p, refresh))
.exceptionally(GadpClient::nullForNotExist);
}
@Override
public CompletableFuture<?> fetchModelValue(List<String> path) {
return fetchModelValue(path, false);
}
protected CompletableFuture<Object> doRequestValue(List<String> path, boolean refresh) {
return doRequestValueWithObjectInfo(path, refresh, false, false, false, false);
}
@Override
@ -823,22 +577,28 @@ public class GadpClient implements DebuggerObjectModel {
@Override
public TargetObjectRef createRef(List<String> path) {
return getProxyOrStub(path);
synchronized (lock) {
GadpClientTargetObject proxy = modelProxies.get(path);
if (proxy != null) {
return proxy;
}
}
return super.createRef(path);
}
@Override
public TargetObject getModelObject(List<String> path) {
return getProxy(path, false);
}
@Override
public void invalidateAllLocalCaches() {
List<GadpClientTargetObject> copy;
synchronized (modelProxies) {
synchronized (lock) {
copy = List.copyOf(modelProxies.values());
}
for (GadpClientTargetObject proxy : copy) {
proxy.getDelegate().doClearCaches();
}
}
@Override
public Executor getClientExecutor() {
return clientExecutor;
}
}

View file

@ -23,7 +23,8 @@ public interface GadpClientTargetAccessConditioned
@GadpAttributeChangeCallback(ACCESSIBLE_ATTRIBUTE_NAME)
default void handleAccessibleChanged(Object accessible) {
getDelegate().listeners.fire(TargetAccessibilityListener.class)
getDelegate().getListeners()
.fire(TargetAccessibilityListener.class)
.accessibilityChanged(this, fromObj(accessible));
}
}

View file

@ -20,7 +20,6 @@ import java.util.concurrent.CompletableFuture;
import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.attributes.TypedTargetObjectRef;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetAttachable;
import ghidra.dbg.target.TargetAttacher;

View file

@ -23,7 +23,6 @@ import ghidra.dbg.attributes.TypedTargetObjectRef;
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.protocol.Gadp.Path;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
@ -64,19 +63,20 @@ public interface GadpClientTargetBreakpointContainer extends GadpClientTargetObj
@GadpEventHandler(Gadp.EventNotification.EvtCase.BREAK_HIT_EVENT)
default void handleBreakHitEvent(Gadp.EventNotification notification) {
Gadp.BreakHitEvent evt = notification.getBreakHitEvent();
TargetObjectRef trapped = getModel().getProxyOrStub(evt.getTrapped().getEList());
TargetObjectRef trapped = getModel().getProxy(evt.getTrapped().getEList(), true);
Path framePath = evt.getFrame();
TypedTargetObjectRef<? extends TargetStackFrame<?>> frame =
framePath == null || framePath.getECount() == 0 ? null
: getModel().getProxyOrStub(framePath.getEList()).as(TargetStackFrame.tclass);
: getModel().getProxy(framePath.getEList(), true).as(TargetStackFrame.tclass);
Path specPath = evt.getSpec();
TypedTargetObjectRef<? extends TargetBreakpointSpec<?>> spec = specPath == null ? null
: getModel().getProxyOrStub(specPath.getEList()).as(TargetBreakpointSpec.tclass);
: getModel().getProxy(specPath.getEList(), true).as(TargetBreakpointSpec.tclass);
Path bptPath = evt.getEffective();
TypedTargetObjectRef<? extends TargetBreakpointLocation<?>> breakpoint = bptPath == null
? null
: getModel().getProxyOrStub(bptPath.getEList()).as(TargetBreakpointLocation.tclass);
getDelegate().listeners.fire(TargetBreakpointListener.class)
: getModel().getProxy(bptPath.getEList(), true).as(TargetBreakpointLocation.tclass);
getDelegate().getListeners()
.fire(TargetBreakpointListener.class)
.breakpointHit(this, trapped, frame, spec, breakpoint);
if (spec instanceof GadpClientTargetBreakpointSpec) {
// If I don't have a cached proxy, then I don't have any listeners

View file

@ -19,7 +19,6 @@ import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetBreakpointSpec;
import ghidra.dbg.util.ValueUtils;
import ghidra.util.datastruct.ListenerSet;
@ -65,7 +64,8 @@ public interface GadpClientTargetBreakpointSpec
@GadpAttributeChangeCallback(ENABLED_ATTRIBUTE_NAME)
default void handleEnabledChanged(Object enabled) {
getDelegate().listeners.fire(TargetBreakpointSpecListener.class)
getDelegate().getListeners()
.fire(TargetBreakpointSpecListener.class)
.breakpointToggled(this, enabledFromObj(enabled));
}
}

View file

@ -20,7 +20,6 @@ import java.util.concurrent.CompletableFuture;
import com.google.protobuf.ByteString;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetConsole;
public interface GadpClientTargetConsole

View file

@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetDeletable;
public interface GadpClientTargetDeletable

View file

@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetDetachable;
public interface GadpClientTargetDetachable

View file

@ -20,7 +20,6 @@ import java.util.List;
import ghidra.dbg.attributes.TypedTargetObjectRef;
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetEventScope;
import ghidra.dbg.target.TargetThread;
@ -32,12 +31,13 @@ public interface GadpClientTargetEventScope
Gadp.Path threadPath = evt.getEventThread();
TypedTargetObjectRef<? extends TargetThread<?>> thread =
threadPath == null || threadPath.getECount() == 0 ? null
: getModel().getProxyOrStub(threadPath.getEList()).as(TargetThread.tclass);
: getModel().getProxy(threadPath.getEList(), true).as(TargetThread.tclass);
TargetEventType type = GadpValueUtils.getTargetEventType(evt.getType());
String description = evt.getDescription();
List<Object> parameters =
GadpValueUtils.getValues(getModel(), evt.getParametersList());
getDelegate().listeners.fire(TargetEventScopeListener.class)
getDelegate().getListeners()
.fire(TargetEventScopeListener.class)
.event(this, thread, type, description, parameters);
}
}

View file

@ -29,7 +29,8 @@ public interface GadpClientTargetExecutionStateful
@GadpAttributeChangeCallback(STATE_ATTRIBUTE_NAME)
default void handleStateChanged(Object state) {
getDelegate().listeners.fire(TargetExecutionStateListener.class)
getDelegate().getListeners()
.fire(TargetExecutionStateListener.class)
.executionStateChanged(this, stateFromObj(state));
}
}

View file

@ -21,7 +21,6 @@ import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.error.DebuggerIllegalArgumentException;
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetFocusScope;
import ghidra.dbg.util.PathUtils;
import ghidra.dbg.util.ValueUtils;
@ -51,7 +50,8 @@ public interface GadpClientTargetFocusScope
@GadpAttributeChangeCallback(FOCUS_ATTRIBUTE_NAME)
default void handleFocusChanged(Object focus) {
getDelegate().listeners.fire(TargetFocusScopeListener.class)
getDelegate().getListeners()
.fire(TargetFocusScopeListener.class)
.focusChanged(this, refFromObj(focus));
}
}

View file

@ -19,7 +19,6 @@ import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetInterpreter;
import ghidra.dbg.util.ValueUtils;
@ -53,7 +52,8 @@ public interface GadpClientTargetInterpreter
@GadpAttributeChangeCallback(PROMPT_ATTRIBUTE_NAME)
default void handlePromptChanged(Object prompt) {
getDelegate().listeners.fire(TargetInterpreterListener.class)
getDelegate().getListeners()
.fire(TargetInterpreterListener.class)
.promptChanged(this, promptFromObj(prompt));
}
}

View file

@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetInterruptible;
public interface GadpClientTargetInterruptible

View file

@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetKillable;
public interface GadpClientTargetKillable

View file

@ -19,7 +19,6 @@ import java.util.Map;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetLauncher;
import ghidra.dbg.target.TargetMethod;
import ghidra.dbg.target.TargetMethod.TargetParameterMap;

View file

@ -22,7 +22,6 @@ import com.google.protobuf.ByteString;
import ghidra.dbg.error.DebuggerMemoryAccessException;
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.memory.MemoryReader;
import ghidra.dbg.memory.MemoryWriter;
import ghidra.dbg.target.TargetMemory;
@ -91,7 +90,7 @@ public interface GadpClientTargetMemory
byte[] data = evt.getContent().toByteArray();
DelegateGadpClientTargetObject delegate = getDelegate();
delegate.getMemoryCache(address.getAddressSpace()).updateMemory(address.getOffset(), data);
delegate.listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, data);
delegate.getListeners().fire(TargetMemoryListener.class).memoryUpdated(this, address, data);
}
@GadpEventHandler(Gadp.EventNotification.EvtCase.MEMORY_ERROR_EVENT)
@ -100,7 +99,8 @@ public interface GadpClientTargetMemory
AddressRange range = GadpValueUtils.getAddressRange(getModel(), evt.getRange());
String message = evt.getMessage();
// Errors are not cached, but recorded in trace
getDelegate().listeners.fire(TargetMemoryListener.class)
getDelegate().getListeners()
.fire(TargetMemoryListener.class)
.memoryReadError(this, range, new DebuggerMemoryAccessException(message));
}
}

View file

@ -19,7 +19,6 @@ import java.util.Map;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetMethod;
public interface GadpClientTargetMethod

View file

@ -15,46 +15,36 @@
*/
package ghidra.dbg.gadp.client;
import java.util.Collection;
import java.util.List;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import ghidra.dbg.agent.SpiTargetObject;
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetConsole.TargetConsoleListener;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.util.ValueUtils;
import ghidra.util.Msg;
public interface GadpClientTargetObject extends TargetObject {
public interface GadpClientTargetObject extends SpiTargetObject {
Lookup LOOKUP = MethodHandles.lookup();
@Override
GadpClient getModel();
@Override
List<String> getProtocolID();
@Override
String getTypeHint();
@Override
Collection<String> getInterfaceNames();
@Override
Collection<Class<? extends TargetObject>> getInterfaces();
DelegateGadpClientTargetObject getDelegate();
@GadpEventHandler(Gadp.EventNotification.EvtCase.MODEL_OBJECT_EVENT)
default void handleModelObjectEvent(Gadp.EventNotification notification) {
Gadp.ModelObjectEvent evt = notification.getModelObjectEvent();
getDelegate().updateWithDelta(evt.getDelta());
getDelegate().updateWithDeltas(evt.getElementDelta(), evt.getAttributeDelta());
}
@GadpEventHandler(Gadp.EventNotification.EvtCase.OBJECT_INVALIDATE_EVENT)
default void handleObjectInvalidateEvent(Gadp.EventNotification notification) {
Gadp.ObjectInvalidateEvent evt = notification.getObjectInvalidateEvent();
getDelegate().doInvalidateSubtree(evt.getReason());
getDelegate().doInvalidate(this, evt.getReason());
}
@GadpEventHandler(Gadp.EventNotification.EvtCase.CACHE_INVALIDATE_EVENT)
@ -69,7 +59,7 @@ public interface GadpClientTargetObject extends TargetObject {
@GadpAttributeChangeCallback(DISPLAY_ATTRIBUTE_NAME)
default void handleDisplayChanged(Object display) {
getDelegate().listeners.fire.displayChanged(this, displayFromObj(display));
getDelegate().getListeners().fire.displayChanged(this, displayFromObj(display));
}
// TODO: It's odd to put this here.... I think it indicates a problem in the API
@ -79,7 +69,8 @@ public interface GadpClientTargetObject extends TargetObject {
int channelIndex = evt.getChannel();
Channel[] allChannels = Channel.values();
if (0 <= channelIndex && channelIndex < allChannels.length) {
getDelegate().listeners.fire(TargetConsoleListener.class)
getDelegate().getListeners()
.fire(TargetConsoleListener.class)
.consoleOutput(this, allChannels[channelIndex], evt.getData().toByteArray());
}
else {

View file

@ -1,57 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.dbg.gadp.client;
import java.util.List;
import ghidra.dbg.attributes.TargetObjectRef;
public class GadpClientTargetObjectStub implements TargetObjectRef {
protected final GadpClient client;
protected final List<String> path;
protected final int hash;
public GadpClientTargetObjectStub(GadpClient client, List<String> path) {
this.client = client;
this.path = path;
this.hash = computeHashCode();
}
@Override
public boolean equals(Object obj) {
return doEquals(obj);
}
@Override
public int hashCode() {
return hash;
}
@Override
public GadpClient getModel() {
return client;
}
@Override
public List<String> getPath() {
return path;
}
@Override
public String toString() {
return "<Ref(GADP Stub) to " + path + " in " + client + ">";
}
}

View file

@ -20,7 +20,6 @@ import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetRegisterBank;
public interface GadpClientTargetRegisterBank
@ -84,6 +83,8 @@ public interface GadpClientTargetRegisterBank
Map<String, byte[]> updates = GadpValueUtils.getRegisterValueMap(evt.getValueList());
DelegateGadpClientTargetObject delegate = getDelegate();
delegate.getRegisterCache().putAll(updates);
delegate.listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, updates);
delegate.getListeners()
.fire(TargetRegisterBankListener.class)
.registersUpdated(this, updates);
}
}

View file

@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetResumable;
public interface GadpClientTargetResumable

View file

@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetSteppable;
public interface GadpClientTargetSteppable

View file

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.dbg.gadp.util;
package ghidra.dbg.gadp.client;
import static ghidra.lifecycle.Unfinished.*;
import static ghidra.lifecycle.Unfinished.TODO;
import java.util.*;
import java.util.Map.Entry;
@ -26,11 +26,8 @@ import com.google.protobuf.ByteString;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.attributes.*;
import ghidra.dbg.attributes.TargetObjectRefList.DefaultTargetObjectRefList;
import ghidra.dbg.gadp.GadpRegistry;
import ghidra.dbg.gadp.client.GadpClient;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.protocol.Gadp.ModelObjectDelta;
import ghidra.dbg.gadp.protocol.Gadp.ModelObjectInfo;
import ghidra.dbg.target.TargetAttacher.TargetAttachKind;
import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet;
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet;
@ -309,28 +306,22 @@ public enum GadpValueUtils {
.build();
}
public static Gadp.ModelObjectInfo makeInfo(TargetObject obj) {
ModelObjectInfo.Builder builder = Gadp.ModelObjectInfo.newBuilder()
.setPath(GadpValueUtils.makePath(obj.getPath()))
.setTypeHint(obj.getTypeHint())
.addAllInterface(GadpRegistry.getInterfaceNames(obj));
builder.addAllElementIndex(obj.getCachedElements().keySet());
for (Entry<String, ?> ent : obj.getCachedAttributes().entrySet()) {
builder.addAttribute(makeAttribute(obj, ent));
public static Gadp.ModelObjectDelta makeElementDelta(List<String> parentPath,
Delta<?, ?> delta) {
ModelObjectDelta.Builder builder = Gadp.ModelObjectDelta.newBuilder()
.addAllRemoved(delta.getKeysRemoved());
for (Entry<String, ?> ent : delta.added.entrySet()) {
builder.addAdded(makeIndexedValue(parentPath, ent));
}
return builder.build();
}
public static Gadp.ModelObjectDelta makeDelta(TargetObject parent,
Delta<?, ? extends TargetObjectRef> deltaE, Delta<?, ?> deltaA) {
public static Gadp.ModelObjectDelta makeAttributeDelta(List<String> parentPath,
Delta<?, ?> delta) {
ModelObjectDelta.Builder builder = Gadp.ModelObjectDelta.newBuilder()
.addAllIndexRemoved(deltaE.getKeysRemoved())
.addAllIndexAdded(deltaE.added.keySet())
.addAllAttributeRemoved(deltaA.getKeysRemoved());
for (Entry<String, ?> ent : deltaA.added.entrySet()) {
builder.addAttributeAdded(makeAttribute(parent, ent));
.addAllRemoved(delta.getKeysRemoved());
for (Entry<String, ?> ent : delta.added.entrySet()) {
builder.addAdded(makeNamedValue(parentPath, ent));
}
return builder.build();
}
@ -649,18 +640,25 @@ public enum GadpValueUtils {
return b.build();
}
public static Gadp.Argument makeArgument(Map.Entry<String, ?> argument) {
return Gadp.Argument.newBuilder()
.setName(argument.getKey())
.setValue(makeValue(null, argument.getValue()))
public static Gadp.NamedValue makeNamedValue(Map.Entry<String, ?> ent) {
return makeNamedValue(null, ent);
}
public static Gadp.NamedValue makeNamedValue(List<String> parentPath,
Map.Entry<String, ?> ent) {
List<String> path = parentPath == null ? null : PathUtils.extend(parentPath, ent.getKey());
return Gadp.NamedValue.newBuilder()
.setName(ent.getKey())
.setValue(makeValue(path, ent.getValue()))
.build();
}
public static Gadp.Attribute makeAttribute(TargetObject parent, Map.Entry<String, ?> ent) {
return Gadp.Attribute.newBuilder()
public static Gadp.NamedValue makeIndexedValue(List<String> parentPath,
Map.Entry<String, ?> ent) {
List<String> path = parentPath == null ? null : PathUtils.index(parentPath, ent.getKey());
return Gadp.NamedValue.newBuilder()
.setName(ent.getKey())
.setValue(
makeValue(PathUtils.extend(parent.getPath(), ent.getKey()), ent.getValue()))
.setValue(makeValue(path, ent.getValue()))
.build();
}
@ -712,14 +710,11 @@ public enum GadpValueUtils {
case PARAMETERS_VALUE:
return getParameters(model, value.getParametersValue());
case PATH_VALUE:
return model.createRef(value.getPathValue().getEList());
return model.getModelObject(value.getPathValue().getEList());
case PATH_LIST_VALUE:
return getRefList(model, value.getPathListValue());
case OBJECT_INFO:
Msg.error(GadpValueUtils.class, "ObjectInfo requires special treatment:" + value);
return model.createRef(path);
case OBJECT_STUB:
return model.createRef(path);
return model.getModelObject(path);
case TYPE_VALUE:
return getValueType(value.getTypeValue());
case SPEC_NOT_SET:
@ -730,17 +725,28 @@ public enum GadpValueUtils {
}
}
public static Object getAttributeValue(TargetObject object, Gadp.Attribute attr) {
public static Object getAttributeValue(GadpClientTargetObject object, Gadp.NamedValue attr) {
return getValue(object.getModel(), PathUtils.extend(object.getPath(), attr.getName()),
attr.getValue());
}
public static Map<String, Object> getAttributeMap(TargetObject object,
List<Gadp.Attribute> list) {
public static GadpClientTargetObject getElementValue(GadpClientTargetObject object,
Gadp.NamedValue elem) {
Object value = getValue(object.getModel(),
PathUtils.index(object.getPath(), elem.getName()), elem.getValue());
if (!(value instanceof GadpClientTargetObject)) {
Msg.error(GadpValueUtils.class, "Received non-object-valued element: " + elem);
return null;
}
return (GadpClientTargetObject) value;
}
public static Map<String, Object> getAttributeMap(GadpClientTargetObject object,
List<Gadp.NamedValue> list) {
Map<String, Object> result = new LinkedHashMap<>();
for (Gadp.Attribute attr : list) {
if (result.put(attr.getName(),
GadpValueUtils.getAttributeValue(object, attr)) != null) {
for (Gadp.NamedValue attr : list) {
Object val = GadpValueUtils.getAttributeValue(object, attr);
if (result.put(attr.getName(), val) != null) {
Msg.warn(GadpValueUtils.class, "Received duplicate attribute: " + attr);
}
}
@ -756,25 +762,30 @@ public enum GadpValueUtils {
return values.stream().map(v -> makeValue(null, v)).collect(Collectors.toList());
}
public static List<Gadp.Argument> makeArguments(Map<String, ?> arguments) {
public static List<Gadp.NamedValue> makeArguments(Map<String, ?> arguments) {
return arguments.entrySet()
.stream()
.map(ent -> makeArgument(ent))
.map(ent -> makeNamedValue(ent))
.collect(Collectors.toList());
}
public static Map<String, TargetObjectRef> getElementMap(TargetObject parent,
List<String> indices) {
Map<String, TargetObjectRef> result = new LinkedHashMap<>();
for (String index : indices) {
result.put(index,
parent.getModel().createRef(PathUtils.index(parent.getPath(), index)));
public static Map<String, GadpClientTargetObject> getElementMap(GadpClientTargetObject parent,
List<Gadp.NamedValue> list) {
Map<String, GadpClientTargetObject> result = new LinkedHashMap<>();
for (Gadp.NamedValue elem : list) {
GadpClientTargetObject val = GadpValueUtils.getElementValue(parent, elem);
if (val == null) {
continue;
}
if (result.put(elem.getName(), val) != null) {
Msg.warn(GadpValueUtils.class, "Received duplicate element: " + elem);
}
}
return result;
}
public static Map<String, ?> getArguments(DebuggerObjectModel model,
List<Gadp.Argument> arguments) {
List<Gadp.NamedValue> arguments) {
return arguments.stream()
.collect(
Collectors.toMap(a -> a.getName(), a -> getValue(model, null, a.getValue())));

View file

@ -24,6 +24,7 @@ import ghidra.dbg.*;
import ghidra.dbg.gadp.error.GadpErrorException;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.program.model.address.*;
import ghidra.util.Msg;
public abstract class AbstractGadpServer
extends AbstractAsyncServer<AbstractGadpServer, GadpClientHandler>
@ -86,4 +87,13 @@ public abstract class AbstractGadpServer
public void setExitOnClosed(boolean exitOnClosed) {
this.exitOnClosed = exitOnClosed;
}
@Override
public void terminate() throws IOException {
super.terminate();
model.close().exceptionally(ex -> {
Msg.error(this, "Problem closing GADP-served model", ex);
return null;
});
}
}

View file

@ -15,40 +15,33 @@
*/
package ghidra.dbg.gadp.server;
import java.nio.channels.AsynchronousByteChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import com.google.protobuf.ByteString;
import com.google.protobuf.ProtocolStringList;
import ghidra.async.*;
import ghidra.async.AsyncUtils;
import ghidra.async.TypeSpec;
import ghidra.comm.service.AbstractAsyncClientHandler;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.attributes.TypedTargetObjectRef;
import ghidra.dbg.error.*;
import ghidra.dbg.gadp.GadpVersion;
import ghidra.dbg.gadp.client.GadpClientTargetAttachable;
import ghidra.dbg.gadp.client.GadpValueUtils;
import ghidra.dbg.gadp.error.GadpErrorException;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.protocol.Gadp.ErrorCode;
import ghidra.dbg.gadp.protocol.Gadp.ObjectInvalidateEvent;
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointListener;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetEventScope.TargetEventScopeListener;
import ghidra.dbg.target.TargetConsole.TargetTextConsoleListener;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
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.TargetObjectListener;
import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.target.schema.XmlSchemaContext;
import ghidra.dbg.util.CollectionUtils.Delta;
@ -88,42 +81,81 @@ public class GadpClientHandler
}
}
protected class ListenerForEvents
implements TargetObjectListener, TargetAccessibilityListener, TargetBreakpointListener,
TargetEventScopeListener, TargetExecutionStateListener, TargetFocusScopeListener,
TargetInterpreterListener, TargetMemoryListener, TargetRegisterBankListener {
protected class ListenerForEvents implements DebuggerModelListener, TargetTextConsoleListener {
@Override
public void created(TargetObject object) {
if (!sock.isOpen()) {
return;
}
channel.write(Gadp.RootMessage.newBuilder()
.setEventNotification(Gadp.EventNotification.newBuilder()
.setPath(GadpValueUtils.makePath(object.getPath()))
.setObjectCreatedEvent(Gadp.ObjectCreatedEvent.newBuilder()
.setTypeHint(object.getTypeHint())
.addAllInterface(object.getInterfaceNames())))
.build()).exceptionally(GadpClientHandler::errorSendNotify);
}
@Override
public void rootAdded(TargetObject root) {
if (!sock.isOpen()) {
return;
}
channel.write(Gadp.RootMessage.newBuilder()
.setEventNotification(Gadp.EventNotification.newBuilder()
.setRootAddedEvent(Gadp.RootAddedEvent.getDefaultInstance()))
.build()).exceptionally(GadpClientHandler::errorSendNotify);
}
@Override
public void attributesChanged(TargetObject parent, Collection<String> removed,
Map<String, ?> added) {
if (!sock.isOpen()) {
return;
}
// TODO: Can elements and attributes be combined into one message?
sendDelta(parent, Delta.empty(), Delta.create(removed, added))
if (removed.isEmpty() && added.isEmpty()) {
return;
}
sendDelta(parent.getPath(), Delta.empty(), Delta.create(removed, added))
.exceptionally(GadpClientHandler::errorSendNotify);
}
@Override
public void elementsChanged(TargetObject parent, Collection<String> removed,
Map<String, ? extends TargetObjectRef> added) {
if (!sock.isOpen()) {
return;
}
// TODO: Can elements and attributes be combined into one message?
sendDelta(parent, Delta.create(removed, added), Delta.empty())
if (removed.isEmpty() && added.isEmpty()) {
return;
}
sendDelta(parent.getPath(), Delta.create(removed, added), Delta.empty())
.exceptionally(GadpClientHandler::errorSendNotify);
}
@Override
public void invalidated(TargetObject object, String reason) {
if (!unsubscribeSubtree(object)) {
public void invalidated(TargetObject object, TargetObject branch, String reason) {
if (!sock.isOpen()) {
return;
}
if (object != branch) {
return;
}
channel.write(Gadp.RootMessage.newBuilder()
.setEventNotification(Gadp.EventNotification.newBuilder()
.setPath(GadpValueUtils.makePath(object.getPath()))
.setObjectInvalidateEvent(
ObjectInvalidateEvent.newBuilder().setReason(reason)))
Gadp.ObjectInvalidateEvent.newBuilder().setReason(reason)))
.build()).exceptionally(GadpClientHandler::errorSendNotify);
}
@Override
public void consoleOutput(TargetObject console, Channel c, byte[] data) {
if (!sock.isOpen()) {
return;
}
if (c == null || console == null) {
Msg.warn(this, "Why is console or channel null in consoleOutput callback?");
return;
@ -139,6 +171,9 @@ public class GadpClientHandler
@Override
public void consoleOutput(TargetObject console, Channel c, String out) {
if (!sock.isOpen()) {
return;
}
consoleOutput(console, c, out.getBytes(TargetConsole.CHARSET));
}
@ -147,6 +182,9 @@ public class GadpClientHandler
TypedTargetObjectRef<? extends TargetStackFrame<?>> frame,
TypedTargetObjectRef<? extends TargetBreakpointSpec<?>> spec,
TypedTargetObjectRef<? extends TargetBreakpointLocation<?>> breakpoint) {
if (!sock.isOpen()) {
return;
}
Gadp.BreakHitEvent.Builder evt = Gadp.BreakHitEvent.newBuilder()
.setTrapped(GadpValueUtils.makePath(trapped.getPath()))
.setSpec(GadpValueUtils.makePath(spec.getPath()))
@ -163,6 +201,9 @@ public class GadpClientHandler
@Override
public void invalidateCacheRequested(TargetObject object) {
if (!sock.isOpen()) {
return;
}
channel.write(Gadp.RootMessage.newBuilder()
.setEventNotification(Gadp.EventNotification.newBuilder()
.setPath(GadpValueUtils.makePath(object.getPath()))
@ -174,6 +215,9 @@ public class GadpClientHandler
@Override
public void memoryReadError(TargetMemory<?> memory, AddressRange range,
DebuggerMemoryAccessException e) {
if (!sock.isOpen()) {
return;
}
// TODO: Ignore those generated by this client
channel.write(Gadp.RootMessage.newBuilder()
.setEventNotification(Gadp.EventNotification.newBuilder()
@ -186,6 +230,9 @@ public class GadpClientHandler
@Override
public void memoryUpdated(TargetMemory<?> memory, Address address, byte[] data) {
if (!sock.isOpen()) {
return;
}
// TODO: Ignore those generated by this client
channel.write(Gadp.RootMessage.newBuilder()
.setEventNotification(Gadp.EventNotification.newBuilder()
@ -198,6 +245,9 @@ public class GadpClientHandler
@Override
public void registersUpdated(TargetRegisterBank<?> bank, Map<String, byte[]> updates) {
if (!sock.isOpen()) {
return;
}
// TODO: Ignore those generated by this client
channel.write(Gadp.RootMessage.newBuilder()
.setEventNotification(Gadp.EventNotification.newBuilder()
@ -211,6 +261,9 @@ public class GadpClientHandler
public void event(TargetEventScope<?> object,
TypedTargetObjectRef<? extends TargetThread<?>> eventThread, TargetEventType type,
String description, List<Object> parameters) {
if (!sock.isOpen()) {
return;
}
Gadp.TargetEvent.Builder evt = Gadp.TargetEvent.newBuilder();
if (eventThread != null) {
evt.setEventThread(GadpValueUtils.makePath(eventThread.getPath()));
@ -235,12 +288,16 @@ public class GadpClientHandler
protected final AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> channel;
protected final ListenerForEvents listenerForEvents = new ListenerForEvents();
// Keeps strong references and tells level of subscription
protected final NavigableSet<TargetObject> subscriptions = new TreeSet<>();
public GadpClientHandler(AbstractGadpServer server, AsynchronousSocketChannel sock) {
super(server, sock);
model = server.model;
channel = new AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage>(sock);
channel = createMessageChannel(sock);
}
protected AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> createMessageChannel(
AsynchronousByteChannel byteChannel) {
return new AsyncProtobufMessageChannel<>(byteChannel);
}
@Override
@ -256,6 +313,7 @@ public class GadpClientHandler
loop.repeat();
try {
processMessage(msg).exceptionally(e -> {
e.printStackTrace();
replyError(msg, e).exceptionally(ee -> {
Msg.error(this, "Could not send error reply: " + ee);
return null;
@ -346,6 +404,8 @@ public class GadpClientHandler
return processFocus(msg.getSequence(), msg.getFocusRequest());
case INTERRUPT_REQUEST:
return processInterrupt(msg.getSequence(), msg.getInterruptRequest());
case INVOKE_REQUEST:
return processInvoke(msg.getSequence(), msg.getInvokeRequest());
case KILL_REQUEST:
return processKill(msg.getSequence(), msg.getKillRequest());
case LAUNCH_REQUEST:
@ -358,14 +418,12 @@ public class GadpClientHandler
return processRegisterRead(msg.getSequence(), msg.getRegisterReadRequest());
case REGISTER_WRITE_REQUEST:
return processRegisterWrite(msg.getSequence(), msg.getRegisterWriteRequest());
case RESYNC_REQUEST:
return processResync(msg.getSequence(), msg.getResyncRequest());
case RESUME_REQUEST:
return processResume(msg.getSequence(), msg.getResumeRequest());
case STEP_REQUEST:
return processStep(msg.getSequence(), msg.getStepRequest());
case SUBSCRIBE_REQUEST:
return processSubscribe(msg.getSequence(), msg.getSubscribeRequest());
case INVOKE_REQUEST:
return processInvoke(msg.getSequence(), msg.getInvokeRequest());
default:
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
"Unrecognized request: " + msg.getMsgCase());
@ -379,13 +437,16 @@ public class GadpClientHandler
"No listed version is supported");
}
TargetObjectSchema rootSchema = model.getRootSchema();
return channel.write(Gadp.RootMessage.newBuilder()
CompletableFuture<Integer> send = channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setConnectReply(Gadp.ConnectReply.newBuilder()
.setVersion(ver)
.setSchemaContext(XmlSchemaContext.serialize(rootSchema.getContext()))
.setRootSchema(rootSchema.getName().toString()))
.build());
return send.thenAccept(__ -> {
model.addModelListener(listenerForEvents, true);
});
}
protected CompletableFuture<?> processPing(int seqno, Gadp.PingRequest req) {
@ -408,99 +469,27 @@ public class GadpClientHandler
return ref;
}
protected void changeSubscription(TargetObject obj, boolean subscribed) {
synchronized (subscriptions) {
if (subscribed) {
if (subscriptions.add(obj)) {
obj.addListener(listenerForEvents);
}
}
else {
subscriptions.remove(obj);
obj.removeListener(listenerForEvents);
}
}
}
protected boolean isSubscribed(TargetObject obj) {
synchronized (subscriptions) {
return subscriptions.contains(obj);
}
}
protected boolean unsubscribeSubtree(TargetObject seed) {
synchronized (subscriptions) {
if (!subscriptions.remove(seed)) {
return false;
}
Set<TargetObject> tail = subscriptions.tailSet(seed);
for (Iterator<TargetObject> it = tail.iterator(); it.hasNext();) {
TargetObject o = it.next();
if (!PathUtils.isAncestor(seed.getPath(), o.getPath())) {
break;
}
o.removeListener(listenerForEvents);
it.remove();
}
return true;
}
}
protected <T extends TargetObjectRef> CompletableFuture<Integer> sendDelta(TargetObject parent,
Delta<TargetObjectRef, T> deltaE, Delta<?, ?> deltaA) {
assert isSubscribed(parent); // If listening, we should be subscribed
protected <T extends TargetObjectRef> CompletableFuture<Integer> sendDelta(
List<String> parentPath, Delta<TargetObjectRef, T> deltaE, Delta<?, ?> deltaA) {
return channel.write(Gadp.RootMessage.newBuilder()
.setEventNotification(Gadp.EventNotification.newBuilder()
.setPath(GadpValueUtils.makePath(parent.getPath()))
.setPath(GadpValueUtils.makePath(parentPath))
.setModelObjectEvent(Gadp.ModelObjectEvent.newBuilder()
.setDelta(GadpValueUtils.makeDelta(parent, deltaE, deltaA))))
.setElementDelta(
GadpValueUtils.makeElementDelta(parentPath, deltaE))
.setAttributeDelta(
GadpValueUtils.makeElementDelta(parentPath, deltaA))))
.build());
}
/**
* @implNote To avoid duplicates and spurious messages, we adopt the following strategy: 1) Get
* any elements and/or attributes for the object as requested. 2) Send the response
* and install the listener on the object. 3) Check if the object's elements and/or
* attributes have changed and send a delta immediately.
*/
protected CompletableFuture<?> processSubscribe(int seqno, Gadp.SubscribeRequest req) {
List<String> path = req.getPath().getEList();
return model.fetchModelValue(path).thenCompose(val -> {
DebuggerObjectModel.requireNonNull(val, path);
TargetObject obj = GadpValueUtils.getTargetObjectNonLink(path, val);
if (obj == null) {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setSubscribeReply(Gadp.SubscribeReply.newBuilder()
.setValue(GadpValueUtils.makeValue(path, val)))
.build());
}
changeSubscription(obj, req.getSubscribe());
AsyncFence fence = new AsyncFence();
if (req.getFetchElements()) {
fence.include(obj.fetchElements(req.getRefreshElements()));
}
if (req.getFetchAttributes()) {
fence.include(obj.fetchAttributes(req.getRefreshAttributes()));
}
return fence.ready().thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setSubscribeReply(Gadp.SubscribeReply.newBuilder()
.setValue(Gadp.Value.newBuilder()
.setObjectInfo(GadpValueUtils.makeInfo(obj))))
.build());
});
});
}
protected CompletableFuture<?> processInvoke(int seqno, Gadp.InvokeRequest req) {
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetMethod<?> method =
DebuggerObjectModel.requireIface(TargetMethod.class, obj, path);
return method.invoke(GadpValueUtils.getArguments(model, req.getArgumentList()));
}).thenCompose(result -> {
return model.flushEvents().thenApply(__ -> result);
}).thenCompose(result -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -510,8 +499,22 @@ public class GadpClientHandler
});
}
protected CompletableFuture<?> processResync(int seqno, Gadp.ResyncRequest req) {
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
return obj.resync(req.getAttributes(), req.getElements());
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setResyncReply(Gadp.ResyncReply.getDefaultInstance())
.build());
});
}
protected CompletableFuture<?> processAttach(int seqno, Gadp.AttachRequest req) {
ProtocolStringList path = req.getPath().getEList();
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetAttacher<?> attacher =
DebuggerObjectModel.requireIface(TargetAttacher.class, obj, path);
@ -527,6 +530,8 @@ public class GadpClientHandler
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
"Unrecognized attach specification:" + req);
}
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -536,9 +541,10 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processBreakCreate(int seqno, Gadp.BreakCreateRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetBreakpointContainer<?> breaks = DebuggerObjectModel
.requireIface(TargetBreakpointContainer.class, obj, req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetBreakpointContainer<?> breaks =
DebuggerObjectModel.requireIface(TargetBreakpointContainer.class, obj, path);
Set<TargetBreakpointKind> kinds = GadpValueUtils.getBreakKindSet(req.getKinds());
switch (req.getSpecCase()) {
case EXPRESSION:
@ -550,6 +556,8 @@ public class GadpClientHandler
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
"Unrecognized breakpoint specification: " + req);
}
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -559,10 +567,13 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processBreakToggle(int seqno, Gadp.BreakToggleRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetBreakpointSpec<?> spec = DebuggerObjectModel
.requireIface(TargetBreakpointSpec.tclass, obj, req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetBreakpointSpec<?> spec =
DebuggerObjectModel.requireIface(TargetBreakpointSpec.tclass, obj, path);
return spec.toggle(req.getEnabled());
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -572,10 +583,13 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processDelete(int seqno, Gadp.DeleteRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetDeletable<?> del = DebuggerObjectModel.requireIface(TargetDeletable.tclass, obj,
req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetDeletable<?> del =
DebuggerObjectModel.requireIface(TargetDeletable.tclass, obj, path);
return del.delete();
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -585,10 +599,13 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processDetach(int seqno, Gadp.DetachRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetDetachable<?> det = DebuggerObjectModel.requireIface(TargetDetachable.tclass, obj,
req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetDetachable<?> det =
DebuggerObjectModel.requireIface(TargetDetachable.tclass, obj, path);
return det.detach();
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -598,13 +615,16 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processExecute(int seqno, Gadp.ExecuteRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetInterpreter<?> interpreter = DebuggerObjectModel
.requireIface(TargetInterpreter.tclass, obj, req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetInterpreter<?> interpreter =
DebuggerObjectModel.requireIface(TargetInterpreter.tclass, obj, path);
if (req.getCapture()) {
return interpreter.executeCapture(req.getCommand());
}
return interpreter.execute(req.getCommand()).thenApply(__ -> "");
}).thenCompose(out -> {
return model.flushEvents().thenApply(__ -> out);
}).thenCompose(out -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -614,10 +634,13 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processFocus(int seqno, Gadp.FocusRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetFocusScope<?> scope = DebuggerObjectModel.requireIface(TargetFocusScope.tclass,
obj, req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetFocusScope<?> scope =
DebuggerObjectModel.requireIface(TargetFocusScope.tclass, obj, path);
return scope.requestFocus(model.createRef(req.getFocus().getEList()));
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -627,10 +650,13 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processInterrupt(int seqno, Gadp.InterruptRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetInterruptible<?> interruptible = DebuggerObjectModel
.requireIface(TargetInterruptible.tclass, obj, req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetInterruptible<?> interruptible =
DebuggerObjectModel.requireIface(TargetInterruptible.tclass, obj, path);
return interruptible.interrupt();
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -641,9 +667,11 @@ public class GadpClientHandler
protected CompletableFuture<?> processCacheInvalidate(int seqno,
Gadp.CacheInvalidateRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
return DebuggerObjectModel.requireNonNull(obj, req.getPath().getEList())
.invalidateCaches();
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
return DebuggerObjectModel.requireNonNull(obj, path).invalidateCaches();
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -653,10 +681,13 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processKill(int seqno, Gadp.KillRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetKillable<?> killable = DebuggerObjectModel.requireIface(TargetKillable.class, obj,
req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetKillable<?> killable =
DebuggerObjectModel.requireIface(TargetKillable.class, obj, path);
return killable.kill();
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -666,11 +697,17 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processLaunch(int seqno, Gadp.LaunchRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetLauncher<?> launcher = DebuggerObjectModel.requireIface(TargetLauncher.class, obj,
req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
Msg.debug(this, "Launching: " + Thread.currentThread());
TargetLauncher<?> launcher =
DebuggerObjectModel.requireIface(TargetLauncher.class, obj, path);
return launcher.launch(GadpValueUtils.getArguments(model, req.getArgumentList()));
}).thenCompose(__ -> {
Msg.debug(this, "Flushing events after launch: " + Thread.currentThread());
return model.flushEvents();
}).thenCompose(__ -> {
Msg.debug(this, "Responding after launch: " + Thread.currentThread());
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setLaunchReply(Gadp.LaunchReply.getDefaultInstance())
@ -679,11 +716,14 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processMemoryRead(int seqno, Gadp.MemoryReadRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetMemory<?> memory =
DebuggerObjectModel.requireIface(TargetMemory.class, obj, req.getPath().getEList());
DebuggerObjectModel.requireIface(TargetMemory.class, obj, path);
AddressRange range = GadpValueUtils.getAddressRange(memory.getModel(), req.getRange());
return memory.readMemory(range.getMinAddress(), (int) range.getLength());
}).thenCompose(data -> {
return model.flushEvents().thenApply(__ -> data);
}).thenCompose(data -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -694,12 +734,15 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processMemoryWrite(int seqno, Gadp.MemoryWriteRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetMemory<?> memory =
DebuggerObjectModel.requireIface(TargetMemory.class, obj, req.getPath().getEList());
DebuggerObjectModel.requireIface(TargetMemory.class, obj, path);
Address start = GadpValueUtils.getAddress(memory.getModel(), req.getStart());
// TODO: Spare a copy by specifying a ByteBuffer variant of writeMemory?
return memory.writeMemory(start, req.getContent().toByteArray());
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -709,10 +752,13 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processRegisterRead(int seqno, Gadp.RegisterReadRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetRegisterBank<?> bank = DebuggerObjectModel.requireIface(TargetRegisterBank.class,
obj, req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetRegisterBank<?> bank =
DebuggerObjectModel.requireIface(TargetRegisterBank.class, obj, path);
return bank.readRegistersNamed(req.getNameList());
}).thenCompose(data -> {
return model.flushEvents().thenApply(__ -> data);
}).thenCompose(data -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -723,14 +769,17 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processRegisterWrite(int seqno, Gadp.RegisterWriteRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetRegisterBank<?> bank = DebuggerObjectModel.requireIface(TargetRegisterBank.class,
obj, req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetRegisterBank<?> bank =
DebuggerObjectModel.requireIface(TargetRegisterBank.class, obj, path);
Map<String, byte[]> values = new LinkedHashMap<>();
for (Gadp.RegisterValue rv : req.getValueList()) {
values.put(rv.getName(), rv.getContent().toByteArray());
}
return bank.writeRegistersNamed(values);
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -740,10 +789,13 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processResume(int seqno, Gadp.ResumeRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetResumable<?> resumable = DebuggerObjectModel.requireIface(TargetResumable.class,
obj, req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetResumable<?> resumable =
DebuggerObjectModel.requireIface(TargetResumable.class, obj, path);
return resumable.resume();
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
@ -753,10 +805,13 @@ public class GadpClientHandler
}
protected CompletableFuture<?> processStep(int seqno, Gadp.StepRequest req) {
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
TargetSteppable<?> steppable = DebuggerObjectModel.requireIface(TargetSteppable.class,
obj, req.getPath().getEList());
List<String> path = req.getPath().getEList();
return model.fetchModelObject(path).thenCompose(obj -> {
TargetSteppable<?> steppable =
DebuggerObjectModel.requireIface(TargetSteppable.class, obj, path);
return steppable.step(GadpValueUtils.getStepKind(req.getKind()));
}).thenCompose(__ -> {
return model.flushEvents();
}).thenCompose(__ -> {
return channel.write(Gadp.RootMessage.newBuilder()
.setSequence(seqno)

View file

@ -210,11 +210,6 @@ message ParameterList {
repeated Parameter parameter = 1;
}
message Argument {
string name = 1;
Value value = 2;
}
message Value {
oneof spec {
bool bool_value = 1;
@ -235,7 +230,6 @@ message Value {
UpdateMode update_mode_value = 16;
Path path_value = 17;
PathList path_list_value = 18;
ModelObjectInfo object_info = 19;
ModelObjectStub object_stub = 20;
ParameterList parameters_value = 21;
ValueType type_value = 22;
@ -243,7 +237,7 @@ message Value {
}
}
message Attribute {
message NamedValue {
string name = 1;
Value value = 2;
}
@ -251,46 +245,19 @@ message Attribute {
message ModelObjectStub {
}
message ModelObjectInfo {
Path path = 1;
string type_hint = 2;
repeated string interface = 3;
repeated string element_index = 4;
repeated Attribute attribute = 5;
}
message ModelObjectDelta {
repeated string index_removed = 2;
repeated string index_added = 3;
// TODO: indices_moved?
repeated string attribute_removed = 4;
repeated Attribute attribute_added = 5;
repeated string removed = 1;
repeated NamedValue added = 2;
}
message ModelObjectEvent {
ModelObjectDelta delta = 1;
ModelObjectDelta element_delta = 1;
ModelObjectDelta attribute_delta = 2;
}
/**
* This message both retrieves the requested object(s) and (un)subscribes to further changes
*
* If subscribe is true, then future changes to element indices and attribute names are sent to the
* client. If fetch is true, then the objects current indices and names are included in the
* response. If refresh is true, then the server-side model is asked to refresh the object's
* indices and names while servicing the request.
*/
message SubscribeRequest {
Path path = 1;
bool subscribe = 2;
bool refresh = 3;
bool fetchElements = 4;
bool fetchAttributes = 5;
bool refreshElements = 6;
bool refreshAttributes = 7;
}
message SubscribeReply {
Value value = 1;
message ObjectCreatedEvent {
string type_hint = 2;
repeated string interface = 3;
}
message ObjectInvalidateEvent {
@ -299,7 +266,7 @@ message ObjectInvalidateEvent {
message LaunchRequest {
Path path = 1;
repeated Argument argument = 2;
repeated NamedValue argument = 2;
}
message LaunchReply {
@ -486,13 +453,22 @@ message FocusReply {
message InvokeRequest {
Path path = 1;
repeated Argument argument = 2;
repeated NamedValue argument = 2;
}
message InvokeReply {
Value result = 1;
}
message ResyncRequest {
Path path = 1;
bool attributes = 2;
bool elements = 3;
}
message ResyncReply {
}
enum TargetEventType {
EV_STOPPED = 0;
EV_RUNNING = 1;
@ -515,6 +491,9 @@ message TargetEvent {
repeated Value parameters = 4;
}
message RootAddedEvent {
}
message EventNotification {
Path path = 1;
oneof evt {
@ -525,9 +504,11 @@ message EventNotification {
ConsoleOutputEvent console_output_event = 312;
MemoryUpdateEvent memory_update_event = 317;
MemoryErrorEvent memory_error_event = 417;
ObjectCreatedEvent object_created_event = 324;
ObjectInvalidateEvent object_invalidate_event = 323;
RegisterUpdateEvent register_update_event = 322;
TargetEvent target_event = 330;
RootAddedEvent root_added_event = 326;
}
}
@ -600,10 +581,10 @@ message RootMessage {
StepRequest step_request = 119;
StepReply step_reply = 219;
SubscribeRequest subscribe_request = 104;
SubscribeReply subscribe_reply = 204;
InvokeRequest invoke_request = 105;
InvokeReply invoke_reply = 205;
ResyncRequest resync_request = 125;
ResyncReply resync_reply = 225;
}
}

View file

@ -15,7 +15,6 @@
*/
package ghidra.dbg.gadp;
import static ghidra.lifecycle.Unfinished.TODO;
import static org.junit.Assert.*;
import java.io.IOException;
@ -27,7 +26,6 @@ import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.junit.Ignore;
@ -39,21 +37,23 @@ import generic.ID;
import generic.Unique;
import ghidra.async.*;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.agent.*;
import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.attributes.TargetStringList;
import ghidra.dbg.gadp.GadpClientServerTest.EventListener.CallEntry;
import ghidra.dbg.gadp.client.GadpClient;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.protocol.Gadp.RootMessage;
import ghidra.dbg.gadp.server.AbstractGadpServer;
import ghidra.dbg.gadp.server.GadpClientHandler;
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener;
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
import ghidra.dbg.target.TargetObject.TargetObjectFetchingListener;
import ghidra.dbg.target.TargetObject.TargetObjectListener;
import ghidra.dbg.target.TargetObject.*;
import ghidra.dbg.target.schema.TargetAttributeType;
import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
import ghidra.dbg.util.*;
@ -232,6 +232,48 @@ public class GadpClientServerTest {
}
}
public static class PrintingAsyncProtobufMessageChannel<//
S extends GeneratedMessageV3, R extends GeneratedMessageV3>
extends AsyncProtobufMessageChannel<S, R> {
private final String sendPrefix;
private final String recvPrefix;
public PrintingAsyncProtobufMessageChannel(String local, String peer,
AsynchronousByteChannel channel) {
super(channel);
this.sendPrefix = local + "->" + peer + ": ";
this.recvPrefix = local + "<-" + peer + ": ";
}
@Override
public CompletableFuture<Integer> write(S msg) {
Msg.debug(this, sendPrefix + msg);
return super.write(msg).thenApplyAsync(c -> {
return c;
});
}
@Override
public <R2 extends R> CompletableFuture<R2> read(IOFunction<R2> receiver) {
return super.read(receiver).thenApplyAsync(msg -> {
Msg.debug(this, recvPrefix + msg);
return msg;
});
}
}
public static class PrintingGadpClient extends GadpClient {
public PrintingGadpClient(String description, AsynchronousByteChannel channel) {
super(description, channel);
}
@Override
protected AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> createMessageChannel(
AsynchronousByteChannel channel) {
return new PrintingAsyncProtobufMessageChannel<>("C", "S", channel);
}
}
protected static <T> T waitOn(CompletableFuture<T> future) throws Throwable {
try {
return future.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
@ -251,12 +293,17 @@ public class GadpClientServerTest {
protected final TestGadpTargetAvailableLinkContainer links =
new TestGadpTargetAvailableLinkContainer(this);
public TestGadpTargetSession(FakeDebuggerObjectModel model) {
public TestGadpTargetSession(TestGadpObjectModel model) {
super(model, "Session");
changeAttributes(List.of(), List.of(available, processes), Map.of(), "Initialized");
}
@Override
public AbstractDebuggerObjectModel getModel() {
return super.getModel();
}
@TargetAttributeType(name = "Available", required = true, fixed = true)
public TestGadpTargetAvailableContainer getAvailable() {
return available;
@ -292,24 +339,14 @@ public class GadpClientServerTest {
public class TestTargetObject<E extends TargetObject, P extends TargetObject>
extends DefaultTargetObject<E, P> {
public TestTargetObject(DebuggerObjectModel model, P parent, String key, String typeHint) {
public TestTargetObject(AbstractDebuggerObjectModel model, P parent, String key,
String typeHint) {
super(model, parent, key, typeHint);
}
@Override
public CompletableFuture<?> fetchAttribute(String name) {
if (!PathUtils.isInvocation(name)) {
return super.fetchAttribute(name);
}
Map.Entry<String, String> invocation = PathUtils.parseInvocation(name);
TestTargetMethod method =
getTypedAttributeNowByName(invocation.getKey(), TestTargetMethod.class, null);
if (method == null) {
return AsyncUtils.nil();
}
Object ret = method.testInvoke(invocation.getValue());
changeAttributes(List.of(), Map.of(name, ret), "Invoked " + name);
return CompletableFuture.completedFuture(ret).thenCompose(model::gateFuture);
public AbstractDebuggerObjectModel getModel() {
return super.getModel();
}
@Override
@ -334,13 +371,11 @@ public class GadpClientServerTest {
false, TargetStringList.of(), "Others to greet",
"List of other people to greet individually")));
public class TestTargetMethod extends TestTargetObject<TargetObject, TargetObject>
implements TargetMethod<TestTargetMethod> {
private Function<String, ?> method;
public class TestGadpTargetMethod extends TestTargetObject<TargetObject, TestTargetObject<?, ?>>
implements TargetMethod<TestGadpTargetMethod> {
public TestTargetMethod(TargetObject parent, String key, Function<String, ?> method) {
public TestGadpTargetMethod(TestTargetObject<?, ?> parent, String key) {
super(parent.getModel(), parent, key, "Method");
this.method = method;
setAttributes(Map.of(
PARAMETERS_ATTRIBUTE_NAME, PARAMS,
@ -348,16 +383,16 @@ public class GadpClientServerTest {
"Initialized");
}
public Object testInvoke(String paramsExpr) {
return method.apply(paramsExpr);
}
@Override
public CompletableFuture<Object> invoke(Map<String, ?> arguments) {
TestMethodInvocation invocation = new TestMethodInvocation(arguments);
invocations.offer(invocation);
invocationCount.set(invocations.size(), null);
return invocation;
return invocation.thenApply(obj -> {
parent.changeAttributes(List.of(), Map.ofEntries(
Map.entry("greet(" + arguments.get("arg") + ")", obj)),
"greet() invoked");
return obj;
}).thenCompose(model::gateFuture);
}
}
@ -389,13 +424,12 @@ public class GadpClientServerTest {
public class TestGadpTargetAvailableContainer
extends TestTargetObject<TestGadpTargetAvailable, TestGadpTargetSession> {
public char punct = '!';
public TestGadpTargetAvailableContainer(TestGadpTargetSession session) {
super(session.getModel(), session, "Available", "AvailableContainer");
setAttributes(List.of(
new TestTargetMethod(this, "greet", p -> "Hello, " + p + punct)),
new TestGadpTargetMethod(this, "greet")),
Map.of(), "Initialized");
}
@ -417,7 +451,7 @@ public class GadpClientServerTest {
String cmd) {
super(available.getModel(), available, PathUtils.makeKey(PathUtils.makeIndex(pid)),
"Available");
setAttributes(List.of(), Map.of(
changeAttributes(List.of(), Map.of(
"pid", pid,
"cmd", cmd //
), "Initialized");
@ -435,38 +469,62 @@ public class GadpClientServerTest {
}
}
// TODO: Refactor with other Fake and Test. Probably put in Framework-Debugging....
public class FakeDebuggerObjectModel extends AbstractDebuggerObjectModel {
private final TestGadpTargetSession session = new TestGadpTargetSession(this);
public class BlankObjectModel extends AbstractDebuggerObjectModel {
private final AddressSpace ram =
new GenericAddressSpace("RAM", 64, AddressSpace.TYPE_RAM, 0);
private final AddressFactory factory =
new DefaultAddressFactory(new AddressSpace[] { ram });
@Override
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
return CompletableFuture.completedFuture(session);
}
@Override
public AddressFactory getAddressFactory() {
return factory;
}
}
@Override
public CompletableFuture<Void> close() {
return AsyncUtils.NIL;
// TODO: Refactor with other Fake and Test. Probably put in Framework-Debugging....
public class TestGadpObjectModel extends BlankObjectModel {
private TestGadpTargetSession session;
public TestGadpObjectModel(boolean createSession) {
if (createSession) {
session = new TestGadpTargetSession(this);
addModelRoot(session);
}
}
@Override // file access
protected void addModelRoot(SpiTargetObject root) {
super.addModelRoot(root);
}
}
public class TestGadpServer extends AbstractGadpServer {
@SuppressWarnings("hiding")
final FakeDebuggerObjectModel model;
final TestGadpObjectModel model;
public TestGadpServer(SocketAddress addr) throws IOException {
super(new FakeDebuggerObjectModel(), addr);
this.model = (FakeDebuggerObjectModel) getModel();
this(new TestGadpObjectModel(true), addr);
}
public TestGadpServer(TestGadpObjectModel model, SocketAddress addr) throws IOException {
super(model, addr);
this.model = model;
}
}
public class PrintingTestGadpServer extends TestGadpServer {
public PrintingTestGadpServer(SocketAddress addr) throws IOException {
super(addr);
}
@Override
protected GadpClientHandler newHandler(AsynchronousSocketChannel sock) {
return new GadpClientHandler(this, sock) {
@Override
protected AsyncProtobufMessageChannel<RootMessage, RootMessage> createMessageChannel(
AsynchronousByteChannel byteChannel) {
return new PrintingAsyncProtobufMessageChannel<>("S", "C", byteChannel);
}
};
}
}
@ -474,16 +532,31 @@ public class GadpClientServerTest {
TestGadpServer server;
public ServerRunner() throws IOException {
server = new TestGadpServer(new InetSocketAddress("localhost", 0));
server = createServer(new InetSocketAddress("localhost", 0));
server.launchAsyncService();
}
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
return new TestGadpServer(addr);
}
@Override
public void close() throws Exception {
server.terminate();
}
}
public class PrintingServerRunner extends ServerRunner {
public PrintingServerRunner() throws IOException {
super();
}
@Override
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
return new PrintingTestGadpServer(addr);
}
}
class TestMethodInvocation extends CompletableFuture<Object> {
final Map<String, ?> args;
@ -493,12 +566,41 @@ public class GadpClientServerTest {
}
protected Deque<Map<String, ?>> launches = new LinkedList<>();
protected AsyncReference<Integer, Void> invocationCount = new AsyncReference<>();
protected Deque<TestMethodInvocation> invocations = new LinkedList<>();
protected static class AsyncDeque<T> {
private final Deque<T> deque = new LinkedList<>();
private final AsyncReference<Integer, Void> count = new AsyncReference<>(0);
public boolean offer(T e) {
boolean result = deque.offer(e);
count.set(deque.size(), null);
return result;
}
public T poll() {
T result = deque.poll();
count.set(deque.size(), null);
return result;
}
public void clear() {
deque.clear();
count.set(0, null);
}
}
protected AsyncDeque<TestMethodInvocation> invocations = new AsyncDeque<>();
protected AsynchronousSocketChannel socketChannel() throws IOException {
// Note, it looks like the executor knows to shut itself down on GC
AsynchronousChannelGroup group =
AsynchronousChannelGroup.withThreadPool(Executors.newSingleThreadExecutor());
return AsynchronousSocketChannel.open(group);
}
@Test
public void testConnectDisconnect() throws Throwable {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
@ -514,7 +616,7 @@ public class GadpClientServerTest {
@Test
public void testFetchModelValue() throws Throwable {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -532,9 +634,39 @@ public class GadpClientServerTest {
}
}
@Test
@Ignore("Developer's desk only")
public void stressTest() throws Throwable {
for (int i = 0; i < 1000; i++) {
try {
System.out.println("ITERATION: " + i);
testFetchModelValueCached();
}
catch (Throwable e) {
System.err.println("Failed on iteration " + i);
throw e;
}
}
}
@Test // Sanity check on underlying model
public void testFetchModelValueCachedNoGadp() throws Throwable {
TestGadpObjectModel model = new TestGadpObjectModel(true);
Object rootVal = waitOn(model.fetchModelValue(List.of()));
TargetObject root = (TargetObject) rootVal;
assertEquals(List.of(), root.getPath());
// Do fetchAll to create objects and populate their caches
Map<String, ? extends TargetObject> available =
waitOn(model.fetchObjectElements(PathUtils.parse("Available"))
.thenCompose(DebugModelConventions::fetchAll));
assertEquals(2, available.size());
Object cmd = waitOn(model.fetchModelValue(PathUtils.parse("Available[1].cmd")));
assertEquals("echo", cmd);
}
@Test
public void testFetchModelValueCached() throws Throwable {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
MonitoredAsyncByteChannel monitored = new MonitoredAsyncByteChannel(socket);
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", monitored);
@ -552,7 +684,8 @@ public class GadpClientServerTest {
monitored.reset();
Object cmd = waitOn(client.fetchModelValue(PathUtils.parse("Available[1].cmd")));
assertEquals("echo", cmd);
assertEquals(0, monitored.writeCount);
// Just 1 to request .cmd, since the above will only have covered Available[]
assertEquals(1, monitored.writeCount);
waitOn(client.close());
}
finally {
@ -562,7 +695,7 @@ public class GadpClientServerTest {
@Test
public void testInvoke() throws Throwable {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -579,7 +712,7 @@ public class GadpClientServerTest {
CompletableFuture<Object> future = method.invoke(Map.of(
"whom", "GADP",
"others", TargetStringList.of("Alice", "Bob")));
waitOn(invocationCount.waitValue(1));
waitOn(invocations.count.waitValue(1));
TestMethodInvocation invocation = invocations.poll();
assertEquals(Map.of(
"whom", "GADP",
@ -598,7 +731,7 @@ public class GadpClientServerTest {
@Test
public void testInvokeMethodCached() throws Throwable {
// TODO: Disambiguate / deconflict these two "method" cases
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
try (AsynchronousSocketChannel socket = socketChannel();
ServerRunner runner = new ServerRunner()) {
MonitoredAsyncByteChannel monitored = new MonitoredAsyncByteChannel(socket);
GadpClient client = new GadpClient("Test", monitored);
@ -616,13 +749,24 @@ public class GadpClientServerTest {
TargetObjectRef methodRef = (TargetObjectRef) attrs.get("greet");
TargetMethod<?> method = waitOn(methodRef.as(TargetMethod.tclass).fetch());
assertNotNull(method);
CompletableFuture<?> future1 = avail.fetchAttribute("greet(World)");
waitOn(invocations.count.waitValue(1));
TestMethodInvocation invocation1 = invocations.poll();
assertEquals(Map.of("arg", "World"), invocation1.args);
invocation1.complete("Hello, World!");
assertEquals("Hello, World!", waitOn(future1));
// Should not have to wait
assertEquals("Hello, World!", waitOn(avail.fetchAttribute("greet(World)")));
runner.server.model.session.available.punct = '?'; // No effect before flush
assertEquals("Hello, World!", waitOn(avail.fetchAttribute("greet(World)")));
assertEquals(0, invocations.count.get().intValue());
// Flush the cache
waitOn(avail.fetchAttributes(true));
assertEquals("Hello, World?", waitOn(avail.fetchAttribute("greet(World)")));
CompletableFuture<?> future2 = avail.fetchAttribute("greet(World)");
waitOn(invocations.count.waitValue(1));
TestMethodInvocation invocation2 = invocations.poll();
invocation2.complete("Hello, World?");
assertEquals("Hello, World?", waitOn(future2));
waitOn(client.close());
}
@ -630,7 +774,7 @@ public class GadpClientServerTest {
@Test
public void testListRoot() throws Throwable {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -657,7 +801,7 @@ public class GadpClientServerTest {
@Test
public void testLaunch() throws Throwable {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -691,7 +835,7 @@ public class GadpClientServerTest {
invocations.add(new ElementsChangedInvocation(parent, removed, added));
}
};
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -724,7 +868,7 @@ public class GadpClientServerTest {
}
}
};
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -757,7 +901,7 @@ public class GadpClientServerTest {
@Test
public void testSubscribeNoSuchPath() throws Throwable {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -774,7 +918,7 @@ public class GadpClientServerTest {
public void testSubscribeLaunchForChildrenChanged() throws Throwable {
ElementsChangedListener elemL = new ElementsChangedListener();
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
@ -811,7 +955,7 @@ public class GadpClientServerTest {
ElementsChangedListener elemL = new ElementsChangedListener();
InvalidatedListener invL = new InvalidatedListener();
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
try (AsynchronousSocketChannel socket = socketChannel();
ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -836,7 +980,7 @@ public class GadpClientServerTest {
), "Changed");
waitOn(invL.count.waitValue(2));
waitOn(elemL.count.waitValue(1));
waitOn(elemL.count.waitValue(2));
for (TargetObject a : avail1.values()) {
assertFalse(a.isValid());
@ -848,17 +992,22 @@ public class GadpClientServerTest {
assertEquals(1, avail2.size());
assertEquals("cat", avail2.get("1").getCachedAttribute("cmd"));
ElementsChangedInvocation removed = elemL.invocations.remove();
assertSame(availCont, removed.parent);
assertEquals(Map.of(), removed.added);
assertEquals(Set.of("1"), Set.copyOf(removed.removed));
ElementsChangedInvocation changed = Unique.assertOne(elemL.invocations);
assertSame(availCont, changed.parent);
// Use equals here, since the listener only gets the ref
assertEquals(avail2.get("1"), Unique.assertOne(changed.added.values()));
assertEquals(Set.of("1"), changed.added.keySet());
assertEquals(Set.of("2"), Set.copyOf(changed.removed));
assertSame(avail2.get("1"), Unique.assertOne(changed.added.values()));
Map<ID<TargetObject>, String> actualInv = invL.invocations.stream()
.collect(Collectors.toMap(ii -> ID.of(ii.object), ii -> ii.reason));
Map<ID<TargetObject>, String> expectedInv =
avail1.values()
.stream()
.collect(Collectors.toMap(o -> ID.of(o), o -> "Changed"));
Map<ID<TargetObject>, String> expectedInv = Map.ofEntries(
Map.entry(ID.of(avail1.get("1")), "Replaced"),
Map.entry(ID.of(avail1.get("2")), "Changed"));
assertEquals(expectedInv, actualInv);
}
}
@ -867,7 +1016,7 @@ public class GadpClientServerTest {
public void testReplaceAttribute() throws Throwable {
AttributesChangedListener attrL = new AttributesChangedListener();
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
try (AsynchronousSocketChannel socket = socketChannel();
ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -877,32 +1026,36 @@ public class GadpClientServerTest {
TargetObject echoAvail =
waitOn(client.fetchModelObject(PathUtils.parse("Available[1]")));
echoAvail.addListener(attrL);
assertEquals(Map.of(
"pid", 1,
"cmd", "echo" //
), waitOn(echoAvail.fetchAttributes()));
assertEquals(Map.ofEntries(
Map.entry("pid", 1),
Map.entry("cmd", "echo"),
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
Map.entry("_display", "[1]")),
waitOn(echoAvail.fetchAttributes()));
TestGadpTargetAvailable ssEchoAvail =
runner.server.model.session.available.getCachedElements().get("1");
ssEchoAvail.setAttributes(Map.of(
"cmd", "echo",
"args", "Hello, World!" //
), "Changed");
ssEchoAvail.changeAttributes(List.of("pid"), Map.ofEntries(
Map.entry("cmd", "echo"),
Map.entry("args", "Hello, World!")),
"Changed");
waitOn(attrL.count.waitValue(1));
assertEquals(Map.of(
"cmd", "echo",
"args", "Hello, World!" //
), echoAvail.getCachedAttributes());
assertEquals(Map.ofEntries(
Map.entry("cmd", "echo"),
Map.entry("args", "Hello, World!"),
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
Map.entry("_display", "[1]")),
echoAvail.getCachedAttributes());
AttributesChangedInvocation changed = Unique.assertOne(attrL.invocations);
assertSame(echoAvail, changed.parent);
assertEquals(Set.of("pid"), Set.copyOf(changed.removed));
assertEquals(Map.of(
"args", "Hello, World!" //
), changed.added);
assertEquals(Map.ofEntries(
Map.entry("args", "Hello, World!")),
changed.added);
}
}
@ -910,7 +1063,7 @@ public class GadpClientServerTest {
public void testSubtreeInvalidationDeduped() throws Throwable {
InvalidatedListener invL = new InvalidatedListener();
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
try (AsynchronousSocketChannel socket = socketChannel();
ServerRunner runner = new ServerRunner()) {
MonitoredGadpClient client = new MonitoredGadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -951,7 +1104,7 @@ public class GadpClientServerTest {
public void testNoEventsAfterInvalidated() throws Throwable {
AttributesChangedListener attrL = new AttributesChangedListener();
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
try (AsynchronousSocketChannel socket = socketChannel();
ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -960,18 +1113,23 @@ public class GadpClientServerTest {
TargetObject echoAvail =
waitOn(client.fetchModelObject(PathUtils.parse("Available[1]")));
// TODO: This comes back null too often...
echoAvail.addListener(attrL);
assertEquals(Map.of(
"pid", 1,
"cmd", "echo" //
), waitOn(echoAvail.fetchAttributes()));
assertEquals(Map.ofEntries(
Map.entry("pid", 1),
Map.entry("cmd", "echo"),
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
Map.entry("_display", "[1]")),
waitOn(echoAvail.fetchAttributes()));
TargetObject ddAvail = waitOn(client.fetchModelObject(PathUtils.parse("Available[2]")));
ddAvail.addListener(attrL);
assertEquals(Map.of(
"pid", 2,
"cmd", "dd" //
), waitOn(ddAvail.fetchAttributes()));
assertEquals(Map.ofEntries(
Map.entry("pid", 2),
Map.entry("cmd", "dd"),
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
Map.entry("_display", "[2]")),
waitOn(ddAvail.fetchAttributes()));
// NB: copy
Map<String, TestGadpTargetAvailable> ssAvail =
@ -980,9 +1138,8 @@ public class GadpClientServerTest {
runner.server.model.session.available.changeElements(List.of("1"), List.of(), Map.of(),
"1 is Gone");
// Should produce nothing
(ssAvail.get("1")).setAttributes(List.of(), Map.of(
"cmd", "echo",
"args", "Hello, World!"),
(ssAvail.get("1")).changeAttributes(List.of(), Map.ofEntries(
Map.entry("args", "Hello, World!")),
"Changed");
// Produce something, so we know we didn't get the other thing
(ssAvail.get("2")).changeAttributes(List.of(), List.of(), Map.of(
@ -1002,7 +1159,7 @@ public class GadpClientServerTest {
@Test
public void testProxyWithLinkedElementsCanonicalFirst() throws Throwable {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -1024,7 +1181,7 @@ public class GadpClientServerTest {
@Test
public void testProxyWithLinkedElementsLinkFirst() throws Throwable {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -1033,9 +1190,10 @@ public class GadpClientServerTest {
runner.server.model.session.addLinks();
TargetObjectRef linkRef =
(TargetObjectRef) waitOn(client.fetchModelValue(PathUtils.parse("Links[1]")));
assertFalse(linkRef instanceof TargetObject);
assertTrue(linkRef instanceof TargetObject);
TargetObject link =
waitOn(client.fetchModelObject(PathUtils.parse("Links[1]")));
assertSame(linkRef, link);
TargetObject canonical =
waitOn(client.fetchModelObject(PathUtils.parse("Available[2]")));
assertSame(canonical, link);
@ -1049,7 +1207,7 @@ public class GadpClientServerTest {
@Test
public void testFetchModelValueFollowsLink() throws Throwable {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner()) {
GadpClient client = new GadpClient("Test", socket);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
@ -1064,64 +1222,221 @@ public class GadpClientServerTest {
}
}
@Test
@Ignore("TODO")
public void testCachingWithLinks() throws Throwable {
public static class EventListener implements DebuggerModelListener {
public static class CallEntry {
public final String methodName;
public final List<Object> args;
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
ServerRunner runner = new ServerRunner()) {
MonitoredGadpClient client = new MonitoredGadpClient("Test", socket);
public CallEntry(String methodName, List<Object> args) {
this.methodName = methodName;
this.args = args;
}
@Override
public String toString() {
return String.format("<CallEntry %s(%s)>", methodName, args);
}
@Override
public int hashCode() {
return Objects.hash(methodName, args);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof CallEntry)) {
return false;
}
CallEntry that = (CallEntry) obj;
if (!Objects.equals(this.methodName, that.methodName)) {
return false;
}
if (!Objects.equals(this.args, that.args)) {
return false;
}
return true;
}
}
public final List<CallEntry> record = new ArrayList<>();
@Override
public void created(TargetObject object) {
record.add(new CallEntry("created", List.of(object)));
}
@Override
public void attributesChanged(TargetObject parent, Collection<String> removed,
Map<String, ?> added) {
record.add(new CallEntry("attributesChanged", List.of(parent, removed, added)));
}
@Override
public void elementsChanged(TargetObject parent, Collection<String> removed,
Map<String, ? extends TargetObjectRef> added) {
record.add(new CallEntry("elementsChanged", List.of(parent, removed, added)));
}
@Override
public void rootAdded(TargetObject root) {
record.add(new CallEntry("rootAdded", List.of(root)));
}
}
@Test
public void testConnectBeforeRootCreated() throws Throwable {
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner() {
@Override
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
return new TestGadpServer(new TestGadpObjectModel(false), addr);
}
}) {
GadpClient client = new PrintingGadpClient("Test", socket);
EventListener listener = new EventListener();
client.addModelListener(listener, true);
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
runner.server.getLocalAddress()));
waitOn(client.connect());
runner.server.model.session.addLinks();
client.getMessageChannel().clear();
assertEquals("echo", waitOn(client.fetchModelValue(PathUtils.parse("Links[2].cmd"))));
assertEquals(2, client.getMessageChannel().record.size());
assertEquals(Gadp.RootMessage.MsgCase.SUBSCRIBE_REQUEST,
client.getMessageChannel().record.get(0).assertSent().getMsgCase());
assertEquals(Gadp.RootMessage.MsgCase.SUBSCRIBE_REPLY,
client.getMessageChannel().record.get(1).assertReceived().getMsgCase());
DefaultTargetObject<?, ?> root =
new DefaultTargetModelRoot(runner.server.model, "Root");
DefaultTargetObject<?, ?> a =
new DefaultTargetObject<>(runner.server.model, root, "a", "A");
a.changeAttributes(List.of(), Map.of("test", 6), "Because");
root.changeAttributes(List.of(), List.of(a), Map.of(), "Because");
runner.server.model.addModelRoot(root);
waitOn(client.fetchModelRoot());
// Since I don't have the parent, as usual, no cache
client.getMessageChannel().clear();
assertEquals("echo", waitOn(client.fetchModelValue(PathUtils.parse("Links[2].cmd"))));
assertEquals(2, client.getMessageChannel().record.size());
assertEquals(List.of(
new CallEntry("created", List.of(
client.getModelRoot())),
new CallEntry("attributesChanged", List.of(
client.getModelRoot(), Set.of(), Map.ofEntries(
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
Map.entry("_display", "<root>")))),
new CallEntry("created", List.of(
client.getModelObject(PathUtils.parse("a")))),
new CallEntry("attributesChanged", List.of(
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
Map.entry("_display", "a")))),
new CallEntry("attributesChanged", List.of(
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
Map.entry("test", 6)))),
new CallEntry("attributesChanged", List.of(
client.getModelRoot(), Set.of(), Map.ofEntries(
Map.entry("a", client.getModelObject(PathUtils.parse("a")))))),
new CallEntry("rootAdded", List.of(client.getModelRoot()))),
listener.record);
}
}
/**
* Now, fetch Links[2] first, and repeat the experiment It should still not use the
* cache, because we won't know if Links[2] still points to Available[1]. Technically,
* Available[1] is what will be cached. Index 2 of Links will not be cached until/if
* Links is fetched.
*/
client.getMessageChannel().clear();
TargetObject avail1 = waitOn(client.fetchModelObject(PathUtils.parse("Links[2]")));
// Odd, but I believe it's correct since the first reply comes back with a ref
assertEquals(4, client.getMessageChannel().record.size());
assertNotNull(avail1);
client.getMessageChannel().clear();
assertEquals("echo", waitOn(client.fetchModelValue(PathUtils.parse("Links[2].cmd"))));
assertEquals(2, client.getMessageChannel().record.size());
client.getMessageChannel().clear();
assertEquals("echo", waitOn(client.fetchModelValue(PathUtils.parse("Links[2].cmd"))));
assertEquals(2, client.getMessageChannel().record.size());
@Test
public void testConnectBetweenRootCreatedAndAdded() throws Throwable {
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner() {
@Override
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
return new TestGadpServer(new TestGadpObjectModel(false), addr);
}
}) {
GadpClient client = new PrintingGadpClient("Test", socket);
EventListener listener = new EventListener();
client.addModelListener(listener, true);
// Now, fetch Links, and its elements to ensure it is cached
TargetObject links = waitOn(client.fetchModelObject(PathUtils.parse("Links")));
assertSame(avail1, waitOn(links.fetchElement("2")));
DefaultTargetObject<?, ?> root =
new DefaultTargetModelRoot(runner.server.model, "Root");
DefaultTargetObject<?, ?> a =
new DefaultTargetObject<>(runner.server.model, root, "a", "A");
a.changeAttributes(List.of(), Map.of("test", 6), "Because");
client.getMessageChannel().clear();
assertSame(avail1, waitOn(links.fetchElement("2")));
assertEquals(2, client.getMessageChannel().record.size());
assertEquals("Links[2]", PathUtils.toString(client.getMessageChannel().record.get(0)
.assertSent()
.getSubscribeRequest()
.getPath()
.getEList()));
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
runner.server.getLocalAddress()));
waitOn(client.connect());
TODO();
waitOn(client.close());
root.changeAttributes(List.of(), List.of(a), Map.of(), "Because");
runner.server.model.addModelRoot(root);
waitOn(client.fetchModelRoot());
waitOn(client.flushEvents());
assertEquals(List.of(
new CallEntry("created", List.of(
client.getModelRoot())),
new CallEntry("attributesChanged", List.of(
client.getModelRoot(), Set.of(), Map.ofEntries(
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
Map.entry("_display", "<root>")))),
new CallEntry("created", List.of(
client.getModelObject(PathUtils.parse("a")))),
new CallEntry("attributesChanged", List.of(
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
Map.entry("_display", "a")))),
new CallEntry("attributesChanged", List.of(
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
Map.entry("test", 6)))),
new CallEntry("attributesChanged", List.of(
client.getModelRoot(), Set.of(), Map.ofEntries(
Map.entry("a", client.getModelObject(PathUtils.parse("a")))))),
new CallEntry("rootAdded", List.of(client.getModelRoot()))),
listener.record);
}
}
@Test
public void testConnectAfterRootAdded() throws Throwable {
AsynchronousSocketChannel socket = socketChannel();
try (ServerRunner runner = new ServerRunner() {
@Override
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
return new TestGadpServer(new TestGadpObjectModel(false), addr);
}
}) {
GadpClient client = new PrintingGadpClient("Test", socket);
EventListener listener = new EventListener();
client.addModelListener(listener, true);
DefaultTargetObject<?, ?> root =
new DefaultTargetModelRoot(runner.server.model, "Root");
DefaultTargetObject<?, ?> a =
new DefaultTargetObject<>(runner.server.model, root, "a", "A");
a.changeAttributes(List.of(), Map.of("test", 6), "Because");
root.changeAttributes(List.of(), List.of(a), Map.of(), "Because");
runner.server.model.addModelRoot(root);
waitOn(runner.server.model.flushEvents());
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
runner.server.getLocalAddress()));
waitOn(client.connect());
waitOn(client.fetchModelRoot());
waitOn(client.flushEvents());
assertEquals(List.of(
new CallEntry("created", List.of(
client.getModelRoot())),
new CallEntry("attributesChanged", List.of( // Defaults upon client-side construction
client.getModelRoot(), Set.of(), Map.ofEntries(
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
Map.entry("_display", "<root>")))),
new CallEntry("created", List.of(
client.getModelObject(PathUtils.parse("a")))),
new CallEntry("attributesChanged", List.of( // Defaults
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
Map.entry("_display", "a")))),
new CallEntry("attributesChanged", List.of(
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
Map.entry("test", 6)))),
new CallEntry("attributesChanged", List.of(
client.getModelRoot(), Set.of(), Map.ofEntries(
Map.entry("a", client.getModelObject(PathUtils.parse("a")))))),
new CallEntry("rootAdded", List.of(client.getModelRoot()))),
listener.record);
}
}
}

View file

@ -16,6 +16,7 @@
package ghidra.dbg.gadp.client;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.net.InetSocketAddress;
@ -23,7 +24,8 @@ import java.net.SocketAddress;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@ -36,7 +38,6 @@ import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.gadp.GadpVersion;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
import ghidra.dbg.gadp.util.GadpValueUtils;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.util.ElementsChangedListener;
import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation;
@ -90,6 +91,10 @@ public class GadpClientTest {
this.cli = srv.accept();
}
public void nextSeq() {
seqno++;
}
public void expect(Gadp.RootMessage msg) throws IOException {
Msg.debug(this, "Expecting: " + msg);
@ -131,13 +136,14 @@ public class GadpClientTest {
}
}
public void handleConnect(GadpVersion version) throws IOException {
public void expectRequestConnect() throws IOException {
expect(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setConnectRequest(GadpVersion.makeRequest())
.build());
// TODO: Test schemas here?
// Seems they're already hit well enough in dependent tests
}
public void sendReplyConnect(GadpVersion version) throws IOException {
send(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setConnectReply(Gadp.ConnectReply.newBuilder()
@ -145,89 +151,163 @@ public class GadpClientTest {
.setSchemaContext("<context/>")
.setRootSchema("OBJECT"))
.build());
seqno++;
}
public void handlePing() throws IOException {
public void handleConnect(GadpVersion version) throws IOException {
expectRequestConnect();
sendReplyConnect(version);
nextSeq();
}
public void sendNotifyObjectCreated(List<String> path, List<String> interfaces,
String typeHint) throws IOException {
send(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setEventNotification(Gadp.EventNotification.newBuilder()
.setPath(GadpValueUtils.makePath(path))
.setObjectCreatedEvent(Gadp.ObjectCreatedEvent.newBuilder()
.addAllInterface(interfaces)
.setTypeHint(typeHint)))
.build());
}
public void sendNotifyRootAdded() throws IOException {
send(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setEventNotification(Gadp.EventNotification.newBuilder()
.setRootAddedEvent(Gadp.RootAddedEvent.getDefaultInstance()))
.build());
}
public void notifyAddRoot() throws IOException {
sendNotifyObjectCreated(List.of(), List.of(), "Root");
sendNotifyRootAdded();
}
public void expectRequestPing(String content) throws IOException {
expect(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setPingRequest(Gadp.PingRequest.newBuilder()
.setContent(HELLO_WORLD))
.setContent(content))
.build());
}
public void sendReplyPing(String content) throws IOException {
send(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setPingReply(Gadp.PingReply.newBuilder()
.setContent(HELLO_WORLD))
.setContent(content))
.build());
seqno++;
}
public void handleSubscribeValue(List<String> path, Object value)
throws Exception {
expect(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setSubscribeRequest(Gadp.SubscribeRequest.newBuilder()
.setPath(GadpValueUtils.makePath(path))
.setSubscribe(true))
.build());
send(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setSubscribeReply(Gadp.SubscribeReply.newBuilder()
.setValue(GadpValueUtils.makeValue(path, value)))
.build());
seqno++;
public void handlePing() throws IOException {
expectRequestPing(HELLO_WORLD);
sendReplyPing(HELLO_WORLD);
nextSeq();
}
public void handleFetchObject(List<String> path) throws Exception {
expect(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setSubscribeRequest(Gadp.SubscribeRequest.newBuilder()
.setPath(GadpValueUtils.makePath(path))
.setSubscribe(true))
.build());
Gadp.SubscribeReply.Builder reply = Gadp.SubscribeReply.newBuilder()
public Gadp.ModelObjectDelta.Builder makeAttributeDelta(List<String> parentPath,
Map<String, Object> primitives, Map<String, List<String>> objects) {
Gadp.ModelObjectDelta.Builder delta = Gadp.ModelObjectDelta.newBuilder();
if (primitives != null) {
for (Map.Entry<String, ?> ent : primitives.entrySet()) {
delta.addAdded(GadpValueUtils.makeNamedValue(parentPath, ent));
}
}
if (objects != null) {
for (Map.Entry<String, List<String>> ent : objects.entrySet()) {
if (PathUtils.isLink(parentPath, ent.getKey(), ent.getValue())) {
delta.addAdded(Gadp.NamedValue.newBuilder()
.setName(ent.getKey())
.setValue(Gadp.Value.newBuilder()
.setObjectInfo(Gadp.ModelObjectInfo.newBuilder()
.setPath(GadpValueUtils.makePath(path))));
send(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setSubscribeReply(reply)
.build());
seqno++;
.setPathValue(GadpValueUtils.makePath(ent.getValue()))));
}
else {
delta.addAdded(Gadp.NamedValue.newBuilder()
.setName(ent.getKey())
.setValue(Gadp.Value.newBuilder()
.setObjectStub(Gadp.ModelObjectStub.getDefaultInstance())));
}
}
}
return delta;
}
public void handleFetchElements(List<String> path, Collection<String> indices)
throws Exception {
public Gadp.ModelObjectDelta.Builder makeElementDelta(List<String> parentPath,
Map<String, List<String>> objects) {
Gadp.ModelObjectDelta.Builder delta = Gadp.ModelObjectDelta.newBuilder();
if (objects != null) {
for (Map.Entry<String, List<String>> ent : objects.entrySet()) {
if (PathUtils.isElementLink(parentPath, ent.getKey(), ent.getValue())) {
delta.addAdded(Gadp.NamedValue.newBuilder()
.setName(ent.getKey())
.setValue(Gadp.Value.newBuilder()
.setPathValue(GadpValueUtils.makePath(ent.getValue()))));
}
else {
delta.addAdded(Gadp.NamedValue.newBuilder()
.setName(ent.getKey())
.setValue(Gadp.Value.newBuilder()
.setObjectStub(Gadp.ModelObjectStub.getDefaultInstance())));
}
}
}
return delta;
}
public void expectRequestResync(List<String> path, boolean refreshAttributes,
boolean refreshElements) throws IOException {
expect(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setSubscribeRequest(Gadp.SubscribeRequest.newBuilder()
.setResyncRequest(Gadp.ResyncRequest.newBuilder()
.setPath(GadpValueUtils.makePath(path))
.setSubscribe(true)
.setFetchElements(true))
.setAttributes(refreshAttributes)
.setElements(refreshElements))
.build());
Gadp.SubscribeReply.Builder reply = Gadp.SubscribeReply.newBuilder()
.setValue(Gadp.Value.newBuilder()
.setObjectInfo(Gadp.ModelObjectInfo.newBuilder()
.setPath(GadpValueUtils.makePath(path))
.addAllElementIndex(indices)));
send(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setSubscribeReply(reply)
.build());
seqno++;
}
public void sendModelEvent(List<String> path, Collection<String> indicesAdded)
throws Exception {
Gadp.ModelObjectEvent.Builder evt = Gadp.ModelObjectEvent.newBuilder();
evt.setDelta(Gadp.ModelObjectDelta.newBuilder()
.addAllIndexAdded(indicesAdded));
public void sendNotifyObjects(List<String> parentPath, Map<String, List<String>> elements,
Map<String, List<String>> attrObjects, Map<String, Object> attrPrimitives)
throws IOException {
send(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setEventNotification(Gadp.EventNotification.newBuilder()
.setPath(GadpValueUtils.makePath(path))
.setModelObjectEvent(evt))
.setPath(GadpValueUtils.makePath(parentPath))
.setModelObjectEvent(Gadp.ModelObjectEvent.newBuilder()
.setAttributeDelta(
makeAttributeDelta(parentPath, attrPrimitives,
attrObjects))
.setElementDelta(makeElementDelta(parentPath, elements))))
.build());
}
public void sendReplyResync() throws IOException {
send(Gadp.RootMessage.newBuilder()
.setSequence(seqno)
.setResyncReply(Gadp.ResyncReply.getDefaultInstance())
.build());
}
public void handleResyncAttributes(List<String> path, boolean refresh,
Map<String, List<String>> objects, Map<String, Object> primitives)
throws Exception {
expectRequestResync(path, refresh, false);
if (primitives != null || objects != null) {
sendNotifyObjects(path, null, objects, primitives);
}
sendReplyResync();
nextSeq();
}
public void handleResyncElements(List<String> path, boolean refresh,
Map<String, List<String>> objects) throws Exception {
expectRequestResync(path, false, refresh);
if (objects != null) {
sendNotifyObjects(path, objects, null, null);
}
sendReplyResync();
nextSeq();
}
}
protected void dumpBuffer(ByteBuffer buf) {
@ -313,29 +393,29 @@ public class GadpClientTest {
srv.handleConnect(GadpVersion.VER1);
waitOn(gadpConnect);
List<String> parentPath = PathUtils.parse("Parent");
CompletableFuture<? extends TargetObject> fetchParent =
client.fetchModelObject(PathUtils.parse("Parent"));
srv.handleFetchObject(PathUtils.parse("Parent"));
client.fetchModelObject(parentPath);
srv.notifyAddRoot();
srv.sendNotifyObjectCreated(parentPath, List.of(), "Parent");
srv.handleResyncAttributes(List.of(), false, Map.of("Parent", parentPath), null);
TargetObject parent = waitOn(fetchParent);
parent.addListener(elemL);
CompletableFuture<? extends Map<String, ? extends TargetObjectRef>> fetchElements =
parent.fetchElements();
srv.handleFetchElements(PathUtils.parse("Parent"), List.of());
srv.handleResyncElements(parentPath, false, Map.of());
assertEquals(Map.of(), waitOn(fetchElements));
srv.sendModelEvent(PathUtils.parse("Parent"), List.of("0"));
List<String> elem0Path = PathUtils.parse("Parent[0]");
srv.sendNotifyObjectCreated(elem0Path, List.of(), "Element");
srv.sendNotifyObjects(parentPath, Map.of("0", elem0Path), null, null);
waitOn(elemL.count.waitValue(1));
ElementsChangedInvocation changed = Unique.assertOne(elemL.invocations);
assertEquals(parent, changed.parent);
TargetObjectRef childRef = Unique.assertOne(changed.added.values());
assertEquals(PathUtils.parse("Parent[0]"), childRef.getPath());
// Not cached, since fetchElements just lists refs
CompletableFuture<? extends TargetObject> fetchChild = childRef.fetch();
srv.handleFetchObject(PathUtils.parse("Parent[0]"));
TargetObject child = waitOn(fetchChild);
assertEquals(Map.of("0", child), parent.getCachedElements());
assertTrue(childRef instanceof GadpClientTargetObject);
assertEquals(elem0Path, childRef.getPath());
}
assertEquals(1, elemL.count.get().intValue()); // After connection is closed
}
@ -354,16 +434,18 @@ public class GadpClientTest {
CompletableFuture<Void> gadpConnect = client.connect();
srv.handleConnect(GadpVersion.VER1);
waitOn(gadpConnect);
CompletableFuture<?> cfRoot = client.fetchModelRoot();
srv.notifyAddRoot();
waitOn(cfRoot);
CompletableFuture<?> fetchVal1 = client.fetchModelValue(PathUtils.parse("value"));
CompletableFuture<?> fetchVal2 = client.fetchModelValue(PathUtils.parse("value"));
srv.handleSubscribeValue(PathUtils.parse("value"), HELLO_WORLD);
srv.handleResyncAttributes(List.of(), false, null, Map.of("value", HELLO_WORLD));
assertEquals(HELLO_WORLD, waitOn(fetchVal1));
assertEquals(HELLO_WORLD, waitOn(fetchVal2));
// Because parent not cached, it should send another request
CompletableFuture<?> fetchVal3 = client.fetchModelValue(PathUtils.parse("value"));
srv.handleSubscribeValue(PathUtils.parse("value"), "Hi");
CompletableFuture<?> fetchVal3 = client.fetchModelValue(PathUtils.parse("value"), true);
srv.handleResyncAttributes(List.of(), true, null, Map.of("value", "Hi"));
assertEquals("Hi", waitOn(fetchVal3));
}
}

View file

@ -21,7 +21,6 @@ import java.util.concurrent.CompletableFuture;
import com.sun.jdi.*;
import ghidra.async.AsyncUtils;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
import ghidra.dbg.jdi.manager.JdiManager;
import ghidra.dbg.target.TargetObject;
@ -64,6 +63,7 @@ public class JdiModelImpl extends AbstractDebuggerObjectModel {
Address start = ram.getAddress(0L);
this.defaultRange = new AddressRangeImpl(start, start.add(BLOCK_SIZE));
addModelRoot(root);
}
@Override
@ -86,7 +86,7 @@ public class JdiModelImpl extends AbstractDebuggerObjectModel {
@Override
public CompletableFuture<Void> close() {
jdi.terminate();
return AsyncUtils.NIL;
return super.close();
}
public JdiModelTargetRoot getRoot() {
@ -227,6 +227,7 @@ public class JdiModelImpl extends AbstractDebuggerObjectModel {
return range;
}
@Override
public AddressFactory getAddressFactory() {
return addressFactory;
}

View file

@ -85,7 +85,7 @@ public class JdiModelTargetClassContainer extends JdiModelTargetObjectImpl {
return getClassesByName().get(name);
}
public CompletableFuture<?> refresh() {
public CompletableFuture<?> refreshInternal() {
if (!isObserved()) {
return AsyncUtils.NIL;
}

View file

@ -91,7 +91,7 @@ public class JdiModelTargetConnectorContainer extends JdiModelTargetObjectImpl {
return null;
}
public CompletableFuture<?> refresh() {
public CompletableFuture<?> refreshInternal() {
if (!isObserved()) {
return AsyncUtils.NIL;
}

View file

@ -118,7 +118,7 @@ public class JdiModelTargetModuleContainer extends JdiModelTargetObjectImpl
return modulesByName.get(name);
}
public CompletableFuture<?> refresh() {
public CompletableFuture<?> refreshInternal() {
if (!isObserved()) {
return AsyncUtils.NIL;
}

View file

@ -46,7 +46,7 @@ public class JdiModelTargetObjectImpl extends
private boolean modified;
public JdiModelTargetObjectImpl(JdiModelTargetObject parent, String id) {
super(parent.getModel(), parent, id, "Object");
super(parent.getModelImpl(), parent, id, "Object");
this.impl = parent.getModelImpl();
this.mirror = (Mirror) parent.getObject();
this.object = null;
@ -65,7 +65,7 @@ public class JdiModelTargetObjectImpl extends
public JdiModelTargetObjectImpl(JdiModelTargetObject parent, String id, Object object,
boolean isElement) {
super(parent.getModel(), parent, isElement ? keyObject(id) : id, "Object");
super(parent.getModelImpl(), parent, isElement ? keyObject(id) : id, "Object");
this.impl = parent.getModelImpl();
this.mirror = object instanceof Mirror ? (Mirror) object : null;
this.object = object;
@ -88,7 +88,7 @@ public class JdiModelTargetObjectImpl extends
}
public JdiModelTargetObjectImpl(JdiModelTargetSectionContainer parent) {
super(parent.getModel(), parent, keyObject("NULL_SPACE"), "Object");
super(parent.getModelImpl(), parent, keyObject("NULL_SPACE"), "Object");
this.impl = parent.getModelImpl();
this.mirror = parent.mirror;
this.display = "NULL_SPACE";

View file

@ -398,7 +398,7 @@ public class JdiModelTargetVM extends JdiModelTargetObjectImpl implements //
}
@Override
public void refresh() {
public void refreshInternal() {
// TODO Auto-generated method stub
}

View file

@ -21,7 +21,7 @@ import ghidra.dbg.target.TargetEnvironment;
public interface JdiModelTargetEnvironment<T extends TargetEnvironment<T>>
extends JdiModelTargetObject, TargetEnvironment<T> {
public void refresh();
public void refreshInternal();
@Override
public default String getArchitecture() {

View file

@ -47,27 +47,33 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
*/
protected class ForInterpreterListener implements TargetInterpreterListener {
@Override
public void consoleOutput(TargetObject console, Channel channel, String out) {
// NB: yes, this is lame... The InterpreterPanel's repositionScrollPane
// method substracts 1 from the text length to compute the new position
// causing it to scroll to the last character printed. We want it to scroll
// to the next line, so...
out += " ";
public void consoleOutput(TargetObject console, Channel channel, byte[] out) {
OutputStream os;
switch (channel) {
case STDOUT:
if (outWriter == null) {
return;
}
outWriter.print(out);
outWriter.flush();
os = stdOut;
break;
case STDERR:
if (errWriter == null) {
os = stdErr;
break;
default:
throw new AssertionError();
}
// It's possible stdOut/Err was not initialized, yet
if (os == null) {
return;
}
errWriter.print(out);
errWriter.flush();
break;
/**
* NB: yes, the extra space is lame... The InterpreterPanel's repositionScrollPane
* method subtracts 1 from the text length to compute the new position causing it to
* scroll to the last character printed. We want it to scroll to the next line, so...
*/
try {
os.write(out);
os.write(' ');
}
catch (IOException e) {
Msg.error(this, "Cannot write to interpreter window: ", e);
}
}
@ -84,7 +90,7 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
}
@Override
public void invalidated(TargetObject object, String reason) {
public void invalidated(TargetObject object, TargetObject branch, String reason) {
Swing.runLater(() -> {
if (object == targetConsole) { // Redundant
if (pinned) {
@ -107,8 +113,8 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
protected Thread thread;
protected InterpreterConsole guiConsole;
protected BufferedReader inReader;
protected PrintWriter outWriter;
protected PrintWriter errWriter;
protected OutputStream stdOut;
protected OutputStream stdErr;
protected ToggleDockingAction actionPin;
protected boolean pinned = false;
@ -146,8 +152,8 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
InterpreterComponentProvider provider = (InterpreterComponentProvider) guiConsole;
provider.setSubTitle(targetConsole.getDisplay());
setErrWriter(guiConsole.getErrWriter());
setOutWriter(guiConsole.getOutWriter());
setStdErr(guiConsole.getStdErr());
setStdOut(guiConsole.getStdOut());
setStdIn(guiConsole.getStdin());
createActions();
@ -161,12 +167,12 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
guiConsole.addAction(actionPin);
}
public void setOutWriter(PrintWriter outWriter) {
this.outWriter = outWriter;
public void setStdOut(OutputStream stdOut) {
this.stdOut = stdOut;
}
public void setErrWriter(PrintWriter errWriter) {
this.errWriter = errWriter;
public void setStdErr(OutputStream stdErr) {
this.stdErr = stdErr;
}
public void setStdIn(InputStream stdIn) {

View file

@ -46,22 +46,15 @@ import ghidra.app.plugin.core.debug.gui.objects.components.*;
import ghidra.app.services.*;
import ghidra.async.AsyncUtils;
import ghidra.async.TypeSpec;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.*;
import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.error.DebuggerMemoryAccessException;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibility;
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionStateListener;
import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener;
import ghidra.dbg.target.TargetInterpreter.TargetInterpreterListener;
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
import ghidra.dbg.target.TargetMemory.TargetMemoryListener;
import ghidra.dbg.target.TargetObject.TargetObjectFetchingListener;
import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener;
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
import ghidra.dbg.util.PathUtils;
import ghidra.framework.options.AutoOptions;
@ -80,9 +73,9 @@ import ghidra.util.table.GhidraTable;
import resources.ResourceManager;
public class DebuggerObjectsProvider extends ComponentProviderAdapter implements //AllTargetObjectListenerAdapter,
TargetObjectFetchingListener, TargetAccessibilityListener, TargetExecutionStateListener,
TargetFocusScopeListener, TargetInterpreterListener, TargetMemoryListener,
TargetRegisterBankListener, ObjectContainerListener {
TargetObjectFetchingListener, //
DebuggerModelListener, //
ObjectContainerListener {
public static final String PATH_JOIN_CHAR = ".";
//private static final String AUTOUPDATE_ATTRIBUTE_NAME = "autoupdate";
@ -310,6 +303,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
public void setModel(DebuggerObjectModel model) {
currentModel = model;
currentModel.addModelListener(this, true);
refresh();
}
@ -1384,14 +1378,6 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
});
}
public void addListener(TargetObject targetObject) {
/*
if (recorder != null) {
recorder.getListenerForRecord().addListener(targetObject);
}
*/
}
public void stopRecording(TargetObject targetObject) {
// TODO: Do `this.recorder = ...` on every object selection change?
TraceRecorder rec = modelService.getRecorderForSuccessor(targetObject);
@ -1681,7 +1667,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
public void elementsChangedObjects(TargetObject parent, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
//System.err.println("local EC: " + parent);
ObjectContainer container = getContainerByPath(parent.getPath());
ObjectContainer container = parent == null ? null : getContainerByPath(parent.getPath());
if (container != null) {
container.augmentElements(removed, added);
boolean visibleChange = false;
@ -1702,7 +1688,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
public void attributesChangedObjects(TargetObject parent, Collection<String> removed,
Map<String, ?> added) {
//System.err.println("local AC: " + parent + ":" + removed + ":" + added);
ObjectContainer container = getContainerByPath(parent.getPath());
ObjectContainer container = parent == null ? null : getContainerByPath(parent.getPath());
if (container != null) {
container.augmentAttributes(removed, added);
boolean visibleChange = false;

View file

@ -15,23 +15,17 @@
*/
package ghidra.app.plugin.core.debug.gui.objects;
import static ghidra.async.AsyncUtils.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import org.jdom.Element;
import ghidra.async.AsyncFence;
import ghidra.async.TypeSpec;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetProcess;
import ghidra.dbg.util.PathUtils;
import ghidra.util.Msg;
import ghidra.util.datastruct.ListenerSet;
import ghidra.util.xml.XmlUtilities;
public class ObjectContainer implements Comparable {
@ -43,9 +37,6 @@ public class ObjectContainer implements Comparable {
private final Map<String, Object> attributeMap = new LinkedHashMap<>();
private Set<ObjectContainer> currentChildren = new TreeSet<>();
public final ListenerSet<ObjectContainerListener> listeners =
new ListenerSet<>(ObjectContainerListener.class);
private boolean immutable;
private boolean visible = true;
private boolean isSubscribed = false;
@ -171,29 +162,14 @@ public class ObjectContainer implements Comparable {
*/
public CompletableFuture<ObjectContainer> getOffspring() {
if (targetObjectRef == null) {
return null;
if (targetObject == null) {
return CompletableFuture.completedFuture(null);
}
AtomicReference<TargetObject> to = new AtomicReference<>();
AtomicReference<Map<String, ? extends TargetObject>> elements = new AtomicReference<>();
AtomicReference<Map<String, ?>> attributes = new AtomicReference<>();
return sequence(TypeSpec.cls(ObjectContainer.class)).then(seq -> {
targetObjectRef.fetch().handle(seq::next);
}, to).then(seq -> {
targetObject = to.get();
AsyncFence fence = new AsyncFence();
fence.include(targetObject.fetchElements(true)
.thenCompose(DebugModelConventions::fetchAll)
.thenAccept(elements::set));
fence.include(targetObject.fetchAttributes(true)
.thenCompose(attrs -> DebugModelConventions.fetchObjAttrs(targetObject, attrs))
.thenAccept(attributes::set));
fence.ready().handle(seq::next);
}).then(seq -> {
rebuildContainers(elements.get(), attributes.get());
return targetObject.resync(true, true).thenApply(__ -> {
rebuildContainers(targetObject.getCachedElements(), targetObject.getCachedAttributes());
propagateProvider(provider);
seq.exit(this);
}).finish();
return this;
});
}
protected void checkAutoRecord() {
@ -375,10 +351,6 @@ public class ObjectContainer implements Comparable {
this.provider = newProvider;
provider.addTargetToMap(this);
}
this.addListener(provider);
//if (targetObject != null && !currentChildren.isEmpty()) {
// targetObject.addListener(provider);
//}
for (ObjectContainer c : currentChildren) {
c.propagateProvider(provider);
}
@ -534,14 +506,6 @@ public class ObjectContainer implements Comparable {
this.immutable = immutable;
}
public void addListener(ObjectContainerListener listener) {
listeners.add(listener);
}
public void removeListener(ObjectContainerListener listener) {
listeners.remove(listener);
}
public boolean isVisible() {
return visible;
}
@ -561,18 +525,10 @@ public class ObjectContainer implements Comparable {
public void subscribe() {
isSubscribed = true;
if (targetObject != null && provider != null) {
targetObject.addListener(provider);
provider.addListener(targetObject);
}
}
public void unsubscribe() {
isSubscribed = false;
targetObject.removeListener(provider);
if (provider.isAutorecord()) {
//provider.stopRecording(targetObject);
}
}
public boolean isModified() {

View file

@ -127,7 +127,6 @@ public class ImportFromFactsAction extends ImportExportAsAction {
if (root != null) {
ObjectContainer c = p.getRoot();
c.setTargetObject(root);
root.addListener(p);
provider.update(c);
}
}

View file

@ -87,7 +87,6 @@ public class ImportFromXMLAction extends ImportExportAsAction {
DummyTargetObject to = xmlToObject(p, root, path);
ObjectContainer c = p.getRoot();
c.setTargetObject(to);
to.addListener(p);
provider.update(c);
}
catch (Exception e) {

View file

@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture;
import org.apache.commons.lang3.StringUtils;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.target.TargetObject;
@ -156,6 +157,11 @@ public class DummyTargetObject implements TargetObject {
return kind;
}
@Override
public CompletableFuture<Void> resync(boolean attributes, boolean elements) {
return AsyncUtils.NIL;
}
@Override
public CompletableFuture<? extends Map<String, ? extends TargetObject>> fetchElements() {
// Why not completedFuture(elements)?

View file

@ -34,7 +34,6 @@ public class ObjectAttributeRow {
ref.fetch().handle(seq::next);
}, targetObject).then(seq -> {
to = targetObject.get();
to.addListener(provider);
}).finish();
}

View file

@ -38,17 +38,10 @@ public class ObjectElementRow {
ref.fetch().handle(seq::next);
}, targetObject).then(seq -> {
to = targetObject.get();
to.addListener(provider);
to.fetchAttributes(true).handle(seq::next);
//to.getAttributes().thenAccept(v -> map = v);
}, attributes).then(seq -> {
map = attributes.get();
for (Object obj : map.values()) {
if (obj instanceof TargetObject) {
TargetObject attr = (TargetObject) obj;
attr.addListener(provider);
}
}
}).finish();
}

View file

@ -86,7 +86,7 @@ public class DebuggerModelServicePlugin extends Plugin
protected TargetObjectListener forRemoval = new TargetObjectListener() {
@Override
public void invalidated(TargetObject object, String reason) {
public void invalidated(TargetObject object, TargetObject branch, String reason) {
synchronized (listenersByModel) {
ListenersForRemovalAndFocus listener = listenersByModel.remove(model);
if (listener == null) {
@ -125,7 +125,7 @@ public class DebuggerModelServicePlugin extends Plugin
}
r.addListener(this.forRemoval);
if (!r.isValid()) {
forRemoval.invalidated(root, "Who knows?");
forRemoval.invalidated(root, root, "Who knows?");
}
CompletableFuture<? extends TargetFocusScope<?>> findSuitable =
DebugModelConventions.findSuitable(TargetFocusScope.tclass, r);

View file

@ -23,7 +23,6 @@ import org.junit.Test;
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceInternal;
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceProxyPlugin;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
@ -35,7 +34,9 @@ import help.screenshot.GhidraScreenShotGenerator;
public class DebuggerTargetsPluginScreenShots extends GhidraScreenShotGenerator {
@FactoryDescription(brief = "Demo Debugger", htmlDetails = "A connection for demonstration purposes")
@FactoryDescription(
brief = "Demo Debugger",
htmlDetails = "A connection for demonstration purposes")
protected static class ScreenShotDebuggerModelFactory implements DebuggerModelFactory {
private void nop() {
@ -63,6 +64,7 @@ public class DebuggerTargetsPluginScreenShots extends GhidraScreenShotGenerator
public ScreenShotDebuggerObjectModel(String display) {
this.display = display;
addModelRoot(root);
}
@Override
@ -79,11 +81,6 @@ public class DebuggerTargetsPluginScreenShots extends GhidraScreenShotGenerator
public AddressFactory getAddressFactory() {
throw new AssertionError();
}
@Override
public CompletableFuture<Void> close() {
return AsyncUtils.NIL;
}
}
DebuggerModelServiceInternal modelService;

View file

@ -19,6 +19,7 @@ import java.lang.ref.Cleaner.Cleanable;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.RejectedExecutionException;
import java.util.function.Function;
import java.util.function.Predicate;
@ -166,6 +167,9 @@ public class AsyncReference<T, C> {
try {
listener.accept(oldVal, newVal, cause);
}
catch (RejectedExecutionException exc) {
Msg.trace(this, "Ignoring rejection", exc);
}
catch (Throwable exc) {
Msg.error(this, "Ignoring exception on async reference listener: ", exc);
}

View file

@ -123,6 +123,7 @@ public abstract class AbstractAsyncServer<S extends AbstractAsyncServer<S, H>, H
}
}
}
group.shutdown();
if (err != null) {
throw err;
}

View file

@ -523,7 +523,7 @@ public enum DebugModelConventions {
protected abstract boolean checkDescend(TargetObjectRef ref);
@Override
public void invalidated(TargetObject object, String reason) {
public void invalidated(TargetObject object, TargetObject branch, String reason) {
runNotInSwing(this, () -> doInvalidated(object, reason), "invalidated");
}

View file

@ -15,6 +15,18 @@
*/
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 ghidra.util.Msg;
/**
* A listener for events related to the debugger model, usually a connection
*
@ -22,7 +34,19 @@ package ghidra.dbg;
* TODO: Most (non-client) models do not implement this. Even the client ones do not implement
* {@link #modelStateChanged()}
*/
public interface DebuggerModelListener {
public interface DebuggerModelListener
extends TargetObjectListener, TargetAccessibilityListener, TargetBreakpointListener,
TargetInterpreterListener, TargetEventScopeListener, TargetExecutionStateListener,
TargetFocusScopeListener, TargetMemoryListener, TargetRegisterBankListener {
/**
* An error occurred such that this listener will no longer receive events
*
* @param t the exception describing the error
*/
default public void catastrophic(Throwable t) {
Msg.error(this, "Catastrophic listener error", t);
}
/**
* The model has been successfully opened
@ -33,6 +57,17 @@ public interface DebuggerModelListener {
default public void modelOpened() {
}
/**
* 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
*/
default public void rootAdded(TargetObject root) {
}
/**
* The model was closed
*

View file

@ -18,7 +18,7 @@ package ghidra.dbg;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import ghidra.async.AsyncUtils;
import ghidra.async.TypeSpec;
@ -146,9 +146,27 @@ public interface DebuggerObjectModel {
/**
* Add a listener for model events
*
* <p>
* If requested, the listener is notified of existing objects via an event replay. It will first
* replay all the created events in the same order they were originally emitted. Any objects
* which have since been invalidated are excluded in the replay. They don't exist anymore, after
* all. Next it will replay the attribute- and element-added events in post order. This is an
* attempt to ensure an object's dependencies are met by the time the client receives its added
* event. This isn't always possible due to cycles, but such cycles are usually informational.
*
* @param listener the listener
* @param replay true to replay object tree events (doesn't include register or memory caches)
*/
public void addModelListener(DebuggerModelListener listener, boolean replay);
/**
* Add a listener for model events, without replay
*
* @param listener the listener
*/
public void addModelListener(DebuggerModelListener listener);
public default void addModelListener(DebuggerModelListener listener) {
addModelListener(listener, false);
}
/**
* Remove a model event listener
@ -309,9 +327,18 @@ public interface DebuggerObjectModel {
* object represents the debugger itself.
*
* @return the root
* @deprecated use {@link #getModelRoot()} instead
*/
@Deprecated(forRemoval = true)
public CompletableFuture<? extends TargetObject> fetchModelRoot();
/**
* Get the root object of the model
*
* @return the root
*/
public TargetObject getModelRoot();
/**
* Fetch the value at the given path
*
@ -362,6 +389,38 @@ public interface DebuggerObjectModel {
return fetchModelValue(List.of(path));
}
/**
* Get the value at a given path
*
* <p>
* If the path does not exist, null is returned. Note that an attempt to access the child of a
* primitive is the same as accessing a path that does not exist; however, an error will be
* logged, since this typically indicates a programming error.
*
* @param path the path
* @return the value
*/
public default Object getModelValue(List<String> path) {
Object cur = getModelRoot();
for (String key : path) {
if (cur == null) {
return null;
}
if (!(cur instanceof TargetObject)) {
Msg.error(this, "Primitive " + cur + " cannot have child '" + key + "'");
return null;
}
TargetObject obj = (TargetObject) cur;
if (PathUtils.isIndex(key)) {
cur = obj.getCachedElements().get(PathUtils.parseIndex(key));
continue;
}
assert PathUtils.isName(key);
cur = obj.getCachedAttribute(key);
}
return cur;
}
/**
* Fetch the object with the given path
*
@ -392,11 +451,27 @@ public interface DebuggerObjectModel {
/**
* @see #fetchModelObject(List)
* @deprecated Use {@link #getModelObject(List)} instead, or {@link #fetchModelObject(List)} if
* a refresh is needed
*/
@Deprecated
public default CompletableFuture<? extends TargetObject> fetchModelObject(List<String> path) {
return fetchModelObject(path, false);
}
/**
* Get an object from the model
*
* <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.
*
* @param path the path of the object
* @return the object
*/
public TargetObject getModelObject(List<String> path);
/**
* @see #fetchModelObject(List)
*/
@ -498,50 +573,23 @@ public interface DebuggerObjectModel {
if (ex == null || DebuggerModelTerminatingException.isIgnorable(ex)) {
Msg.warn(origin, message + ": " + ex);
}
else if (AsyncUtils.unwrapThrowable(ex) instanceof RejectedExecutionException) {
Msg.trace(origin, "Ignoring rejection", ex);
}
else {
Msg.error(origin, message, ex);
}
}
/**
* Get the executor used to invoke client callback routines
*
* @return the executor
*/
Executor getClientExecutor();
/**
* Ensure that dependent computations occur on the client executor
* Permit all callbacks to be invoked before proceeding
*
* <p>
* This also preserves scheduling order on the executor. Using just
* {@link CompletableFuture#thenApplyAsync(java.util.function.Function)} makes no guarantees
* about execution order, because that invocation could occur before invocations in the chained
* actions. This one instead uses
* {@link CompletableFuture#thenCompose(java.util.function.Function)} to schedule a final action
* which performs the actual completion via the executor.
* This operates by placing the request into the queue itself, so that any event callbacks
* queued <em>at the time of the flush invocation</em> are completed first. There are no
* guarantees with respect to events which get queued <em>after the flush invocation</em>.
*
* @param <T> the type of the future value
* @param cf the future
* @return a future gated via the client executor
* @return a future which completes when all queued callbacks have been invoked
*/
default <T> CompletableFuture<T> gateFuture(CompletableFuture<T> cf) {
return cf.thenCompose(this::gateFuture);
}
/**
* 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
*/
default <T> CompletableFuture<T> gateFuture(T v) {
return CompletableFuture.supplyAsync(() -> v, getClientExecutor());
}
CompletableFuture<Void> flushEvents();
}

View file

@ -15,29 +15,190 @@
*/
package ghidra.dbg.agent;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.*;
import java.util.concurrent.*;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.attributes.TargetObjectRef;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.util.PathUtils;
import ghidra.util.datastruct.ListenerSet;
public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectModel {
protected final Executor clientExecutor = Executors.newSingleThreadExecutor();
protected final Object lock = new Object();
protected final ExecutorService clientExecutor = Executors.newSingleThreadExecutor();
protected final ListenerSet<DebuggerModelListener> listeners =
new ListenerSet<>(DebuggerModelListener.class, clientExecutor);
protected SpiTargetObject root;
protected boolean rootAdded;
protected CompletableFuture<SpiTargetObject> completedRoot = new CompletableFuture<>();
// Remember the order of creation events
protected final Map<List<String>, SpiTargetObject> creationLog = new LinkedHashMap<>();
protected void objectCreated(SpiTargetObject object) {
synchronized (lock) {
creationLog.put(object.getPath(), object);
if (object.isRoot()) {
if (this.root != null) {
throw new IllegalStateException("Already have a root");
}
this.root = object;
}
}
}
protected void objectInvalidated(TargetObject object) {
synchronized (lock) {
creationLog.remove(object);
}
}
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);
}
@Override
public void addModelListener(DebuggerModelListener listener) {
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
return completedRoot;
}
@Override
public SpiTargetObject getModelRoot() {
synchronized (lock) {
return root;
}
}
protected void replayTreeEvents(DebuggerModelListener listener) {
if (root == null) {
assert creationLog.isEmpty();
return;
}
for (SpiTargetObject object : creationLog.values()) {
listener.created(object);
}
Set<SpiTargetObject> visited = new HashSet<>();
for (SpiTargetObject object : creationLog.values()) {
replayAddEvents(listener, object, visited);
}
if (rootAdded) {
listener.rootAdded(root);
}
}
protected void replayAddEvents(DebuggerModelListener listener, SpiTargetObject object,
Set<SpiTargetObject> visited) {
if (!visited.add(object)) {
return;
}
for (Object val : object.getCachedAttributes().values()) {
if (!(val instanceof TargetObjectRef)) {
continue;
}
assert val instanceof SpiTargetObject;
replayAddEvents(listener, (SpiTargetObject) val, visited);
}
listener.attributesChanged(object, List.of(), object.getCachedAttributes());
for (TargetObjectRef elem : object.getCachedElements().values()) {
assert elem instanceof SpiTargetObject;
replayAddEvents(listener, (SpiTargetObject) elem, visited);
}
listener.elementsChanged(object, List.of(), object.getCachedElements());
}
@Override
public void addModelListener(DebuggerModelListener listener, boolean replay) {
CompletableFuture.runAsync(() -> {
synchronized (lock) {
if (replay) {
replayTreeEvents(listener);
}
listeners.add(listener);
}
}, clientExecutor).exceptionally(ex -> {
listener.catastrophic(ex);
return null;
});
}
@Override
public void removeModelListener(DebuggerModelListener listener) {
listeners.remove(listener);
}
/**
* 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
*/
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;
}, clientExecutor);
}
@Override
public Executor getClientExecutor() {
return clientExecutor;
public CompletableFuture<Void> flushEvents() {
return gateFuture(null);
//return CompletableFuture.supplyAsync(() -> gateFuture((Void) null)).thenCompose(f -> f);
}
@Override
public CompletableFuture<Void> close() {
clientExecutor.shutdown();
return AsyncUtils.NIL;
}
public void removeExisting(List<String> path) {
TargetObject existing = getModelObject(path);
// It had better be. This also checks for null
if (existing == null) {
return;
}
TargetObjectRef parent = existing.getParent();
if (parent == null) {
assert existing == root;
throw new IllegalStateException("Cannot replace the root");
}
if (!path.equals(existing.getPath())) {
return; // Is a link
}
if (parent instanceof DefaultTargetObject<?, ?>) { // It had better be
DefaultTargetObject<?, ?> dtoParent = (DefaultTargetObject<?, ?>) parent;
if (PathUtils.isIndex(path)) {
dtoParent.changeElements(List.of(PathUtils.getIndex(path)), List.of(), "Replaced");
}
else {
assert PathUtils.isName(path);
dtoParent.changeAttributes(List.of(PathUtils.getKey(path)), Map.of(), "Replaced");
}
}
}
@Override
public TargetObject getModelObject(List<String> path) {
synchronized (lock) {
return creationLog.get(path);
}
}
}

View file

@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TypedTargetObject;
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.dbg.util.PathUtils;
@ -39,13 +40,20 @@ import ghidra.util.datastruct.ListenerSet;
* @param <P> the type of the parent
*/
public abstract class AbstractTargetObject<P extends TargetObject>
implements TargetObject, InvalidatableTargetObjectIf {
implements SpiTargetObject {
public static interface ProxyFactory<I> {
SpiTargetObject createProxy(AbstractTargetObject<?> delegate, I info);
}
protected static final ProxyFactory<Void> THIS_FACTORY = (d, i) -> d;
protected static final CompletableFuture<Map<String, TargetObject>> COMPLETED_EMPTY_ELEMENTS =
CompletableFuture.completedFuture(Map.of());
protected static final CompletableFuture<Map<String, Object>> COMPLETED_EMPTY_ATTRIBUTES =
CompletableFuture.completedFuture(Map.of());
protected final DebuggerObjectModel model;
protected final AbstractDebuggerObjectModel model;
protected final SpiTargetObject proxy;
protected final P parent;
protected final CompletableFuture<P> completedParent;
protected final List<String> path;
@ -55,12 +63,15 @@ public abstract class AbstractTargetObject<P extends TargetObject>
protected boolean valid = true;
// TODO: Remove both of these, and just do invocations on model's listeners
protected final ListenerSet<TargetObjectListener> listeners;
public AbstractTargetObject(DebuggerObjectModel model, P parent, String key, String typeHint,
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.getClientExecutor());
this.listeners = new ListenerSet<>(TargetObjectListener.class, model.clientExecutor);
this.model = model;
listeners.addChained(model.listeners);
this.parent = parent;
this.completedParent = CompletableFuture.completedFuture(parent);
if (parent == null) {
@ -69,10 +80,28 @@ public abstract class AbstractTargetObject<P extends TargetObject>
else {
this.path = PathUtils.extend(parent.getPath(), key);
}
model.removeExisting(path);
this.hash = computeHashCode();
this.typeHint = typeHint;
this.schema = schema;
this.proxy = proxyFactory.createProxy(this, proxyInfo);
fireCreated();
}
public AbstractTargetObject(AbstractDebuggerObjectModel model, P parent, String key,
String typeHint, TargetObjectSchema schema) {
this(THIS_FACTORY, null, model, parent, key, typeHint, schema);
}
protected void fireCreated() {
SpiTargetObject proxy = getProxy();
assert proxy != null;
model.objectCreated(proxy);
listeners.fire.created(proxy);
}
/**
@ -82,14 +111,23 @@ public abstract class AbstractTargetObject<P extends TargetObject>
* Some implementations may use on a proxy-delegate pattern to implement target objects with
* various combinations of supported interfaces. When this pattern is employed, the delegate
* will extend {@link DefaultTargetObject}, causing {@code this} to refer to the delegate rather
* than the proxy. When invoking listeners, the proxy given by this method is used instead. By
* default, it simply returns {@code this}, providing the expected behavior for typical
* implementations. The proxy is also used for schema interface validation.
* than the proxy. When invoking listeners, the proxy given by this method is used instead. The
* proxy is also used for schema interface validation.
*
* @return the proxy or this
*/
public TargetObject getProxy() {
return this;
public SpiTargetObject getProxy() {
return proxy;
}
@Override
public P getParent() {
return parent;
}
@Override
public <T extends TypedTargetObject<T>> T as(Class<T> iface) {
return DebuggerObjectModel.requireIface(iface, getProxy(), path);
}
/**
@ -107,7 +145,8 @@ public abstract class AbstractTargetObject<P extends TargetObject>
*
* @return true to throw exceptions on schema violations.
*/
protected boolean enforcesStrictSchema() {
@Override
public boolean enforcesStrictSchema() {
return false;
}
@ -148,6 +187,9 @@ public abstract class AbstractTargetObject<P extends TargetObject>
@Override
public void addListener(TargetObjectListener l) {
if (!valid) {
throw new IllegalStateException("Object is no longer valid: " + getProxy());
}
listeners.add(l);
}
@ -157,7 +199,7 @@ public abstract class AbstractTargetObject<P extends TargetObject>
}
@Override
public DebuggerObjectModel getModel() {
public AbstractDebuggerObjectModel getModel() {
return model;
}
@ -212,38 +254,68 @@ public abstract class AbstractTargetObject<P extends TargetObject>
return parent;
}
protected void doInvalidate(String reason) {
protected void doInvalidate(TargetObject branch, String reason) {
valid = false;
listeners.fire.invalidated(this, reason);
model.objectInvalidated(getProxy());
listeners.fire.invalidated(getProxy(), branch, reason);
listeners.clear();
}
protected void doInvalidateElements(Collection<?> elems, String reason) {
for (Object e : elems) {
if (e instanceof InvalidatableTargetObjectIf) {
if (e instanceof InvalidatableTargetObjectIf && e instanceof TargetObject) {
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) e;
obj.invalidateSubtree(reason);
obj.invalidateSubtree((TargetObject) e, reason);
}
}
}
protected void doInvalidateAttributes(Map<String, ?> attrs, String reason) {
protected void doInvalidateElements(TargetObject branch, Collection<?> elems, String reason) {
for (Object e : elems) {
if (e instanceof InvalidatableTargetObjectIf) {
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) e;
obj.invalidateSubtree(branch, 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);
}
}
}
}
protected void doInvalidateAttributes(TargetObject branch, Map<String, ?> attrs,
String reason) {
for (Map.Entry<String, ?> ent : attrs.entrySet()) {
String name = ent.getKey();
Object a = ent.getValue();
if (a instanceof InvalidatableTargetObjectIf) {
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) a;
if (!PathUtils.isLink(getPath(), name, obj.getPath())) {
obj.invalidateSubtree(reason);
obj.invalidateSubtree(branch, reason);
}
}
}
}
@Override
public void invalidateSubtree(String reason) {
public void invalidateSubtree(TargetObject branch, String reason) {
// Pre-ordered traversal
doInvalidate(reason);
doInvalidateElements(getCachedElements().values(), reason);
doInvalidateAttributes(getCachedAttributes(), reason);
doInvalidate(branch, reason);
doInvalidateElements(branch, getCachedElements().values(), reason);
doInvalidateAttributes(branch, getCachedAttributes(), reason);
}
public ListenerSet<TargetObjectListener> getListeners() {
return listeners;
}
}

View file

@ -24,11 +24,11 @@ import ghidra.dbg.target.schema.TargetObjectSchema;
public class DefaultTargetModelRoot extends DefaultTargetObject<TargetObject, TargetObject>
implements TargetAggregate {
public DefaultTargetModelRoot(DebuggerObjectModel model, String typeHint) {
public DefaultTargetModelRoot(AbstractDebuggerObjectModel model, String typeHint) {
this(model, typeHint, EnumerableTargetObjectSchema.OBJECT);
}
public DefaultTargetModelRoot(DebuggerObjectModel model, String typeHint,
public DefaultTargetModelRoot(AbstractDebuggerObjectModel model, String typeHint,
TargetObjectSchema schema) {
super(model, null, null, typeHint, schema);
}

View file

@ -27,7 +27,6 @@ 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
@ -59,7 +58,8 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
* @param key the key (attribute name or element index) of this object
* @param typeHint the type hint for this object
*/
public DefaultTargetObject(DebuggerObjectModel model, P parent, String key, String typeHint) {
public DefaultTargetObject(AbstractDebuggerObjectModel model, P parent, String key,
String typeHint) {
this(model, parent, key, typeHint, parent.getSchema().getChildSchema(key));
}
@ -88,14 +88,48 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
* @param typeHint the type hint for this object
* @param schema the schema of this object
*/
public DefaultTargetObject(DebuggerObjectModel model, P parent, String key, String typeHint,
public DefaultTargetObject(AbstractDebuggerObjectModel model, P parent, String key,
String typeHint, TargetObjectSchema schema) {
this(THIS_FACTORY, null, model, parent, key, typeHint, schema);
}
/**
* Construct a new (delegate) default target object
*
* <p>
* This behaves similarly to
* {@link #DefaultTargetObject(AbstractDebuggerObjectModel, TargetObject, String, String, TargetObjectSchema)}
* when this object is meant to be the delegate of a proxy. The {@code proxyFactory} and
* {@code proxyInfo} arguments are necessary to sidestep Java's insistence that the
* super-constructor be invoked first. It allows information to be passed straight to the
* factory. Using method overrides doesn't work, because the factory method gets called during
* construction, before extensions have a chance to initialize fields, on which the proxy
* inevitably depends.
*
* @param proxyFactory a factory to create the proxy, invoked in the super constructor
* @param proxyInfo additional information passed to the proxy factory
* @param model the model to which the object belongs
* @param parent the parent of this object
* @param key the key (attribute name or element index) of this object
* @param typeHint the type hint for this object
* @param schema the schema of this object
*/
public <I> DefaultTargetObject(ProxyFactory<I> proxyFactory, I proxyInfo,
AbstractDebuggerObjectModel model, P parent, String key, String typeHint,
TargetObjectSchema schema) {
super(model, parent, key, typeHint, schema);
changeAttributes(List.of(), List.of(), Map.of(DISPLAY_ATTRIBUTE_NAME,
key == null ? "<root>" : key, UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED),
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");
}
public <I> DefaultTargetObject(ProxyFactory<I> proxyFactory, I proxyInfo,
AbstractDebuggerObjectModel model, P parent, String key, String typeHint) {
this(proxyFactory, proxyInfo, model, parent, key, typeHint,
parent.getSchema().getChildSchema(key));
}
/**
* Check if this object is being observed
*
@ -115,11 +149,20 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
* 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),
fetchElements(refreshElements));
}
/**
* The elements for this object need to be updated, optionally invalidating caches
*
@ -159,11 +202,11 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
synchronized (elements) {
if (refresh || curElemsRequest == null || curElemsRequest.isCompletedExceptionally() ||
getUpdateMode() == TargetUpdateMode.SOLICITED) {
curElemsRequest = requestElements(refresh);
curElemsRequest = requestElements(refresh).thenCompose(model::gateFuture);
}
req = curElemsRequest;
}
return req.thenApply(__ -> getCachedElements()).thenCompose(model::gateFuture);
return req.thenApply(__ -> getCachedElements());
}
@Override
@ -173,7 +216,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
@Override
public Map<String, E> getCachedElements() {
synchronized (elements) {
synchronized (model.lock) {
return Map.copyOf(elements);
}
}
@ -234,7 +277,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
private Delta<E, E> setElements(Map<String, E> elements, String reason) {
Delta<E, E> delta;
synchronized (this.elements) {
synchronized (model.lock) {
delta = Delta.computeAndSet(this.elements, elements, Delta.SAME);
}
TargetObjectSchema schemax = getSchema();
@ -279,7 +322,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
private Delta<E, E> changeElements(Collection<String> remove, Map<String, E> add,
String reason) {
Delta<E, E> delta;
synchronized (elements) {
synchronized (model.lock) {
delta = Delta.apply(this.elements, remove, add, Delta.SAME);
}
TargetObjectSchema schemax = getSchema();
@ -326,18 +369,18 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
synchronized (attributes) {
// update_mode does not affect attributes. They always behave as if UNSOLICITED.
if (refresh || curAttrsRequest == null || curAttrsRequest.isCompletedExceptionally()) {
curAttrsRequest = requestAttributes(refresh);
curAttrsRequest = requestAttributes(refresh).thenCompose(model::gateFuture);
}
req = curAttrsRequest;
}
return req.thenApply(__ -> {
synchronized (attributes) {
synchronized (model.lock) {
if (schema != null) { // TODO: Remove this. Schema should never be null.
schema.validateRequiredAttributes(this, enforcesStrictSchema());
}
return getCachedAttributes();
}
}).thenCompose(model::gateFuture);
});
}
@Override
@ -347,14 +390,14 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
@Override
public Map<String, ?> getCachedAttributes() {
synchronized (attributes) {
synchronized (model.lock) {
return Map.copyOf(attributes);
}
}
@Override
public Object getCachedAttribute(String name) {
synchronized (attributes) {
synchronized (model.lock) {
return attributes.get(name);
}
}
@ -405,7 +448,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
*/
public Delta<?, ?> setAttributes(Map<String, ?> attributes, String reason) {
Delta<?, ?> delta;
synchronized (this.attributes) {
synchronized (model.lock) {
delta = Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL);
}
TargetObjectSchema schemax = getSchema();
@ -450,7 +493,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
*/
public Delta<?, ?> changeAttributes(List<String> remove, Map<String, ?> add, String reason) {
Delta<?, ?> delta;
synchronized (attributes) {
synchronized (model.lock) {
delta = Delta.apply(this.attributes, remove, add, Delta.EQUAL);
}
TargetObjectSchema schemax = getSchema();
@ -463,8 +506,4 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
}
return delta;
}
public ListenerSet<TargetObjectListener> getListeners() {
return listeners;
}
}

View file

@ -56,7 +56,8 @@ public interface InvalidatableTargetObjectIf extends TargetObjectRef {
* {@link DefaultTargetObject#setElements(Collection, String)} will automatically invoke this
* method when they detect object removal.
*
* @param branch the root of the sub-tree that is being removed
* @param reason a human-consumable explanation for the removal
*/
void invalidateSubtree(String reason);
void invalidateSubtree(TargetObject branch, String reason);
}

View file

@ -43,7 +43,7 @@ public interface SpiDebuggerObjectModel extends DebuggerObjectModel {
return new DefaultTargetObjectRef(this, path);
}
public default CompletableFuture<Object> fetchFreshChild(TargetObject obj, String key) {
public static CompletableFuture<Object> fetchFreshChild(TargetObject obj, String key) {
if (PathUtils.isIndex(key)) {
return obj.fetchElements(true).thenApply(elements -> {
return elements.get(PathUtils.parseIndex(key));
@ -54,7 +54,7 @@ public interface SpiDebuggerObjectModel extends DebuggerObjectModel {
});
}
public default CompletableFuture<Object> fetchSuccessorValue(TargetObject obj,
public static CompletableFuture<Object> fetchSuccessorValue(TargetObject obj,
List<String> path, boolean refresh, boolean followLinks) {
if (path.isEmpty()) {
return CompletableFuture.completedFuture(obj);

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.agent;
import ghidra.dbg.target.TargetObject;
public interface SpiTargetObject extends TargetObject, InvalidatableTargetObjectIf {
@Override
AbstractDebuggerObjectModel getModel();
// TODO:
//@Override
//Map<String, ? extends SpiTargetObject> getCachedElements();
boolean enforcesStrictSchema();
}

View file

@ -40,7 +40,11 @@ import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
* <p>
* Note that it is OK for more than one {@link TargetObjectRef} to refer to the same path. These
* objects must override {@link #equals(Object)} and {@link #hashCode()}.
*
* @deprecated Use {@link TargetObjectPath} for model-bound path manipulation instead. Models should
* not longer return nor push stubs, but actual objects.
*/
@Deprecated
public interface TargetObjectRef extends Comparable<TargetObjectRef> {
/**
@ -143,7 +147,10 @@ public interface TargetObjectRef extends Comparable<TargetObjectRef> {
* Get the actual object
*
* @return a future which completes with the object
* @deprecated Just cast straight to {@link TargetObject}. There should never exist a
* {@link TargetObjectRef} that is not already a {@link TargetObject}, anymore.
*/
@Deprecated
public default CompletableFuture<? extends TargetObject> fetch() {
return getModel().fetchModelObject(getPath());
}
@ -298,8 +305,20 @@ public interface TargetObjectRef extends Comparable<TargetObjectRef> {
* does not exist
*/
public default CompletableFuture<?> fetchAttribute(String name) {
if (!PathUtils.isInvocation(name)) {
return fetchAttributes().thenApply(m -> m.get(name));
}
// TODO: Make a type for the invocation and parse arguments better?
Entry<String, String> invocation = PathUtils.parseInvocation(name);
return fetchAttribute(invocation.getKey()).thenCompose(obj -> {
if (!(obj instanceof TargetMethod<?>)) {
throw new DebuggerModelTypeException(invocation.getKey() + " is not a method");
}
TargetMethod<?> method = (TargetMethod<?>) obj;
// Just blindly invoke and let it sort it out
return method.invoke(Map.of("arg", invocation.getValue()));
});
}
/**
* Fetch all the elements of this object

View file

@ -22,6 +22,13 @@ import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TypedTargetObject;
/**
* A reference having a known or expected type
*
* @param <T> the type
* @deprecated I don't think this adds any real value.
*/
@Deprecated(forRemoval = true)
public interface TypedTargetObjectRef<T extends TargetObject> extends TargetObjectRef {
public class CastingTargetObjectRef<T extends TypedTargetObject<T>>
implements TypedTargetObjectRef<T> {

View file

@ -74,22 +74,26 @@ public interface TargetConsole<T extends TargetConsole<T>> extends TypedTargetOb
*/
default void consoleOutput(TargetObject console, Channel channel, byte[] data) {
}
}
public interface TargetTextConsoleListener extends TargetConsoleListener {
/**
* 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));
}
}
@Override
default void consoleOutput(TargetObject console, Channel channel, byte[] data) {
consoleOutput(console, channel, new String(data, CHARSET));
}
public interface TargetTextConsoleListener extends TargetConsoleListener {
}
}

View file

@ -529,6 +529,30 @@ public interface TargetObject extends TargetObjectRef {
return CompletableFuture.completedFuture(this);
}
/**
* 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.
*
* @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
* @return a future which completes when the children are updated.
*/
CompletableFuture<Void> resync(boolean refreshAttributes, boolean refreshElements);
/**
* Refresh the elements of this object
*
* @return a future which completes when the children are updated.
*/
default CompletableFuture<Void> resync() {
return resync(false, true);
}
/**
* Get the (usually opaque) identifier that the underlying connection uses for this object
*
@ -585,6 +609,14 @@ public interface TargetObject extends TargetObjectRef {
return getCachedAttributes().get(name);
}
@Override
default CompletableFuture<?> fetchAttribute(String name) {
if (PathUtils.isInvocation(name) && getCachedAttributes().containsKey(name)) {
return CompletableFuture.completedFuture(getCachedAttributes().get(name));
}
return TargetObjectRef.super.fetchAttribute(name);
}
/**
* Cast the named attribute to the given type, if possible
*
@ -675,13 +707,19 @@ public interface TargetObject extends TargetObjectRef {
}
public interface TargetObjectListener {
/**
* The object's display string has changed
* The object was created
*
* @param object the object
* @param display the new display string
* <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 displayChanged(TargetObject object, String display) {
default void created(TargetObject object) {
}
/**
@ -704,9 +742,19 @@ public interface TargetObject extends TargetObjectRef {
* 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, String reason) {
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) {
}
/**
@ -760,7 +808,11 @@ public interface TargetObject extends TargetObjectRef {
/**
* An adapter which automatically gets new children from the model
*
* @deprecated {@link TargetObjectRef} is being deprecated, so this is no longer necessary. Just
* cast refs to {@link TargetObject}
*/
@Deprecated(forRemoval = true)
public interface TargetObjectFetchingListener extends TargetObjectListener {
@Override
default void elementsChanged(TargetObject parent, Collection<String> removed,

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.target;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.PathUtils;
import ghidra.dbg.util.PathUtils.PathComparator;
public class TargetObjectPath implements Comparable<TargetObjectPath> {
protected final DebuggerObjectModel model;
protected final List<String> keyList;
protected final int hash;
public TargetObjectPath(DebuggerObjectModel model, List<String> keyList) {
this.model = model;
this.keyList = keyList;
this.hash = Objects.hash(model, keyList);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof TargetObjectPath)) {
return false;
}
TargetObjectPath that = (TargetObjectPath) obj;
return this.getModel() == that.getModel() &&
Objects.equals(this.getKeyList(), that.getKeyList());
}
@Override
public int hashCode() {
return hash;
}
@Override
public int compareTo(TargetObjectPath that) {
if (this == that) {
return 0;
}
DebuggerObjectModel thisModel = this.getModel();
DebuggerObjectModel thatModel = that.getModel();
if (thisModel != thatModel) {
if (thisModel == null) {
return -1;
}
if (thatModel == null) {
return 1;
}
int result = thisModel.toString().compareTo(thatModel.toString());
if (result == 0) {
return Integer.compare(
System.identityHashCode(thisModel),
System.identityHashCode(thatModel));
}
return result;
}
return PathComparator.KEYED.compare(this.getKeyList(), that.getKeyList());
}
@Override
public String toString() {
return String.format("<%s in %s>", toPathString(), model);
}
public DebuggerObjectModel getModel() {
return model;
}
public List<String> getKeyList() {
return keyList;
}
public String name() {
return PathUtils.getKey(keyList);
}
public String index() {
return PathUtils.getIndex(keyList);
}
public boolean isRoot() {
return keyList.isEmpty();
}
public CompletableFuture<TargetObject> fetch() {
return model.fetchModelObject(getKeyList()).thenApply(obj -> obj);
}
public String toPathString() {
return PathUtils.toString(keyList);
}
public TargetObjectPath parent() {
List<String> pkl = PathUtils.parent(keyList);
return pkl == null ? null : new TargetObjectPath(model, pkl);
}
public TargetObjectPath successor(List<String> subKeyList) {
return new TargetObjectPath(model, PathUtils.extend(keyList, subKeyList));
}
public TargetObjectPath successor(String... subKeyList) {
return successor(Arrays.asList(subKeyList));
}
}

View file

@ -516,6 +516,24 @@ public enum PathUtils {
return !Objects.equals(extend(parentPath, name), attributePath);
}
/**
* Check whether a given element is a link.
*
* <p>
* Consider an object {@code O} with an element {@code [1]}. {@code [1]}'s value is a link, iff
* its path does <em>not</em> match that generated by extending {@code O}'s path with
* {@code [1]}'s key.
*
* @param parentPath the path of the parent object of the given element
* @param index the index of the given element
* @param elementPath the canonical path of the element
* @return true if the value is a link (i.e., it's object has a different path)
*/
public static boolean isElementLink(List<String> parentPath, String index,
List<String> elementPath) {
return !Objects.equals(index(parentPath, index), elementPath);
}
/**
* Check whether a given attribute should be displayed.
*

View file

@ -18,17 +18,20 @@ package ghidra.dbg.agent;
import static ghidra.lifecycle.Unfinished.TODO;
import static org.junit.Assert.*;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Test;
import generic.Unique;
import ghidra.async.AsyncTestUtils;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.DebuggerModelListener;
import ghidra.dbg.attributes.TargetObjectRef;
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;
@ -39,16 +42,38 @@ import ghidra.program.model.address.AddressSpace;
public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
public static class FakeTargetObject extends DefaultTargetObject<TargetObject, TargetObject> {
public FakeTargetObject(DebuggerObjectModel model, TargetObject parent, String name) {
public FakeTargetObject(AbstractDebuggerObjectModel model, TargetObject parent,
String name) {
super(model, parent, name, "Fake");
}
}
public static class FakeTargetRegisterBank<T extends FakeTargetRegisterBank<T>>
extends FakeTargetObject implements TargetRegisterBank<T> {
public FakeTargetRegisterBank(AbstractDebuggerObjectModel model, TargetObject parent,
String name) {
super(model, parent, name);
}
@Override
public CompletableFuture<? extends Map<String, byte[]>> readRegistersNamed(
Collection<String> names) {
throw new UnsupportedOperationException();
}
@Override
public CompletableFuture<Void> writeRegistersNamed(Map<String, byte[]> values) {
throw new UnsupportedOperationException();
}
}
/**
* Functionally identical to a Fake, but intrinsically different
*/
public static class PhonyTargetObject extends DefaultTargetObject<TargetObject, TargetObject> {
public PhonyTargetObject(DebuggerObjectModel model, TargetObject parent, String name) {
public PhonyTargetObject(AbstractDebuggerObjectModel model, TargetObject parent,
String name) {
super(model, parent, name, "Phony");
}
}
@ -56,6 +81,10 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
public static class FakeDebuggerObjectModel extends AbstractDebuggerObjectModel {
DefaultTargetModelRoot root = new DefaultTargetModelRoot(this, "Root");
public FakeDebuggerObjectModel() {
addModelRoot(root);
}
@Override
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
return CompletableFuture.completedFuture(root);
@ -70,16 +99,11 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
public AddressSpace getAddressSpace(String name) {
return TODO();
}
@Override
public CompletableFuture<Void> close() {
return AsyncUtils.NIL;
}
}
static class OffThreadTargetObject extends DefaultTargetObject<TargetObject, TargetObject> {
public OffThreadTargetObject(DebuggerObjectModel model, TargetObject parent, String name,
String typeHint) {
public OffThreadTargetObject(AbstractDebuggerObjectModel model, TargetObject parent,
String name, String typeHint) {
super(model, parent, name, typeHint);
}
@ -149,59 +173,63 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
fakeA.addListener(invL);
PhonyTargetObject phonyA = new PhonyTargetObject(model, model.root, "[A]");
// mere creation causes removal of old
waitOn(elemL.count.waitValue(1));
ElementsChangedInvocation changed1 = Unique.assertOne(elemL.invocations);
assertSame(model.root, changed1.parent);
assertEquals(Set.of("A"), changed1.removed);
assertTrue(changed1.added.isEmpty());
waitOn(invL.count.waitValue(1));
InvalidatedInvocation invalidated = Unique.assertOne(invL.invocations);
assertSame(fakeA, invalidated.object);
elemL.clear();
invL.clear();
model.root.setElements(List.of(phonyA), "Replace");
assertSame(phonyA, waitOn(model.fetchModelObject("[A]")));
assertFalse(fakeA.isValid());
ElementsChangedInvocation changed = Unique.assertOne(elemL.invocations);
assertSame(model.root, changed.parent);
assertSame(phonyA, Unique.assertOne(changed.added.values()));
InvalidatedInvocation invalidated = Unique.assertOne(invL.invocations);
assertSame(fakeA, invalidated.object);
assertEquals("Replace", invalidated.reason);
ElementsChangedInvocation changed2 = Unique.assertOne(elemL.invocations);
assertSame(model.root, changed2.parent);
assertSame(phonyA, Unique.assertOne(changed2.added.values()));
assertTrue(changed2.removed.isEmpty());
}
@Test
public void testAttributeReplacement() throws Throwable {
AttributesChangedListener attrL = new AttributesChangedListener();
InvalidatedListener invL = new InvalidatedListener();
FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A");
model.root.setAttributes(Map.of("A", fakeA), "Init");
String str1 = new String("EqualStrings");
String str2 = new String("EqualStrings");
model.root.setAttributes(Map.of("a", str1), "Init");
model.root.addListener(attrL);
fakeA.addListener(invL);
PhonyTargetObject phonyA = new PhonyTargetObject(model, model.root, "A");
model.root.setAttributes(Map.of("A", phonyA), "Replace");
// Note: mere object creation will cause "prior removal"
// We'll do this test just with primitives
// Should not cause replacement, since they're equal
model.root.setAttributes(Map.of("a", str2), "Replace");
waitOn(model.clientExecutor);
// Object-valued attribute replacement requires prior removal
assertSame(fakeA, waitOn(model.fetchModelObject("A")));
assertSame(str1, waitOn(model.fetchModelValue("a")));
assertEquals(0, attrL.invocations.size());
assertEquals(0, invL.invocations.size());
// Now, with prior removal
// TODO: Should I permit custom equality check?
model.root.setAttributes(Map.of(), "Clear");
model.root.setAttributes(Map.of("A", phonyA), "Replace");
model.root.setAttributes(Map.of("a", str2), "Replace");
waitOn(model.clientExecutor);
assertEquals(2, attrL.invocations.size());
AttributesChangedInvocation changed = attrL.invocations.get(0);
assertEquals(model.root, changed.parent);
assertSame("A", Unique.assertOne(changed.removed));
assertEquals("a", Unique.assertOne(changed.removed));
assertEquals(0, changed.added.size());
changed = attrL.invocations.get(1);
assertEquals(model.root, changed.parent);
assertSame(phonyA, Unique.assertOne(changed.added.values()));
assertSame(str2, Unique.assertOne(changed.added.values()));
assertEquals(0, changed.removed.size());
InvalidatedInvocation invalidated = Unique.assertOne(invL.invocations);
assertSame(fakeA, invalidated.object);
assertEquals("Clear", invalidated.reason);
}
@Test
@ -223,4 +251,83 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
waitOn(invL.count.waitValue(3));
}
public static class EventRecordingListener implements DebuggerModelListener {
List<Pair<String, TargetObject>> record = new ArrayList<>();
@Override
public void created(TargetObject object) {
record.add(new ImmutablePair<>("created", object));
}
@Override
public void elementsChanged(TargetObject parent, Collection<String> removed,
Map<String, ? extends TargetObjectRef> added) {
for (TargetObjectRef elem : added.values()) {
record.add(new ImmutablePair<>("addedElem", (TargetObject) elem));
}
}
@Override
public void attributesChanged(TargetObject parent, Collection<String> removed,
Map<String, ?> added) {
for (Object attr : added.values()) {
if (attr instanceof TargetObject) {
record.add(new ImmutablePair<>("addedAttr", (TargetObject) attr));
}
}
}
@Override
public void registersUpdated(TargetRegisterBank<?> bank, Map<String, byte[]> updates) {
record.add(new ImmutablePair<>("registersUpdated", bank));
}
}
@Test
public void testCreationAndModelListenerWithoutReplay() throws Throwable {
EventRecordingListener listener = new EventRecordingListener();
model.addModelListener(listener, false);
waitOn(model.clientExecutor);
FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A");
FakeTargetRegisterBank<?> fakeA1rb = new FakeTargetRegisterBank<>(model, fakeA, "[1]");
fakeA1rb.listeners.fire(TargetRegisterBankListener.class)
.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),
new ImmutablePair<>("created", fakeA1rb),
new ImmutablePair<>("registersUpdated", fakeA1rb),
new ImmutablePair<>("addedElem", fakeA1rb),
new ImmutablePair<>("addedAttr", fakeA)),
listener.record);
}
@Test
public void testAddListenerWithReplay() throws Throwable {
FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A");
FakeTargetRegisterBank<?> fakeA1rb = new FakeTargetRegisterBank<>(model, fakeA, "[1]");
fakeA1rb.listeners.fire(TargetRegisterBankListener.class)
.registersUpdated(fakeA1rb, Map.of());
fakeA.setElements(List.of(fakeA1rb), "Init");
model.root.setAttributes(List.of(fakeA), Map.of(), "Init");
EventRecordingListener listener = new EventRecordingListener();
model.addModelListener(listener, true);
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)),
listener.record);
}
}

View file

@ -46,8 +46,13 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel {
this("Session");
}
public Executor getClientExecutor() {
return clientExecutor;
}
public TestDebuggerObjectModel(String rootHint) {
this.session = new TestTargetSession(this, rootHint);
addModelRoot(session);
}
@Override
@ -67,8 +72,8 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel {
@Override
public CompletableFuture<Void> close() {
session.invalidateSubtree("Model closed");
return future(null);
session.invalidateSubtree(session, "Model closed");
return super.close().thenCompose(__ -> future(null));
}
public TestTargetProcess addProcess(int pid) {

View file

@ -15,9 +15,9 @@
*/
package ghidra.dbg.model;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.agent.SpiTargetObject;
public interface TestTargetObject extends TargetObject {
public interface TestTargetObject extends SpiTargetObject {
@Override
TestDebuggerObjectModel getModel();
}

View file

@ -22,9 +22,7 @@ import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.agent.DefaultTargetModelRoot;
import ghidra.dbg.agent.DefaultTargetObject;
import ghidra.dbg.agent.*;
import ghidra.dbg.target.*;
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
@ -53,7 +51,7 @@ public class AnnotatedTargetObjectSchemaTest {
@TargetObjectSchemaInfo
static class TestAnnotatedTargetRootPlain extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootPlain(DebuggerObjectModel model, String typeHint) {
public TestAnnotatedTargetRootPlain(AbstractDebuggerObjectModel model, String typeHint) {
super(model, typeHint);
}
}
@ -71,7 +69,7 @@ public class AnnotatedTargetObjectSchemaTest {
@TargetObjectSchemaInfo(elements = @TargetElementType(type = Void.class))
static class TestAnnotatedTargetRootNoElems extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootNoElems(DebuggerObjectModel model, String typeHint) {
public TestAnnotatedTargetRootNoElems(AbstractDebuggerObjectModel model, String typeHint) {
super(model, typeHint);
}
}
@ -92,15 +90,15 @@ public class AnnotatedTargetObjectSchemaTest {
static class TestAnnotatedTargetProcessStub
extends DefaultTargetObject<TargetObject, TargetObject>
implements TargetProcess<TestAnnotatedTargetProcessStub> {
public TestAnnotatedTargetProcessStub(DebuggerObjectModel model, TargetObject parent,
String key, String typeHint) {
public TestAnnotatedTargetProcessStub(AbstractDebuggerObjectModel model,
TargetObject parent, String key, String typeHint) {
super(model, parent, key, typeHint);
}
}
@TargetObjectSchemaInfo(name = "Root")
static class TestAnnotatedTargetRootOverriddenFetchElems extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootOverriddenFetchElems(DebuggerObjectModel model,
public TestAnnotatedTargetRootOverriddenFetchElems(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
@ -131,7 +129,7 @@ public class AnnotatedTargetObjectSchemaTest {
@TargetObjectSchemaInfo(name = "ProcessContainer")
static class TestAnnotatedProcessContainer
extends DefaultTargetObject<TestAnnotatedTargetProcessStub, TargetObject> {
public TestAnnotatedProcessContainer(DebuggerObjectModel model, TargetObject parent,
public TestAnnotatedProcessContainer(AbstractDebuggerObjectModel model, TargetObject parent,
String key, String typeHint) {
super(model, parent, key, typeHint);
}
@ -154,15 +152,15 @@ public class AnnotatedTargetObjectSchemaTest {
static class TestAnnotatedTargetProcessParam<T>
extends DefaultTargetObject<TargetObject, TargetObject>
implements TargetProcess<TestAnnotatedTargetProcessParam<T>> {
public TestAnnotatedTargetProcessParam(DebuggerObjectModel model, TargetObject parent,
String key, String typeHint) {
public TestAnnotatedTargetProcessParam(AbstractDebuggerObjectModel model,
TargetObject parent, String key, String typeHint) {
super(model, parent, key, typeHint);
}
}
@TargetObjectSchemaInfo
static class TestAnnotatedTargetRootWithAnnotatedAttrs extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithAnnotatedAttrs(DebuggerObjectModel model,
public TestAnnotatedTargetRootWithAnnotatedAttrs(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
@ -209,7 +207,7 @@ public class AnnotatedTargetObjectSchemaTest {
@TargetElementType(index = "reserved", type = Void.class)
})
static class TestAnnotatedTargetRootWithListedAttrs extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithListedAttrs(DebuggerObjectModel model,
public TestAnnotatedTargetRootWithListedAttrs(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
@ -243,7 +241,7 @@ public class AnnotatedTargetObjectSchemaTest {
@TargetObjectSchemaInfo
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadType extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithAnnotatedAttrsBadType(DebuggerObjectModel model,
public TestAnnotatedTargetRootWithAnnotatedAttrsBadType(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
@ -273,7 +271,7 @@ public class AnnotatedTargetObjectSchemaTest {
static class TestAnnotatedTargetRootWithAnnotatedAttrsNonUnique<T extends Dummy & TargetProcess<T> & TargetInterpreter<T>>
extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithAnnotatedAttrsNonUnique(DebuggerObjectModel model,
public TestAnnotatedTargetRootWithAnnotatedAttrsNonUnique(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
@ -294,7 +292,7 @@ public class AnnotatedTargetObjectSchemaTest {
static class TestAnnotatedTargetRootWithElemsNonUnique<T extends Dummy & TargetProcess<T> & TargetInterpreter<T>>
extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithElemsNonUnique(DebuggerObjectModel model,
public TestAnnotatedTargetRootWithElemsNonUnique(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
@ -314,7 +312,7 @@ public class AnnotatedTargetObjectSchemaTest {
@TargetObjectSchemaInfo
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadName extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithAnnotatedAttrsBadName(DebuggerObjectModel model,
public TestAnnotatedTargetRootWithAnnotatedAttrsBadName(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
@ -333,7 +331,7 @@ public class AnnotatedTargetObjectSchemaTest {
@TargetObjectSchemaInfo
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadGetter extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithAnnotatedAttrsBadGetter(DebuggerObjectModel model,
public TestAnnotatedTargetRootWithAnnotatedAttrsBadGetter(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}
@ -353,7 +351,7 @@ public class AnnotatedTargetObjectSchemaTest {
@TargetObjectSchemaInfo(
attributes = @TargetAttributeType(name = "some_attr", type = NotAPrimitive.class))
static class TestAnnotatedTargetRootWithListedAttrsBadType extends DefaultTargetModelRoot {
public TestAnnotatedTargetRootWithListedAttrsBadType(DebuggerObjectModel model,
public TestAnnotatedTargetRootWithListedAttrsBadType(AbstractDebuggerObjectModel model,
String typeHint) {
super(model, typeHint);
}

View file

@ -25,7 +25,6 @@ import java.util.concurrent.ExecutionException;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.Test;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.agent.*;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetObject.TargetUpdateMode;
@ -61,12 +60,6 @@ public class TargetObjectSchemaValidationTest {
fail();
return null;
}
@Override
public CompletableFuture<Void> close() {
fail();
return null;
}
};
@Test
@ -107,29 +100,29 @@ public class TargetObjectSchemaValidationTest {
}
static class ValidatedModelRoot extends DefaultTargetModelRoot {
public ValidatedModelRoot(DebuggerObjectModel model, String typeHint,
public ValidatedModelRoot(AbstractDebuggerObjectModel model, String typeHint,
TargetObjectSchema schema) {
super(model, typeHint, schema);
}
@Override
protected boolean enforcesStrictSchema() {
public boolean enforcesStrictSchema() {
return true;
}
}
static class ValidatedObject extends DefaultTargetObject<TargetObject, TargetObject> {
public ValidatedObject(DebuggerObjectModel model, TargetObject parent, String key,
public ValidatedObject(AbstractDebuggerObjectModel model, TargetObject parent, String key,
TargetObjectSchema schema) {
super(model, parent, key, "Object", schema);
}
public ValidatedObject(DebuggerObjectModel model, TargetObject parent, String key) {
public ValidatedObject(AbstractDebuggerObjectModel model, TargetObject parent, String key) {
super(model, parent, key, "Object");
}
@Override
protected boolean enforcesStrictSchema() {
public boolean enforcesStrictSchema() {
return true;
}
}

View file

@ -15,13 +15,12 @@
*/
package ghidra.dbg.util;
import java.util.ArrayList;
import java.util.List;
import java.util.LinkedList;
import ghidra.async.AsyncReference;
public abstract class AbstractInvocationListener<T> {
public final List<T> invocations = new ArrayList<>();
public final LinkedList<T> invocations = new LinkedList<>();
public final AsyncReference<Integer, Void> count = new AsyncReference<>(0);
protected void record(T rec) {

View file

@ -59,7 +59,7 @@ public interface AllTargetObjectListenerAdapter
}
@Override
default void consoleOutput(TargetObject console, Channel channel, String out) {
default void consoleOutput(TargetObject console, Channel channel, byte[] out) {
//fail();
}

View file

@ -23,10 +23,12 @@ public class InvalidatedListener extends
AbstractInvocationListener<InvalidatedInvocation> implements TargetObjectListener {
public static class InvalidatedInvocation {
public final TargetObject object;
public final TargetObject branch;
public final String reason;
public InvalidatedInvocation(TargetObject object, String reason) {
public InvalidatedInvocation(TargetObject object, TargetObject branch, String reason) {
this.object = object;
this.branch = branch;
this.reason = reason;
}
@ -37,7 +39,7 @@ public class InvalidatedListener extends
}
@Override
public void invalidated(TargetObject object, String reason) {
record(new InvalidatedInvocation(object, reason));
public void invalidated(TargetObject object, TargetObject branch, String reason) {
record(new InvalidatedInvocation(object, branch, reason));
}
}

View file

@ -18,6 +18,7 @@ package ghidra.util.datastruct;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import com.google.common.cache.*;
@ -110,15 +111,27 @@ public class ListenerMap<K, P, V extends P> {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Collection<V> listenersVolatile = map.values();
//Msg.debug(this, "Queuing invocation: " + method.getName() + " @" +
// System.identityHashCode(executor));
Collection<V> listenersVolatile;
Set<ListenerMap<?, ? extends P, ?>> chainedVolatile;
synchronized (lock) {
listenersVolatile = map.values();
chainedVolatile = chained;
}
for (V l : listenersVolatile) {
if (!ext.isAssignableFrom(l.getClass())) {
continue;
}
executor.execute(() -> {
//Msg.debug(this,
// "Invoking: " + method.getName() + " @" + System.identityHashCode(executor));
try {
method.invoke(l, args);
}
catch (RejectedExecutionException e) {
Msg.trace(this, "Listener invocation rejected", e);
}
catch (InvocationTargetException e) {
Throwable cause = e.getCause();
reportError(l, cause);
@ -128,13 +141,30 @@ public class ListenerMap<K, P, V extends P> {
}
});
}
for (ListenerMap<?, ? extends P, ?> c : chained) {
// Invocation will check if assignable
@SuppressWarnings("unchecked")
T l = ((ListenerMap<?, P, ?>) c).fire(ext);
try {
method.invoke(l, args);
}
catch (InvocationTargetException e) {
Throwable cause = e.getCause();
reportError(l, cause);
}
catch (Throwable e) {
reportError(l, e);
}
}
return null; // TODO: Assumes void return type
}
}
private final Object lock = new Object();
private final Class<P> iface;
private final Executor executor;
private Map<K, V> map = createMap();
private Set<ListenerMap<?, ? extends P, ?>> chained = new LinkedHashSet<>();
/**
* A proxy which passes invocations to each value of this map
@ -210,6 +240,7 @@ public class ListenerMap<K, P, V extends P> {
}
public V put(K key, V val) {
synchronized (lock) {
if (map.get(key) == val) {
return val;
}
@ -219,19 +250,23 @@ public class ListenerMap<K, P, V extends P> {
map = newMap;
return result;
}
}
public void putAll(ListenerMap<? extends K, P, ? extends V> that) {
synchronized (lock) {
Map<K, V> newMap = createMap();
newMap.putAll(map);
newMap.putAll(that.map);
map = newMap;
}
}
public V get(K key) {
return map.get(key);
}
public V remove(K key) {
synchronized (lock) {
if (!map.containsKey(key)) {
return null;
}
@ -241,11 +276,47 @@ public class ListenerMap<K, P, V extends P> {
map = newMap;
return result;
}
}
public void clear() {
synchronized (lock) {
if (map.isEmpty()) {
return;
}
map = createMap();
}
}
public void addChained(ListenerMap<?, ? extends P, ?> map) {
synchronized (lock) {
if (chained.contains(map)) {
return;
}
Set<ListenerMap<?, ? extends P, ?>> newChained = new LinkedHashSet<>();
newChained.addAll(chained);
newChained.add(map);
chained = newChained;
}
}
public void removeChained(ListenerMap<?, ?, ?> map) {
synchronized (lock) {
if (!chained.contains(map)) {
return;
}
Set<ListenerMap<?, ? extends P, ?>> newChained = new LinkedHashSet<>();
newChained.addAll(chained);
newChained.remove(map);
chained = newChained;
}
}
public void clearChained() {
synchronized (lock) {
if (chained.isEmpty()) {
return;
}
chained = new LinkedHashSet<>();
}
}
}

View file

@ -107,4 +107,16 @@ public class ListenerSet<E> {
public void clear() {
map.clear();
}
public void addChained(ListenerSet<? extends E> set) {
map.addChained(set.map);
}
public void removeChained(ListenerSet<?> set) {
map.removeChained(set.map);
}
public void clearChained() {
map.clearChained();
}
}

View file

@ -110,8 +110,8 @@ public enum ProxyUtilities {
/**
* NOTE: I cannot replace the delegate with the proxy here (say, to prevent accidental
* leakage) for at least two reasons. 1) I may want direct access to the delegate. 2) It
* wouldn't work when the return value itself wraps or will provde the delegate (e.g., a
* future).
* wouldn't work when the return value itself wraps or will provide the delegate (e.g.,
* a future).
*/
return result;
}