From 5bb6f95a841925cdb3d15396c9ec217d17f0744f Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Tue, 23 Feb 2021 10:57:51 -0500 Subject: [PATCH] GP-704: Converting models to a push-centric comm pattern. --- .../iface1/DbgModelTargetEnvironment.java | 2 +- .../iface2/DbgModelTargetEnvironmentEx.java | 2 +- .../model/iface2/DbgModelTargetObject.java | 4 +- .../agent/dbgeng/model/impl/DbgModelImpl.java | 3 +- .../model/impl/DbgModelTargetObjectImpl.java | 4 +- .../model/impl/DbgModelTargetRootImpl.java | 37 +- .../DbgModelTargetSessionAttributesImpl.java | 22 +- ...delTargetSessionAttributesMachineImpl.java | 6 +- .../dbgmodel/model/impl/DbgModel2Impl.java | 20 +- .../model/impl/DbgModel2TargetObjectImpl.java | 21 +- .../model/impl/DbgModel2TargetRootImpl.java | 7 - .../impl/DelegateDbgModel2TargetObject.java | 24 +- .../agent/gdb/model/impl/GdbModelImpl.java | 5 +- .../model/impl/GdbModelTargetEnvironment.java | 4 +- .../model/impl/GdbModelTargetInferior.java | 6 +- .../impl/GdbModelTargetInferiorContainer.java | 12 +- .../impl/GdbModelTargetModuleContainer.java | 11 +- .../gdb/model/impl/GdbModelTargetSession.java | 24 +- .../gdb/model/AbstractModelForGdbTest.java | 4 +- .../java/agent/gdb/model/GadpForGdbTest.java | 2 +- .../DelegateGadpClientTargetObject.java | 313 ++------- .../ghidra/dbg/gadp/client/GadpClient.java | 454 +++---------- .../GadpClientTargetAccessConditioned.java | 3 +- .../gadp/client/GadpClientTargetAttacher.java | 1 - .../GadpClientTargetBreakpointContainer.java | 12 +- .../GadpClientTargetBreakpointSpec.java | 4 +- .../gadp/client/GadpClientTargetConsole.java | 1 - .../client/GadpClientTargetDeletable.java | 1 - .../client/GadpClientTargetDetachable.java | 1 - .../client/GadpClientTargetEventScope.java | 6 +- .../GadpClientTargetExecutionStateful.java | 3 +- .../client/GadpClientTargetFocusScope.java | 4 +- .../client/GadpClientTargetInterpreter.java | 4 +- .../client/GadpClientTargetInterruptible.java | 1 - .../gadp/client/GadpClientTargetKillable.java | 1 - .../gadp/client/GadpClientTargetLauncher.java | 1 - .../gadp/client/GadpClientTargetMemory.java | 6 +- .../gadp/client/GadpClientTargetMethod.java | 1 - .../gadp/client/GadpClientTargetObject.java | 31 +- .../client/GadpClientTargetObjectStub.java | 57 -- .../client/GadpClientTargetRegisterBank.java | 5 +- .../client/GadpClientTargetResumable.java | 1 - .../client/GadpClientTargetSteppable.java | 1 - .../gadp/{util => client}/GadpValueUtils.java | 111 +-- .../dbg/gadp/server/AbstractGadpServer.java | 10 + .../dbg/gadp/server/GadpClientHandler.java | 369 +++++----- .../Debugger-gadp/src/main/proto/gadp.proto | 73 +- .../ghidra/dbg/gadp/GadpClientServerTest.java | 631 +++++++++++++----- .../dbg/gadp/client/GadpClientTest.java | 242 ++++--- .../ghidra/dbg/jdi/model/JdiModelImpl.java | 5 +- .../model/JdiModelTargetClassContainer.java | 2 +- .../JdiModelTargetConnectorContainer.java | 2 +- .../model/JdiModelTargetModuleContainer.java | 2 +- .../jdi/model/JdiModelTargetObjectImpl.java | 6 +- .../dbg/jdi/model/JdiModelTargetVM.java | 2 +- .../iface1/JdiModelTargetEnvironment.java | 2 +- ...tractDebuggerWrappedConsoleConnection.java | 56 +- .../gui/objects/DebuggerObjectsProvider.java | 28 +- .../debug/gui/objects/ObjectContainer.java | 56 +- .../actions/ImportFromFactsAction.java | 1 - .../objects/actions/ImportFromXMLAction.java | 1 - .../objects/components/DummyTargetObject.java | 6 + .../components/ObjectAttributeRow.java | 1 - .../objects/components/ObjectElementRow.java | 7 - .../model/DebuggerModelServicePlugin.java | 4 +- .../DebuggerTargetsPluginScreenShots.java | 11 +- .../java/ghidra/async/AsyncReference.java | 4 + .../comm/service/AbstractAsyncServer.java | 1 + .../ghidra/dbg/DebugModelConventions.java | 2 +- .../ghidra/dbg/DebuggerModelListener.java | 37 +- .../java/ghidra/dbg/DebuggerObjectModel.java | 124 ++-- .../agent/AbstractDebuggerObjectModel.java | 175 ++++- .../dbg/agent/AbstractTargetObject.java | 114 +++- .../dbg/agent/DefaultTargetModelRoot.java | 4 +- .../ghidra/dbg/agent/DefaultTargetObject.java | 83 ++- .../agent/InvalidatableTargetObjectIf.java | 3 +- .../dbg/agent/SpiDebuggerObjectModel.java | 4 +- .../ghidra/dbg/agent/SpiTargetObject.java | 29 + .../dbg/attributes/TargetObjectRef.java | 21 +- .../dbg/attributes/TypedTargetObjectRef.java | 7 + .../java/ghidra/dbg/target/TargetConsole.java | 18 +- .../java/ghidra/dbg/target/TargetObject.java | 62 +- .../ghidra/dbg/target/TargetObjectPath.java | 124 ++++ .../main/java/ghidra/dbg/util/PathUtils.java | 18 + .../agent/DefaultDebuggerObjectModelTest.java | 181 ++++- .../dbg/model/TestDebuggerObjectModel.java | 9 +- .../ghidra/dbg/model/TestTargetObject.java | 4 +- .../AnnotatedTargetObjectSchemaTest.java | 36 +- .../TargetObjectSchemaValidationTest.java | 17 +- .../dbg/util/AbstractInvocationListener.java | 5 +- .../util/AllTargetObjectListenerAdapter.java | 2 +- .../ghidra/dbg/util/InvalidatedListener.java | 8 +- .../ghidra/util/datastruct/ListenerMap.java | 115 +++- .../ghidra/util/datastruct/ListenerSet.java | 12 + .../java/utilities/util/ProxyUtilities.java | 4 +- 95 files changed, 2348 insertions(+), 1635 deletions(-) delete mode 100644 Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObjectStub.java rename Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/{util => client}/GadpValueUtils.java (88%) create mode 100644 Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/SpiTargetObject.java create mode 100644 Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObjectPath.java diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetEnvironment.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetEnvironment.java index 9f2dc718c6..db2ea1a181 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetEnvironment.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetEnvironment.java @@ -21,7 +21,7 @@ import ghidra.dbg.target.TargetEnvironment; public interface DbgModelTargetEnvironment> extends DbgModelTargetObject, TargetEnvironment { - public void refresh(); + public void refreshInternal(); @Override public default String getArchitecture() { diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetEnvironmentEx.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetEnvironmentEx.java index be4545ce00..3f98101500 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetEnvironmentEx.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetEnvironmentEx.java @@ -20,7 +20,7 @@ import ghidra.dbg.target.TargetEnvironment; public interface DbgModelTargetEnvironmentEx extends DbgModelTargetObject, TargetEnvironment { - public void refresh(); + public void refreshInternal(); /* @Override diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetObject.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetObject.java index c1c4f6ec62..1139a5a5cf 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetObject.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetObject.java @@ -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(); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImpl.java index f2cd5895c3..bf5d16f02e 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImpl.java @@ -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 close() { try { terminate(); - return CompletableFuture.completedFuture(null); + return super.close(); } catch (Throwable t) { return CompletableFuture.failedFuture(t); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetObjectImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetObjectImpl.java index 13cab1ef6f..876dfe8fad 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetObjectImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetObjectImpl.java @@ -56,8 +56,8 @@ public class DbgModelTargetObjectImpl extends DefaultTargetObject>> { // 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> 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 close() { try { terminate(); - return CompletableFuture.completedFuture(null); + return super.close(); } catch (Throwable t) { return CompletableFuture.failedFuture(t); diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java index 01323cceab..a18a0d8063 100644 --- a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java @@ -59,8 +59,6 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject DbgModel2TargetObjectImpl(ProxyFactory 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 requestAttributes(boolean refresh) { - fireAttributesChanged = true; Map nmap = new HashMap<>(); return requestNativeAttributes().thenCompose(map -> { synchronized (attributes) { @@ -418,13 +421,10 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject, // @@ -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 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> 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 as(Class iface) { - return DebuggerObjectModel.requireIface(iface, proxy, getPath()); - } - @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public CompletableFuture 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 getActions() { return breakpointActions; } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java index 05a48da813..e249c5f3c6 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java @@ -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 close() { try { terminate(); - return AsyncUtils.NIL; + return super.close(); } catch (Throwable t) { return CompletableFuture.failedFuture(t); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetEnvironment.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetEnvironment.java index 02101ed344..fcb01d51fc 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetEnvironment.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetEnvironment.java @@ -59,7 +59,7 @@ public class GdbModelTargetEnvironment VISIBLE_ENDIAN_ATTRIBUTE_NAME, endian, UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED), "Initialized"); - refresh(); + refreshInternal(); } protected CompletableFuture refreshArchitecture() { @@ -156,7 +156,7 @@ public class GdbModelTargetEnvironment }); } - protected CompletableFuture refresh() { + protected CompletableFuture refreshInternal() { AsyncFence fence = new AsyncFence(); fence.include(refreshArchitecture()); fence.include(refreshOS()); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java index 19dff2b858..cb330264ca 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java @@ -220,9 +220,9 @@ public class GdbModelTargetInferior protected CompletableFuture 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( // diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferiorContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferiorContainer.java index 6e80717376..a66ae42915 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferiorContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferiorContainer.java @@ -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 = { - @TargetAttributeType(type = Void.class) -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "InferiorContainer", + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class GdbModelTargetInferiorContainer extends DefaultTargetObject 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; }); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModuleContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModuleContainer.java index eccbc76e61..b3a7fccb62 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModuleContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModuleContainer.java @@ -32,9 +32,12 @@ import ghidra.dbg.target.schema.TargetObjectSchemaInfo; import ghidra.lifecycle.Internal; import ghidra.util.Msg; -@TargetObjectSchemaInfo(name = "ModuleContainer", attributes = { - @TargetAttributeType(type = Void.class) -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "ModuleContainer", + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class GdbModelTargetModuleContainer extends DefaultTargetObject implements TargetModuleContainer { @@ -118,7 +121,7 @@ public class GdbModelTargetModuleContainer return modulesByName.get(name); } - public CompletableFuture refresh() { + public CompletableFuture refreshInternal() { if (!isObserved()) { return AsyncUtils.NIL; } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSession.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSession.java index 8637fa030b..a1831fae3e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSession.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSession.java @@ -33,11 +33,14 @@ import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; import ghidra.util.Msg; -@TargetObjectSchemaInfo(name = "Session", elements = { - @TargetElementType(type = Void.class) -}, attributes = { - @TargetAttributeType(type = Void.class) -}) +@TargetObjectSchemaInfo( + name = "Session", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(type = Void.class) + }) public class GdbModelTargetSession extends DefaultTargetModelRoot implements // TargetAccessConditioned, TargetAttacher, @@ -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 diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbTest.java index 8c0c949209..3c4741b023 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbTest.java @@ -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); } }; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GadpForGdbTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GadpForGdbTest.java index ace9ae8ba3..4ed46328e2 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GadpForGdbTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GadpForGdbTest.java @@ -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()); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/DelegateGadpClientTargetObject.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/DelegateGadpClientTargetObject.java index ef51a79be4..8314b16193 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/DelegateGadpClientTargetObject.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/DelegateGadpClientTargetObject.java @@ -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 + implements GadpClientTargetObject { protected abstract static class GadpHandlerMap { protected final Class 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 path; - protected boolean valid = true; - - public ProxyState(GadpClient client, List 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>, GadpEventHandlerMap> EVENT_HANDLER_MAPS_BY_COMPOSITION = new HashMap<>(); protected static final Map>, GadpAttributeChangeCallbackMap> ATTRIBUTE_CHANGE_CALLBACKS_MAPS_BY_COMPOSITION = new HashMap<>(); - protected static GadpClientTargetObject makeModelProxy(GadpClient client, List path, - String typeHint, List ifaceNames) { + protected static GadpClientTargetObject makeModelProxy(GadpClient client, + GadpClientTargetObject parent, String key, String typeHint, List ifaceNames) { List> ifaces = TargetObject.getInterfacesByName(ifaceNames); List> 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 ifaceNames; private final List> ifaces; private final GadpEventHandlerMap eventHandlers; private final GadpAttributeChangeCallbackMap attributeChangeCallbacks; - protected final ListenerSet listeners; - - // TODO: Use path element comparators? - protected final Map elements = new TreeMap<>(); - // TODO: Use path element comparators? - protected final Map attributes = new TreeMap<>(); protected Map memCache = null; // Becomes active if this is a TargetMemory protected Map regCache = null; // Becomes active if this is a TargtRegisterBank protected ListenerSet actions = null; // Becomes active is this is a TargetBreakpointSpec - public DelegateGadpClientTargetObject(GadpClient client, List path, String typeHint, - List ifaceNames, List> ifaces, + public DelegateGadpClientTargetObject(GadpClient client, GadpClientTargetObject parent, + String key, String typeHint, TargetObjectSchema schema, List ifaceNames, + List> ifaces, List> 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 ""; - } - @Override public GadpClient getModel() { - return state.client; + return client; } @Override - public List getProtocolID() { - return state.path; - } - - @Override - public List 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> getInterfaces() { + public Collection> getInterfaces() { return ifaces; } - @Override - public boolean isValid() { - return state.valid; - } - - @Override - public Map getCachedElements() { - synchronized (this.elements) { - return Map.copyOf(elements); - } - } - - @Override - public Map 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 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 as(Class iface) { - return DebuggerObjectModel.requireIface(iface, proxy, state.path); - } - @Override public CompletableFuture 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 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 requestAttributes(boolean refresh) { + return resync(refresh, false); } @Override - public void removeListener(TargetObjectListener l) { - listeners.remove(l); + protected CompletableFuture 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 elements = - GadpValueUtils.getElementMap(this, info.getElementIndexList()); - Map attributes = - GadpValueUtils.getAttributeMap(this, info.getAttributeList()); - - Delta deltaE = setElements(elements); - Delta deltaA = setAttributes(attributes); - fireElementsChanged(deltaE); - fireAttributesChanged(deltaA); - } - - public void updateWithDelta(Gadp.ModelObjectDelta delta) { - Map elementsAdded = - GadpValueUtils.getElementMap(this, delta.getIndexAddedList()); + public void updateWithDeltas(Gadp.ModelObjectDelta deltaE, Gadp.ModelObjectDelta deltaA) { + Map elementsAdded = + GadpValueUtils.getElementMap(this, deltaE.getAddedList()); Map attributesAdded = - GadpValueUtils.getAttributeMap(this, delta.getAttributeAddedList()); + GadpValueUtils.getAttributeMap(this, deltaA.getAddedList()); - Delta deltaE = - updateElements(Delta.create(delta.getIndexRemovedList(), elementsAdded)); - Delta 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 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 Delta updateElements( - Delta delta) { - synchronized (this.elements) { - return delta.apply(elements); - } - } - - protected Delta setElements( - Map elements) { - synchronized (this.elements) { - return Delta.computeAndSet(this.elements, elements, Delta.SAME); - } - } - - protected Delta updateAttributes(Delta delta) { - synchronized (this.attributes) { - return delta.apply(attributes, Delta.EQUAL); - } - } - - protected Delta setAttributes(Map attributes) { - synchronized (this.attributes) { - return Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL); - } - } - - protected void fireElementsChanged(Delta 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 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 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); + } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClient.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClient.java index a28a36beae..97346e0f66 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClient.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClient.java @@ -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>> { 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 messageChannel; protected AsyncReference 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 listenersClient = - new ListenerSet<>(DebuggerModelListener.class, clientExecutor); protected final TriConsumer listenerForChannelState = this::channelStateChanged; protected final MessagePairingCache messageMatcher = new MessagePairingCache(); protected final AtomicInteger sequencer = new AtomicInteger(); - protected final NavigableMap, 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, Object> valueRequests = - new AsyncLazyMap<>(new HashMap<>(), p -> doRequestValue(p, false)); - protected final AsyncLazyMap, GadpClientTargetObject> attrsRequests = - new AsyncLazyMap<>(new HashMap<>(), p -> doRequestAttributes(p, false)); - protected final AsyncLazyMap, GadpClientTargetObject> elemsRequests = - new AsyncLazyMap<>(new HashMap<>(), p -> doRequestElements(p, false)); + protected final Map, 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> mixins) { + return ProxyUtilities.composeOnDelegate(GadpClientTargetObject.class, + (GadpClientTargetObject) delegate, mixins, GadpClientTargetObject.LOOKUP); + } + protected AsyncProtobufMessageChannel 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 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 CompletableFuture 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 connect() { Gadp.ConnectRequest.Builder req = GadpVersion.makeRequest(); if (channelState.get() != ChannelState.INACTIVE) { @@ -493,8 +487,13 @@ public class GadpClient implements DebuggerObjectModel { public CompletableFuture close() { try { messageChannel.close(); - channelState.set(ChannelState.CLOSED, DebuggerModelClosedReason.normal()); - return AsyncUtils.NIL; + CompletableFuture.runAsync(() -> { + channelState.set(ChannelState.CLOSED, DebuggerModelClosedReason.normal()); + }, 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 path) { - synchronized (modelProxies) { - return modelProxies.get(path); - } - } - - protected GadpClientTargetObject removeCachedProxy(List 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 getCachedValue(List path) { - GadpClientTargetObject proxy = getCachedProxy(path); - if (proxy != null) { - return Optional.of(proxy); - } - List parentPath = PathUtils.parent(path); - if (parentPath == null) { - return null; - } - GadpClientTargetObject parent = getCachedProxy(parentPath); - if (parent == null) { - return null; - } - Optional val = parent.getDelegate().cachedChild(PathUtils.getKey(path)); - if (val == null) { - return null; - } - if (val.isEmpty()) { - if (PathUtils.isInvocation(PathUtils.getKey(path))) { - return null; + protected GadpClientTargetObject getProxy(List 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 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 path, GadpClientTargetObject proxy) { - List 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); - return proxy; - }); - } - } - - @Internal - public TargetObjectRef getProxyOrStub(List path) { - GadpClientTargetObject cached = getCachedProxy(path); - if (cached != null) { - return cached; - } - return new GadpClientTargetObjectStub(this, path); - } - - protected CompletableFuture unsubscribe(List 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 path, String reason) { - List> pathsToInvalidate = new ArrayList<>(); - List proxiesToInvalidate; - synchronized (modelProxies) { - // keySet is the only one which isn't a copy. - // TODO: Would rather iterate entries when AbstractWeakValueMap is fixed - for (List 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 path) { - return valueRequests.containsKey(path) || attrsRequests.containsKey(path) || - elemsRequests.containsKey(path); - } - - public boolean hasPendingAttributes(List path) { - return attrsRequests.containsKey(path); - } - - public boolean hasPendingElements(List path) { - return elemsRequests.containsKey(path); - } - - protected void cleanRequests(List path) { - valueRequests.forget(path); - attrsRequests.forget(path); - elemsRequests.forget(path); - } - - @Override - public CompletableFuture fetchModelRoot() { - return fetchModelObject(List.of()); - } - - protected CompletableFuture doRequestValueWithObjectInfo( - List path, boolean refresh, - boolean fetchElements, boolean refreshElements, - boolean fetchAttributes, boolean refreshAttributes) { - CompletableFuture 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 doRequestElements(List path, - boolean refresh) { - return doRequestValueWithObjectInfo(path, false, true, refresh, false, false) - .thenApply(GadpClient::targetObjectOrNull); - } - - protected CompletableFuture doRequestAttributes(List path, - boolean refresh) { - return doRequestValueWithObjectInfo(path, false, false, false, true, refresh) - .thenApply(GadpClient::targetObjectOrNull); - } - - protected boolean forgetElementsRequests(List path, GadpClientTargetObject proxy) { - return proxy == null || !proxy.isValid() || - proxy.getUpdateMode() == TargetUpdateMode.SOLICITED; - } - - protected CompletableFuture checkProcessedElemsReply(List 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> fetchObjectElements( - List path, boolean refresh) { - CompletableFuture processedReply = - checkProcessedElemsReply(path, refresh); - return processedReply.thenApply(proxy -> { - if (proxy == null) { // The path doesn't exist, so return null, per the docs - return null; + protected GadpClientTargetObject createProxy(List 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); } - return proxy.getCachedElements(); - }).exceptionally(GadpClient::nullForNotExist); - } - - protected boolean forgetAttributesRequests(List path, GadpClientTargetObject proxy) { - return proxy == null || !proxy.isValid(); - } - - protected CompletableFuture checkProcessedAttrsReply(List path, - boolean refresh) { - if (refresh) { - attrsRequests.forget(path); - return attrsRequests.get(path, p -> doRequestAttributes(p, refresh)); - } - return attrsRequests.get(path); - } - - @Override - public CompletableFuture> fetchObjectAttributes(List path, - boolean refresh) { - CompletableFuture processedReply = - checkProcessedAttrsReply(path, refresh); - return processedReply.thenApply(proxy -> { - if (proxy == null) { // The path doesn't exist, so return null, per the docs - return null; + List parentPath = PathUtils.parent(path); + GadpClientTargetObject parent; + if (parentPath == null) { + parent = null; } - return proxy.getCachedAttributes(); - }).exceptionally(GadpClient::nullForNotExist); - } - - @Override - public CompletableFuture fetchModelValue(List 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 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); + else { + parent = getProxy(parentPath, true); + if (parent == null) { + Msg.error(this, "Got object's created event before its parent's: " + + PathUtils.toString(path)); } } + GadpClientTargetObject proxy = DelegateGadpClientTargetObject.makeModelProxy(this, + parent, PathUtils.getKey(path), evt.getTypeHint(), evt.getInterfaceList()); + modelProxies.put(path, proxy); + return proxy; } - return valueRequests.get(path, p -> doRequestValue(p, refresh)) - .exceptionally(GadpClient::nullForNotExist); } - @Override - public CompletableFuture fetchModelValue(List path) { - return fetchModelValue(path, false); - } - - protected CompletableFuture doRequestValue(List path, boolean refresh) { - return doRequestValueWithObjectInfo(path, refresh, false, false, false, false); + protected void removeProxy(List path, String reason) { + synchronized (lock) { + modelProxies.remove(path); + } } @Override @@ -823,22 +577,28 @@ public class GadpClient implements DebuggerObjectModel { @Override public TargetObjectRef createRef(List 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 path) { + return getProxy(path, false); } @Override public void invalidateAllLocalCaches() { List copy; - synchronized (modelProxies) { + synchronized (lock) { copy = List.copyOf(modelProxies.values()); } for (GadpClientTargetObject proxy : copy) { proxy.getDelegate().doClearCaches(); } } - - @Override - public Executor getClientExecutor() { - return clientExecutor; - } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAccessConditioned.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAccessConditioned.java index 1f260c3b31..fb01a4f52c 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAccessConditioned.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAccessConditioned.java @@ -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)); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAttacher.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAttacher.java index d63fc5f012..5ef5e2714c 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAttacher.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAttacher.java @@ -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; diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointContainer.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointContainer.java index 7e2e39fdd4..003dc05b54 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointContainer.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointContainer.java @@ -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> 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> spec = specPath == null ? null - : getModel().getProxyOrStub(specPath.getEList()).as(TargetBreakpointSpec.tclass); + : getModel().getProxy(specPath.getEList(), true).as(TargetBreakpointSpec.tclass); Path bptPath = evt.getEffective(); TypedTargetObjectRef> 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 diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpec.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpec.java index f26d54dc41..3e24193ee2 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpec.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpec.java @@ -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)); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetConsole.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetConsole.java index 7d1d9d3f47..0e18e61ec9 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetConsole.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetConsole.java @@ -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 diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetDeletable.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetDeletable.java index 4cb007ee20..19a6072791 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetDeletable.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetDeletable.java @@ -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 diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetDetachable.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetDetachable.java index 251bbd1c51..d38e4a130f 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetDetachable.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetDetachable.java @@ -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 diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetEventScope.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetEventScope.java index e5304c00cd..3f82a492ac 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetEventScope.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetEventScope.java @@ -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> 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 parameters = GadpValueUtils.getValues(getModel(), evt.getParametersList()); - getDelegate().listeners.fire(TargetEventScopeListener.class) + getDelegate().getListeners() + .fire(TargetEventScopeListener.class) .event(this, thread, type, description, parameters); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetExecutionStateful.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetExecutionStateful.java index d249dc6481..eb82410a95 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetExecutionStateful.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetExecutionStateful.java @@ -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)); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetFocusScope.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetFocusScope.java index 5e11c998e9..b6f3897eae 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetFocusScope.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetFocusScope.java @@ -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)); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterpreter.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterpreter.java index ed8d91d552..4f1bc22d26 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterpreter.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterpreter.java @@ -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)); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterruptible.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterruptible.java index 0c5803cd0d..a66a864268 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterruptible.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterruptible.java @@ -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 diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetKillable.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetKillable.java index 876d0f2310..59f6465d2c 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetKillable.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetKillable.java @@ -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 diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetLauncher.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetLauncher.java index 69d85794b0..4f02517187 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetLauncher.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetLauncher.java @@ -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; diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMemory.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMemory.java index d690462b7e..e0035d2a39 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMemory.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMemory.java @@ -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)); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMethod.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMethod.java index 2b91bffbdc..9a51e43ee6 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMethod.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMethod.java @@ -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 diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObject.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObject.java index b080b854e2..5e56c7ed24 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObject.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObject.java @@ -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 getProtocolID(); - - @Override - String getTypeHint(); - - @Override - Collection getInterfaceNames(); - - @Override - Collection> 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 { diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObjectStub.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObjectStub.java deleted file mode 100644 index 50805de7d1..0000000000 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObjectStub.java +++ /dev/null @@ -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 path; - protected final int hash; - - public GadpClientTargetObjectStub(GadpClient client, List 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 getPath() { - return path; - } - - @Override - public String toString() { - return ""; - } -} diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetRegisterBank.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetRegisterBank.java index 943aba4233..2bafa2ecca 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetRegisterBank.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetRegisterBank.java @@ -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 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); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetResumable.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetResumable.java index 13deb8aabe..3a7c5dfd23 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetResumable.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetResumable.java @@ -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 diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetSteppable.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetSteppable.java index 0a8825b968..8c217eb123 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetSteppable.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetSteppable.java @@ -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 diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/util/GadpValueUtils.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java similarity index 88% rename from Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/util/GadpValueUtils.java rename to Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java index dd63ebe0bc..486aa928f2 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/util/GadpValueUtils.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java @@ -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 ent : obj.getCachedAttributes().entrySet()) { - builder.addAttribute(makeAttribute(obj, ent)); + public static Gadp.ModelObjectDelta makeElementDelta(List parentPath, + Delta delta) { + ModelObjectDelta.Builder builder = Gadp.ModelObjectDelta.newBuilder() + .addAllRemoved(delta.getKeysRemoved()); + for (Entry ent : delta.added.entrySet()) { + builder.addAdded(makeIndexedValue(parentPath, ent)); } - return builder.build(); } - public static Gadp.ModelObjectDelta makeDelta(TargetObject parent, - Delta deltaE, Delta deltaA) { + public static Gadp.ModelObjectDelta makeAttributeDelta(List parentPath, + Delta delta) { ModelObjectDelta.Builder builder = Gadp.ModelObjectDelta.newBuilder() - .addAllIndexRemoved(deltaE.getKeysRemoved()) - .addAllIndexAdded(deltaE.added.keySet()) - .addAllAttributeRemoved(deltaA.getKeysRemoved()); - for (Entry ent : deltaA.added.entrySet()) { - builder.addAttributeAdded(makeAttribute(parent, ent)); + .addAllRemoved(delta.getKeysRemoved()); + for (Entry 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 argument) { - return Gadp.Argument.newBuilder() - .setName(argument.getKey()) - .setValue(makeValue(null, argument.getValue())) + public static Gadp.NamedValue makeNamedValue(Map.Entry ent) { + return makeNamedValue(null, ent); + } + + public static Gadp.NamedValue makeNamedValue(List parentPath, + Map.Entry ent) { + List 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 ent) { - return Gadp.Attribute.newBuilder() + public static Gadp.NamedValue makeIndexedValue(List parentPath, + Map.Entry ent) { + List 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 getAttributeMap(TargetObject object, - List 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 getAttributeMap(GadpClientTargetObject object, + List list) { Map 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 makeArguments(Map arguments) { + public static List makeArguments(Map arguments) { return arguments.entrySet() .stream() - .map(ent -> makeArgument(ent)) + .map(ent -> makeNamedValue(ent)) .collect(Collectors.toList()); } - public static Map getElementMap(TargetObject parent, - List indices) { - Map result = new LinkedHashMap<>(); - for (String index : indices) { - result.put(index, - parent.getModel().createRef(PathUtils.index(parent.getPath(), index))); + public static Map getElementMap(GadpClientTargetObject parent, + List list) { + Map 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 getArguments(DebuggerObjectModel model, - List arguments) { + List arguments) { return arguments.stream() .collect( Collectors.toMap(a -> a.getName(), a -> getValue(model, null, a.getValue()))); diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpServer.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpServer.java index fbbf2d5de8..83c05df394 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpServer.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpServer.java @@ -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 @@ -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; + }); + } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/GadpClientHandler.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/GadpClientHandler.java index 4827d8bb2c..127139bfdb 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/GadpClientHandler.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/GadpClientHandler.java @@ -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 removed, Map 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 removed, Map 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> frame, TypedTargetObjectRef> spec, TypedTargetObjectRef> 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 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> eventThread, TargetEventType type, String description, List 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 channel; protected final ListenerForEvents listenerForEvents = new ListenerForEvents(); // Keeps strong references and tells level of subscription - protected final NavigableSet subscriptions = new TreeSet<>(); public GadpClientHandler(AbstractGadpServer server, AsynchronousSocketChannel sock) { super(server, sock); model = server.model; - channel = new AsyncProtobufMessageChannel(sock); + channel = createMessageChannel(sock); + } + + protected AsyncProtobufMessageChannel 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 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 tail = subscriptions.tailSet(seed); - for (Iterator 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 CompletableFuture sendDelta(TargetObject parent, - Delta deltaE, Delta deltaA) { - assert isSubscribed(parent); // If listening, we should be subscribed + protected CompletableFuture sendDelta( + List parentPath, Delta 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 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 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 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 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 path = req.getPath().getEList(); + return model.fetchModelObject(path).thenCompose(obj -> { + TargetBreakpointContainer breaks = + DebuggerObjectModel.requireIface(TargetBreakpointContainer.class, obj, path); Set 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 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 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 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 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 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 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 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 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 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 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 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 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 path = req.getPath().getEList(); + return model.fetchModelObject(path).thenCompose(obj -> { + TargetRegisterBank bank = + DebuggerObjectModel.requireIface(TargetRegisterBank.class, obj, path); Map 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 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 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) diff --git a/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto b/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto index e5c9676c5c..6e742176c0 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto +++ b/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto @@ -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; } } diff --git a/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/GadpClientServerTest.java b/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/GadpClientServerTest.java index 407f249823..c2467fcb1d 100644 --- a/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/GadpClientServerTest.java +++ b/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/GadpClientServerTest.java @@ -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 + extends AsyncProtobufMessageChannel { + 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 write(S msg) { + Msg.debug(this, sendPrefix + msg); + return super.write(msg).thenApplyAsync(c -> { + return c; + }); + } + + @Override + public CompletableFuture read(IOFunction 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 createMessageChannel( + AsynchronousByteChannel channel) { + return new PrintingAsyncProtobufMessageChannel<>("C", "S", channel); + } + } + protected static T waitOn(CompletableFuture 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 extends DefaultTargetObject { - 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 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 - implements TargetMethod { - private Function method; + public class TestGadpTargetMethod extends TestTargetObject> + implements TargetMethod { - public TestTargetMethod(TargetObject parent, String key, Function 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 invoke(Map 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 { - 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 fetchModelRoot() { - return CompletableFuture.completedFuture(session); - } - @Override public AddressFactory getAddressFactory() { return factory; } + } - @Override - public CompletableFuture 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 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 { final Map args; @@ -493,12 +566,41 @@ public class GadpClientServerTest { } protected Deque> launches = new LinkedList<>(); - protected AsyncReference invocationCount = new AsyncReference<>(); - protected Deque invocations = new LinkedList<>(); + + protected static class AsyncDeque { + private final Deque deque = new LinkedList<>(); + private final AsyncReference 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 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 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 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, String> actualInv = invL.invocations.stream() .collect(Collectors.toMap(ii -> ID.of(ii.object), ii -> ii.reason)); - Map, String> expectedInv = - avail1.values() - .stream() - .collect(Collectors.toMap(o -> ID.of(o), o -> "Changed")); + Map, 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 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 args; - try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open(); - ServerRunner runner = new ServerRunner()) { - MonitoredGadpClient client = new MonitoredGadpClient("Test", socket); + public CallEntry(String methodName, List args) { + this.methodName = methodName; + this.args = args; + } + + @Override + public String toString() { + return String.format("", 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 record = new ArrayList<>(); + + @Override + public void created(TargetObject object) { + record.add(new CallEntry("created", List.of(object))); + } + + @Override + public void attributesChanged(TargetObject parent, Collection removed, + Map added) { + record.add(new CallEntry("attributesChanged", List.of(parent, removed, added))); + } + + @Override + public void elementsChanged(TargetObject parent, Collection removed, + Map 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", "")))), + 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", "")))), + 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", "")))), + 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); } } } diff --git a/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/client/GadpClientTest.java b/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/client/GadpClientTest.java index f011285d65..a4a8dc29c0 100644 --- a/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/client/GadpClientTest.java +++ b/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/client/GadpClientTest.java @@ -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("") .setRootSchema("OBJECT")) .build()); - seqno++; } - public void handlePing() throws IOException { + public void handleConnect(GadpVersion version) throws IOException { + expectRequestConnect(); + sendReplyConnect(version); + nextSeq(); + } + + public void sendNotifyObjectCreated(List path, List 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 path, Object value) - throws Exception { + public void handlePing() throws IOException { + expectRequestPing(HELLO_WORLD); + sendReplyPing(HELLO_WORLD); + nextSeq(); + } + + public Gadp.ModelObjectDelta.Builder makeAttributeDelta(List parentPath, + Map primitives, Map> objects) { + Gadp.ModelObjectDelta.Builder delta = Gadp.ModelObjectDelta.newBuilder(); + if (primitives != null) { + for (Map.Entry ent : primitives.entrySet()) { + delta.addAdded(GadpValueUtils.makeNamedValue(parentPath, ent)); + } + } + if (objects != null) { + for (Map.Entry> ent : objects.entrySet()) { + if (PathUtils.isLink(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 Gadp.ModelObjectDelta.Builder makeElementDelta(List parentPath, + Map> objects) { + Gadp.ModelObjectDelta.Builder delta = Gadp.ModelObjectDelta.newBuilder(); + if (objects != null) { + for (Map.Entry> 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 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)) + .setAttributes(refreshAttributes) + .setElements(refreshElements)) .build()); - send(Gadp.RootMessage.newBuilder() - .setSequence(seqno) - .setSubscribeReply(Gadp.SubscribeReply.newBuilder() - .setValue(GadpValueUtils.makeValue(path, value))) - .build()); - seqno++; } - public void handleFetchObject(List 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() - .setValue(Gadp.Value.newBuilder() - .setObjectInfo(Gadp.ModelObjectInfo.newBuilder() - .setPath(GadpValueUtils.makePath(path)))); + public void sendNotifyObjects(List parentPath, Map> elements, + Map> attrObjects, Map attrPrimitives) + throws IOException { send(Gadp.RootMessage.newBuilder() .setSequence(seqno) - .setSubscribeReply(reply) - .build()); - seqno++; - } - - public void handleFetchElements(List path, Collection indices) - throws Exception { - expect(Gadp.RootMessage.newBuilder() - .setSequence(seqno) - .setSubscribeRequest(Gadp.SubscribeRequest.newBuilder() - .setPath(GadpValueUtils.makePath(path)) - .setSubscribe(true) - .setFetchElements(true)) - .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 path, Collection indicesAdded) - throws Exception { - Gadp.ModelObjectEvent.Builder evt = Gadp.ModelObjectEvent.newBuilder(); - evt.setDelta(Gadp.ModelObjectDelta.newBuilder() - .addAllIndexAdded(indicesAdded)); - send(Gadp.RootMessage.newBuilder() .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 path, boolean refresh, + Map> objects, Map primitives) + throws Exception { + expectRequestResync(path, refresh, false); + if (primitives != null || objects != null) { + sendNotifyObjects(path, null, objects, primitives); + } + sendReplyResync(); + nextSeq(); + } + + public void handleResyncElements(List path, boolean refresh, + Map> 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 parentPath = PathUtils.parse("Parent"); CompletableFuture 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> 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 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 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 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)); } } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelImpl.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelImpl.java index 282a0c6145..a97380a23e 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelImpl.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelImpl.java @@ -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 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; } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetClassContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetClassContainer.java index e6dec7b001..55e411fc99 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetClassContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetClassContainer.java @@ -85,7 +85,7 @@ public class JdiModelTargetClassContainer extends JdiModelTargetObjectImpl { return getClassesByName().get(name); } - public CompletableFuture refresh() { + public CompletableFuture refreshInternal() { if (!isObserved()) { return AsyncUtils.NIL; } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConnectorContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConnectorContainer.java index 55eda7b5c0..6c4ce6c31a 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConnectorContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConnectorContainer.java @@ -91,7 +91,7 @@ public class JdiModelTargetConnectorContainer extends JdiModelTargetObjectImpl { return null; } - public CompletableFuture refresh() { + public CompletableFuture refreshInternal() { if (!isObserved()) { return AsyncUtils.NIL; } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModuleContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModuleContainer.java index fee8b5ee65..3d613a7f2f 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModuleContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModuleContainer.java @@ -118,7 +118,7 @@ public class JdiModelTargetModuleContainer extends JdiModelTargetObjectImpl return modulesByName.get(name); } - public CompletableFuture refresh() { + public CompletableFuture refreshInternal() { if (!isObserved()) { return AsyncUtils.NIL; } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectImpl.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectImpl.java index bdc0a1f042..56c6f1584c 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectImpl.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectImpl.java @@ -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"; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVM.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVM.java index bb7ebcfed9..dce9995d9c 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVM.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVM.java @@ -398,7 +398,7 @@ public class JdiModelTargetVM extends JdiModelTargetObjectImpl implements // } @Override - public void refresh() { + public void refreshInternal() { // TODO Auto-generated method stub } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface1/JdiModelTargetEnvironment.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface1/JdiModelTargetEnvironment.java index 65ae05e199..504a17a61d 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface1/JdiModelTargetEnvironment.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface1/JdiModelTargetEnvironment.java @@ -21,7 +21,7 @@ import ghidra.dbg.target.TargetEnvironment; public interface JdiModelTargetEnvironment> extends JdiModelTargetObject, TargetEnvironment { - public void refresh(); + public void refreshInternal(); @Override public default String getArchitecture() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java index f5a74b38ef..b9bb6d92e9 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java @@ -47,27 +47,33 @@ public abstract class AbstractDebuggerWrappedConsoleConnection { if (object == targetConsole) { // Redundant if (pinned) { @@ -107,8 +113,8 @@ public abstract class AbstractDebuggerWrappedConsoleConnection removed, Map 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 removed, Map 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; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/ObjectContainer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/ObjectContainer.java index 324b3c7cd0..2ae657dfc7 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/ObjectContainer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/ObjectContainer.java @@ -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 attributeMap = new LinkedHashMap<>(); private Set currentChildren = new TreeSet<>(); - public final ListenerSet 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 getOffspring() { - if (targetObjectRef == null) { - return null; + if (targetObject == null) { + return CompletableFuture.completedFuture(null); } - AtomicReference to = new AtomicReference<>(); - AtomicReference> elements = new AtomicReference<>(); - AtomicReference> 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() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/actions/ImportFromFactsAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/actions/ImportFromFactsAction.java index e1b2a5fb37..9e4dd56e8e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/actions/ImportFromFactsAction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/actions/ImportFromFactsAction.java @@ -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); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/actions/ImportFromXMLAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/actions/ImportFromXMLAction.java index 31713abf01..af97a71ece 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/actions/ImportFromXMLAction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/actions/ImportFromXMLAction.java @@ -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) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DummyTargetObject.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DummyTargetObject.java index b2af0e43ed..9171560529 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DummyTargetObject.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DummyTargetObject.java @@ -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 resync(boolean attributes, boolean elements) { + return AsyncUtils.NIL; + } + @Override public CompletableFuture> fetchElements() { // Why not completedFuture(elements)? diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectAttributeRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectAttributeRow.java index d97a5635c4..514872723b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectAttributeRow.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectAttributeRow.java @@ -34,7 +34,6 @@ public class ObjectAttributeRow { ref.fetch().handle(seq::next); }, targetObject).then(seq -> { to = targetObject.get(); - to.addListener(provider); }).finish(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectElementRow.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectElementRow.java index d9afd520e0..7592437881 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectElementRow.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectElementRow.java @@ -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(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java index 353d1340e8..d6999f0007 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java @@ -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> findSuitable = DebugModelConventions.findSuitable(TargetFocusScope.tclass, r); diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPluginScreenShots.java index fdae905c4d..4ac6c5df48 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPluginScreenShots.java @@ -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 close() { - return AsyncUtils.NIL; - } } DebuggerModelServiceInternal modelService; diff --git a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncReference.java b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncReference.java index 4013df5c2d..4a80c805a2 100644 --- a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncReference.java +++ b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncReference.java @@ -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 { 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); } diff --git a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/comm/service/AbstractAsyncServer.java b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/comm/service/AbstractAsyncServer.java index 4382b75788..c88ff3a29c 100644 --- a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/comm/service/AbstractAsyncServer.java +++ b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/comm/service/AbstractAsyncServer.java @@ -123,6 +123,7 @@ public abstract class AbstractAsyncServer, H } } } + group.shutdown(); if (err != null) { throw err; } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java index 0bfe185c35..7962720918 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java @@ -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"); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerModelListener.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerModelListener.java index 632cca9bf6..42eca3afad 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerModelListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerModelListener.java @@ -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 + * + *

+ * 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 * diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java index acaca6a010..76b2aaa0bb 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java @@ -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 * + *

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

+ * 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 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 fetchModelObject(List path) { return fetchModelObject(path, false); } + /** + * Get an object from the model + * + *

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

- * 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 at the time of the flush invocation are completed first. There are no + * guarantees with respect to events which get queued after the flush invocation. * - * @param 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 CompletableFuture gateFuture(CompletableFuture cf) { - return cf.thenCompose(this::gateFuture); - } - - /** - * Ensure that dependent computations occur on the client executor - * - *

- * 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 the type of the future value - * @param v the value - * @return a future while completes with the given value on the client executor - */ - default CompletableFuture gateFuture(T v) { - return CompletableFuture.supplyAsync(() -> v, getClientExecutor()); - } + CompletableFuture flushEvents(); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java index fc79f0750b..520ea90be8 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java @@ -15,20 +15,119 @@ */ 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 listeners = new ListenerSet<>(DebuggerModelListener.class, clientExecutor); + protected SpiTargetObject root; + protected boolean rootAdded; + protected CompletableFuture completedRoot = new CompletableFuture<>(); + + // Remember the order of creation events + protected final Map, 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) { - listeners.add(listener); + public CompletableFuture 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 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 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 @@ -36,8 +135,70 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo listeners.remove(listener); } + /** + * Ensure that dependent computations occur on the client executor + * + *

+ * 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 the type of the future value + * @param v the value + * @return a future while completes with the given value on the client executor + */ + public CompletableFuture 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 flushEvents() { + return gateFuture(null); + //return CompletableFuture.supplyAsync(() -> gateFuture((Void) null)).thenCompose(f -> f); + } + + @Override + public CompletableFuture close() { + clientExecutor.shutdown(); + return AsyncUtils.NIL; + } + + public void removeExisting(List 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 path) { + synchronized (lock) { + return creationLog.get(path); + } } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractTargetObject.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractTargetObject.java index 9fd342eded..2559b4f3ca 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractTargetObject.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractTargetObject.java @@ -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

the type of the parent */ public abstract class AbstractTargetObject

- implements TargetObject, InvalidatableTargetObjectIf { + implements SpiTargetObject { + public static interface ProxyFactory { + SpiTargetObject createProxy(AbstractTargetObject delegate, I info); + } + + protected static final ProxyFactory THIS_FACTORY = (d, i) -> d; + protected static final CompletableFuture> COMPLETED_EMPTY_ELEMENTS = CompletableFuture.completedFuture(Map.of()); protected static final CompletableFuture> 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

completedParent; protected final List path; @@ -55,12 +63,15 @@ public abstract class AbstractTargetObject

protected boolean valid = true; + // TODO: Remove both of these, and just do invocations on model's listeners protected final ListenerSet listeners; - public AbstractTargetObject(DebuggerObjectModel model, P parent, String key, String typeHint, + public AbstractTargetObject(ProxyFactory 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

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

* 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 as(Class iface) { + return DebuggerObjectModel.requireIface(iface, getProxy(), path); } /** @@ -107,7 +145,8 @@ public abstract class AbstractTargetObject

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

@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

} @Override - public DebuggerObjectModel getModel() { + public AbstractDebuggerObjectModel getModel() { return model; } @@ -212,38 +254,68 @@ public abstract class AbstractTargetObject

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 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 attrs, + String reason) { + for (Map.Entry 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 attrs, + String reason) { for (Map.Entry 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 getListeners() { + return listeners; } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetModelRoot.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetModelRoot.java index 7f273ba746..2451620db4 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetModelRoot.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetModelRoot.java @@ -24,11 +24,11 @@ import ghidra.dbg.target.schema.TargetObjectSchema; public class DefaultTargetModelRoot extends DefaultTargetObject 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); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetObject.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetObject.java index 9c3c1b22ed..5fc660f4de 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetObject.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetObject.java @@ -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 * @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 * @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 + * + *

+ * 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 DefaultTargetObject(ProxyFactory 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 ? "" : 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 ? "" : key), + Map.entry(UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED)), "Initialized"); } + public DefaultTargetObject(ProxyFactory 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 * 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 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 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 @Override public Map getCachedElements() { - synchronized (elements) { + synchronized (model.lock) { return Map.copyOf(elements); } } @@ -234,7 +277,7 @@ public class DefaultTargetObject private Delta setElements(Map elements, String reason) { Delta 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 private Delta changeElements(Collection remove, Map add, String reason) { Delta 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 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 @Override public Map 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 */ public Delta setAttributes(Map 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 */ public Delta changeAttributes(List remove, Map 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 } return delta; } - - public ListenerSet getListeners() { - return listeners; - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/InvalidatableTargetObjectIf.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/InvalidatableTargetObjectIf.java index 61aa702267..2509722c60 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/InvalidatableTargetObjectIf.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/InvalidatableTargetObjectIf.java @@ -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); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/SpiDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/SpiDebuggerObjectModel.java index cc5890e01e..7f0ef5114a 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/SpiDebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/SpiDebuggerObjectModel.java @@ -43,7 +43,7 @@ public interface SpiDebuggerObjectModel extends DebuggerObjectModel { return new DefaultTargetObjectRef(this, path); } - public default CompletableFuture fetchFreshChild(TargetObject obj, String key) { + public static CompletableFuture 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 fetchSuccessorValue(TargetObject obj, + public static CompletableFuture fetchSuccessorValue(TargetObject obj, List path, boolean refresh, boolean followLinks) { if (path.isEmpty()) { return CompletableFuture.completedFuture(obj); diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/SpiTargetObject.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/SpiTargetObject.java new file mode 100644 index 0000000000..5b4bd72466 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/SpiTargetObject.java @@ -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 getCachedElements(); + + boolean enforcesStrictSchema(); +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/attributes/TargetObjectRef.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/attributes/TargetObjectRef.java index d22b9d242d..882b66f2ba 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/attributes/TargetObjectRef.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/attributes/TargetObjectRef.java @@ -40,7 +40,11 @@ import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator; *

* 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 { /** @@ -143,7 +147,10 @@ public interface TargetObjectRef extends Comparable { * 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 fetch() { return getModel().fetchModelObject(getPath()); } @@ -298,7 +305,19 @@ public interface TargetObjectRef extends Comparable { * does not exist */ public default CompletableFuture fetchAttribute(String name) { - return fetchAttributes().thenApply(m -> m.get(name)); + if (!PathUtils.isInvocation(name)) { + return fetchAttributes().thenApply(m -> m.get(name)); + } + // TODO: Make a type for the invocation and parse arguments better? + Entry 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())); + }); } /** diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/attributes/TypedTargetObjectRef.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/attributes/TypedTargetObjectRef.java index db6b9c996d..63ca074c26 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/attributes/TypedTargetObjectRef.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/attributes/TypedTargetObjectRef.java @@ -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 the type + * @deprecated I don't think this adds any real value. + */ +@Deprecated(forRemoval = true) public interface TypedTargetObjectRef extends TargetObjectRef { public class CastingTargetObjectRef> implements TypedTargetObjectRef { diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetConsole.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetConsole.java index 4b9eda0420..dde8b7d39b 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetConsole.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetConsole.java @@ -74,22 +74,26 @@ public interface TargetConsole> 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) { - } - - @Override - default void consoleOutput(TargetObject console, Channel channel, byte[] data) { - consoleOutput(console, channel, new String(data, CHARSET)); + consoleOutput(console, channel, text.getBytes(CHARSET)); } } + + public interface TargetTextConsoleListener extends TargetConsoleListener { + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java index f78494ed22..544c964e6d 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java @@ -529,6 +529,30 @@ public interface TargetObject extends TargetObjectRef { return CompletableFuture.completedFuture(this); } + /** + * Refresh the children of this object + * + *

+ * 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 resync(boolean refreshAttributes, boolean refreshElements); + + /** + * Refresh the elements of this object + * + * @return a future which completes when the children are updated. + */ + default CompletableFuture 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 + *

+ * 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 removed, diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObjectPath.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObjectPath.java new file mode 100644 index 0000000000..07dd4ca80e --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObjectPath.java @@ -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 { + protected final DebuggerObjectModel model; + protected final List keyList; + protected final int hash; + + public TargetObjectPath(DebuggerObjectModel model, List 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 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 fetch() { + return model.fetchModelObject(getKeyList()).thenApply(obj -> obj); + } + + public String toPathString() { + return PathUtils.toString(keyList); + } + + public TargetObjectPath parent() { + List pkl = PathUtils.parent(keyList); + return pkl == null ? null : new TargetObjectPath(model, pkl); + } + + public TargetObjectPath successor(List subKeyList) { + return new TargetObjectPath(model, PathUtils.extend(keyList, subKeyList)); + } + + public TargetObjectPath successor(String... subKeyList) { + return successor(Arrays.asList(subKeyList)); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathUtils.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathUtils.java index 80ed568fa8..f147a57c4e 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathUtils.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathUtils.java @@ -516,6 +516,24 @@ public enum PathUtils { return !Objects.equals(extend(parentPath, name), attributePath); } + /** + * Check whether a given element is a link. + * + *

+ * Consider an object {@code O} with an element {@code [1]}. {@code [1]}'s value is a link, iff + * its path does not 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 parentPath, String index, + List elementPath) { + return !Objects.equals(index(parentPath, index), elementPath); + } + /** * Check whether a given attribute should be displayed. * diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/agent/DefaultDebuggerObjectModelTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/agent/DefaultDebuggerObjectModelTest.java index dcc07dddea..6253ea5e9b 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/agent/DefaultDebuggerObjectModelTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/agent/DefaultDebuggerObjectModelTest.java @@ -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 { - 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> + extends FakeTargetObject implements TargetRegisterBank { + + public FakeTargetRegisterBank(AbstractDebuggerObjectModel model, TargetObject parent, + String name) { + super(model, parent, name); + } + + @Override + public CompletableFuture> readRegistersNamed( + Collection names) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture writeRegistersNamed(Map values) { + throw new UnsupportedOperationException(); + } + } + /** * Functionally identical to a Fake, but intrinsically different */ public static class PhonyTargetObject extends DefaultTargetObject { - 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 fetchModelRoot() { return CompletableFuture.completedFuture(root); @@ -70,16 +99,11 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils { public AddressSpace getAddressSpace(String name) { return TODO(); } - - @Override - public CompletableFuture close() { - return AsyncUtils.NIL; - } } static class OffThreadTargetObject extends DefaultTargetObject { - 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> record = new ArrayList<>(); + + @Override + public void created(TargetObject object) { + record.add(new ImmutablePair<>("created", object)); + } + + @Override + public void elementsChanged(TargetObject parent, Collection removed, + Map added) { + for (TargetObjectRef elem : added.values()) { + record.add(new ImmutablePair<>("addedElem", (TargetObject) elem)); + } + } + + @Override + public void attributesChanged(TargetObject parent, Collection removed, + Map added) { + for (Object attr : added.values()) { + if (attr instanceof TargetObject) { + record.add(new ImmutablePair<>("addedAttr", (TargetObject) attr)); + } + } + } + + @Override + public void registersUpdated(TargetRegisterBank bank, Map 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); + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java index a5af500f20..3bb3975c42 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java @@ -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 close() { - session.invalidateSubtree("Model closed"); - return future(null); + session.invalidateSubtree(session, "Model closed"); + return super.close().thenCompose(__ -> future(null)); } public TestTargetProcess addProcess(int pid) { diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetObject.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetObject.java index 758cf74085..3aba4d187c 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetObject.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetObject.java @@ -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(); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java index 17bc08f9ea..1adbbcc6cc 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java @@ -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 implements TargetProcess { - 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 { - 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 extends DefaultTargetObject implements TargetProcess> { - 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 & TargetInterpreter> 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 & TargetInterpreter> 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); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/TargetObjectSchemaValidationTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/TargetObjectSchemaValidationTest.java index 23c96edf66..38ffdeb022 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/TargetObjectSchemaValidationTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/TargetObjectSchemaValidationTest.java @@ -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 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 { - 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; } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AbstractInvocationListener.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AbstractInvocationListener.java index 13da00e81a..c742f45514 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AbstractInvocationListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AbstractInvocationListener.java @@ -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 { - public final List invocations = new ArrayList<>(); + public final LinkedList invocations = new LinkedList<>(); public final AsyncReference count = new AsyncReference<>(0); protected void record(T rec) { diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AllTargetObjectListenerAdapter.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AllTargetObjectListenerAdapter.java index 09430cca35..72479acc7e 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AllTargetObjectListenerAdapter.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AllTargetObjectListenerAdapter.java @@ -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(); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/InvalidatedListener.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/InvalidatedListener.java index 1df1c33dd1..7f7c3d9fd8 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/InvalidatedListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/InvalidatedListener.java @@ -23,10 +23,12 @@ public class InvalidatedListener extends AbstractInvocationListener 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)); } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerMap.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerMap.java index 68c2ef4e53..19aa2db55d 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerMap.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerMap.java @@ -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 { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - Collection listenersVolatile = map.values(); + //Msg.debug(this, "Queuing invocation: " + method.getName() + " @" + + // System.identityHashCode(executor)); + Collection listenersVolatile; + Set> 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 { } }); } + for (ListenerMap c : chained) { + // Invocation will check if assignable + @SuppressWarnings("unchecked") + T l = ((ListenerMap) 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

iface; private final Executor executor; private Map map = createMap(); + private Set> chained = new LinkedHashSet<>(); /** * A proxy which passes invocations to each value of this map @@ -210,21 +240,25 @@ public class ListenerMap { } public V put(K key, V val) { - if (map.get(key) == val) { - return val; + synchronized (lock) { + if (map.get(key) == val) { + return val; + } + Map newMap = createMap(); + newMap.putAll(map); + V result = newMap.put(key, val); + map = newMap; + return result; } - Map newMap = createMap(); - newMap.putAll(map); - V result = newMap.put(key, val); - map = newMap; - return result; } public void putAll(ListenerMap that) { - Map newMap = createMap(); - newMap.putAll(map); - newMap.putAll(that.map); - map = newMap; + synchronized (lock) { + Map newMap = createMap(); + newMap.putAll(map); + newMap.putAll(that.map); + map = newMap; + } } public V get(K key) { @@ -232,20 +266,57 @@ public class ListenerMap { } public V remove(K key) { - if (!map.containsKey(key)) { - return null; + synchronized (lock) { + if (!map.containsKey(key)) { + return null; + } + Map newMap = createMap(); + newMap.putAll(map); + V result = newMap.remove(key); + map = newMap; + return result; } - Map newMap = createMap(); - newMap.putAll(map); - V result = newMap.remove(key); - map = newMap; - return result; } public void clear() { - if (map.isEmpty()) { - return; + synchronized (lock) { + if (map.isEmpty()) { + return; + } + map = createMap(); + } + } + + public void addChained(ListenerMap map) { + synchronized (lock) { + if (chained.contains(map)) { + return; + } + Set> newChained = new LinkedHashSet<>(); + newChained.addAll(chained); + newChained.add(map); + chained = newChained; + } + } + + public void removeChained(ListenerMap map) { + synchronized (lock) { + if (!chained.contains(map)) { + return; + } + Set> newChained = new LinkedHashSet<>(); + newChained.addAll(chained); + newChained.remove(map); + chained = newChained; + } + } + + public void clearChained() { + synchronized (lock) { + if (chained.isEmpty()) { + return; + } + chained = new LinkedHashSet<>(); } - map = createMap(); } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerSet.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerSet.java index 8ae184812d..bf813be5ec 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerSet.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerSet.java @@ -107,4 +107,16 @@ public class ListenerSet { public void clear() { map.clear(); } + + public void addChained(ListenerSet set) { + map.addChained(set.map); + } + + public void removeChained(ListenerSet set) { + map.removeChained(set.map); + } + + public void clearChained() { + map.clearChained(); + } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/ProxyUtilities.java b/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/ProxyUtilities.java index df3334d880..21d9b01584 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/ProxyUtilities.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/ProxyUtilities.java @@ -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; }