mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 10:49:34 +02:00
GP-704: Converting models to a push-centric comm pattern.
This commit is contained in:
parent
dd37995833
commit
5bb6f95a84
95 changed files with 2348 additions and 1635 deletions
|
@ -21,7 +21,7 @@ import ghidra.dbg.target.TargetEnvironment;
|
|||
public interface DbgModelTargetEnvironment<T extends TargetEnvironment<T>>
|
||||
extends DbgModelTargetObject, TargetEnvironment<T> {
|
||||
|
||||
public void refresh();
|
||||
public void refreshInternal();
|
||||
|
||||
@Override
|
||||
public default String getArchitecture() {
|
||||
|
|
|
@ -20,7 +20,7 @@ import ghidra.dbg.target.TargetEnvironment;
|
|||
public interface DbgModelTargetEnvironmentEx
|
||||
extends DbgModelTargetObject, TargetEnvironment<DbgModelTargetEnvironmentEx> {
|
||||
|
||||
public void refresh();
|
||||
public void refreshInternal();
|
||||
|
||||
/*
|
||||
@Override
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -60,6 +60,7 @@ public class DbgModelImpl extends AbstractDbgModel {
|
|||
s.add();
|
||||
DbgModelTargetSessionContainer sessions = root.sessions;
|
||||
this.session = (DbgModelTargetSessionImpl) sessions.getTargetSession(s);
|
||||
addModelRoot(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -110,7 +111,7 @@ public class DbgModelImpl extends AbstractDbgModel {
|
|||
public CompletableFuture<Void> close() {
|
||||
try {
|
||||
terminate();
|
||||
return CompletableFuture.completedFuture(null);
|
||||
return super.close();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
return CompletableFuture.failedFuture(t);
|
||||
|
|
|
@ -56,8 +56,8 @@ public class DbgModelTargetObjectImpl extends DefaultTargetObject<TargetObject,
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void doInvalidate(String reason) {
|
||||
super.doInvalidate(reason);
|
||||
protected void doInvalidate(TargetObject branch, String reason) {
|
||||
super.doInvalidate(branch, reason);
|
||||
getManager().removeStateListener(accessListener);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,14 +30,29 @@ import ghidra.dbg.target.*;
|
|||
import ghidra.dbg.target.schema.*;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
|
||||
@TargetObjectSchemaInfo(name = "Debugger", elements = { //
|
||||
@TargetObjectSchemaInfo(
|
||||
name = "Debugger",
|
||||
elements = { //
|
||||
@TargetElementType(type = Void.class) //
|
||||
}, attributes = { //
|
||||
@TargetAttributeType(name = "Available", type = DbgModelTargetAvailableContainerImpl.class, required = true, fixed = true), //
|
||||
@TargetAttributeType(name = "Connectors", type = DbgModelTargetConnectorContainerImpl.class, required = true, fixed = true), //
|
||||
@TargetAttributeType(name = "Sessions", type = DbgModelTargetSessionContainerImpl.class, required = true, fixed = true), //
|
||||
},
|
||||
attributes = { //
|
||||
@TargetAttributeType(
|
||||
name = "Available",
|
||||
type = DbgModelTargetAvailableContainerImpl.class,
|
||||
required = true,
|
||||
fixed = true), //
|
||||
@TargetAttributeType(
|
||||
name = "Connectors",
|
||||
type = DbgModelTargetConnectorContainerImpl.class,
|
||||
required = true,
|
||||
fixed = true), //
|
||||
@TargetAttributeType(
|
||||
name = "Sessions",
|
||||
type = DbgModelTargetSessionContainerImpl.class,
|
||||
required = true,
|
||||
fixed = true), //
|
||||
@TargetAttributeType(type = Void.class) //
|
||||
})
|
||||
})
|
||||
public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot
|
||||
implements DbgModelTargetRoot {
|
||||
|
||||
|
@ -136,12 +151,6 @@ public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot
|
|||
), reason.desc());
|
||||
}
|
||||
|
||||
//@Override
|
||||
public void refresh() {
|
||||
// TODO ???
|
||||
System.err.println("root:refresh");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetAccessibility getAccessibility() {
|
||||
return accessibility;
|
||||
|
|
|
@ -23,12 +23,18 @@ import agent.dbgeng.model.iface2.DbgModelTargetSession;
|
|||
import agent.dbgeng.model.iface2.DbgModelTargetSessionAttributes;
|
||||
import ghidra.dbg.target.schema.*;
|
||||
|
||||
@TargetObjectSchemaInfo(name = "SessionAttributes", elements = { //
|
||||
@TargetObjectSchemaInfo(
|
||||
name = "SessionAttributes",
|
||||
elements = { //
|
||||
@TargetElementType(type = Void.class) //
|
||||
}, attributes = { //
|
||||
@TargetAttributeType(name = "Machine", type = DbgModelTargetSessionAttributesMachineImpl.class, fixed = true), //
|
||||
},
|
||||
attributes = { //
|
||||
@TargetAttributeType(
|
||||
name = "Machine",
|
||||
type = DbgModelTargetSessionAttributesMachineImpl.class,
|
||||
fixed = true), //
|
||||
@TargetAttributeType(type = Void.class) //
|
||||
})
|
||||
})
|
||||
public class DbgModelTargetSessionAttributesImpl extends DbgModelTargetObjectImpl
|
||||
implements DbgModelTargetSessionAttributes {
|
||||
|
||||
|
@ -75,8 +81,8 @@ public class DbgModelTargetSessionAttributesImpl extends DbgModelTargetObjectImp
|
|||
*/
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
machineAttributes.refresh();
|
||||
public void refreshInternal() {
|
||||
machineAttributes.refreshInternal();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -60,15 +60,15 @@ public class DbgModelTargetSessionAttributesMachineImpl extends DbgModelTargetOb
|
|||
|
||||
@Override
|
||||
public void sessionAdded(DbgSession session, DbgCause cause) {
|
||||
refresh();
|
||||
refreshInternal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processAdded(DbgProcess process, DbgCause cause) {
|
||||
refresh();
|
||||
refreshInternal();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
public void refreshInternal() {
|
||||
DebugControl control = getManager().getControl();
|
||||
int processorType = control.getActualProcessorType();
|
||||
if (processorType < 0) {
|
||||
|
|
|
@ -16,20 +16,27 @@
|
|||
package agent.dbgmodel.model.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.jdom.JDOMException;
|
||||
|
||||
import agent.dbgeng.manager.impl.DbgManagerImpl;
|
||||
import agent.dbgeng.model.AbstractDbgModel;
|
||||
import agent.dbgeng.model.iface2.DbgModelTargetObject;
|
||||
import agent.dbgeng.model.iface2.DbgModelTargetSession;
|
||||
import agent.dbgmodel.manager.DbgManager2Impl;
|
||||
import ghidra.dbg.agent.AbstractTargetObject;
|
||||
import ghidra.dbg.agent.AbstractTargetObject.ProxyFactory;
|
||||
import ghidra.dbg.agent.SpiTargetObject;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.dbg.target.schema.XmlSchemaContext;
|
||||
import ghidra.program.model.address.*;
|
||||
import utilities.util.ProxyUtilities;
|
||||
|
||||
public class DbgModel2Impl extends AbstractDbgModel {
|
||||
public class DbgModel2Impl extends AbstractDbgModel
|
||||
implements ProxyFactory<List<Class<? extends TargetObject>>> {
|
||||
// TODO: Need some minimal memory modeling per architecture on the model/agent side.
|
||||
// The model must convert to and from Ghidra's address space names
|
||||
protected static final String SPACE_NAME = "ram";
|
||||
|
@ -66,6 +73,15 @@ public class DbgModel2Impl extends AbstractDbgModel {
|
|||
//System.out.println(XmlSchemaContext.serialize(SCHEMA_CTX));
|
||||
this.root = new DbgModel2TargetRootImpl(this, ROOT_SCHEMA);
|
||||
this.completedRoot = CompletableFuture.completedFuture(root);
|
||||
addModelRoot(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpiTargetObject createProxy(AbstractTargetObject<?> delegate,
|
||||
List<Class<? extends TargetObject>> mixins) {
|
||||
mixins.add(DbgModel2TargetProxy.class);
|
||||
return ProxyUtilities.composeOnDelegate(DbgModelTargetObject.class,
|
||||
(DbgModelTargetObject) delegate, mixins, DelegateDbgModel2TargetObject.LOOKUP);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -116,7 +132,7 @@ public class DbgModel2Impl extends AbstractDbgModel {
|
|||
public CompletableFuture<Void> close() {
|
||||
try {
|
||||
terminate();
|
||||
return CompletableFuture.completedFuture(null);
|
||||
return super.close();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
return CompletableFuture.failedFuture(t);
|
||||
|
|
|
@ -59,8 +59,6 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
|
|||
|
||||
protected String DBG_PROMPT = "(kd2)"; // Used by DbgModelTargetEnvironment
|
||||
|
||||
protected boolean fireAttributesChanged = false;
|
||||
|
||||
protected static String indexObject(ModelObject obj) {
|
||||
return obj.getSearchKey();
|
||||
}
|
||||
|
@ -84,6 +82,12 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
|
|||
super(model, parent, name, typeHint, schema);
|
||||
}
|
||||
|
||||
public <I> DbgModel2TargetObjectImpl(ProxyFactory<I> proxyFactory, I proxyInfo,
|
||||
AbstractDbgModel model, TargetObject parent, String name,
|
||||
String typeHint) {
|
||||
super(proxyFactory, proxyInfo, model, parent, name, typeHint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DbgModel2Impl getModel() {
|
||||
return (DbgModel2Impl) super.getModel();
|
||||
|
@ -127,7 +131,6 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
|
|||
|
||||
@Override
|
||||
public CompletableFuture<Void> requestAttributes(boolean refresh) {
|
||||
fireAttributesChanged = true;
|
||||
Map<String, Object> nmap = new HashMap<>();
|
||||
return requestNativeAttributes().thenCompose(map -> {
|
||||
synchronized (attributes) {
|
||||
|
@ -418,13 +421,10 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
|
|||
schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
|
||||
}
|
||||
doInvalidateAttributes(delta.removed, reason);
|
||||
if (parent == null && !delta.isEmpty()) {
|
||||
if (!delta.isEmpty()) {
|
||||
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||
return delta;
|
||||
}
|
||||
if (fireAttributesChanged && !delta.isEmpty()) {
|
||||
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
|
@ -439,14 +439,9 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
|
|||
schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
|
||||
}
|
||||
doInvalidateAttributes(delta.removed, reason);
|
||||
if (fireAttributesChanged && !delta.isEmpty()) {
|
||||
if (!delta.isEmpty()) {
|
||||
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean enforcesStrictSchema() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,7 +109,6 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot
|
|||
}
|
||||
if (doFire) {
|
||||
this.focus = sel;
|
||||
fireAttributesChanged = true;
|
||||
changeAttributes(List.of(), List.of(), Map.of( //
|
||||
TargetFocusScope.FOCUS_ATTRIBUTE_NAME, focus //
|
||||
), "Focus changed");
|
||||
|
@ -492,12 +491,6 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot
|
|||
});
|
||||
}
|
||||
|
||||
//@Override
|
||||
public void refresh() {
|
||||
// TODO ???
|
||||
System.err.println("root:refresh");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetAccessibility getAccessibility() {
|
||||
return accessibility;
|
||||
|
|
|
@ -28,13 +28,11 @@ import agent.dbgeng.model.iface2.*;
|
|||
import agent.dbgmodel.dbgmodel.main.ModelObject;
|
||||
import agent.dbgmodel.jna.dbgmodel.DbgModelNative.ModelObjectKind;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.util.datastruct.ListenerSet;
|
||||
import utilities.util.ProxyUtilities;
|
||||
|
||||
public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl implements //
|
||||
DbgModelTargetAccessConditioned<DelegateDbgModel2TargetObject>, //
|
||||
|
@ -160,7 +158,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
|||
mixins.add(mixin);
|
||||
}
|
||||
}
|
||||
return new DelegateDbgModel2TargetObject(model, parent, key, object, mixins).proxy;
|
||||
return new DelegateDbgModel2TargetObject(model, parent, key, object, mixins).getProxy();
|
||||
}
|
||||
|
||||
protected static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||
|
@ -170,8 +168,6 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
|||
protected final ProxyState state;
|
||||
protected final Cleanable cleanable;
|
||||
|
||||
private final DbgModelTargetObject proxy;
|
||||
|
||||
private boolean breakpointEnabled;
|
||||
private final ListenerSet<TargetBreakpointAction> breakpointActions =
|
||||
new ListenerSet<>(TargetBreakpointAction.class) {
|
||||
|
@ -189,15 +185,12 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
|||
|
||||
public DelegateDbgModel2TargetObject(DbgModel2Impl model, DbgModelTargetObject parent,
|
||||
String key, ModelObject modelObject, List<Class<? extends TargetObject>> mixins) {
|
||||
super(model, parent.getProxy(), key, getHintForObject(modelObject));
|
||||
super(model, mixins, model, parent.getProxy(), key, getHintForObject(modelObject));
|
||||
this.state = new ProxyState(model, modelObject);
|
||||
this.cleanable = CLEANER.register(this, state);
|
||||
|
||||
getManager().addStateListener(accessListener);
|
||||
|
||||
mixins.add(DbgModel2TargetProxy.class);
|
||||
this.proxy =
|
||||
ProxyUtilities.composeOnDelegate(DbgModelTargetObject.class, this, mixins, LOOKUP);
|
||||
if (proxy instanceof DbgEventsListener) {
|
||||
model.getManager().addEventsListener((DbgEventsListener) proxy);
|
||||
}
|
||||
|
@ -216,11 +209,6 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
|||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends TypedTargetObject<T>> T as(Class<T> iface) {
|
||||
return DebuggerObjectModel.requireIface(iface, proxy, getPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public CompletableFuture<? extends DelegateDbgModel2TargetObject> fetch() {
|
||||
|
@ -228,8 +216,8 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
|||
}
|
||||
|
||||
@Override
|
||||
public TargetObject getProxy() {
|
||||
return proxy;
|
||||
public DbgModelTargetObject getProxy() {
|
||||
return (DbgModelTargetObject) proxy;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
@ -311,7 +299,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
|||
return;
|
||||
}
|
||||
if (proxy instanceof DbgModelTargetRegister || proxy instanceof DbgModelTargetStackFrame) {
|
||||
DbgThread thread = proxy.getParentThread().getThread();
|
||||
DbgThread thread = getProxy().getParentThread().getThread();
|
||||
if (thread.equals(getManager().getEventThread())) {
|
||||
requestAttributes(true);
|
||||
}
|
||||
|
@ -354,6 +342,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DelegateDbgModel2TargetObject getDelegate() {
|
||||
return this;
|
||||
}
|
||||
|
@ -388,6 +377,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
|||
this.breakpointEnabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenerSet<TargetBreakpointAction> getActions() {
|
||||
return breakpointActions;
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
|
|||
this.completedSession = CompletableFuture.completedFuture(session);
|
||||
|
||||
gdb.addStateListener(gdbExitListener);
|
||||
addModelRoot(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -136,7 +137,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
|
|||
|
||||
public void terminate() throws IOException {
|
||||
listeners.fire.modelClosed(DebuggerModelClosedReason.NORMAL);
|
||||
session.invalidateSubtree("GDB is terminating");
|
||||
session.invalidateSubtree(session, "GDB is terminating");
|
||||
gdb.terminate();
|
||||
}
|
||||
|
||||
|
@ -154,7 +155,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
|
|||
public CompletableFuture<Void> close() {
|
||||
try {
|
||||
terminate();
|
||||
return AsyncUtils.NIL;
|
||||
return super.close();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
return CompletableFuture.failedFuture(t);
|
||||
|
|
|
@ -59,7 +59,7 @@ public class GdbModelTargetEnvironment
|
|||
VISIBLE_ENDIAN_ATTRIBUTE_NAME, endian,
|
||||
UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED),
|
||||
"Initialized");
|
||||
refresh();
|
||||
refreshInternal();
|
||||
}
|
||||
|
||||
protected CompletableFuture<Void> refreshArchitecture() {
|
||||
|
@ -156,7 +156,7 @@ public class GdbModelTargetEnvironment
|
|||
});
|
||||
}
|
||||
|
||||
protected CompletableFuture<Void> refresh() {
|
||||
protected CompletableFuture<Void> refreshInternal() {
|
||||
AsyncFence fence = new AsyncFence();
|
||||
fence.include(refreshArchitecture());
|
||||
fence.include(refreshOS());
|
||||
|
|
|
@ -220,9 +220,9 @@ public class GdbModelTargetInferior
|
|||
|
||||
protected CompletableFuture<Void> inferiorStarted(Long pid) {
|
||||
AsyncFence fence = new AsyncFence();
|
||||
fence.include(modules.refresh());
|
||||
fence.include(registers.refresh());
|
||||
fence.include(environment.refresh());
|
||||
fence.include(modules.refreshInternal());
|
||||
fence.include(registers.resync());
|
||||
fence.include(environment.refreshInternal());
|
||||
return fence.ready().thenAccept(__ -> {
|
||||
if (pid != null) {
|
||||
changeAttributes(List.of(), Map.of( //
|
||||
|
|
|
@ -29,12 +29,14 @@ import ghidra.dbg.target.TargetExecutionStateful;
|
|||
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
|
||||
import ghidra.dbg.target.schema.TargetAttributeType;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.datastruct.WeakValueHashMap;
|
||||
|
||||
@TargetObjectSchemaInfo(name = "InferiorContainer", attributes = {
|
||||
@TargetObjectSchemaInfo(
|
||||
name = "InferiorContainer",
|
||||
attributes = {
|
||||
@TargetAttributeType(type = Void.class)
|
||||
}, canonicalContainer = true)
|
||||
},
|
||||
canonicalContainer = true)
|
||||
public class GdbModelTargetInferiorContainer
|
||||
extends DefaultTargetObject<GdbModelTargetInferior, GdbModelTargetSession>
|
||||
implements GdbEventsListenerAdapter {
|
||||
|
@ -70,7 +72,7 @@ public class GdbModelTargetInferiorContainer
|
|||
" started " + inf.getExecutable() + " pid=" + inf.getPid(),
|
||||
List.of(inferior));
|
||||
}).exceptionally(ex -> {
|
||||
Msg.error(this, "Could not notify inferior started", ex);
|
||||
impl.reportError(this, "Could not notify inferior started", ex);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -32,9 +32,12 @@ import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
|
|||
import ghidra.lifecycle.Internal;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
@TargetObjectSchemaInfo(name = "ModuleContainer", attributes = {
|
||||
@TargetObjectSchemaInfo(
|
||||
name = "ModuleContainer",
|
||||
attributes = {
|
||||
@TargetAttributeType(type = Void.class)
|
||||
}, canonicalContainer = true)
|
||||
},
|
||||
canonicalContainer = true)
|
||||
public class GdbModelTargetModuleContainer
|
||||
extends DefaultTargetObject<GdbModelTargetModule, GdbModelTargetInferior>
|
||||
implements TargetModuleContainer<GdbModelTargetModuleContainer> {
|
||||
|
@ -118,7 +121,7 @@ public class GdbModelTargetModuleContainer
|
|||
return modulesByName.get(name);
|
||||
}
|
||||
|
||||
public CompletableFuture<?> refresh() {
|
||||
public CompletableFuture<?> refreshInternal() {
|
||||
if (!isObserved()) {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
|
|
|
@ -33,11 +33,14 @@ import ghidra.dbg.target.schema.*;
|
|||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
@TargetObjectSchemaInfo(name = "Session", elements = {
|
||||
@TargetObjectSchemaInfo(
|
||||
name = "Session",
|
||||
elements = {
|
||||
@TargetElementType(type = Void.class)
|
||||
}, attributes = {
|
||||
},
|
||||
attributes = {
|
||||
@TargetAttributeType(type = Void.class)
|
||||
})
|
||||
})
|
||||
public class GdbModelTargetSession extends DefaultTargetModelRoot implements //
|
||||
TargetAccessConditioned<GdbModelTargetSession>,
|
||||
TargetAttacher<GdbModelTargetSession>,
|
||||
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,27 +18,24 @@ package ghidra.dbg.gadp.client;
|
|||
import java.lang.annotation.Annotation;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.ref.Cleaner.Cleanable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
import ghidra.dbg.agent.DefaultTargetObject;
|
||||
import ghidra.dbg.gadp.GadpRegistry;
|
||||
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
|
||||
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.dbg.gadp.protocol.Gadp.EventNotification.EvtCase;
|
||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
||||
import ghidra.dbg.memory.CachedMemory;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.TargetAccessConditioned;
|
||||
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibility;
|
||||
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
|
||||
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.dbg.util.CollectionUtils.Delta;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.program.model.address.AddressSpace;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.datastruct.ListenerSet;
|
||||
|
@ -47,7 +44,9 @@ import utilities.util.ProxyUtilities;
|
|||
/**
|
||||
* This class is meant to be used as a delegate to a composed proxy
|
||||
*/
|
||||
public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
||||
public class DelegateGadpClientTargetObject
|
||||
extends DefaultTargetObject<GadpClientTargetObject, GadpClientTargetObject>
|
||||
implements GadpClientTargetObject {
|
||||
protected abstract static class GadpHandlerMap<A extends Annotation, K> {
|
||||
protected final Class<A> annotationType;
|
||||
protected final Class<?>[] paramClasses;
|
||||
|
@ -148,75 +147,38 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
|||
}
|
||||
}
|
||||
|
||||
protected static class ProxyState implements Runnable {
|
||||
protected final GadpClient client;
|
||||
protected final List<String> path;
|
||||
protected boolean valid = true;
|
||||
|
||||
public ProxyState(GadpClient client, List<String> path) {
|
||||
this.client = client;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
client.unsubscribe(path).exceptionally(e -> {
|
||||
Msg.error(this, "Could not unsubscribe from " + path + ": " + e);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||
protected static final Map<Set<Class<? extends TargetObject>>, GadpEventHandlerMap> EVENT_HANDLER_MAPS_BY_COMPOSITION =
|
||||
new HashMap<>();
|
||||
protected static final Map<Set<Class<? extends TargetObject>>, GadpAttributeChangeCallbackMap> ATTRIBUTE_CHANGE_CALLBACKS_MAPS_BY_COMPOSITION =
|
||||
new HashMap<>();
|
||||
|
||||
protected static GadpClientTargetObject makeModelProxy(GadpClient client, List<String> path,
|
||||
String typeHint, List<String> ifaceNames) {
|
||||
protected static GadpClientTargetObject makeModelProxy(GadpClient client,
|
||||
GadpClientTargetObject parent, String key, String typeHint, List<String> ifaceNames) {
|
||||
List<Class<? extends TargetObject>> ifaces = TargetObject.getInterfacesByName(ifaceNames);
|
||||
List<Class<? extends TargetObject>> mixins = GadpRegistry.getMixins(ifaces);
|
||||
return new DelegateGadpClientTargetObject(client, path, typeHint, ifaceNames, ifaces,
|
||||
mixins).proxy;
|
||||
TargetObjectSchema schema =
|
||||
parent == null ? client.getRootSchema() : parent.getSchema().getChildSchema(key);
|
||||
return new DelegateGadpClientTargetObject(client, parent, key, typeHint, schema, ifaceNames,
|
||||
ifaces, mixins).getProxy();
|
||||
}
|
||||
|
||||
protected final ProxyState state;
|
||||
protected final int hash;
|
||||
protected final Cleanable cleanable;
|
||||
|
||||
private final GadpClientTargetObject proxy;
|
||||
private TargetObjectSchema schema; // lazily evaluated
|
||||
private final String typeHint;
|
||||
private final GadpClient client;
|
||||
private final List<String> ifaceNames;
|
||||
private final List<Class<? extends TargetObject>> ifaces;
|
||||
private final GadpEventHandlerMap eventHandlers;
|
||||
private final GadpAttributeChangeCallbackMap attributeChangeCallbacks;
|
||||
protected final ListenerSet<TargetObjectListener> listeners;
|
||||
|
||||
// TODO: Use path element comparators?
|
||||
protected final Map<String, TargetObjectRef> elements = new TreeMap<>();
|
||||
// TODO: Use path element comparators?
|
||||
protected final Map<String, Object> attributes = new TreeMap<>();
|
||||
|
||||
protected Map<AddressSpace, CachedMemory> memCache = null; // Becomes active if this is a TargetMemory
|
||||
protected Map<String, byte[]> regCache = null; // Becomes active if this is a TargtRegisterBank
|
||||
protected ListenerSet<TargetBreakpointAction> actions = null; // Becomes active is this is a TargetBreakpointSpec
|
||||
|
||||
public DelegateGadpClientTargetObject(GadpClient client, List<String> path, String typeHint,
|
||||
List<String> ifaceNames, List<Class<? extends TargetObject>> ifaces,
|
||||
public DelegateGadpClientTargetObject(GadpClient client, GadpClientTargetObject parent,
|
||||
String key, String typeHint, TargetObjectSchema schema, List<String> ifaceNames,
|
||||
List<Class<? extends TargetObject>> ifaces,
|
||||
List<Class<? extends TargetObject>> mixins) {
|
||||
this.listeners = new ListenerSet<>(TargetObjectListener.class, client.getClientExecutor());
|
||||
this.state = new ProxyState(client, path);
|
||||
this.hash = computeHashCode();
|
||||
this.cleanable = GadpClient.CLEANER.register(this, state);
|
||||
|
||||
this.proxy = ProxyUtilities.composeOnDelegate(GadpClientTargetObject.class,
|
||||
this, mixins, MethodHandles.lookup());
|
||||
this.typeHint = typeHint;
|
||||
super(client, mixins, client, parent, key, typeHint, schema);
|
||||
this.client = client;
|
||||
this.ifaceNames = ifaceNames;
|
||||
this.ifaces = ifaces;
|
||||
|
||||
|
@ -229,48 +191,14 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
|||
GadpAttributeChangeCallbackMap::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return doEquals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<GADP TargetObject: '" + PathUtils.toString(getPath()) + "' via " +
|
||||
getModel().description + ">";
|
||||
}
|
||||
|
||||
@Override
|
||||
public GadpClient getModel() {
|
||||
return state.client;
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getProtocolID() {
|
||||
return state.path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPath() {
|
||||
return state.path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetObjectSchema getSchema() {
|
||||
if (schema == null) {
|
||||
schema = getModel().getRootSchema().getSuccessorSchema(getPath());
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeHint() {
|
||||
return typeHint;
|
||||
public GadpClientTargetObject getProxy() {
|
||||
return (GadpClientTargetObject) super.getProxy();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -279,104 +207,32 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends TargetObject>> getInterfaces() {
|
||||
public Collection<? extends Class<? extends TargetObject>> getInterfaces() {
|
||||
return ifaces;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return state.valid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, TargetObjectRef> getCachedElements() {
|
||||
synchronized (this.elements) {
|
||||
return Map.copyOf(elements);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ?> getCachedAttributes() {
|
||||
synchronized (this.attributes) {
|
||||
return Map.copyOf(attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCachedAttribute(String name) {
|
||||
synchronized (attributes) {
|
||||
return attributes.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
protected void putCachedProxy(String key, GadpClientTargetObject proxy) {
|
||||
if (PathUtils.isIndex(key)) {
|
||||
synchronized (elements) {
|
||||
elements.put(PathUtils.parseIndex(key), proxy);
|
||||
}
|
||||
}
|
||||
else {
|
||||
synchronized (attributes) {
|
||||
attributes.put(key, proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Optional<Object> cachedChild(String key) {
|
||||
/**
|
||||
* TODO: Object metadata which indicates whether the attributes/elements support
|
||||
* subscription (push notifications). Otherwise, if the parent is cached, GADP will assume
|
||||
* the server is sending updates. If the model actually requires pulling, the GADP client
|
||||
* will not know, and will instead use its (likely stale) cache.
|
||||
*/
|
||||
assert key != null;
|
||||
if (PathUtils.isIndex(key)) {
|
||||
/**
|
||||
* NOTE: I do not need to check the subscription level. The level has to do with
|
||||
* including object info. Having OBJECT level is sufficient to have up-to-date keys.
|
||||
*/
|
||||
synchronized (elements) {
|
||||
return Optional.ofNullable(elements.get(PathUtils.parseIndex(key)));
|
||||
}
|
||||
}
|
||||
assert PathUtils.isName(key);
|
||||
synchronized (attributes) {
|
||||
return Optional.ofNullable(attributes.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* The delegate has to override defaults which introspect on, or otherwise would leak "this".
|
||||
* "this" is the delegate; we must instead operate on the proxy.
|
||||
*/
|
||||
@Override
|
||||
public <T extends TypedTargetObject<T>> T as(Class<T> iface) {
|
||||
return DebuggerObjectModel.requireIface(iface, proxy, state.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<? extends TargetObject> fetch() {
|
||||
return CompletableFuture.completedFuture(proxy);
|
||||
return CompletableFuture.completedFuture(getProxy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> fetchAttribute(String name) {
|
||||
if (!PathUtils.isInvocation(name)) {
|
||||
return GadpClientTargetObject.super.fetchAttribute(name);
|
||||
}
|
||||
return state.client.fetchModelValue(PathUtils.extend(state.path, name));
|
||||
public CompletableFuture<Void> resync(boolean attributes, boolean elements) {
|
||||
return client.sendChecked(Gadp.ResyncRequest.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path))
|
||||
.setAttributes(attributes)
|
||||
.setElements(elements),
|
||||
Gadp.ResyncReply.getDefaultInstance()).thenApply(rep -> null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(TargetObjectListener l) {
|
||||
listeners.add(l);
|
||||
protected CompletableFuture<Void> requestAttributes(boolean refresh) {
|
||||
return resync(refresh, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(TargetObjectListener l) {
|
||||
listeners.remove(l);
|
||||
protected CompletableFuture<Void> requestElements(boolean refresh) {
|
||||
return resync(false, refresh);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -384,34 +240,25 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
|||
return this;
|
||||
}
|
||||
|
||||
public void updateWithInfo(Gadp.ModelObjectInfo info) {
|
||||
Map<String, TargetObjectRef> elements =
|
||||
GadpValueUtils.getElementMap(this, info.getElementIndexList());
|
||||
Map<String, Object> attributes =
|
||||
GadpValueUtils.getAttributeMap(this, info.getAttributeList());
|
||||
|
||||
Delta<TargetObjectRef, TargetObjectRef> deltaE = setElements(elements);
|
||||
Delta<Object, Object> deltaA = setAttributes(attributes);
|
||||
fireElementsChanged(deltaE);
|
||||
fireAttributesChanged(deltaA);
|
||||
}
|
||||
|
||||
public void updateWithDelta(Gadp.ModelObjectDelta delta) {
|
||||
Map<String, TargetObjectRef> elementsAdded =
|
||||
GadpValueUtils.getElementMap(this, delta.getIndexAddedList());
|
||||
public void updateWithDeltas(Gadp.ModelObjectDelta deltaE, Gadp.ModelObjectDelta deltaA) {
|
||||
Map<String, GadpClientTargetObject> elementsAdded =
|
||||
GadpValueUtils.getElementMap(this, deltaE.getAddedList());
|
||||
Map<String, Object> attributesAdded =
|
||||
GadpValueUtils.getAttributeMap(this, delta.getAttributeAddedList());
|
||||
GadpValueUtils.getAttributeMap(this, deltaA.getAddedList());
|
||||
|
||||
Delta<TargetObjectRef, TargetObjectRef> deltaE =
|
||||
updateElements(Delta.create(delta.getIndexRemovedList(), elementsAdded));
|
||||
Delta<Object, Object> deltaA =
|
||||
updateAttributes(Delta.create(delta.getAttributeRemovedList(), attributesAdded));
|
||||
fireElementsChanged(deltaE);
|
||||
fireAttributesChanged(deltaA);
|
||||
changeElements(deltaE.getRemovedList(), List.of(), elementsAdded, "Updated");
|
||||
Delta<?, ?> attrDelta =
|
||||
changeAttributes(deltaA.getRemovedList(), attributesAdded, "Updated");
|
||||
for (String name : attrDelta.getKeysRemoved()) {
|
||||
handleAttributeChange(name, null);
|
||||
}
|
||||
for (Map.Entry<String, ?> a : attrDelta.added.entrySet()) {
|
||||
handleAttributeChange(a.getKey(), a.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleEvent(Gadp.EventNotification notify) {
|
||||
eventHandlers.handle(proxy, notify.getEvtCase(), notify);
|
||||
eventHandlers.handle(getProxy(), notify.getEvtCase(), notify);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -437,61 +284,11 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
|||
* @param value the new value of the attribute
|
||||
*/
|
||||
protected void handleAttributeChange(String name, Object value) {
|
||||
attributeChangeCallbacks.handle(proxy, name, value);
|
||||
}
|
||||
|
||||
protected <U extends TargetObjectRef> Delta<TargetObjectRef, U> updateElements(
|
||||
Delta<TargetObjectRef, U> delta) {
|
||||
synchronized (this.elements) {
|
||||
return delta.apply(elements);
|
||||
}
|
||||
}
|
||||
|
||||
protected <U extends TargetObjectRef> Delta<TargetObjectRef, U> setElements(
|
||||
Map<String, U> elements) {
|
||||
synchronized (this.elements) {
|
||||
return Delta.computeAndSet(this.elements, elements, Delta.SAME);
|
||||
}
|
||||
}
|
||||
|
||||
protected <U> Delta<Object, U> updateAttributes(Delta<Object, U> delta) {
|
||||
synchronized (this.attributes) {
|
||||
return delta.apply(attributes, Delta.EQUAL);
|
||||
}
|
||||
}
|
||||
|
||||
protected <U> Delta<Object, U> setAttributes(Map<String, U> attributes) {
|
||||
synchronized (this.attributes) {
|
||||
return Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL);
|
||||
}
|
||||
}
|
||||
|
||||
protected void fireElementsChanged(Delta<?, ? extends TargetObjectRef> delta) {
|
||||
if (!delta.isEmpty()) {
|
||||
listeners.fire.elementsChanged(proxy, delta.getKeysRemoved(), delta.added);
|
||||
}
|
||||
}
|
||||
|
||||
protected void fireAttributesChanged(Delta<?, ?> delta) {
|
||||
if (!delta.isEmpty()) {
|
||||
listeners.fire.attributesChanged(proxy, delta.getKeysRemoved(), delta.added);
|
||||
for (Map.Entry<String, ?> a : delta.added.entrySet()) {
|
||||
handleAttributeChange(a.getKey(), a.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void doInvalidateSubtree(String reason) {
|
||||
state.client.invalidateSubtree(state.path, reason);
|
||||
}
|
||||
|
||||
protected void doInvalidate(String reason) {
|
||||
state.valid = false;
|
||||
listeners.fire.invalidated(proxy, reason);
|
||||
attributeChangeCallbacks.handle(getProxy(), name, value);
|
||||
}
|
||||
|
||||
protected void assertValid() {
|
||||
if (!state.valid) {
|
||||
if (!valid) {
|
||||
throw new IllegalStateException("Object is no longer valid: " + toString());
|
||||
}
|
||||
}
|
||||
|
@ -505,13 +302,13 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
|||
public synchronized CompletableFuture<Void> invalidateCaches() {
|
||||
assertValid();
|
||||
doClearCaches();
|
||||
return state.client.sendChecked(Gadp.CacheInvalidateRequest.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(state.path)),
|
||||
return client.sendChecked(Gadp.CacheInvalidateRequest.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path)),
|
||||
Gadp.CacheInvalidateReply.getDefaultInstance()).thenApply(rep -> null);
|
||||
}
|
||||
|
||||
protected synchronized CachedMemory getMemoryCache(AddressSpace space) {
|
||||
GadpClientTargetMemory memory = (GadpClientTargetMemory) proxy;
|
||||
GadpClientTargetMemory memory = (GadpClientTargetMemory) getProxy();
|
||||
if (memCache == null) {
|
||||
memCache = new HashMap<>();
|
||||
}
|
||||
|
@ -552,4 +349,10 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
|||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInvalidate(TargetObject branch, String reason) {
|
||||
client.removeProxy(path, reason);
|
||||
super.doInvalidate(branch, reason);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,11 @@ package ghidra.dbg.gadp.client;
|
|||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.Cleaner;
|
||||
import java.nio.channels.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.jdom.JDOMException;
|
||||
|
@ -33,29 +31,29 @@ import com.google.protobuf.Message;
|
|||
import com.google.protobuf.ProtocolStringList;
|
||||
|
||||
import ghidra.async.*;
|
||||
import ghidra.dbg.*;
|
||||
import ghidra.dbg.DebuggerModelClosedReason;
|
||||
import ghidra.dbg.agent.*;
|
||||
import ghidra.dbg.agent.AbstractTargetObject.ProxyFactory;
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
import ghidra.dbg.error.*;
|
||||
import ghidra.dbg.gadp.GadpVersion;
|
||||
import ghidra.dbg.gadp.error.*;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.dbg.gadp.protocol.Gadp.*;
|
||||
import ghidra.dbg.gadp.util.*;
|
||||
import ghidra.dbg.gadp.protocol.Gadp.ObjectCreatedEvent;
|
||||
import ghidra.dbg.gadp.protocol.Gadp.RootMessage;
|
||||
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
|
||||
import ghidra.dbg.gadp.util.ProtobufOneofByTypeHelper;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.TargetObject.TargetUpdateMode;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.dbg.target.schema.XmlSchemaContext;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.dbg.util.PathUtils.PathComparator;
|
||||
import ghidra.lifecycle.Internal;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.datastruct.ListenerSet;
|
||||
import ghidra.util.datastruct.WeakValueTreeMap;
|
||||
import ghidra.util.exception.DuplicateNameException;
|
||||
import utilities.util.ProxyUtilities;
|
||||
|
||||
public class GadpClient implements DebuggerObjectModel {
|
||||
protected static final Cleaner CLEANER = Cleaner.create();
|
||||
public class GadpClient extends AbstractDebuggerObjectModel
|
||||
implements ProxyFactory<List<Class<? extends TargetObject>>> {
|
||||
|
||||
protected static final int WARN_OUTSTANDING_REQUESTS = 10000;
|
||||
// TODO: More sophisticated cache management
|
||||
|
@ -264,6 +262,7 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
}
|
||||
|
||||
protected final String description;
|
||||
protected final AsynchronousByteChannel byteChannel;
|
||||
protected final AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> messageChannel;
|
||||
|
||||
protected AsyncReference<ChannelState, DebuggerModelClosedReason> channelState =
|
||||
|
@ -272,44 +271,32 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
protected XmlSchemaContext schemaContext;
|
||||
protected TargetObjectSchema rootSchema;
|
||||
|
||||
protected final Executor clientExecutor = Executors.newSingleThreadExecutor();
|
||||
protected final ListenerSet<DebuggerModelListener> listenersClient =
|
||||
new ListenerSet<>(DebuggerModelListener.class, clientExecutor);
|
||||
protected final TriConsumer<ChannelState, ChannelState, DebuggerModelClosedReason> listenerForChannelState =
|
||||
this::channelStateChanged;
|
||||
protected final MessagePairingCache messageMatcher = new MessagePairingCache();
|
||||
protected final AtomicInteger sequencer = new AtomicInteger();
|
||||
|
||||
protected final NavigableMap<List<String>, GadpClientTargetObject> modelProxies =
|
||||
new WeakValueTreeMap<>(PathComparator.KEYED);
|
||||
|
||||
/**
|
||||
* Forget all values and rely on getCachedValue instead. This lazy map will just de-dup pending
|
||||
* requests. Once received, the behavior depends on what we know about the parent object. If
|
||||
* nothing is known about the parent object, we assume we cannot cache.
|
||||
*/
|
||||
protected final AsyncLazyMap<List<String>, Object> valueRequests =
|
||||
new AsyncLazyMap<>(new HashMap<>(), p -> doRequestValue(p, false));
|
||||
protected final AsyncLazyMap<List<String>, GadpClientTargetObject> attrsRequests =
|
||||
new AsyncLazyMap<>(new HashMap<>(), p -> doRequestAttributes(p, false));
|
||||
protected final AsyncLazyMap<List<String>, GadpClientTargetObject> elemsRequests =
|
||||
new AsyncLazyMap<>(new HashMap<>(), p -> doRequestElements(p, false));
|
||||
protected final Map<List<String>, GadpClientTargetObject> modelProxies = new HashMap<>();
|
||||
|
||||
protected final GadpAddressFactory factory = new GadpAddressFactory();
|
||||
|
||||
{
|
||||
channelState.addChangeListener(listenerForChannelState);
|
||||
|
||||
valueRequests.forgetValues((p, v) -> true);
|
||||
elemsRequests.forgetValues(this::forgetElementsRequests);
|
||||
attrsRequests.forgetValues(this::forgetAttributesRequests);
|
||||
}
|
||||
|
||||
public GadpClient(String description, AsynchronousByteChannel channel) {
|
||||
this.description = description;
|
||||
this.byteChannel = channel;
|
||||
this.messageChannel = createMessageChannel(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpiTargetObject createProxy(AbstractTargetObject<?> delegate,
|
||||
List<Class<? extends TargetObject>> mixins) {
|
||||
return ProxyUtilities.composeOnDelegate(GadpClientTargetObject.class,
|
||||
(GadpClientTargetObject) delegate, mixins, GadpClientTargetObject.LOOKUP);
|
||||
}
|
||||
|
||||
protected AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> createMessageChannel(
|
||||
AsynchronousByteChannel channel) {
|
||||
return new AsyncProtobufMessageChannel<>(channel);
|
||||
|
@ -323,16 +310,16 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
protected void channelStateChanged(ChannelState old, ChannelState set,
|
||||
DebuggerModelClosedReason reason) {
|
||||
if (old == ChannelState.NEGOTIATING && set == ChannelState.ACTIVE) {
|
||||
listenersClient.fire.modelOpened();
|
||||
listeners.fire.modelOpened();
|
||||
}
|
||||
else if (old == ChannelState.ACTIVE && set == ChannelState.CLOSED) {
|
||||
listenersClient.fire.modelClosed(reason);
|
||||
listeners.fire.modelClosed(reason);
|
||||
List<GadpClientTargetObject> copy;
|
||||
synchronized (modelProxies) {
|
||||
synchronized (lock) {
|
||||
copy = List.copyOf(modelProxies.values());
|
||||
}
|
||||
for (GadpClientTargetObject proxy : copy) {
|
||||
proxy.getDelegate().doInvalidate("GADP Client disconnected");
|
||||
proxy.getDelegate().doInvalidate(root, "GADP Client disconnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -369,7 +356,10 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
|
||||
protected <M extends Message> CompletableFuture<M> sendChecked(Message.Builder req,
|
||||
M exampleRep) {
|
||||
return sendCommand(req).thenApply(msg -> require(exampleRep, checkError(msg)));
|
||||
return sendCommand(req)
|
||||
.thenApply(msg -> require(exampleRep, checkError(msg)));
|
||||
//.thenCompose(msg -> flushEvents().thenApply(__ -> msg));
|
||||
// messageMatcher.fulfill happens on clientExecutor already
|
||||
}
|
||||
|
||||
protected void receiveLoop() {
|
||||
|
@ -380,9 +370,7 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
}
|
||||
messageChannel.read(Gadp.RootMessage::parseFrom).handle(loop::consume);
|
||||
}, TypeSpec.cls(Gadp.RootMessage.class), (msg, loop) -> {
|
||||
loop.repeat(); // All async loop to continue while we process
|
||||
|
||||
try {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
Gadp.EventNotification notify =
|
||||
MSG_HELPER.expect(msg, Gadp.EventNotification.getDefaultInstance());
|
||||
if (notify != null) {
|
||||
|
@ -391,10 +379,11 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
else {
|
||||
messageMatcher.fulfill(msg.getSequence(), msg);
|
||||
}
|
||||
}
|
||||
catch (Throwable e) {
|
||||
Msg.error(this, "Error processing message: " + msg, e);
|
||||
}
|
||||
}, clientExecutor).exceptionally(ex -> {
|
||||
Msg.error(this, "Error processing message: ", ex);
|
||||
return null;
|
||||
});
|
||||
loop.repeat(); // All async loop to continue while we process
|
||||
}).exceptionally(exc -> {
|
||||
exc = AsyncUtils.unwrapThrowable(exc);
|
||||
if (exc instanceof NotYetConnectedException) {
|
||||
|
@ -409,6 +398,9 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
else if (exc instanceof CancelledKeyException) {
|
||||
Msg.error(this, "Channel key is cancelled. Probably closed");
|
||||
}
|
||||
else if (exc instanceof RejectedExecutionException) {
|
||||
Msg.trace(this, "Ignoring rejection", exc);
|
||||
}
|
||||
else {
|
||||
Msg.error(this, "Receive failed for an unknown reason", exc);
|
||||
}
|
||||
|
@ -418,19 +410,31 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
}
|
||||
|
||||
protected void processNotification(Gadp.EventNotification notify) {
|
||||
if (!byteChannel.isOpen()) {
|
||||
return;
|
||||
}
|
||||
ProtocolStringList path = notify.getPath().getEList();
|
||||
GadpClientTargetObject obj = getCachedProxy(path);
|
||||
if (obj == null) {
|
||||
if (!hasPendingRequest(path)) {
|
||||
/**
|
||||
* For pending, I guess we just miss the event. NB: If it was a model event, then
|
||||
* the pending subscribe reply ought to already reflect the update we're ignoring
|
||||
* here.
|
||||
*/
|
||||
Msg.error(this, "Server sent notification for non-cached object: " + notify);
|
||||
//Msg.debug(this, "Processing notification: " + path + " " + notify.getEvtCase());
|
||||
if (notify.hasObjectCreatedEvent()) {
|
||||
notify.getObjectCreatedEvent();
|
||||
// AbstractTargetObject invokes created event
|
||||
createProxy(path, notify.getObjectCreatedEvent());
|
||||
return;
|
||||
}
|
||||
if (notify.hasRootAddedEvent()) {
|
||||
if (!path.isEmpty()) {
|
||||
Msg.warn(this, "Server gave non-root path for root-added event: " +
|
||||
PathUtils.toString(path));
|
||||
}
|
||||
synchronized (lock) {
|
||||
addModelRoot(getProxy(List.of(), true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
GadpClientTargetObject obj = getProxy(path, true);
|
||||
if (obj == null) {
|
||||
return; // Error already logged
|
||||
}
|
||||
obj.getDelegate().handleEvent(notify);
|
||||
}
|
||||
|
||||
|
@ -439,16 +443,6 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
return description + " via GADP (" + channelState.get().name().toLowerCase() + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addModelListener(DebuggerModelListener listener) {
|
||||
listenersClient.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeModelListener(DebuggerModelListener listener) {
|
||||
listenersClient.remove(listener);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> connect() {
|
||||
Gadp.ConnectRequest.Builder req = GadpVersion.makeRequest();
|
||||
if (channelState.get() != ChannelState.INACTIVE) {
|
||||
|
@ -493,8 +487,13 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
public CompletableFuture<Void> close() {
|
||||
try {
|
||||
messageChannel.close();
|
||||
CompletableFuture.runAsync(() -> {
|
||||
channelState.set(ChannelState.CLOSED, DebuggerModelClosedReason.normal());
|
||||
return AsyncUtils.NIL;
|
||||
}, clientExecutor).exceptionally(ex -> {
|
||||
Msg.error("Problem upon firing channel state change", ex);
|
||||
return null;
|
||||
});
|
||||
return super.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
return CompletableFuture.failedFuture(e);
|
||||
|
@ -528,292 +527,47 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
});
|
||||
}
|
||||
|
||||
protected GadpClientTargetObject getCachedProxy(List<String> path) {
|
||||
synchronized (modelProxies) {
|
||||
protected GadpClientTargetObject getProxy(List<String> path, boolean internal) {
|
||||
synchronized (lock) {
|
||||
GadpClientTargetObject proxy = modelProxies.get(path);
|
||||
if (proxy == null && internal) {
|
||||
Msg.error(this,
|
||||
"Server referred to non-existent object at path: " + PathUtils.toString(path));
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
}
|
||||
|
||||
protected GadpClientTargetObject createProxy(List<String> path, ObjectCreatedEvent evt) {
|
||||
synchronized (lock) {
|
||||
if (modelProxies.containsKey(path)) {
|
||||
Msg.error(this, "Agent announced creation of an already-existing object: " +
|
||||
PathUtils.toString(path));
|
||||
return modelProxies.get(path);
|
||||
}
|
||||
}
|
||||
|
||||
protected GadpClientTargetObject removeCachedProxy(List<String> path) {
|
||||
synchronized (modelProxies) {
|
||||
return modelProxies.remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a cached value
|
||||
*
|
||||
* This first checks if the given path is a cached object and returns it if so. Otherwise, it
|
||||
* checks if the parent is cached and, if so, examines its cached children.
|
||||
*
|
||||
* Note, if {@code null} is returned, the cache has no knowledge of the given path; the server
|
||||
* must be queried. If the returned optional has no value, the cache knows the path does not
|
||||
* exist.
|
||||
*
|
||||
* @param path the path
|
||||
* @return an optional value, or {@code null} if not cached
|
||||
*/
|
||||
protected Optional<Object> getCachedValue(List<String> path) {
|
||||
GadpClientTargetObject proxy = getCachedProxy(path);
|
||||
if (proxy != null) {
|
||||
return Optional.of(proxy);
|
||||
}
|
||||
List<String> parentPath = PathUtils.parent(path);
|
||||
GadpClientTargetObject parent;
|
||||
if (parentPath == null) {
|
||||
return null;
|
||||
parent = null;
|
||||
}
|
||||
GadpClientTargetObject parent = getCachedProxy(parentPath);
|
||||
else {
|
||||
parent = getProxy(parentPath, true);
|
||||
if (parent == null) {
|
||||
return null;
|
||||
Msg.error(this, "Got object's created event before its parent's: " +
|
||||
PathUtils.toString(path));
|
||||
}
|
||||
Optional<Object> val = parent.getDelegate().cachedChild(PathUtils.getKey(path));
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
if (val.isEmpty()) {
|
||||
if (PathUtils.isInvocation(PathUtils.getKey(path))) {
|
||||
return null;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
Object v = val.get();
|
||||
/**
|
||||
* NOTE: val should not be a TargetObject, otherwise it should have hit via
|
||||
* getCachedProxy(path). If this is a TargetObject, it's because the proxy was created
|
||||
* between then and the call to cachedChild -- possible due to a race condition. In that
|
||||
* case, it seems harmless to just return it anyway.
|
||||
*/
|
||||
if (v instanceof TargetObject) {
|
||||
return val;
|
||||
}
|
||||
if (!(v instanceof TargetObjectRef)) {
|
||||
return val;
|
||||
}
|
||||
TargetObjectRef r = (TargetObjectRef) v;
|
||||
if (path.equals(r.getPath())) {
|
||||
// An TargetObject is expected, but we only have the placeholder cached
|
||||
return null;
|
||||
}
|
||||
// else a link, which we are not required to fetch
|
||||
return val;
|
||||
}
|
||||
|
||||
protected void cacheInParent(List<String> path, GadpClientTargetObject proxy) {
|
||||
List<String> parentPath = PathUtils.parent(path);
|
||||
if (parentPath == null) {
|
||||
return;
|
||||
}
|
||||
GadpClientTargetObject parent = modelProxies.get(parentPath);
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
parent.getDelegate().putCachedProxy(PathUtils.getKey(path), proxy);
|
||||
}
|
||||
|
||||
@Internal
|
||||
public GadpClientTargetObject getProxyForInfo(ModelObjectInfo info) {
|
||||
synchronized (modelProxies) {
|
||||
return modelProxies.computeIfAbsent(info.getPath().getEList(), path -> {
|
||||
GadpClientTargetObject proxy = DelegateGadpClientTargetObject.makeModelProxy(this,
|
||||
path, info.getTypeHint(), info.getInterfaceList());
|
||||
cacheInParent(path, proxy);
|
||||
parent, PathUtils.getKey(path), evt.getTypeHint(), evt.getInterfaceList());
|
||||
modelProxies.put(path, proxy);
|
||||
return proxy;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Internal
|
||||
public TargetObjectRef getProxyOrStub(List<String> path) {
|
||||
GadpClientTargetObject cached = getCachedProxy(path);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
protected void removeProxy(List<String> path, String reason) {
|
||||
synchronized (lock) {
|
||||
modelProxies.remove(path);
|
||||
}
|
||||
return new GadpClientTargetObjectStub(this, path);
|
||||
}
|
||||
|
||||
protected CompletableFuture<Void> unsubscribe(List<String> path) {
|
||||
AsyncFence fence = new AsyncFence();
|
||||
cleanRequests(path);
|
||||
fence.include(sendChecked(
|
||||
Gadp.SubscribeRequest.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path))
|
||||
.setSubscribe(false),
|
||||
Gadp.SubscribeReply.getDefaultInstance()));
|
||||
return fence.ready();
|
||||
}
|
||||
|
||||
protected void invalidateSubtree(List<String> path, String reason) {
|
||||
List<List<String>> pathsToInvalidate = new ArrayList<>();
|
||||
List<GadpClientTargetObject> proxiesToInvalidate;
|
||||
synchronized (modelProxies) {
|
||||
// keySet is the only one which isn't a copy.
|
||||
// TODO: Would rather iterate entries when AbstractWeakValueMap is fixed
|
||||
for (List<String> succPath : modelProxies.tailMap(path, true).keySet()) {
|
||||
if (!PathUtils.isAncestor(path, succPath)) {
|
||||
break;
|
||||
}
|
||||
pathsToInvalidate.add(succPath);
|
||||
}
|
||||
proxiesToInvalidate =
|
||||
pathsToInvalidate.stream().map(modelProxies::remove).collect(Collectors.toList());
|
||||
}
|
||||
for (GadpClientTargetObject proxy : proxiesToInvalidate) {
|
||||
proxy.getDelegate().doInvalidate(reason);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasPendingRequest(List<String> path) {
|
||||
return valueRequests.containsKey(path) || attrsRequests.containsKey(path) ||
|
||||
elemsRequests.containsKey(path);
|
||||
}
|
||||
|
||||
public boolean hasPendingAttributes(List<String> path) {
|
||||
return attrsRequests.containsKey(path);
|
||||
}
|
||||
|
||||
public boolean hasPendingElements(List<String> path) {
|
||||
return elemsRequests.containsKey(path);
|
||||
}
|
||||
|
||||
protected void cleanRequests(List<String> path) {
|
||||
valueRequests.forget(path);
|
||||
attrsRequests.forget(path);
|
||||
elemsRequests.forget(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
|
||||
return fetchModelObject(List.of());
|
||||
}
|
||||
|
||||
protected CompletableFuture<Object> doRequestValueWithObjectInfo(
|
||||
List<String> path, boolean refresh,
|
||||
boolean fetchElements, boolean refreshElements,
|
||||
boolean fetchAttributes, boolean refreshAttributes) {
|
||||
CompletableFuture<SubscribeReply> reply = sendChecked(Gadp.SubscribeRequest.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path))
|
||||
.setSubscribe(true)
|
||||
.setRefresh(refresh)
|
||||
.setFetchElements(fetchElements)
|
||||
.setRefreshElements(refreshElements)
|
||||
.setFetchAttributes(fetchAttributes)
|
||||
.setRefreshAttributes(refreshAttributes),
|
||||
Gadp.SubscribeReply.getDefaultInstance());
|
||||
return reply.thenApplyAsync(rep -> { // Async to avoid processing info with a lock
|
||||
Gadp.Value value = rep.getValue();
|
||||
if (value.getSpecCase() == Gadp.Value.SpecCase.OBJECT_STUB) {
|
||||
Msg.error(this, "Server responded to object request with a stub!");
|
||||
return null;
|
||||
}
|
||||
if (value.getSpecCase() != Gadp.Value.SpecCase.OBJECT_INFO) {
|
||||
return GadpValueUtils.getValue(this, path, value);
|
||||
}
|
||||
Gadp.ModelObjectInfo info = value.getObjectInfo();
|
||||
GadpClientTargetObject proxy = getProxyForInfo(info);
|
||||
proxy.getDelegate().updateWithInfo(info);
|
||||
return proxy;
|
||||
}, AsyncUtils.FRAMEWORK_EXECUTOR);
|
||||
}
|
||||
|
||||
protected CompletableFuture<GadpClientTargetObject> doRequestElements(List<String> path,
|
||||
boolean refresh) {
|
||||
return doRequestValueWithObjectInfo(path, false, true, refresh, false, false)
|
||||
.thenApply(GadpClient::targetObjectOrNull);
|
||||
}
|
||||
|
||||
protected CompletableFuture<GadpClientTargetObject> doRequestAttributes(List<String> path,
|
||||
boolean refresh) {
|
||||
return doRequestValueWithObjectInfo(path, false, false, false, true, refresh)
|
||||
.thenApply(GadpClient::targetObjectOrNull);
|
||||
}
|
||||
|
||||
protected boolean forgetElementsRequests(List<String> path, GadpClientTargetObject proxy) {
|
||||
return proxy == null || !proxy.isValid() ||
|
||||
proxy.getUpdateMode() == TargetUpdateMode.SOLICITED;
|
||||
}
|
||||
|
||||
protected CompletableFuture<GadpClientTargetObject> checkProcessedElemsReply(List<String> path,
|
||||
boolean refresh) {
|
||||
if (refresh) { // NB: the map pre-tests the forget condition, too
|
||||
elemsRequests.forget(path);
|
||||
return elemsRequests.get(path, p -> doRequestElements(p, refresh));
|
||||
}
|
||||
return elemsRequests.get(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<? extends Map<String, ? extends TargetObjectRef>> fetchObjectElements(
|
||||
List<String> path, boolean refresh) {
|
||||
CompletableFuture<GadpClientTargetObject> processedReply =
|
||||
checkProcessedElemsReply(path, refresh);
|
||||
return processedReply.thenApply(proxy -> {
|
||||
if (proxy == null) { // The path doesn't exist, so return null, per the docs
|
||||
return null;
|
||||
}
|
||||
return proxy.getCachedElements();
|
||||
}).exceptionally(GadpClient::nullForNotExist);
|
||||
}
|
||||
|
||||
protected boolean forgetAttributesRequests(List<String> path, GadpClientTargetObject proxy) {
|
||||
return proxy == null || !proxy.isValid();
|
||||
}
|
||||
|
||||
protected CompletableFuture<GadpClientTargetObject> checkProcessedAttrsReply(List<String> path,
|
||||
boolean refresh) {
|
||||
if (refresh) {
|
||||
attrsRequests.forget(path);
|
||||
return attrsRequests.get(path, p -> doRequestAttributes(p, refresh));
|
||||
}
|
||||
return attrsRequests.get(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<? extends Map<String, ?>> fetchObjectAttributes(List<String> path,
|
||||
boolean refresh) {
|
||||
CompletableFuture<GadpClientTargetObject> processedReply =
|
||||
checkProcessedAttrsReply(path, refresh);
|
||||
return processedReply.thenApply(proxy -> {
|
||||
if (proxy == null) { // The path doesn't exist, so return null, per the docs
|
||||
return null;
|
||||
}
|
||||
return proxy.getCachedAttributes();
|
||||
}).exceptionally(GadpClient::nullForNotExist);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> fetchModelValue(List<String> path, boolean refresh) {
|
||||
/**
|
||||
* NB. Not sure there's value in checking for element/attribute requests on the parent. May
|
||||
* cull some requests, but the logic could get complicated. E.g., if we wait for it to
|
||||
* complete, and it comes back a TargetObjectRef, well, we have to fetch anyway. For
|
||||
* attributes, we'd also have to consider the case where it's absent, because it's a method
|
||||
* invocation.
|
||||
*/
|
||||
if (!refresh) {
|
||||
Optional<Object> cached = getCachedValue(path);
|
||||
if (cached != null) {
|
||||
Object val = cached.orElse(null);
|
||||
if (!(val instanceof TargetObjectRef)) {
|
||||
return CompletableFuture.completedFuture(val);
|
||||
}
|
||||
TargetObjectRef ref = (TargetObjectRef) val;
|
||||
TargetObject obj = GadpValueUtils.getTargetObjectNonLink(path, ref);
|
||||
if (obj != null) {
|
||||
return CompletableFuture.completedFuture(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
return valueRequests.get(path, p -> doRequestValue(p, refresh))
|
||||
.exceptionally(GadpClient::nullForNotExist);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> fetchModelValue(List<String> path) {
|
||||
return fetchModelValue(path, false);
|
||||
}
|
||||
|
||||
protected CompletableFuture<Object> doRequestValue(List<String> path, boolean refresh) {
|
||||
return doRequestValueWithObjectInfo(path, refresh, false, false, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -823,22 +577,28 @@ public class GadpClient implements DebuggerObjectModel {
|
|||
|
||||
@Override
|
||||
public TargetObjectRef createRef(List<String> path) {
|
||||
return getProxyOrStub(path);
|
||||
synchronized (lock) {
|
||||
GadpClientTargetObject proxy = modelProxies.get(path);
|
||||
if (proxy != null) {
|
||||
return proxy;
|
||||
}
|
||||
}
|
||||
return super.createRef(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetObject getModelObject(List<String> path) {
|
||||
return getProxy(path, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateAllLocalCaches() {
|
||||
List<GadpClientTargetObject> copy;
|
||||
synchronized (modelProxies) {
|
||||
synchronized (lock) {
|
||||
copy = List.copyOf(modelProxies.values());
|
||||
}
|
||||
for (GadpClientTargetObject proxy : copy) {
|
||||
proxy.getDelegate().doClearCaches();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Executor getClientExecutor() {
|
||||
return clientExecutor;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import ghidra.dbg.attributes.TypedTargetObjectRef;
|
|||
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.dbg.gadp.protocol.Gadp.Path;
|
||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
|
||||
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
|
||||
|
@ -64,19 +63,20 @@ public interface GadpClientTargetBreakpointContainer extends GadpClientTargetObj
|
|||
@GadpEventHandler(Gadp.EventNotification.EvtCase.BREAK_HIT_EVENT)
|
||||
default void handleBreakHitEvent(Gadp.EventNotification notification) {
|
||||
Gadp.BreakHitEvent evt = notification.getBreakHitEvent();
|
||||
TargetObjectRef trapped = getModel().getProxyOrStub(evt.getTrapped().getEList());
|
||||
TargetObjectRef trapped = getModel().getProxy(evt.getTrapped().getEList(), true);
|
||||
Path framePath = evt.getFrame();
|
||||
TypedTargetObjectRef<? extends TargetStackFrame<?>> frame =
|
||||
framePath == null || framePath.getECount() == 0 ? null
|
||||
: getModel().getProxyOrStub(framePath.getEList()).as(TargetStackFrame.tclass);
|
||||
: getModel().getProxy(framePath.getEList(), true).as(TargetStackFrame.tclass);
|
||||
Path specPath = evt.getSpec();
|
||||
TypedTargetObjectRef<? extends TargetBreakpointSpec<?>> spec = specPath == null ? null
|
||||
: getModel().getProxyOrStub(specPath.getEList()).as(TargetBreakpointSpec.tclass);
|
||||
: getModel().getProxy(specPath.getEList(), true).as(TargetBreakpointSpec.tclass);
|
||||
Path bptPath = evt.getEffective();
|
||||
TypedTargetObjectRef<? extends TargetBreakpointLocation<?>> breakpoint = bptPath == null
|
||||
? null
|
||||
: getModel().getProxyOrStub(bptPath.getEList()).as(TargetBreakpointLocation.tclass);
|
||||
getDelegate().listeners.fire(TargetBreakpointListener.class)
|
||||
: getModel().getProxy(bptPath.getEList(), true).as(TargetBreakpointLocation.tclass);
|
||||
getDelegate().getListeners()
|
||||
.fire(TargetBreakpointListener.class)
|
||||
.breakpointHit(this, trapped, frame, spec, breakpoint);
|
||||
if (spec instanceof GadpClientTargetBreakpointSpec) {
|
||||
// If I don't have a cached proxy, then I don't have any listeners
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -20,7 +20,6 @@ import java.util.List;
|
|||
import ghidra.dbg.attributes.TypedTargetObjectRef;
|
||||
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
||||
import ghidra.dbg.target.TargetEventScope;
|
||||
import ghidra.dbg.target.TargetThread;
|
||||
|
||||
|
@ -32,12 +31,13 @@ public interface GadpClientTargetEventScope
|
|||
Gadp.Path threadPath = evt.getEventThread();
|
||||
TypedTargetObjectRef<? extends TargetThread<?>> thread =
|
||||
threadPath == null || threadPath.getECount() == 0 ? null
|
||||
: getModel().getProxyOrStub(threadPath.getEList()).as(TargetThread.tclass);
|
||||
: getModel().getProxy(threadPath.getEList(), true).as(TargetThread.tclass);
|
||||
TargetEventType type = GadpValueUtils.getTargetEventType(evt.getType());
|
||||
String description = evt.getDescription();
|
||||
List<Object> parameters =
|
||||
GadpValueUtils.getValues(getModel(), evt.getParametersList());
|
||||
getDelegate().listeners.fire(TargetEventScopeListener.class)
|
||||
getDelegate().getListeners()
|
||||
.fire(TargetEventScopeListener.class)
|
||||
.event(this, thread, type, description, parameters);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -15,46 +15,36 @@
|
|||
*/
|
||||
package ghidra.dbg.gadp.client;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodHandles.Lookup;
|
||||
|
||||
import ghidra.dbg.agent.SpiTargetObject;
|
||||
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
|
||||
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.dbg.target.TargetConsole.Channel;
|
||||
import ghidra.dbg.target.TargetConsole.TargetConsoleListener;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.util.ValueUtils;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public interface GadpClientTargetObject extends TargetObject {
|
||||
public interface GadpClientTargetObject extends SpiTargetObject {
|
||||
Lookup LOOKUP = MethodHandles.lookup();
|
||||
|
||||
@Override
|
||||
GadpClient getModel();
|
||||
|
||||
@Override
|
||||
List<String> getProtocolID();
|
||||
|
||||
@Override
|
||||
String getTypeHint();
|
||||
|
||||
@Override
|
||||
Collection<String> getInterfaceNames();
|
||||
|
||||
@Override
|
||||
Collection<Class<? extends TargetObject>> getInterfaces();
|
||||
|
||||
DelegateGadpClientTargetObject getDelegate();
|
||||
|
||||
@GadpEventHandler(Gadp.EventNotification.EvtCase.MODEL_OBJECT_EVENT)
|
||||
default void handleModelObjectEvent(Gadp.EventNotification notification) {
|
||||
Gadp.ModelObjectEvent evt = notification.getModelObjectEvent();
|
||||
getDelegate().updateWithDelta(evt.getDelta());
|
||||
getDelegate().updateWithDeltas(evt.getElementDelta(), evt.getAttributeDelta());
|
||||
}
|
||||
|
||||
@GadpEventHandler(Gadp.EventNotification.EvtCase.OBJECT_INVALIDATE_EVENT)
|
||||
default void handleObjectInvalidateEvent(Gadp.EventNotification notification) {
|
||||
Gadp.ObjectInvalidateEvent evt = notification.getObjectInvalidateEvent();
|
||||
getDelegate().doInvalidateSubtree(evt.getReason());
|
||||
getDelegate().doInvalidate(this, evt.getReason());
|
||||
}
|
||||
|
||||
@GadpEventHandler(Gadp.EventNotification.EvtCase.CACHE_INVALIDATE_EVENT)
|
||||
|
@ -69,7 +59,7 @@ public interface GadpClientTargetObject extends TargetObject {
|
|||
|
||||
@GadpAttributeChangeCallback(DISPLAY_ATTRIBUTE_NAME)
|
||||
default void handleDisplayChanged(Object display) {
|
||||
getDelegate().listeners.fire.displayChanged(this, displayFromObj(display));
|
||||
getDelegate().getListeners().fire.displayChanged(this, displayFromObj(display));
|
||||
}
|
||||
|
||||
// TODO: It's odd to put this here.... I think it indicates a problem in the API
|
||||
|
@ -79,7 +69,8 @@ public interface GadpClientTargetObject extends TargetObject {
|
|||
int channelIndex = evt.getChannel();
|
||||
Channel[] allChannels = Channel.values();
|
||||
if (0 <= channelIndex && channelIndex < allChannels.length) {
|
||||
getDelegate().listeners.fire(TargetConsoleListener.class)
|
||||
getDelegate().getListeners()
|
||||
.fire(TargetConsoleListener.class)
|
||||
.consoleOutput(this, allChannels[channelIndex], evt.getData().toByteArray());
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.dbg.gadp.client;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
|
||||
public class GadpClientTargetObjectStub implements TargetObjectRef {
|
||||
protected final GadpClient client;
|
||||
protected final List<String> path;
|
||||
protected final int hash;
|
||||
|
||||
public GadpClientTargetObjectStub(GadpClient client, List<String> path) {
|
||||
this.client = client;
|
||||
this.path = path;
|
||||
this.hash = computeHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return doEquals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GadpClient getModel() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<Ref(GADP Stub) to " + path + " in " + client + ">";
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ import java.util.concurrent.CompletableFuture;
|
|||
|
||||
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
||||
import ghidra.dbg.target.TargetRegisterBank;
|
||||
|
||||
public interface GadpClientTargetRegisterBank
|
||||
|
@ -84,6 +83,8 @@ public interface GadpClientTargetRegisterBank
|
|||
Map<String, byte[]> updates = GadpValueUtils.getRegisterValueMap(evt.getValueList());
|
||||
DelegateGadpClientTargetObject delegate = getDelegate();
|
||||
delegate.getRegisterCache().putAll(updates);
|
||||
delegate.listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, updates);
|
||||
delegate.getListeners()
|
||||
.fire(TargetRegisterBankListener.class)
|
||||
.registersUpdated(this, updates);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.dbg.gadp.util;
|
||||
package ghidra.dbg.gadp.client;
|
||||
|
||||
import static ghidra.lifecycle.Unfinished.*;
|
||||
import static ghidra.lifecycle.Unfinished.TODO;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
@ -26,11 +26,8 @@ import com.google.protobuf.ByteString;
|
|||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.attributes.*;
|
||||
import ghidra.dbg.attributes.TargetObjectRefList.DefaultTargetObjectRefList;
|
||||
import ghidra.dbg.gadp.GadpRegistry;
|
||||
import ghidra.dbg.gadp.client.GadpClient;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.dbg.gadp.protocol.Gadp.ModelObjectDelta;
|
||||
import ghidra.dbg.gadp.protocol.Gadp.ModelObjectInfo;
|
||||
import ghidra.dbg.target.TargetAttacher.TargetAttachKind;
|
||||
import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet;
|
||||
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet;
|
||||
|
@ -309,28 +306,22 @@ public enum GadpValueUtils {
|
|||
.build();
|
||||
}
|
||||
|
||||
public static Gadp.ModelObjectInfo makeInfo(TargetObject obj) {
|
||||
ModelObjectInfo.Builder builder = Gadp.ModelObjectInfo.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(obj.getPath()))
|
||||
.setTypeHint(obj.getTypeHint())
|
||||
.addAllInterface(GadpRegistry.getInterfaceNames(obj));
|
||||
|
||||
builder.addAllElementIndex(obj.getCachedElements().keySet());
|
||||
for (Entry<String, ?> ent : obj.getCachedAttributes().entrySet()) {
|
||||
builder.addAttribute(makeAttribute(obj, ent));
|
||||
public static Gadp.ModelObjectDelta makeElementDelta(List<String> parentPath,
|
||||
Delta<?, ?> delta) {
|
||||
ModelObjectDelta.Builder builder = Gadp.ModelObjectDelta.newBuilder()
|
||||
.addAllRemoved(delta.getKeysRemoved());
|
||||
for (Entry<String, ?> ent : delta.added.entrySet()) {
|
||||
builder.addAdded(makeIndexedValue(parentPath, ent));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Gadp.ModelObjectDelta makeDelta(TargetObject parent,
|
||||
Delta<?, ? extends TargetObjectRef> deltaE, Delta<?, ?> deltaA) {
|
||||
public static Gadp.ModelObjectDelta makeAttributeDelta(List<String> parentPath,
|
||||
Delta<?, ?> delta) {
|
||||
ModelObjectDelta.Builder builder = Gadp.ModelObjectDelta.newBuilder()
|
||||
.addAllIndexRemoved(deltaE.getKeysRemoved())
|
||||
.addAllIndexAdded(deltaE.added.keySet())
|
||||
.addAllAttributeRemoved(deltaA.getKeysRemoved());
|
||||
for (Entry<String, ?> ent : deltaA.added.entrySet()) {
|
||||
builder.addAttributeAdded(makeAttribute(parent, ent));
|
||||
.addAllRemoved(delta.getKeysRemoved());
|
||||
for (Entry<String, ?> ent : delta.added.entrySet()) {
|
||||
builder.addAdded(makeNamedValue(parentPath, ent));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
@ -649,18 +640,25 @@ public enum GadpValueUtils {
|
|||
return b.build();
|
||||
}
|
||||
|
||||
public static Gadp.Argument makeArgument(Map.Entry<String, ?> argument) {
|
||||
return Gadp.Argument.newBuilder()
|
||||
.setName(argument.getKey())
|
||||
.setValue(makeValue(null, argument.getValue()))
|
||||
public static Gadp.NamedValue makeNamedValue(Map.Entry<String, ?> ent) {
|
||||
return makeNamedValue(null, ent);
|
||||
}
|
||||
|
||||
public static Gadp.NamedValue makeNamedValue(List<String> parentPath,
|
||||
Map.Entry<String, ?> ent) {
|
||||
List<String> path = parentPath == null ? null : PathUtils.extend(parentPath, ent.getKey());
|
||||
return Gadp.NamedValue.newBuilder()
|
||||
.setName(ent.getKey())
|
||||
.setValue(makeValue(path, ent.getValue()))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Gadp.Attribute makeAttribute(TargetObject parent, Map.Entry<String, ?> ent) {
|
||||
return Gadp.Attribute.newBuilder()
|
||||
public static Gadp.NamedValue makeIndexedValue(List<String> parentPath,
|
||||
Map.Entry<String, ?> ent) {
|
||||
List<String> path = parentPath == null ? null : PathUtils.index(parentPath, ent.getKey());
|
||||
return Gadp.NamedValue.newBuilder()
|
||||
.setName(ent.getKey())
|
||||
.setValue(
|
||||
makeValue(PathUtils.extend(parent.getPath(), ent.getKey()), ent.getValue()))
|
||||
.setValue(makeValue(path, ent.getValue()))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -712,14 +710,11 @@ public enum GadpValueUtils {
|
|||
case PARAMETERS_VALUE:
|
||||
return getParameters(model, value.getParametersValue());
|
||||
case PATH_VALUE:
|
||||
return model.createRef(value.getPathValue().getEList());
|
||||
return model.getModelObject(value.getPathValue().getEList());
|
||||
case PATH_LIST_VALUE:
|
||||
return getRefList(model, value.getPathListValue());
|
||||
case OBJECT_INFO:
|
||||
Msg.error(GadpValueUtils.class, "ObjectInfo requires special treatment:" + value);
|
||||
return model.createRef(path);
|
||||
case OBJECT_STUB:
|
||||
return model.createRef(path);
|
||||
return model.getModelObject(path);
|
||||
case TYPE_VALUE:
|
||||
return getValueType(value.getTypeValue());
|
||||
case SPEC_NOT_SET:
|
||||
|
@ -730,17 +725,28 @@ public enum GadpValueUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static Object getAttributeValue(TargetObject object, Gadp.Attribute attr) {
|
||||
public static Object getAttributeValue(GadpClientTargetObject object, Gadp.NamedValue attr) {
|
||||
return getValue(object.getModel(), PathUtils.extend(object.getPath(), attr.getName()),
|
||||
attr.getValue());
|
||||
}
|
||||
|
||||
public static Map<String, Object> getAttributeMap(TargetObject object,
|
||||
List<Gadp.Attribute> list) {
|
||||
public static GadpClientTargetObject getElementValue(GadpClientTargetObject object,
|
||||
Gadp.NamedValue elem) {
|
||||
Object value = getValue(object.getModel(),
|
||||
PathUtils.index(object.getPath(), elem.getName()), elem.getValue());
|
||||
if (!(value instanceof GadpClientTargetObject)) {
|
||||
Msg.error(GadpValueUtils.class, "Received non-object-valued element: " + elem);
|
||||
return null;
|
||||
}
|
||||
return (GadpClientTargetObject) value;
|
||||
}
|
||||
|
||||
public static Map<String, Object> getAttributeMap(GadpClientTargetObject object,
|
||||
List<Gadp.NamedValue> list) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (Gadp.Attribute attr : list) {
|
||||
if (result.put(attr.getName(),
|
||||
GadpValueUtils.getAttributeValue(object, attr)) != null) {
|
||||
for (Gadp.NamedValue attr : list) {
|
||||
Object val = GadpValueUtils.getAttributeValue(object, attr);
|
||||
if (result.put(attr.getName(), val) != null) {
|
||||
Msg.warn(GadpValueUtils.class, "Received duplicate attribute: " + attr);
|
||||
}
|
||||
}
|
||||
|
@ -756,25 +762,30 @@ public enum GadpValueUtils {
|
|||
return values.stream().map(v -> makeValue(null, v)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static List<Gadp.Argument> makeArguments(Map<String, ?> arguments) {
|
||||
public static List<Gadp.NamedValue> makeArguments(Map<String, ?> arguments) {
|
||||
return arguments.entrySet()
|
||||
.stream()
|
||||
.map(ent -> makeArgument(ent))
|
||||
.map(ent -> makeNamedValue(ent))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static Map<String, TargetObjectRef> getElementMap(TargetObject parent,
|
||||
List<String> indices) {
|
||||
Map<String, TargetObjectRef> result = new LinkedHashMap<>();
|
||||
for (String index : indices) {
|
||||
result.put(index,
|
||||
parent.getModel().createRef(PathUtils.index(parent.getPath(), index)));
|
||||
public static Map<String, GadpClientTargetObject> getElementMap(GadpClientTargetObject parent,
|
||||
List<Gadp.NamedValue> list) {
|
||||
Map<String, GadpClientTargetObject> result = new LinkedHashMap<>();
|
||||
for (Gadp.NamedValue elem : list) {
|
||||
GadpClientTargetObject val = GadpValueUtils.getElementValue(parent, elem);
|
||||
if (val == null) {
|
||||
continue;
|
||||
}
|
||||
if (result.put(elem.getName(), val) != null) {
|
||||
Msg.warn(GadpValueUtils.class, "Received duplicate element: " + elem);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Map<String, ?> getArguments(DebuggerObjectModel model,
|
||||
List<Gadp.Argument> arguments) {
|
||||
List<Gadp.NamedValue> arguments) {
|
||||
return arguments.stream()
|
||||
.collect(
|
||||
Collectors.toMap(a -> a.getName(), a -> getValue(model, null, a.getValue())));
|
|
@ -24,6 +24,7 @@ import ghidra.dbg.*;
|
|||
import ghidra.dbg.gadp.error.GadpErrorException;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public abstract class AbstractGadpServer
|
||||
extends AbstractAsyncServer<AbstractGadpServer, GadpClientHandler>
|
||||
|
@ -86,4 +87,13 @@ public abstract class AbstractGadpServer
|
|||
public void setExitOnClosed(boolean exitOnClosed) {
|
||||
this.exitOnClosed = exitOnClosed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() throws IOException {
|
||||
super.terminate();
|
||||
model.close().exceptionally(ex -> {
|
||||
Msg.error(this, "Problem closing GADP-served model", ex);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,40 +15,33 @@
|
|||
*/
|
||||
package ghidra.dbg.gadp.server;
|
||||
|
||||
import java.nio.channels.AsynchronousByteChannel;
|
||||
import java.nio.channels.AsynchronousSocketChannel;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.ProtocolStringList;
|
||||
|
||||
import ghidra.async.*;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.async.TypeSpec;
|
||||
import ghidra.comm.service.AbstractAsyncClientHandler;
|
||||
import ghidra.dbg.DebuggerModelListener;
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
import ghidra.dbg.attributes.TypedTargetObjectRef;
|
||||
import ghidra.dbg.error.*;
|
||||
import ghidra.dbg.gadp.GadpVersion;
|
||||
import ghidra.dbg.gadp.client.GadpClientTargetAttachable;
|
||||
import ghidra.dbg.gadp.client.GadpValueUtils;
|
||||
import ghidra.dbg.gadp.error.GadpErrorException;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.dbg.gadp.protocol.Gadp.ErrorCode;
|
||||
import ghidra.dbg.gadp.protocol.Gadp.ObjectInvalidateEvent;
|
||||
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
|
||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
|
||||
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointListener;
|
||||
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
|
||||
import ghidra.dbg.target.TargetConsole.Channel;
|
||||
import ghidra.dbg.target.TargetEventScope.TargetEventScopeListener;
|
||||
import ghidra.dbg.target.TargetConsole.TargetTextConsoleListener;
|
||||
import ghidra.dbg.target.TargetEventScope.TargetEventType;
|
||||
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionStateListener;
|
||||
import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener;
|
||||
import ghidra.dbg.target.TargetInterpreter.TargetInterpreterListener;
|
||||
import ghidra.dbg.target.TargetMemory.TargetMemoryListener;
|
||||
import ghidra.dbg.target.TargetObject.TargetObjectListener;
|
||||
import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.dbg.target.schema.XmlSchemaContext;
|
||||
import ghidra.dbg.util.CollectionUtils.Delta;
|
||||
|
@ -88,42 +81,81 @@ public class GadpClientHandler
|
|||
}
|
||||
}
|
||||
|
||||
protected class ListenerForEvents
|
||||
implements TargetObjectListener, TargetAccessibilityListener, TargetBreakpointListener,
|
||||
TargetEventScopeListener, TargetExecutionStateListener, TargetFocusScopeListener,
|
||||
TargetInterpreterListener, TargetMemoryListener, TargetRegisterBankListener {
|
||||
protected class ListenerForEvents implements DebuggerModelListener, TargetTextConsoleListener {
|
||||
@Override
|
||||
public void created(TargetObject object) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(object.getPath()))
|
||||
.setObjectCreatedEvent(Gadp.ObjectCreatedEvent.newBuilder()
|
||||
.setTypeHint(object.getTypeHint())
|
||||
.addAllInterface(object.getInterfaceNames())))
|
||||
.build()).exceptionally(GadpClientHandler::errorSendNotify);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rootAdded(TargetObject root) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
.setRootAddedEvent(Gadp.RootAddedEvent.getDefaultInstance()))
|
||||
.build()).exceptionally(GadpClientHandler::errorSendNotify);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attributesChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ?> added) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
// TODO: Can elements and attributes be combined into one message?
|
||||
sendDelta(parent, Delta.empty(), Delta.create(removed, added))
|
||||
if (removed.isEmpty() && added.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sendDelta(parent.getPath(), Delta.empty(), Delta.create(removed, added))
|
||||
.exceptionally(GadpClientHandler::errorSendNotify);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void elementsChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ? extends TargetObjectRef> added) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
// TODO: Can elements and attributes be combined into one message?
|
||||
sendDelta(parent, Delta.create(removed, added), Delta.empty())
|
||||
if (removed.isEmpty() && added.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sendDelta(parent.getPath(), Delta.create(removed, added), Delta.empty())
|
||||
.exceptionally(GadpClientHandler::errorSendNotify);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(TargetObject object, String reason) {
|
||||
if (!unsubscribeSubtree(object)) {
|
||||
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
if (object != branch) {
|
||||
return;
|
||||
}
|
||||
channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(object.getPath()))
|
||||
.setObjectInvalidateEvent(
|
||||
ObjectInvalidateEvent.newBuilder().setReason(reason)))
|
||||
Gadp.ObjectInvalidateEvent.newBuilder().setReason(reason)))
|
||||
.build()).exceptionally(GadpClientHandler::errorSendNotify);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consoleOutput(TargetObject console, Channel c, byte[] data) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
if (c == null || console == null) {
|
||||
Msg.warn(this, "Why is console or channel null in consoleOutput callback?");
|
||||
return;
|
||||
|
@ -139,6 +171,9 @@ public class GadpClientHandler
|
|||
|
||||
@Override
|
||||
public void consoleOutput(TargetObject console, Channel c, String out) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
consoleOutput(console, c, out.getBytes(TargetConsole.CHARSET));
|
||||
}
|
||||
|
||||
|
@ -147,6 +182,9 @@ public class GadpClientHandler
|
|||
TypedTargetObjectRef<? extends TargetStackFrame<?>> frame,
|
||||
TypedTargetObjectRef<? extends TargetBreakpointSpec<?>> spec,
|
||||
TypedTargetObjectRef<? extends TargetBreakpointLocation<?>> breakpoint) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
Gadp.BreakHitEvent.Builder evt = Gadp.BreakHitEvent.newBuilder()
|
||||
.setTrapped(GadpValueUtils.makePath(trapped.getPath()))
|
||||
.setSpec(GadpValueUtils.makePath(spec.getPath()))
|
||||
|
@ -163,6 +201,9 @@ public class GadpClientHandler
|
|||
|
||||
@Override
|
||||
public void invalidateCacheRequested(TargetObject object) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(object.getPath()))
|
||||
|
@ -174,6 +215,9 @@ public class GadpClientHandler
|
|||
@Override
|
||||
public void memoryReadError(TargetMemory<?> memory, AddressRange range,
|
||||
DebuggerMemoryAccessException e) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
// TODO: Ignore those generated by this client
|
||||
channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
|
@ -186,6 +230,9 @@ public class GadpClientHandler
|
|||
|
||||
@Override
|
||||
public void memoryUpdated(TargetMemory<?> memory, Address address, byte[] data) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
// TODO: Ignore those generated by this client
|
||||
channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
|
@ -198,6 +245,9 @@ public class GadpClientHandler
|
|||
|
||||
@Override
|
||||
public void registersUpdated(TargetRegisterBank<?> bank, Map<String, byte[]> updates) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
// TODO: Ignore those generated by this client
|
||||
channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
|
@ -211,6 +261,9 @@ public class GadpClientHandler
|
|||
public void event(TargetEventScope<?> object,
|
||||
TypedTargetObjectRef<? extends TargetThread<?>> eventThread, TargetEventType type,
|
||||
String description, List<Object> parameters) {
|
||||
if (!sock.isOpen()) {
|
||||
return;
|
||||
}
|
||||
Gadp.TargetEvent.Builder evt = Gadp.TargetEvent.newBuilder();
|
||||
if (eventThread != null) {
|
||||
evt.setEventThread(GadpValueUtils.makePath(eventThread.getPath()));
|
||||
|
@ -235,12 +288,16 @@ public class GadpClientHandler
|
|||
protected final AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> channel;
|
||||
protected final ListenerForEvents listenerForEvents = new ListenerForEvents();
|
||||
// Keeps strong references and tells level of subscription
|
||||
protected final NavigableSet<TargetObject> subscriptions = new TreeSet<>();
|
||||
|
||||
public GadpClientHandler(AbstractGadpServer server, AsynchronousSocketChannel sock) {
|
||||
super(server, sock);
|
||||
model = server.model;
|
||||
channel = new AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage>(sock);
|
||||
channel = createMessageChannel(sock);
|
||||
}
|
||||
|
||||
protected AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> createMessageChannel(
|
||||
AsynchronousByteChannel byteChannel) {
|
||||
return new AsyncProtobufMessageChannel<>(byteChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -256,6 +313,7 @@ public class GadpClientHandler
|
|||
loop.repeat();
|
||||
try {
|
||||
processMessage(msg).exceptionally(e -> {
|
||||
e.printStackTrace();
|
||||
replyError(msg, e).exceptionally(ee -> {
|
||||
Msg.error(this, "Could not send error reply: " + ee);
|
||||
return null;
|
||||
|
@ -346,6 +404,8 @@ public class GadpClientHandler
|
|||
return processFocus(msg.getSequence(), msg.getFocusRequest());
|
||||
case INTERRUPT_REQUEST:
|
||||
return processInterrupt(msg.getSequence(), msg.getInterruptRequest());
|
||||
case INVOKE_REQUEST:
|
||||
return processInvoke(msg.getSequence(), msg.getInvokeRequest());
|
||||
case KILL_REQUEST:
|
||||
return processKill(msg.getSequence(), msg.getKillRequest());
|
||||
case LAUNCH_REQUEST:
|
||||
|
@ -358,14 +418,12 @@ public class GadpClientHandler
|
|||
return processRegisterRead(msg.getSequence(), msg.getRegisterReadRequest());
|
||||
case REGISTER_WRITE_REQUEST:
|
||||
return processRegisterWrite(msg.getSequence(), msg.getRegisterWriteRequest());
|
||||
case RESYNC_REQUEST:
|
||||
return processResync(msg.getSequence(), msg.getResyncRequest());
|
||||
case RESUME_REQUEST:
|
||||
return processResume(msg.getSequence(), msg.getResumeRequest());
|
||||
case STEP_REQUEST:
|
||||
return processStep(msg.getSequence(), msg.getStepRequest());
|
||||
case SUBSCRIBE_REQUEST:
|
||||
return processSubscribe(msg.getSequence(), msg.getSubscribeRequest());
|
||||
case INVOKE_REQUEST:
|
||||
return processInvoke(msg.getSequence(), msg.getInvokeRequest());
|
||||
default:
|
||||
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
|
||||
"Unrecognized request: " + msg.getMsgCase());
|
||||
|
@ -379,13 +437,16 @@ public class GadpClientHandler
|
|||
"No listed version is supported");
|
||||
}
|
||||
TargetObjectSchema rootSchema = model.getRootSchema();
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
CompletableFuture<Integer> send = channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setConnectReply(Gadp.ConnectReply.newBuilder()
|
||||
.setVersion(ver)
|
||||
.setSchemaContext(XmlSchemaContext.serialize(rootSchema.getContext()))
|
||||
.setRootSchema(rootSchema.getName().toString()))
|
||||
.build());
|
||||
return send.thenAccept(__ -> {
|
||||
model.addModelListener(listenerForEvents, true);
|
||||
});
|
||||
}
|
||||
|
||||
protected CompletableFuture<?> processPing(int seqno, Gadp.PingRequest req) {
|
||||
|
@ -408,99 +469,27 @@ public class GadpClientHandler
|
|||
return ref;
|
||||
}
|
||||
|
||||
protected void changeSubscription(TargetObject obj, boolean subscribed) {
|
||||
synchronized (subscriptions) {
|
||||
if (subscribed) {
|
||||
if (subscriptions.add(obj)) {
|
||||
obj.addListener(listenerForEvents);
|
||||
}
|
||||
}
|
||||
else {
|
||||
subscriptions.remove(obj);
|
||||
obj.removeListener(listenerForEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isSubscribed(TargetObject obj) {
|
||||
synchronized (subscriptions) {
|
||||
return subscriptions.contains(obj);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean unsubscribeSubtree(TargetObject seed) {
|
||||
synchronized (subscriptions) {
|
||||
if (!subscriptions.remove(seed)) {
|
||||
return false;
|
||||
}
|
||||
Set<TargetObject> tail = subscriptions.tailSet(seed);
|
||||
for (Iterator<TargetObject> it = tail.iterator(); it.hasNext();) {
|
||||
TargetObject o = it.next();
|
||||
if (!PathUtils.isAncestor(seed.getPath(), o.getPath())) {
|
||||
break;
|
||||
}
|
||||
o.removeListener(listenerForEvents);
|
||||
it.remove();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected <T extends TargetObjectRef> CompletableFuture<Integer> sendDelta(TargetObject parent,
|
||||
Delta<TargetObjectRef, T> deltaE, Delta<?, ?> deltaA) {
|
||||
assert isSubscribed(parent); // If listening, we should be subscribed
|
||||
protected <T extends TargetObjectRef> CompletableFuture<Integer> sendDelta(
|
||||
List<String> parentPath, Delta<TargetObjectRef, T> deltaE, Delta<?, ?> deltaA) {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(parent.getPath()))
|
||||
.setPath(GadpValueUtils.makePath(parentPath))
|
||||
.setModelObjectEvent(Gadp.ModelObjectEvent.newBuilder()
|
||||
.setDelta(GadpValueUtils.makeDelta(parent, deltaE, deltaA))))
|
||||
.setElementDelta(
|
||||
GadpValueUtils.makeElementDelta(parentPath, deltaE))
|
||||
.setAttributeDelta(
|
||||
GadpValueUtils.makeElementDelta(parentPath, deltaA))))
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* @implNote To avoid duplicates and spurious messages, we adopt the following strategy: 1) Get
|
||||
* any elements and/or attributes for the object as requested. 2) Send the response
|
||||
* and install the listener on the object. 3) Check if the object's elements and/or
|
||||
* attributes have changed and send a delta immediately.
|
||||
*/
|
||||
protected CompletableFuture<?> processSubscribe(int seqno, Gadp.SubscribeRequest req) {
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelValue(path).thenCompose(val -> {
|
||||
DebuggerObjectModel.requireNonNull(val, path);
|
||||
TargetObject obj = GadpValueUtils.getTargetObjectNonLink(path, val);
|
||||
if (obj == null) {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setSubscribeReply(Gadp.SubscribeReply.newBuilder()
|
||||
.setValue(GadpValueUtils.makeValue(path, val)))
|
||||
.build());
|
||||
}
|
||||
|
||||
changeSubscription(obj, req.getSubscribe());
|
||||
AsyncFence fence = new AsyncFence();
|
||||
if (req.getFetchElements()) {
|
||||
fence.include(obj.fetchElements(req.getRefreshElements()));
|
||||
}
|
||||
if (req.getFetchAttributes()) {
|
||||
fence.include(obj.fetchAttributes(req.getRefreshAttributes()));
|
||||
}
|
||||
return fence.ready().thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setSubscribeReply(Gadp.SubscribeReply.newBuilder()
|
||||
.setValue(Gadp.Value.newBuilder()
|
||||
.setObjectInfo(GadpValueUtils.makeInfo(obj))))
|
||||
.build());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected CompletableFuture<?> processInvoke(int seqno, Gadp.InvokeRequest req) {
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetMethod<?> method =
|
||||
DebuggerObjectModel.requireIface(TargetMethod.class, obj, path);
|
||||
return method.invoke(GadpValueUtils.getArguments(model, req.getArgumentList()));
|
||||
}).thenCompose(result -> {
|
||||
return model.flushEvents().thenApply(__ -> result);
|
||||
}).thenCompose(result -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -510,8 +499,22 @@ public class GadpClientHandler
|
|||
});
|
||||
}
|
||||
|
||||
protected CompletableFuture<?> processResync(int seqno, Gadp.ResyncRequest req) {
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
return obj.resync(req.getAttributes(), req.getElements());
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setResyncReply(Gadp.ResyncReply.getDefaultInstance())
|
||||
.build());
|
||||
});
|
||||
}
|
||||
|
||||
protected CompletableFuture<?> processAttach(int seqno, Gadp.AttachRequest req) {
|
||||
ProtocolStringList path = req.getPath().getEList();
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetAttacher<?> attacher =
|
||||
DebuggerObjectModel.requireIface(TargetAttacher.class, obj, path);
|
||||
|
@ -527,6 +530,8 @@ public class GadpClientHandler
|
|||
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
|
||||
"Unrecognized attach specification:" + req);
|
||||
}
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -536,9 +541,10 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processBreakCreate(int seqno, Gadp.BreakCreateRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetBreakpointContainer<?> breaks = DebuggerObjectModel
|
||||
.requireIface(TargetBreakpointContainer.class, obj, req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetBreakpointContainer<?> breaks =
|
||||
DebuggerObjectModel.requireIface(TargetBreakpointContainer.class, obj, path);
|
||||
Set<TargetBreakpointKind> kinds = GadpValueUtils.getBreakKindSet(req.getKinds());
|
||||
switch (req.getSpecCase()) {
|
||||
case EXPRESSION:
|
||||
|
@ -550,6 +556,8 @@ public class GadpClientHandler
|
|||
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
|
||||
"Unrecognized breakpoint specification: " + req);
|
||||
}
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -559,10 +567,13 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processBreakToggle(int seqno, Gadp.BreakToggleRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetBreakpointSpec<?> spec = DebuggerObjectModel
|
||||
.requireIface(TargetBreakpointSpec.tclass, obj, req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetBreakpointSpec<?> spec =
|
||||
DebuggerObjectModel.requireIface(TargetBreakpointSpec.tclass, obj, path);
|
||||
return spec.toggle(req.getEnabled());
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -572,10 +583,13 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processDelete(int seqno, Gadp.DeleteRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetDeletable<?> del = DebuggerObjectModel.requireIface(TargetDeletable.tclass, obj,
|
||||
req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetDeletable<?> del =
|
||||
DebuggerObjectModel.requireIface(TargetDeletable.tclass, obj, path);
|
||||
return del.delete();
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -585,10 +599,13 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processDetach(int seqno, Gadp.DetachRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetDetachable<?> det = DebuggerObjectModel.requireIface(TargetDetachable.tclass, obj,
|
||||
req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetDetachable<?> det =
|
||||
DebuggerObjectModel.requireIface(TargetDetachable.tclass, obj, path);
|
||||
return det.detach();
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -598,13 +615,16 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processExecute(int seqno, Gadp.ExecuteRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetInterpreter<?> interpreter = DebuggerObjectModel
|
||||
.requireIface(TargetInterpreter.tclass, obj, req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetInterpreter<?> interpreter =
|
||||
DebuggerObjectModel.requireIface(TargetInterpreter.tclass, obj, path);
|
||||
if (req.getCapture()) {
|
||||
return interpreter.executeCapture(req.getCommand());
|
||||
}
|
||||
return interpreter.execute(req.getCommand()).thenApply(__ -> "");
|
||||
}).thenCompose(out -> {
|
||||
return model.flushEvents().thenApply(__ -> out);
|
||||
}).thenCompose(out -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -614,10 +634,13 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processFocus(int seqno, Gadp.FocusRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetFocusScope<?> scope = DebuggerObjectModel.requireIface(TargetFocusScope.tclass,
|
||||
obj, req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetFocusScope<?> scope =
|
||||
DebuggerObjectModel.requireIface(TargetFocusScope.tclass, obj, path);
|
||||
return scope.requestFocus(model.createRef(req.getFocus().getEList()));
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -627,10 +650,13 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processInterrupt(int seqno, Gadp.InterruptRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetInterruptible<?> interruptible = DebuggerObjectModel
|
||||
.requireIface(TargetInterruptible.tclass, obj, req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetInterruptible<?> interruptible =
|
||||
DebuggerObjectModel.requireIface(TargetInterruptible.tclass, obj, path);
|
||||
return interruptible.interrupt();
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -641,9 +667,11 @@ public class GadpClientHandler
|
|||
|
||||
protected CompletableFuture<?> processCacheInvalidate(int seqno,
|
||||
Gadp.CacheInvalidateRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
return DebuggerObjectModel.requireNonNull(obj, req.getPath().getEList())
|
||||
.invalidateCaches();
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
return DebuggerObjectModel.requireNonNull(obj, path).invalidateCaches();
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -653,10 +681,13 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processKill(int seqno, Gadp.KillRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetKillable<?> killable = DebuggerObjectModel.requireIface(TargetKillable.class, obj,
|
||||
req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetKillable<?> killable =
|
||||
DebuggerObjectModel.requireIface(TargetKillable.class, obj, path);
|
||||
return killable.kill();
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -666,11 +697,17 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processLaunch(int seqno, Gadp.LaunchRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetLauncher<?> launcher = DebuggerObjectModel.requireIface(TargetLauncher.class, obj,
|
||||
req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
Msg.debug(this, "Launching: " + Thread.currentThread());
|
||||
TargetLauncher<?> launcher =
|
||||
DebuggerObjectModel.requireIface(TargetLauncher.class, obj, path);
|
||||
return launcher.launch(GadpValueUtils.getArguments(model, req.getArgumentList()));
|
||||
}).thenCompose(__ -> {
|
||||
Msg.debug(this, "Flushing events after launch: " + Thread.currentThread());
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
Msg.debug(this, "Responding after launch: " + Thread.currentThread());
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setLaunchReply(Gadp.LaunchReply.getDefaultInstance())
|
||||
|
@ -679,11 +716,14 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processMemoryRead(int seqno, Gadp.MemoryReadRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetMemory<?> memory =
|
||||
DebuggerObjectModel.requireIface(TargetMemory.class, obj, req.getPath().getEList());
|
||||
DebuggerObjectModel.requireIface(TargetMemory.class, obj, path);
|
||||
AddressRange range = GadpValueUtils.getAddressRange(memory.getModel(), req.getRange());
|
||||
return memory.readMemory(range.getMinAddress(), (int) range.getLength());
|
||||
}).thenCompose(data -> {
|
||||
return model.flushEvents().thenApply(__ -> data);
|
||||
}).thenCompose(data -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -694,12 +734,15 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processMemoryWrite(int seqno, Gadp.MemoryWriteRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetMemory<?> memory =
|
||||
DebuggerObjectModel.requireIface(TargetMemory.class, obj, req.getPath().getEList());
|
||||
DebuggerObjectModel.requireIface(TargetMemory.class, obj, path);
|
||||
Address start = GadpValueUtils.getAddress(memory.getModel(), req.getStart());
|
||||
// TODO: Spare a copy by specifying a ByteBuffer variant of writeMemory?
|
||||
return memory.writeMemory(start, req.getContent().toByteArray());
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -709,10 +752,13 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processRegisterRead(int seqno, Gadp.RegisterReadRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetRegisterBank<?> bank = DebuggerObjectModel.requireIface(TargetRegisterBank.class,
|
||||
obj, req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetRegisterBank<?> bank =
|
||||
DebuggerObjectModel.requireIface(TargetRegisterBank.class, obj, path);
|
||||
return bank.readRegistersNamed(req.getNameList());
|
||||
}).thenCompose(data -> {
|
||||
return model.flushEvents().thenApply(__ -> data);
|
||||
}).thenCompose(data -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -723,14 +769,17 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processRegisterWrite(int seqno, Gadp.RegisterWriteRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetRegisterBank<?> bank = DebuggerObjectModel.requireIface(TargetRegisterBank.class,
|
||||
obj, req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetRegisterBank<?> bank =
|
||||
DebuggerObjectModel.requireIface(TargetRegisterBank.class, obj, path);
|
||||
Map<String, byte[]> values = new LinkedHashMap<>();
|
||||
for (Gadp.RegisterValue rv : req.getValueList()) {
|
||||
values.put(rv.getName(), rv.getContent().toByteArray());
|
||||
}
|
||||
return bank.writeRegistersNamed(values);
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -740,10 +789,13 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processResume(int seqno, Gadp.ResumeRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetResumable<?> resumable = DebuggerObjectModel.requireIface(TargetResumable.class,
|
||||
obj, req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetResumable<?> resumable =
|
||||
DebuggerObjectModel.requireIface(TargetResumable.class, obj, path);
|
||||
return resumable.resume();
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
@ -753,10 +805,13 @@ public class GadpClientHandler
|
|||
}
|
||||
|
||||
protected CompletableFuture<?> processStep(int seqno, Gadp.StepRequest req) {
|
||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
||||
TargetSteppable<?> steppable = DebuggerObjectModel.requireIface(TargetSteppable.class,
|
||||
obj, req.getPath().getEList());
|
||||
List<String> path = req.getPath().getEList();
|
||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||
TargetSteppable<?> steppable =
|
||||
DebuggerObjectModel.requireIface(TargetSteppable.class, obj, path);
|
||||
return steppable.step(GadpValueUtils.getStepKind(req.getKind()));
|
||||
}).thenCompose(__ -> {
|
||||
return model.flushEvents();
|
||||
}).thenCompose(__ -> {
|
||||
return channel.write(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
package ghidra.dbg.gadp;
|
||||
|
||||
import static ghidra.lifecycle.Unfinished.TODO;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -27,7 +26,6 @@ import java.util.*;
|
|||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Ignore;
|
||||
|
@ -39,21 +37,23 @@ import generic.ID;
|
|||
import generic.Unique;
|
||||
import ghidra.async.*;
|
||||
import ghidra.dbg.DebugModelConventions;
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.DebuggerModelListener;
|
||||
import ghidra.dbg.agent.*;
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
import ghidra.dbg.attributes.TargetStringList;
|
||||
import ghidra.dbg.gadp.GadpClientServerTest.EventListener.CallEntry;
|
||||
import ghidra.dbg.gadp.client.GadpClient;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.dbg.gadp.protocol.Gadp.RootMessage;
|
||||
import ghidra.dbg.gadp.server.AbstractGadpServer;
|
||||
import ghidra.dbg.gadp.server.GadpClientHandler;
|
||||
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener;
|
||||
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
||||
import ghidra.dbg.target.TargetObject.TargetObjectFetchingListener;
|
||||
import ghidra.dbg.target.TargetObject.TargetObjectListener;
|
||||
import ghidra.dbg.target.TargetObject.*;
|
||||
import ghidra.dbg.target.schema.TargetAttributeType;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
|
||||
import ghidra.dbg.util.*;
|
||||
|
@ -232,6 +232,48 @@ public class GadpClientServerTest {
|
|||
}
|
||||
}
|
||||
|
||||
public static class PrintingAsyncProtobufMessageChannel<//
|
||||
S extends GeneratedMessageV3, R extends GeneratedMessageV3>
|
||||
extends AsyncProtobufMessageChannel<S, R> {
|
||||
private final String sendPrefix;
|
||||
private final String recvPrefix;
|
||||
|
||||
public PrintingAsyncProtobufMessageChannel(String local, String peer,
|
||||
AsynchronousByteChannel channel) {
|
||||
super(channel);
|
||||
this.sendPrefix = local + "->" + peer + ": ";
|
||||
this.recvPrefix = local + "<-" + peer + ": ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Integer> write(S msg) {
|
||||
Msg.debug(this, sendPrefix + msg);
|
||||
return super.write(msg).thenApplyAsync(c -> {
|
||||
return c;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R2 extends R> CompletableFuture<R2> read(IOFunction<R2> receiver) {
|
||||
return super.read(receiver).thenApplyAsync(msg -> {
|
||||
Msg.debug(this, recvPrefix + msg);
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class PrintingGadpClient extends GadpClient {
|
||||
public PrintingGadpClient(String description, AsynchronousByteChannel channel) {
|
||||
super(description, channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> createMessageChannel(
|
||||
AsynchronousByteChannel channel) {
|
||||
return new PrintingAsyncProtobufMessageChannel<>("C", "S", channel);
|
||||
}
|
||||
}
|
||||
|
||||
protected static <T> T waitOn(CompletableFuture<T> future) throws Throwable {
|
||||
try {
|
||||
return future.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
|
||||
|
@ -251,12 +293,17 @@ public class GadpClientServerTest {
|
|||
protected final TestGadpTargetAvailableLinkContainer links =
|
||||
new TestGadpTargetAvailableLinkContainer(this);
|
||||
|
||||
public TestGadpTargetSession(FakeDebuggerObjectModel model) {
|
||||
public TestGadpTargetSession(TestGadpObjectModel model) {
|
||||
super(model, "Session");
|
||||
|
||||
changeAttributes(List.of(), List.of(available, processes), Map.of(), "Initialized");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDebuggerObjectModel getModel() {
|
||||
return super.getModel();
|
||||
}
|
||||
|
||||
@TargetAttributeType(name = "Available", required = true, fixed = true)
|
||||
public TestGadpTargetAvailableContainer getAvailable() {
|
||||
return available;
|
||||
|
@ -292,24 +339,14 @@ public class GadpClientServerTest {
|
|||
public class TestTargetObject<E extends TargetObject, P extends TargetObject>
|
||||
extends DefaultTargetObject<E, P> {
|
||||
|
||||
public TestTargetObject(DebuggerObjectModel model, P parent, String key, String typeHint) {
|
||||
public TestTargetObject(AbstractDebuggerObjectModel model, P parent, String key,
|
||||
String typeHint) {
|
||||
super(model, parent, key, typeHint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> fetchAttribute(String name) {
|
||||
if (!PathUtils.isInvocation(name)) {
|
||||
return super.fetchAttribute(name);
|
||||
}
|
||||
Map.Entry<String, String> invocation = PathUtils.parseInvocation(name);
|
||||
TestTargetMethod method =
|
||||
getTypedAttributeNowByName(invocation.getKey(), TestTargetMethod.class, null);
|
||||
if (method == null) {
|
||||
return AsyncUtils.nil();
|
||||
}
|
||||
Object ret = method.testInvoke(invocation.getValue());
|
||||
changeAttributes(List.of(), Map.of(name, ret), "Invoked " + name);
|
||||
return CompletableFuture.completedFuture(ret).thenCompose(model::gateFuture);
|
||||
public AbstractDebuggerObjectModel getModel() {
|
||||
return super.getModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -334,13 +371,11 @@ public class GadpClientServerTest {
|
|||
false, TargetStringList.of(), "Others to greet",
|
||||
"List of other people to greet individually")));
|
||||
|
||||
public class TestTargetMethod extends TestTargetObject<TargetObject, TargetObject>
|
||||
implements TargetMethod<TestTargetMethod> {
|
||||
private Function<String, ?> method;
|
||||
public class TestGadpTargetMethod extends TestTargetObject<TargetObject, TestTargetObject<?, ?>>
|
||||
implements TargetMethod<TestGadpTargetMethod> {
|
||||
|
||||
public TestTargetMethod(TargetObject parent, String key, Function<String, ?> method) {
|
||||
public TestGadpTargetMethod(TestTargetObject<?, ?> parent, String key) {
|
||||
super(parent.getModel(), parent, key, "Method");
|
||||
this.method = method;
|
||||
|
||||
setAttributes(Map.of(
|
||||
PARAMETERS_ATTRIBUTE_NAME, PARAMS,
|
||||
|
@ -348,16 +383,16 @@ public class GadpClientServerTest {
|
|||
"Initialized");
|
||||
}
|
||||
|
||||
public Object testInvoke(String paramsExpr) {
|
||||
return method.apply(paramsExpr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Object> invoke(Map<String, ?> arguments) {
|
||||
TestMethodInvocation invocation = new TestMethodInvocation(arguments);
|
||||
invocations.offer(invocation);
|
||||
invocationCount.set(invocations.size(), null);
|
||||
return invocation;
|
||||
return invocation.thenApply(obj -> {
|
||||
parent.changeAttributes(List.of(), Map.ofEntries(
|
||||
Map.entry("greet(" + arguments.get("arg") + ")", obj)),
|
||||
"greet() invoked");
|
||||
return obj;
|
||||
}).thenCompose(model::gateFuture);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -389,13 +424,12 @@ public class GadpClientServerTest {
|
|||
|
||||
public class TestGadpTargetAvailableContainer
|
||||
extends TestTargetObject<TestGadpTargetAvailable, TestGadpTargetSession> {
|
||||
public char punct = '!';
|
||||
|
||||
public TestGadpTargetAvailableContainer(TestGadpTargetSession session) {
|
||||
super(session.getModel(), session, "Available", "AvailableContainer");
|
||||
|
||||
setAttributes(List.of(
|
||||
new TestTargetMethod(this, "greet", p -> "Hello, " + p + punct)),
|
||||
new TestGadpTargetMethod(this, "greet")),
|
||||
Map.of(), "Initialized");
|
||||
}
|
||||
|
||||
|
@ -417,7 +451,7 @@ public class GadpClientServerTest {
|
|||
String cmd) {
|
||||
super(available.getModel(), available, PathUtils.makeKey(PathUtils.makeIndex(pid)),
|
||||
"Available");
|
||||
setAttributes(List.of(), Map.of(
|
||||
changeAttributes(List.of(), Map.of(
|
||||
"pid", pid,
|
||||
"cmd", cmd //
|
||||
), "Initialized");
|
||||
|
@ -435,38 +469,62 @@ public class GadpClientServerTest {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Refactor with other Fake and Test. Probably put in Framework-Debugging....
|
||||
public class FakeDebuggerObjectModel extends AbstractDebuggerObjectModel {
|
||||
private final TestGadpTargetSession session = new TestGadpTargetSession(this);
|
||||
|
||||
public class BlankObjectModel extends AbstractDebuggerObjectModel {
|
||||
private final AddressSpace ram =
|
||||
new GenericAddressSpace("RAM", 64, AddressSpace.TYPE_RAM, 0);
|
||||
private final AddressFactory factory =
|
||||
new DefaultAddressFactory(new AddressSpace[] { ram });
|
||||
|
||||
@Override
|
||||
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
|
||||
return CompletableFuture.completedFuture(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddressFactory getAddressFactory() {
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> close() {
|
||||
return AsyncUtils.NIL;
|
||||
// TODO: Refactor with other Fake and Test. Probably put in Framework-Debugging....
|
||||
public class TestGadpObjectModel extends BlankObjectModel {
|
||||
private TestGadpTargetSession session;
|
||||
|
||||
public TestGadpObjectModel(boolean createSession) {
|
||||
if (createSession) {
|
||||
session = new TestGadpTargetSession(this);
|
||||
addModelRoot(session);
|
||||
}
|
||||
}
|
||||
|
||||
@Override // file access
|
||||
protected void addModelRoot(SpiTargetObject root) {
|
||||
super.addModelRoot(root);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestGadpServer extends AbstractGadpServer {
|
||||
@SuppressWarnings("hiding")
|
||||
final FakeDebuggerObjectModel model;
|
||||
final TestGadpObjectModel model;
|
||||
|
||||
public TestGadpServer(SocketAddress addr) throws IOException {
|
||||
super(new FakeDebuggerObjectModel(), addr);
|
||||
this.model = (FakeDebuggerObjectModel) getModel();
|
||||
this(new TestGadpObjectModel(true), addr);
|
||||
}
|
||||
|
||||
public TestGadpServer(TestGadpObjectModel model, SocketAddress addr) throws IOException {
|
||||
super(model, addr);
|
||||
this.model = model;
|
||||
}
|
||||
}
|
||||
|
||||
public class PrintingTestGadpServer extends TestGadpServer {
|
||||
public PrintingTestGadpServer(SocketAddress addr) throws IOException {
|
||||
super(addr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GadpClientHandler newHandler(AsynchronousSocketChannel sock) {
|
||||
return new GadpClientHandler(this, sock) {
|
||||
@Override
|
||||
protected AsyncProtobufMessageChannel<RootMessage, RootMessage> createMessageChannel(
|
||||
AsynchronousByteChannel byteChannel) {
|
||||
return new PrintingAsyncProtobufMessageChannel<>("S", "C", byteChannel);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -474,16 +532,31 @@ public class GadpClientServerTest {
|
|||
TestGadpServer server;
|
||||
|
||||
public ServerRunner() throws IOException {
|
||||
server = new TestGadpServer(new InetSocketAddress("localhost", 0));
|
||||
server = createServer(new InetSocketAddress("localhost", 0));
|
||||
server.launchAsyncService();
|
||||
}
|
||||
|
||||
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
|
||||
return new TestGadpServer(addr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
server.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
public class PrintingServerRunner extends ServerRunner {
|
||||
public PrintingServerRunner() throws IOException {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
|
||||
return new PrintingTestGadpServer(addr);
|
||||
}
|
||||
}
|
||||
|
||||
class TestMethodInvocation extends CompletableFuture<Object> {
|
||||
final Map<String, ?> args;
|
||||
|
||||
|
@ -493,12 +566,41 @@ public class GadpClientServerTest {
|
|||
}
|
||||
|
||||
protected Deque<Map<String, ?>> launches = new LinkedList<>();
|
||||
protected AsyncReference<Integer, Void> invocationCount = new AsyncReference<>();
|
||||
protected Deque<TestMethodInvocation> invocations = new LinkedList<>();
|
||||
|
||||
protected static class AsyncDeque<T> {
|
||||
private final Deque<T> deque = new LinkedList<>();
|
||||
private final AsyncReference<Integer, Void> count = new AsyncReference<>(0);
|
||||
|
||||
public boolean offer(T e) {
|
||||
boolean result = deque.offer(e);
|
||||
count.set(deque.size(), null);
|
||||
return result;
|
||||
}
|
||||
|
||||
public T poll() {
|
||||
T result = deque.poll();
|
||||
count.set(deque.size(), null);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
deque.clear();
|
||||
count.set(0, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected AsyncDeque<TestMethodInvocation> invocations = new AsyncDeque<>();
|
||||
|
||||
protected AsynchronousSocketChannel socketChannel() throws IOException {
|
||||
// Note, it looks like the executor knows to shut itself down on GC
|
||||
AsynchronousChannelGroup group =
|
||||
AsynchronousChannelGroup.withThreadPool(Executors.newSingleThreadExecutor());
|
||||
return AsynchronousSocketChannel.open(group);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectDisconnect() throws Throwable {
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
|
||||
|
@ -514,7 +616,7 @@ public class GadpClientServerTest {
|
|||
|
||||
@Test
|
||||
public void testFetchModelValue() throws Throwable {
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -532,9 +634,39 @@ public class GadpClientServerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Developer's desk only")
|
||||
public void stressTest() throws Throwable {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
try {
|
||||
System.out.println("ITERATION: " + i);
|
||||
testFetchModelValueCached();
|
||||
}
|
||||
catch (Throwable e) {
|
||||
System.err.println("Failed on iteration " + i);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test // Sanity check on underlying model
|
||||
public void testFetchModelValueCachedNoGadp() throws Throwable {
|
||||
TestGadpObjectModel model = new TestGadpObjectModel(true);
|
||||
Object rootVal = waitOn(model.fetchModelValue(List.of()));
|
||||
TargetObject root = (TargetObject) rootVal;
|
||||
assertEquals(List.of(), root.getPath());
|
||||
// Do fetchAll to create objects and populate their caches
|
||||
Map<String, ? extends TargetObject> available =
|
||||
waitOn(model.fetchObjectElements(PathUtils.parse("Available"))
|
||||
.thenCompose(DebugModelConventions::fetchAll));
|
||||
assertEquals(2, available.size());
|
||||
Object cmd = waitOn(model.fetchModelValue(PathUtils.parse("Available[1].cmd")));
|
||||
assertEquals("echo", cmd);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFetchModelValueCached() throws Throwable {
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
MonitoredAsyncByteChannel monitored = new MonitoredAsyncByteChannel(socket);
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", monitored);
|
||||
|
@ -552,7 +684,8 @@ public class GadpClientServerTest {
|
|||
monitored.reset();
|
||||
Object cmd = waitOn(client.fetchModelValue(PathUtils.parse("Available[1].cmd")));
|
||||
assertEquals("echo", cmd);
|
||||
assertEquals(0, monitored.writeCount);
|
||||
// Just 1 to request .cmd, since the above will only have covered Available[]
|
||||
assertEquals(1, monitored.writeCount);
|
||||
waitOn(client.close());
|
||||
}
|
||||
finally {
|
||||
|
@ -562,7 +695,7 @@ public class GadpClientServerTest {
|
|||
|
||||
@Test
|
||||
public void testInvoke() throws Throwable {
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -579,7 +712,7 @@ public class GadpClientServerTest {
|
|||
CompletableFuture<Object> future = method.invoke(Map.of(
|
||||
"whom", "GADP",
|
||||
"others", TargetStringList.of("Alice", "Bob")));
|
||||
waitOn(invocationCount.waitValue(1));
|
||||
waitOn(invocations.count.waitValue(1));
|
||||
TestMethodInvocation invocation = invocations.poll();
|
||||
assertEquals(Map.of(
|
||||
"whom", "GADP",
|
||||
|
@ -598,7 +731,7 @@ public class GadpClientServerTest {
|
|||
@Test
|
||||
public void testInvokeMethodCached() throws Throwable {
|
||||
// TODO: Disambiguate / deconflict these two "method" cases
|
||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
try (AsynchronousSocketChannel socket = socketChannel();
|
||||
ServerRunner runner = new ServerRunner()) {
|
||||
MonitoredAsyncByteChannel monitored = new MonitoredAsyncByteChannel(socket);
|
||||
GadpClient client = new GadpClient("Test", monitored);
|
||||
|
@ -616,13 +749,24 @@ public class GadpClientServerTest {
|
|||
TargetObjectRef methodRef = (TargetObjectRef) attrs.get("greet");
|
||||
TargetMethod<?> method = waitOn(methodRef.as(TargetMethod.tclass).fetch());
|
||||
assertNotNull(method);
|
||||
CompletableFuture<?> future1 = avail.fetchAttribute("greet(World)");
|
||||
|
||||
waitOn(invocations.count.waitValue(1));
|
||||
TestMethodInvocation invocation1 = invocations.poll();
|
||||
assertEquals(Map.of("arg", "World"), invocation1.args);
|
||||
invocation1.complete("Hello, World!");
|
||||
assertEquals("Hello, World!", waitOn(future1));
|
||||
// Should not have to wait
|
||||
assertEquals("Hello, World!", waitOn(avail.fetchAttribute("greet(World)")));
|
||||
runner.server.model.session.available.punct = '?'; // No effect before flush
|
||||
assertEquals("Hello, World!", waitOn(avail.fetchAttribute("greet(World)")));
|
||||
assertEquals(0, invocations.count.get().intValue());
|
||||
|
||||
// Flush the cache
|
||||
waitOn(avail.fetchAttributes(true));
|
||||
assertEquals("Hello, World?", waitOn(avail.fetchAttribute("greet(World)")));
|
||||
CompletableFuture<?> future2 = avail.fetchAttribute("greet(World)");
|
||||
waitOn(invocations.count.waitValue(1));
|
||||
TestMethodInvocation invocation2 = invocations.poll();
|
||||
invocation2.complete("Hello, World?");
|
||||
assertEquals("Hello, World?", waitOn(future2));
|
||||
|
||||
waitOn(client.close());
|
||||
}
|
||||
|
@ -630,7 +774,7 @@ public class GadpClientServerTest {
|
|||
|
||||
@Test
|
||||
public void testListRoot() throws Throwable {
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -657,7 +801,7 @@ public class GadpClientServerTest {
|
|||
|
||||
@Test
|
||||
public void testLaunch() throws Throwable {
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -691,7 +835,7 @@ public class GadpClientServerTest {
|
|||
invocations.add(new ElementsChangedInvocation(parent, removed, added));
|
||||
}
|
||||
};
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -724,7 +868,7 @@ public class GadpClientServerTest {
|
|||
}
|
||||
}
|
||||
};
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -757,7 +901,7 @@ public class GadpClientServerTest {
|
|||
|
||||
@Test
|
||||
public void testSubscribeNoSuchPath() throws Throwable {
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -774,7 +918,7 @@ public class GadpClientServerTest {
|
|||
public void testSubscribeLaunchForChildrenChanged() throws Throwable {
|
||||
ElementsChangedListener elemL = new ElementsChangedListener();
|
||||
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
|
||||
|
@ -811,7 +955,7 @@ public class GadpClientServerTest {
|
|||
ElementsChangedListener elemL = new ElementsChangedListener();
|
||||
InvalidatedListener invL = new InvalidatedListener();
|
||||
|
||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
try (AsynchronousSocketChannel socket = socketChannel();
|
||||
ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -836,7 +980,7 @@ public class GadpClientServerTest {
|
|||
), "Changed");
|
||||
|
||||
waitOn(invL.count.waitValue(2));
|
||||
waitOn(elemL.count.waitValue(1));
|
||||
waitOn(elemL.count.waitValue(2));
|
||||
|
||||
for (TargetObject a : avail1.values()) {
|
||||
assertFalse(a.isValid());
|
||||
|
@ -848,17 +992,22 @@ public class GadpClientServerTest {
|
|||
assertEquals(1, avail2.size());
|
||||
assertEquals("cat", avail2.get("1").getCachedAttribute("cmd"));
|
||||
|
||||
ElementsChangedInvocation removed = elemL.invocations.remove();
|
||||
assertSame(availCont, removed.parent);
|
||||
assertEquals(Map.of(), removed.added);
|
||||
assertEquals(Set.of("1"), Set.copyOf(removed.removed));
|
||||
|
||||
ElementsChangedInvocation changed = Unique.assertOne(elemL.invocations);
|
||||
assertSame(availCont, changed.parent);
|
||||
// Use equals here, since the listener only gets the ref
|
||||
assertEquals(avail2.get("1"), Unique.assertOne(changed.added.values()));
|
||||
assertEquals(Set.of("1"), changed.added.keySet());
|
||||
assertEquals(Set.of("2"), Set.copyOf(changed.removed));
|
||||
assertSame(avail2.get("1"), Unique.assertOne(changed.added.values()));
|
||||
|
||||
Map<ID<TargetObject>, String> actualInv = invL.invocations.stream()
|
||||
.collect(Collectors.toMap(ii -> ID.of(ii.object), ii -> ii.reason));
|
||||
Map<ID<TargetObject>, String> expectedInv =
|
||||
avail1.values()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(o -> ID.of(o), o -> "Changed"));
|
||||
Map<ID<TargetObject>, String> expectedInv = Map.ofEntries(
|
||||
Map.entry(ID.of(avail1.get("1")), "Replaced"),
|
||||
Map.entry(ID.of(avail1.get("2")), "Changed"));
|
||||
assertEquals(expectedInv, actualInv);
|
||||
}
|
||||
}
|
||||
|
@ -867,7 +1016,7 @@ public class GadpClientServerTest {
|
|||
public void testReplaceAttribute() throws Throwable {
|
||||
AttributesChangedListener attrL = new AttributesChangedListener();
|
||||
|
||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
try (AsynchronousSocketChannel socket = socketChannel();
|
||||
ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -877,32 +1026,36 @@ public class GadpClientServerTest {
|
|||
TargetObject echoAvail =
|
||||
waitOn(client.fetchModelObject(PathUtils.parse("Available[1]")));
|
||||
echoAvail.addListener(attrL);
|
||||
assertEquals(Map.of(
|
||||
"pid", 1,
|
||||
"cmd", "echo" //
|
||||
), waitOn(echoAvail.fetchAttributes()));
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("pid", 1),
|
||||
Map.entry("cmd", "echo"),
|
||||
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||
Map.entry("_display", "[1]")),
|
||||
waitOn(echoAvail.fetchAttributes()));
|
||||
|
||||
TestGadpTargetAvailable ssEchoAvail =
|
||||
runner.server.model.session.available.getCachedElements().get("1");
|
||||
|
||||
ssEchoAvail.setAttributes(Map.of(
|
||||
"cmd", "echo",
|
||||
"args", "Hello, World!" //
|
||||
), "Changed");
|
||||
ssEchoAvail.changeAttributes(List.of("pid"), Map.ofEntries(
|
||||
Map.entry("cmd", "echo"),
|
||||
Map.entry("args", "Hello, World!")),
|
||||
"Changed");
|
||||
|
||||
waitOn(attrL.count.waitValue(1));
|
||||
|
||||
assertEquals(Map.of(
|
||||
"cmd", "echo",
|
||||
"args", "Hello, World!" //
|
||||
), echoAvail.getCachedAttributes());
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("cmd", "echo"),
|
||||
Map.entry("args", "Hello, World!"),
|
||||
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||
Map.entry("_display", "[1]")),
|
||||
echoAvail.getCachedAttributes());
|
||||
|
||||
AttributesChangedInvocation changed = Unique.assertOne(attrL.invocations);
|
||||
assertSame(echoAvail, changed.parent);
|
||||
assertEquals(Set.of("pid"), Set.copyOf(changed.removed));
|
||||
assertEquals(Map.of(
|
||||
"args", "Hello, World!" //
|
||||
), changed.added);
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("args", "Hello, World!")),
|
||||
changed.added);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -910,7 +1063,7 @@ public class GadpClientServerTest {
|
|||
public void testSubtreeInvalidationDeduped() throws Throwable {
|
||||
InvalidatedListener invL = new InvalidatedListener();
|
||||
|
||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
try (AsynchronousSocketChannel socket = socketChannel();
|
||||
ServerRunner runner = new ServerRunner()) {
|
||||
MonitoredGadpClient client = new MonitoredGadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -951,7 +1104,7 @@ public class GadpClientServerTest {
|
|||
public void testNoEventsAfterInvalidated() throws Throwable {
|
||||
AttributesChangedListener attrL = new AttributesChangedListener();
|
||||
|
||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
try (AsynchronousSocketChannel socket = socketChannel();
|
||||
ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -960,18 +1113,23 @@ public class GadpClientServerTest {
|
|||
|
||||
TargetObject echoAvail =
|
||||
waitOn(client.fetchModelObject(PathUtils.parse("Available[1]")));
|
||||
// TODO: This comes back null too often...
|
||||
echoAvail.addListener(attrL);
|
||||
assertEquals(Map.of(
|
||||
"pid", 1,
|
||||
"cmd", "echo" //
|
||||
), waitOn(echoAvail.fetchAttributes()));
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("pid", 1),
|
||||
Map.entry("cmd", "echo"),
|
||||
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||
Map.entry("_display", "[1]")),
|
||||
waitOn(echoAvail.fetchAttributes()));
|
||||
|
||||
TargetObject ddAvail = waitOn(client.fetchModelObject(PathUtils.parse("Available[2]")));
|
||||
ddAvail.addListener(attrL);
|
||||
assertEquals(Map.of(
|
||||
"pid", 2,
|
||||
"cmd", "dd" //
|
||||
), waitOn(ddAvail.fetchAttributes()));
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("pid", 2),
|
||||
Map.entry("cmd", "dd"),
|
||||
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||
Map.entry("_display", "[2]")),
|
||||
waitOn(ddAvail.fetchAttributes()));
|
||||
|
||||
// NB: copy
|
||||
Map<String, TestGadpTargetAvailable> ssAvail =
|
||||
|
@ -980,9 +1138,8 @@ public class GadpClientServerTest {
|
|||
runner.server.model.session.available.changeElements(List.of("1"), List.of(), Map.of(),
|
||||
"1 is Gone");
|
||||
// Should produce nothing
|
||||
(ssAvail.get("1")).setAttributes(List.of(), Map.of(
|
||||
"cmd", "echo",
|
||||
"args", "Hello, World!"),
|
||||
(ssAvail.get("1")).changeAttributes(List.of(), Map.ofEntries(
|
||||
Map.entry("args", "Hello, World!")),
|
||||
"Changed");
|
||||
// Produce something, so we know we didn't get the other thing
|
||||
(ssAvail.get("2")).changeAttributes(List.of(), List.of(), Map.of(
|
||||
|
@ -1002,7 +1159,7 @@ public class GadpClientServerTest {
|
|||
|
||||
@Test
|
||||
public void testProxyWithLinkedElementsCanonicalFirst() throws Throwable {
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -1024,7 +1181,7 @@ public class GadpClientServerTest {
|
|||
|
||||
@Test
|
||||
public void testProxyWithLinkedElementsLinkFirst() throws Throwable {
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -1033,9 +1190,10 @@ public class GadpClientServerTest {
|
|||
runner.server.model.session.addLinks();
|
||||
TargetObjectRef linkRef =
|
||||
(TargetObjectRef) waitOn(client.fetchModelValue(PathUtils.parse("Links[1]")));
|
||||
assertFalse(linkRef instanceof TargetObject);
|
||||
assertTrue(linkRef instanceof TargetObject);
|
||||
TargetObject link =
|
||||
waitOn(client.fetchModelObject(PathUtils.parse("Links[1]")));
|
||||
assertSame(linkRef, link);
|
||||
TargetObject canonical =
|
||||
waitOn(client.fetchModelObject(PathUtils.parse("Available[2]")));
|
||||
assertSame(canonical, link);
|
||||
|
@ -1049,7 +1207,7 @@ public class GadpClientServerTest {
|
|||
|
||||
@Test
|
||||
public void testFetchModelValueFollowsLink() throws Throwable {
|
||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner()) {
|
||||
GadpClient client = new GadpClient("Test", socket);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
|
@ -1064,64 +1222,221 @@ public class GadpClientServerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("TODO")
|
||||
public void testCachingWithLinks() throws Throwable {
|
||||
public static class EventListener implements DebuggerModelListener {
|
||||
public static class CallEntry {
|
||||
public final String methodName;
|
||||
public final List<Object> args;
|
||||
|
||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
||||
ServerRunner runner = new ServerRunner()) {
|
||||
MonitoredGadpClient client = new MonitoredGadpClient("Test", socket);
|
||||
public CallEntry(String methodName, List<Object> args) {
|
||||
this.methodName = methodName;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("<CallEntry %s(%s)>", methodName, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(methodName, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof CallEntry)) {
|
||||
return false;
|
||||
}
|
||||
CallEntry that = (CallEntry) obj;
|
||||
if (!Objects.equals(this.methodName, that.methodName)) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(this.args, that.args)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public final List<CallEntry> record = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void created(TargetObject object) {
|
||||
record.add(new CallEntry("created", List.of(object)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attributesChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ?> added) {
|
||||
record.add(new CallEntry("attributesChanged", List.of(parent, removed, added)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void elementsChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ? extends TargetObjectRef> added) {
|
||||
record.add(new CallEntry("elementsChanged", List.of(parent, removed, added)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rootAdded(TargetObject root) {
|
||||
record.add(new CallEntry("rootAdded", List.of(root)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectBeforeRootCreated() throws Throwable {
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner() {
|
||||
@Override
|
||||
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
|
||||
return new TestGadpServer(new TestGadpObjectModel(false), addr);
|
||||
}
|
||||
}) {
|
||||
GadpClient client = new PrintingGadpClient("Test", socket);
|
||||
EventListener listener = new EventListener();
|
||||
client.addModelListener(listener, true);
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
runner.server.getLocalAddress()));
|
||||
waitOn(client.connect());
|
||||
runner.server.model.session.addLinks();
|
||||
|
||||
client.getMessageChannel().clear();
|
||||
assertEquals("echo", waitOn(client.fetchModelValue(PathUtils.parse("Links[2].cmd"))));
|
||||
assertEquals(2, client.getMessageChannel().record.size());
|
||||
assertEquals(Gadp.RootMessage.MsgCase.SUBSCRIBE_REQUEST,
|
||||
client.getMessageChannel().record.get(0).assertSent().getMsgCase());
|
||||
assertEquals(Gadp.RootMessage.MsgCase.SUBSCRIBE_REPLY,
|
||||
client.getMessageChannel().record.get(1).assertReceived().getMsgCase());
|
||||
DefaultTargetObject<?, ?> root =
|
||||
new DefaultTargetModelRoot(runner.server.model, "Root");
|
||||
DefaultTargetObject<?, ?> a =
|
||||
new DefaultTargetObject<>(runner.server.model, root, "a", "A");
|
||||
a.changeAttributes(List.of(), Map.of("test", 6), "Because");
|
||||
root.changeAttributes(List.of(), List.of(a), Map.of(), "Because");
|
||||
runner.server.model.addModelRoot(root);
|
||||
waitOn(client.fetchModelRoot());
|
||||
|
||||
// Since I don't have the parent, as usual, no cache
|
||||
client.getMessageChannel().clear();
|
||||
assertEquals("echo", waitOn(client.fetchModelValue(PathUtils.parse("Links[2].cmd"))));
|
||||
assertEquals(2, client.getMessageChannel().record.size());
|
||||
assertEquals(List.of(
|
||||
new CallEntry("created", List.of(
|
||||
client.getModelRoot())),
|
||||
new CallEntry("attributesChanged", List.of(
|
||||
client.getModelRoot(), Set.of(), Map.ofEntries(
|
||||
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||
Map.entry("_display", "<root>")))),
|
||||
new CallEntry("created", List.of(
|
||||
client.getModelObject(PathUtils.parse("a")))),
|
||||
new CallEntry("attributesChanged", List.of(
|
||||
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
|
||||
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||
Map.entry("_display", "a")))),
|
||||
new CallEntry("attributesChanged", List.of(
|
||||
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
|
||||
Map.entry("test", 6)))),
|
||||
new CallEntry("attributesChanged", List.of(
|
||||
client.getModelRoot(), Set.of(), Map.ofEntries(
|
||||
Map.entry("a", client.getModelObject(PathUtils.parse("a")))))),
|
||||
new CallEntry("rootAdded", List.of(client.getModelRoot()))),
|
||||
listener.record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Now, fetch Links[2] first, and repeat the experiment It should still not use the
|
||||
* cache, because we won't know if Links[2] still points to Available[1]. Technically,
|
||||
* Available[1] is what will be cached. Index 2 of Links will not be cached until/if
|
||||
* Links is fetched.
|
||||
*/
|
||||
client.getMessageChannel().clear();
|
||||
TargetObject avail1 = waitOn(client.fetchModelObject(PathUtils.parse("Links[2]")));
|
||||
// Odd, but I believe it's correct since the first reply comes back with a ref
|
||||
assertEquals(4, client.getMessageChannel().record.size());
|
||||
assertNotNull(avail1);
|
||||
client.getMessageChannel().clear();
|
||||
assertEquals("echo", waitOn(client.fetchModelValue(PathUtils.parse("Links[2].cmd"))));
|
||||
assertEquals(2, client.getMessageChannel().record.size());
|
||||
client.getMessageChannel().clear();
|
||||
assertEquals("echo", waitOn(client.fetchModelValue(PathUtils.parse("Links[2].cmd"))));
|
||||
assertEquals(2, client.getMessageChannel().record.size());
|
||||
@Test
|
||||
public void testConnectBetweenRootCreatedAndAdded() throws Throwable {
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner() {
|
||||
@Override
|
||||
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
|
||||
return new TestGadpServer(new TestGadpObjectModel(false), addr);
|
||||
}
|
||||
}) {
|
||||
GadpClient client = new PrintingGadpClient("Test", socket);
|
||||
EventListener listener = new EventListener();
|
||||
client.addModelListener(listener, true);
|
||||
|
||||
// Now, fetch Links, and its elements to ensure it is cached
|
||||
TargetObject links = waitOn(client.fetchModelObject(PathUtils.parse("Links")));
|
||||
assertSame(avail1, waitOn(links.fetchElement("2")));
|
||||
DefaultTargetObject<?, ?> root =
|
||||
new DefaultTargetModelRoot(runner.server.model, "Root");
|
||||
DefaultTargetObject<?, ?> a =
|
||||
new DefaultTargetObject<>(runner.server.model, root, "a", "A");
|
||||
a.changeAttributes(List.of(), Map.of("test", 6), "Because");
|
||||
|
||||
client.getMessageChannel().clear();
|
||||
assertSame(avail1, waitOn(links.fetchElement("2")));
|
||||
assertEquals(2, client.getMessageChannel().record.size());
|
||||
assertEquals("Links[2]", PathUtils.toString(client.getMessageChannel().record.get(0)
|
||||
.assertSent()
|
||||
.getSubscribeRequest()
|
||||
.getPath()
|
||||
.getEList()));
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
runner.server.getLocalAddress()));
|
||||
waitOn(client.connect());
|
||||
|
||||
TODO();
|
||||
waitOn(client.close());
|
||||
root.changeAttributes(List.of(), List.of(a), Map.of(), "Because");
|
||||
runner.server.model.addModelRoot(root);
|
||||
waitOn(client.fetchModelRoot());
|
||||
waitOn(client.flushEvents());
|
||||
|
||||
assertEquals(List.of(
|
||||
new CallEntry("created", List.of(
|
||||
client.getModelRoot())),
|
||||
new CallEntry("attributesChanged", List.of(
|
||||
client.getModelRoot(), Set.of(), Map.ofEntries(
|
||||
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||
Map.entry("_display", "<root>")))),
|
||||
new CallEntry("created", List.of(
|
||||
client.getModelObject(PathUtils.parse("a")))),
|
||||
new CallEntry("attributesChanged", List.of(
|
||||
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
|
||||
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||
Map.entry("_display", "a")))),
|
||||
new CallEntry("attributesChanged", List.of(
|
||||
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
|
||||
Map.entry("test", 6)))),
|
||||
new CallEntry("attributesChanged", List.of(
|
||||
client.getModelRoot(), Set.of(), Map.ofEntries(
|
||||
Map.entry("a", client.getModelObject(PathUtils.parse("a")))))),
|
||||
new CallEntry("rootAdded", List.of(client.getModelRoot()))),
|
||||
listener.record);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectAfterRootAdded() throws Throwable {
|
||||
AsynchronousSocketChannel socket = socketChannel();
|
||||
try (ServerRunner runner = new ServerRunner() {
|
||||
@Override
|
||||
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
|
||||
return new TestGadpServer(new TestGadpObjectModel(false), addr);
|
||||
}
|
||||
}) {
|
||||
|
||||
GadpClient client = new PrintingGadpClient("Test", socket);
|
||||
EventListener listener = new EventListener();
|
||||
client.addModelListener(listener, true);
|
||||
|
||||
DefaultTargetObject<?, ?> root =
|
||||
new DefaultTargetModelRoot(runner.server.model, "Root");
|
||||
DefaultTargetObject<?, ?> a =
|
||||
new DefaultTargetObject<>(runner.server.model, root, "a", "A");
|
||||
a.changeAttributes(List.of(), Map.of("test", 6), "Because");
|
||||
root.changeAttributes(List.of(), List.of(a), Map.of(), "Because");
|
||||
runner.server.model.addModelRoot(root);
|
||||
waitOn(runner.server.model.flushEvents());
|
||||
|
||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||
runner.server.getLocalAddress()));
|
||||
waitOn(client.connect());
|
||||
waitOn(client.fetchModelRoot());
|
||||
waitOn(client.flushEvents());
|
||||
|
||||
assertEquals(List.of(
|
||||
new CallEntry("created", List.of(
|
||||
client.getModelRoot())),
|
||||
new CallEntry("attributesChanged", List.of( // Defaults upon client-side construction
|
||||
client.getModelRoot(), Set.of(), Map.ofEntries(
|
||||
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||
Map.entry("_display", "<root>")))),
|
||||
new CallEntry("created", List.of(
|
||||
client.getModelObject(PathUtils.parse("a")))),
|
||||
new CallEntry("attributesChanged", List.of( // Defaults
|
||||
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
|
||||
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||
Map.entry("_display", "a")))),
|
||||
new CallEntry("attributesChanged", List.of(
|
||||
client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries(
|
||||
Map.entry("test", 6)))),
|
||||
new CallEntry("attributesChanged", List.of(
|
||||
client.getModelRoot(), Set.of(), Map.ofEntries(
|
||||
Map.entry("a", client.getModelObject(PathUtils.parse("a")))))),
|
||||
new CallEntry("rootAdded", List.of(client.getModelRoot()))),
|
||||
listener.record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package ghidra.dbg.gadp.client;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
|
@ -23,7 +24,8 @@ import java.net.SocketAddress;
|
|||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.*;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
@ -36,7 +38,6 @@ import ghidra.dbg.attributes.TargetObjectRef;
|
|||
import ghidra.dbg.gadp.GadpVersion;
|
||||
import ghidra.dbg.gadp.protocol.Gadp;
|
||||
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
|
||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.util.ElementsChangedListener;
|
||||
import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation;
|
||||
|
@ -90,6 +91,10 @@ public class GadpClientTest {
|
|||
this.cli = srv.accept();
|
||||
}
|
||||
|
||||
public void nextSeq() {
|
||||
seqno++;
|
||||
}
|
||||
|
||||
public void expect(Gadp.RootMessage msg) throws IOException {
|
||||
Msg.debug(this, "Expecting: " + msg);
|
||||
|
||||
|
@ -131,13 +136,14 @@ public class GadpClientTest {
|
|||
}
|
||||
}
|
||||
|
||||
public void handleConnect(GadpVersion version) throws IOException {
|
||||
public void expectRequestConnect() throws IOException {
|
||||
expect(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setConnectRequest(GadpVersion.makeRequest())
|
||||
.build());
|
||||
// TODO: Test schemas here?
|
||||
// Seems they're already hit well enough in dependent tests
|
||||
}
|
||||
|
||||
public void sendReplyConnect(GadpVersion version) throws IOException {
|
||||
send(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setConnectReply(Gadp.ConnectReply.newBuilder()
|
||||
|
@ -145,89 +151,163 @@ public class GadpClientTest {
|
|||
.setSchemaContext("<context/>")
|
||||
.setRootSchema("OBJECT"))
|
||||
.build());
|
||||
seqno++;
|
||||
}
|
||||
|
||||
public void handlePing() throws IOException {
|
||||
public void handleConnect(GadpVersion version) throws IOException {
|
||||
expectRequestConnect();
|
||||
sendReplyConnect(version);
|
||||
nextSeq();
|
||||
}
|
||||
|
||||
public void sendNotifyObjectCreated(List<String> path, List<String> interfaces,
|
||||
String typeHint) throws IOException {
|
||||
send(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path))
|
||||
.setObjectCreatedEvent(Gadp.ObjectCreatedEvent.newBuilder()
|
||||
.addAllInterface(interfaces)
|
||||
.setTypeHint(typeHint)))
|
||||
.build());
|
||||
}
|
||||
|
||||
public void sendNotifyRootAdded() throws IOException {
|
||||
send(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
.setRootAddedEvent(Gadp.RootAddedEvent.getDefaultInstance()))
|
||||
.build());
|
||||
}
|
||||
|
||||
public void notifyAddRoot() throws IOException {
|
||||
sendNotifyObjectCreated(List.of(), List.of(), "Root");
|
||||
sendNotifyRootAdded();
|
||||
}
|
||||
|
||||
public void expectRequestPing(String content) throws IOException {
|
||||
expect(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setPingRequest(Gadp.PingRequest.newBuilder()
|
||||
.setContent(HELLO_WORLD))
|
||||
.setContent(content))
|
||||
.build());
|
||||
}
|
||||
|
||||
public void sendReplyPing(String content) throws IOException {
|
||||
send(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setPingReply(Gadp.PingReply.newBuilder()
|
||||
.setContent(HELLO_WORLD))
|
||||
.setContent(content))
|
||||
.build());
|
||||
seqno++;
|
||||
}
|
||||
|
||||
public void handleSubscribeValue(List<String> path, Object value)
|
||||
throws Exception {
|
||||
expect(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setSubscribeRequest(Gadp.SubscribeRequest.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path))
|
||||
.setSubscribe(true))
|
||||
.build());
|
||||
send(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setSubscribeReply(Gadp.SubscribeReply.newBuilder()
|
||||
.setValue(GadpValueUtils.makeValue(path, value)))
|
||||
.build());
|
||||
seqno++;
|
||||
public void handlePing() throws IOException {
|
||||
expectRequestPing(HELLO_WORLD);
|
||||
sendReplyPing(HELLO_WORLD);
|
||||
nextSeq();
|
||||
}
|
||||
|
||||
public void handleFetchObject(List<String> path) throws Exception {
|
||||
expect(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setSubscribeRequest(Gadp.SubscribeRequest.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path))
|
||||
.setSubscribe(true))
|
||||
.build());
|
||||
Gadp.SubscribeReply.Builder reply = Gadp.SubscribeReply.newBuilder()
|
||||
public Gadp.ModelObjectDelta.Builder makeAttributeDelta(List<String> parentPath,
|
||||
Map<String, Object> primitives, Map<String, List<String>> objects) {
|
||||
Gadp.ModelObjectDelta.Builder delta = Gadp.ModelObjectDelta.newBuilder();
|
||||
if (primitives != null) {
|
||||
for (Map.Entry<String, ?> ent : primitives.entrySet()) {
|
||||
delta.addAdded(GadpValueUtils.makeNamedValue(parentPath, ent));
|
||||
}
|
||||
}
|
||||
if (objects != null) {
|
||||
for (Map.Entry<String, List<String>> ent : objects.entrySet()) {
|
||||
if (PathUtils.isLink(parentPath, ent.getKey(), ent.getValue())) {
|
||||
delta.addAdded(Gadp.NamedValue.newBuilder()
|
||||
.setName(ent.getKey())
|
||||
.setValue(Gadp.Value.newBuilder()
|
||||
.setObjectInfo(Gadp.ModelObjectInfo.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path))));
|
||||
send(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setSubscribeReply(reply)
|
||||
.build());
|
||||
seqno++;
|
||||
.setPathValue(GadpValueUtils.makePath(ent.getValue()))));
|
||||
}
|
||||
else {
|
||||
delta.addAdded(Gadp.NamedValue.newBuilder()
|
||||
.setName(ent.getKey())
|
||||
.setValue(Gadp.Value.newBuilder()
|
||||
.setObjectStub(Gadp.ModelObjectStub.getDefaultInstance())));
|
||||
}
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
public void handleFetchElements(List<String> path, Collection<String> indices)
|
||||
throws Exception {
|
||||
public Gadp.ModelObjectDelta.Builder makeElementDelta(List<String> parentPath,
|
||||
Map<String, List<String>> objects) {
|
||||
Gadp.ModelObjectDelta.Builder delta = Gadp.ModelObjectDelta.newBuilder();
|
||||
if (objects != null) {
|
||||
for (Map.Entry<String, List<String>> ent : objects.entrySet()) {
|
||||
if (PathUtils.isElementLink(parentPath, ent.getKey(), ent.getValue())) {
|
||||
delta.addAdded(Gadp.NamedValue.newBuilder()
|
||||
.setName(ent.getKey())
|
||||
.setValue(Gadp.Value.newBuilder()
|
||||
.setPathValue(GadpValueUtils.makePath(ent.getValue()))));
|
||||
}
|
||||
else {
|
||||
delta.addAdded(Gadp.NamedValue.newBuilder()
|
||||
.setName(ent.getKey())
|
||||
.setValue(Gadp.Value.newBuilder()
|
||||
.setObjectStub(Gadp.ModelObjectStub.getDefaultInstance())));
|
||||
}
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
public void expectRequestResync(List<String> path, boolean refreshAttributes,
|
||||
boolean refreshElements) throws IOException {
|
||||
expect(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setSubscribeRequest(Gadp.SubscribeRequest.newBuilder()
|
||||
.setResyncRequest(Gadp.ResyncRequest.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path))
|
||||
.setSubscribe(true)
|
||||
.setFetchElements(true))
|
||||
.setAttributes(refreshAttributes)
|
||||
.setElements(refreshElements))
|
||||
.build());
|
||||
Gadp.SubscribeReply.Builder reply = Gadp.SubscribeReply.newBuilder()
|
||||
.setValue(Gadp.Value.newBuilder()
|
||||
.setObjectInfo(Gadp.ModelObjectInfo.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path))
|
||||
.addAllElementIndex(indices)));
|
||||
send(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setSubscribeReply(reply)
|
||||
.build());
|
||||
seqno++;
|
||||
}
|
||||
|
||||
public void sendModelEvent(List<String> path, Collection<String> indicesAdded)
|
||||
throws Exception {
|
||||
Gadp.ModelObjectEvent.Builder evt = Gadp.ModelObjectEvent.newBuilder();
|
||||
evt.setDelta(Gadp.ModelObjectDelta.newBuilder()
|
||||
.addAllIndexAdded(indicesAdded));
|
||||
public void sendNotifyObjects(List<String> parentPath, Map<String, List<String>> elements,
|
||||
Map<String, List<String>> attrObjects, Map<String, Object> attrPrimitives)
|
||||
throws IOException {
|
||||
send(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||
.setPath(GadpValueUtils.makePath(path))
|
||||
.setModelObjectEvent(evt))
|
||||
.setPath(GadpValueUtils.makePath(parentPath))
|
||||
.setModelObjectEvent(Gadp.ModelObjectEvent.newBuilder()
|
||||
.setAttributeDelta(
|
||||
makeAttributeDelta(parentPath, attrPrimitives,
|
||||
attrObjects))
|
||||
.setElementDelta(makeElementDelta(parentPath, elements))))
|
||||
.build());
|
||||
}
|
||||
|
||||
public void sendReplyResync() throws IOException {
|
||||
send(Gadp.RootMessage.newBuilder()
|
||||
.setSequence(seqno)
|
||||
.setResyncReply(Gadp.ResyncReply.getDefaultInstance())
|
||||
.build());
|
||||
}
|
||||
|
||||
public void handleResyncAttributes(List<String> path, boolean refresh,
|
||||
Map<String, List<String>> objects, Map<String, Object> primitives)
|
||||
throws Exception {
|
||||
expectRequestResync(path, refresh, false);
|
||||
if (primitives != null || objects != null) {
|
||||
sendNotifyObjects(path, null, objects, primitives);
|
||||
}
|
||||
sendReplyResync();
|
||||
nextSeq();
|
||||
}
|
||||
|
||||
public void handleResyncElements(List<String> path, boolean refresh,
|
||||
Map<String, List<String>> objects) throws Exception {
|
||||
expectRequestResync(path, false, refresh);
|
||||
if (objects != null) {
|
||||
sendNotifyObjects(path, objects, null, null);
|
||||
}
|
||||
sendReplyResync();
|
||||
nextSeq();
|
||||
}
|
||||
}
|
||||
|
||||
protected void dumpBuffer(ByteBuffer buf) {
|
||||
|
@ -313,29 +393,29 @@ public class GadpClientTest {
|
|||
srv.handleConnect(GadpVersion.VER1);
|
||||
waitOn(gadpConnect);
|
||||
|
||||
List<String> parentPath = PathUtils.parse("Parent");
|
||||
CompletableFuture<? extends TargetObject> fetchParent =
|
||||
client.fetchModelObject(PathUtils.parse("Parent"));
|
||||
srv.handleFetchObject(PathUtils.parse("Parent"));
|
||||
client.fetchModelObject(parentPath);
|
||||
srv.notifyAddRoot();
|
||||
srv.sendNotifyObjectCreated(parentPath, List.of(), "Parent");
|
||||
srv.handleResyncAttributes(List.of(), false, Map.of("Parent", parentPath), null);
|
||||
TargetObject parent = waitOn(fetchParent);
|
||||
parent.addListener(elemL);
|
||||
|
||||
CompletableFuture<? extends Map<String, ? extends TargetObjectRef>> fetchElements =
|
||||
parent.fetchElements();
|
||||
srv.handleFetchElements(PathUtils.parse("Parent"), List.of());
|
||||
srv.handleResyncElements(parentPath, false, Map.of());
|
||||
assertEquals(Map.of(), waitOn(fetchElements));
|
||||
|
||||
srv.sendModelEvent(PathUtils.parse("Parent"), List.of("0"));
|
||||
List<String> elem0Path = PathUtils.parse("Parent[0]");
|
||||
srv.sendNotifyObjectCreated(elem0Path, List.of(), "Element");
|
||||
srv.sendNotifyObjects(parentPath, Map.of("0", elem0Path), null, null);
|
||||
waitOn(elemL.count.waitValue(1));
|
||||
ElementsChangedInvocation changed = Unique.assertOne(elemL.invocations);
|
||||
assertEquals(parent, changed.parent);
|
||||
TargetObjectRef childRef = Unique.assertOne(changed.added.values());
|
||||
assertEquals(PathUtils.parse("Parent[0]"), childRef.getPath());
|
||||
|
||||
// Not cached, since fetchElements just lists refs
|
||||
CompletableFuture<? extends TargetObject> fetchChild = childRef.fetch();
|
||||
srv.handleFetchObject(PathUtils.parse("Parent[0]"));
|
||||
TargetObject child = waitOn(fetchChild);
|
||||
assertEquals(Map.of("0", child), parent.getCachedElements());
|
||||
assertTrue(childRef instanceof GadpClientTargetObject);
|
||||
assertEquals(elem0Path, childRef.getPath());
|
||||
}
|
||||
assertEquals(1, elemL.count.get().intValue()); // After connection is closed
|
||||
}
|
||||
|
@ -354,16 +434,18 @@ public class GadpClientTest {
|
|||
CompletableFuture<Void> gadpConnect = client.connect();
|
||||
srv.handleConnect(GadpVersion.VER1);
|
||||
waitOn(gadpConnect);
|
||||
CompletableFuture<?> cfRoot = client.fetchModelRoot();
|
||||
srv.notifyAddRoot();
|
||||
waitOn(cfRoot);
|
||||
|
||||
CompletableFuture<?> fetchVal1 = client.fetchModelValue(PathUtils.parse("value"));
|
||||
CompletableFuture<?> fetchVal2 = client.fetchModelValue(PathUtils.parse("value"));
|
||||
srv.handleSubscribeValue(PathUtils.parse("value"), HELLO_WORLD);
|
||||
srv.handleResyncAttributes(List.of(), false, null, Map.of("value", HELLO_WORLD));
|
||||
assertEquals(HELLO_WORLD, waitOn(fetchVal1));
|
||||
assertEquals(HELLO_WORLD, waitOn(fetchVal2));
|
||||
|
||||
// Because parent not cached, it should send another request
|
||||
CompletableFuture<?> fetchVal3 = client.fetchModelValue(PathUtils.parse("value"));
|
||||
srv.handleSubscribeValue(PathUtils.parse("value"), "Hi");
|
||||
CompletableFuture<?> fetchVal3 = client.fetchModelValue(PathUtils.parse("value"), true);
|
||||
srv.handleResyncAttributes(List.of(), true, null, Map.of("value", "Hi"));
|
||||
assertEquals("Hi", waitOn(fetchVal3));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import java.util.concurrent.CompletableFuture;
|
|||
|
||||
import com.sun.jdi.*;
|
||||
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
||||
import ghidra.dbg.jdi.manager.JdiManager;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
|
@ -64,6 +63,7 @@ public class JdiModelImpl extends AbstractDebuggerObjectModel {
|
|||
|
||||
Address start = ram.getAddress(0L);
|
||||
this.defaultRange = new AddressRangeImpl(start, start.add(BLOCK_SIZE));
|
||||
addModelRoot(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -86,7 +86,7 @@ public class JdiModelImpl extends AbstractDebuggerObjectModel {
|
|||
@Override
|
||||
public CompletableFuture<Void> close() {
|
||||
jdi.terminate();
|
||||
return AsyncUtils.NIL;
|
||||
return super.close();
|
||||
}
|
||||
|
||||
public JdiModelTargetRoot getRoot() {
|
||||
|
@ -227,6 +227,7 @@ public class JdiModelImpl extends AbstractDebuggerObjectModel {
|
|||
return range;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddressFactory getAddressFactory() {
|
||||
return addressFactory;
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ public class JdiModelTargetClassContainer extends JdiModelTargetObjectImpl {
|
|||
return getClassesByName().get(name);
|
||||
}
|
||||
|
||||
public CompletableFuture<?> refresh() {
|
||||
public CompletableFuture<?> refreshInternal() {
|
||||
if (!isObserved()) {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ public class JdiModelTargetConnectorContainer extends JdiModelTargetObjectImpl {
|
|||
return null;
|
||||
}
|
||||
|
||||
public CompletableFuture<?> refresh() {
|
||||
public CompletableFuture<?> refreshInternal() {
|
||||
if (!isObserved()) {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ public class JdiModelTargetModuleContainer extends JdiModelTargetObjectImpl
|
|||
return modulesByName.get(name);
|
||||
}
|
||||
|
||||
public CompletableFuture<?> refresh() {
|
||||
public CompletableFuture<?> refreshInternal() {
|
||||
if (!isObserved()) {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -398,7 +398,7 @@ public class JdiModelTargetVM extends JdiModelTargetObjectImpl implements //
|
|||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
public void refreshInternal() {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import ghidra.dbg.target.TargetEnvironment;
|
|||
public interface JdiModelTargetEnvironment<T extends TargetEnvironment<T>>
|
||||
extends JdiModelTargetObject, TargetEnvironment<T> {
|
||||
|
||||
public void refresh();
|
||||
public void refreshInternal();
|
||||
|
||||
@Override
|
||||
public default String getArchitecture() {
|
||||
|
|
|
@ -47,27 +47,33 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
|
|||
*/
|
||||
protected class ForInterpreterListener implements TargetInterpreterListener {
|
||||
@Override
|
||||
public void consoleOutput(TargetObject console, Channel channel, String out) {
|
||||
// NB: yes, this is lame... The InterpreterPanel's repositionScrollPane
|
||||
// method substracts 1 from the text length to compute the new position
|
||||
// causing it to scroll to the last character printed. We want it to scroll
|
||||
// to the next line, so...
|
||||
out += " ";
|
||||
public void consoleOutput(TargetObject console, Channel channel, byte[] out) {
|
||||
OutputStream os;
|
||||
switch (channel) {
|
||||
case STDOUT:
|
||||
if (outWriter == null) {
|
||||
return;
|
||||
}
|
||||
outWriter.print(out);
|
||||
outWriter.flush();
|
||||
os = stdOut;
|
||||
break;
|
||||
case STDERR:
|
||||
if (errWriter == null) {
|
||||
os = stdErr;
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
// It's possible stdOut/Err was not initialized, yet
|
||||
if (os == null) {
|
||||
return;
|
||||
}
|
||||
errWriter.print(out);
|
||||
errWriter.flush();
|
||||
break;
|
||||
/**
|
||||
* NB: yes, the extra space is lame... The InterpreterPanel's repositionScrollPane
|
||||
* method subtracts 1 from the text length to compute the new position causing it to
|
||||
* scroll to the last character printed. We want it to scroll to the next line, so...
|
||||
*/
|
||||
try {
|
||||
os.write(out);
|
||||
os.write(' ');
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.error(this, "Cannot write to interpreter window: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +90,7 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
|
|||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(TargetObject object, String reason) {
|
||||
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||
Swing.runLater(() -> {
|
||||
if (object == targetConsole) { // Redundant
|
||||
if (pinned) {
|
||||
|
@ -107,8 +113,8 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
|
|||
protected Thread thread;
|
||||
protected InterpreterConsole guiConsole;
|
||||
protected BufferedReader inReader;
|
||||
protected PrintWriter outWriter;
|
||||
protected PrintWriter errWriter;
|
||||
protected OutputStream stdOut;
|
||||
protected OutputStream stdErr;
|
||||
|
||||
protected ToggleDockingAction actionPin;
|
||||
protected boolean pinned = false;
|
||||
|
@ -146,8 +152,8 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
|
|||
InterpreterComponentProvider provider = (InterpreterComponentProvider) guiConsole;
|
||||
provider.setSubTitle(targetConsole.getDisplay());
|
||||
|
||||
setErrWriter(guiConsole.getErrWriter());
|
||||
setOutWriter(guiConsole.getOutWriter());
|
||||
setStdErr(guiConsole.getStdErr());
|
||||
setStdOut(guiConsole.getStdOut());
|
||||
setStdIn(guiConsole.getStdin());
|
||||
|
||||
createActions();
|
||||
|
@ -161,12 +167,12 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
|
|||
guiConsole.addAction(actionPin);
|
||||
}
|
||||
|
||||
public void setOutWriter(PrintWriter outWriter) {
|
||||
this.outWriter = outWriter;
|
||||
public void setStdOut(OutputStream stdOut) {
|
||||
this.stdOut = stdOut;
|
||||
}
|
||||
|
||||
public void setErrWriter(PrintWriter errWriter) {
|
||||
this.errWriter = errWriter;
|
||||
public void setStdErr(OutputStream stdErr) {
|
||||
this.stdErr = stdErr;
|
||||
}
|
||||
|
||||
public void setStdIn(InputStream stdIn) {
|
||||
|
|
|
@ -46,22 +46,15 @@ import ghidra.app.plugin.core.debug.gui.objects.components.*;
|
|||
import ghidra.app.services.*;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.async.TypeSpec;
|
||||
import ghidra.dbg.DebugModelConventions;
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.*;
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
import ghidra.dbg.error.DebuggerMemoryAccessException;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibility;
|
||||
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
|
||||
import ghidra.dbg.target.TargetConsole.Channel;
|
||||
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
|
||||
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionStateListener;
|
||||
import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener;
|
||||
import ghidra.dbg.target.TargetInterpreter.TargetInterpreterListener;
|
||||
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
|
||||
import ghidra.dbg.target.TargetMemory.TargetMemoryListener;
|
||||
import ghidra.dbg.target.TargetObject.TargetObjectFetchingListener;
|
||||
import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener;
|
||||
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.framework.options.AutoOptions;
|
||||
|
@ -80,9 +73,9 @@ import ghidra.util.table.GhidraTable;
|
|||
import resources.ResourceManager;
|
||||
|
||||
public class DebuggerObjectsProvider extends ComponentProviderAdapter implements //AllTargetObjectListenerAdapter,
|
||||
TargetObjectFetchingListener, TargetAccessibilityListener, TargetExecutionStateListener,
|
||||
TargetFocusScopeListener, TargetInterpreterListener, TargetMemoryListener,
|
||||
TargetRegisterBankListener, ObjectContainerListener {
|
||||
TargetObjectFetchingListener, //
|
||||
DebuggerModelListener, //
|
||||
ObjectContainerListener {
|
||||
|
||||
public static final String PATH_JOIN_CHAR = ".";
|
||||
//private static final String AUTOUPDATE_ATTRIBUTE_NAME = "autoupdate";
|
||||
|
@ -310,6 +303,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
|
|||
|
||||
public void setModel(DebuggerObjectModel model) {
|
||||
currentModel = model;
|
||||
currentModel.addModelListener(this, true);
|
||||
refresh();
|
||||
}
|
||||
|
||||
|
@ -1384,14 +1378,6 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
|
|||
});
|
||||
}
|
||||
|
||||
public void addListener(TargetObject targetObject) {
|
||||
/*
|
||||
if (recorder != null) {
|
||||
recorder.getListenerForRecord().addListener(targetObject);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public void stopRecording(TargetObject targetObject) {
|
||||
// TODO: Do `this.recorder = ...` on every object selection change?
|
||||
TraceRecorder rec = modelService.getRecorderForSuccessor(targetObject);
|
||||
|
@ -1681,7 +1667,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
|
|||
public void elementsChangedObjects(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ? extends TargetObject> added) {
|
||||
//System.err.println("local EC: " + parent);
|
||||
ObjectContainer container = getContainerByPath(parent.getPath());
|
||||
ObjectContainer container = parent == null ? null : getContainerByPath(parent.getPath());
|
||||
if (container != null) {
|
||||
container.augmentElements(removed, added);
|
||||
boolean visibleChange = false;
|
||||
|
@ -1702,7 +1688,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
|
|||
public void attributesChangedObjects(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ?> added) {
|
||||
//System.err.println("local AC: " + parent + ":" + removed + ":" + added);
|
||||
ObjectContainer container = getContainerByPath(parent.getPath());
|
||||
ObjectContainer container = parent == null ? null : getContainerByPath(parent.getPath());
|
||||
if (container != null) {
|
||||
container.augmentAttributes(removed, added);
|
||||
boolean visibleChange = false;
|
||||
|
|
|
@ -15,23 +15,17 @@
|
|||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.objects;
|
||||
|
||||
import static ghidra.async.AsyncUtils.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.jdom.Element;
|
||||
|
||||
import ghidra.async.AsyncFence;
|
||||
import ghidra.async.TypeSpec;
|
||||
import ghidra.dbg.DebugModelConventions;
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.TargetProcess;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.datastruct.ListenerSet;
|
||||
import ghidra.util.xml.XmlUtilities;
|
||||
|
||||
public class ObjectContainer implements Comparable {
|
||||
|
@ -43,9 +37,6 @@ public class ObjectContainer implements Comparable {
|
|||
private final Map<String, Object> attributeMap = new LinkedHashMap<>();
|
||||
private Set<ObjectContainer> currentChildren = new TreeSet<>();
|
||||
|
||||
public final ListenerSet<ObjectContainerListener> listeners =
|
||||
new ListenerSet<>(ObjectContainerListener.class);
|
||||
|
||||
private boolean immutable;
|
||||
private boolean visible = true;
|
||||
private boolean isSubscribed = false;
|
||||
|
@ -171,29 +162,14 @@ public class ObjectContainer implements Comparable {
|
|||
*/
|
||||
|
||||
public CompletableFuture<ObjectContainer> getOffspring() {
|
||||
if (targetObjectRef == null) {
|
||||
return null;
|
||||
if (targetObject == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
AtomicReference<TargetObject> to = new AtomicReference<>();
|
||||
AtomicReference<Map<String, ? extends TargetObject>> elements = new AtomicReference<>();
|
||||
AtomicReference<Map<String, ?>> attributes = new AtomicReference<>();
|
||||
return sequence(TypeSpec.cls(ObjectContainer.class)).then(seq -> {
|
||||
targetObjectRef.fetch().handle(seq::next);
|
||||
}, to).then(seq -> {
|
||||
targetObject = to.get();
|
||||
AsyncFence fence = new AsyncFence();
|
||||
fence.include(targetObject.fetchElements(true)
|
||||
.thenCompose(DebugModelConventions::fetchAll)
|
||||
.thenAccept(elements::set));
|
||||
fence.include(targetObject.fetchAttributes(true)
|
||||
.thenCompose(attrs -> DebugModelConventions.fetchObjAttrs(targetObject, attrs))
|
||||
.thenAccept(attributes::set));
|
||||
fence.ready().handle(seq::next);
|
||||
}).then(seq -> {
|
||||
rebuildContainers(elements.get(), attributes.get());
|
||||
return targetObject.resync(true, true).thenApply(__ -> {
|
||||
rebuildContainers(targetObject.getCachedElements(), targetObject.getCachedAttributes());
|
||||
propagateProvider(provider);
|
||||
seq.exit(this);
|
||||
}).finish();
|
||||
return this;
|
||||
});
|
||||
}
|
||||
|
||||
protected void checkAutoRecord() {
|
||||
|
@ -375,10 +351,6 @@ public class ObjectContainer implements Comparable {
|
|||
this.provider = newProvider;
|
||||
provider.addTargetToMap(this);
|
||||
}
|
||||
this.addListener(provider);
|
||||
//if (targetObject != null && !currentChildren.isEmpty()) {
|
||||
// targetObject.addListener(provider);
|
||||
//}
|
||||
for (ObjectContainer c : currentChildren) {
|
||||
c.propagateProvider(provider);
|
||||
}
|
||||
|
@ -534,14 +506,6 @@ public class ObjectContainer implements Comparable {
|
|||
this.immutable = immutable;
|
||||
}
|
||||
|
||||
public void addListener(ObjectContainerListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(ObjectContainerListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
@ -561,18 +525,10 @@ public class ObjectContainer implements Comparable {
|
|||
|
||||
public void subscribe() {
|
||||
isSubscribed = true;
|
||||
if (targetObject != null && provider != null) {
|
||||
targetObject.addListener(provider);
|
||||
provider.addListener(targetObject);
|
||||
}
|
||||
}
|
||||
|
||||
public void unsubscribe() {
|
||||
isSubscribed = false;
|
||||
targetObject.removeListener(provider);
|
||||
if (provider.isAutorecord()) {
|
||||
//provider.stopRecording(targetObject);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isModified() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture;
|
|||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
|
@ -156,6 +157,11 @@ public class DummyTargetObject implements TargetObject {
|
|||
return kind;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> resync(boolean attributes, boolean elements) {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<? extends Map<String, ? extends TargetObject>> fetchElements() {
|
||||
// Why not completedFuture(elements)?
|
||||
|
|
|
@ -34,7 +34,6 @@ public class ObjectAttributeRow {
|
|||
ref.fetch().handle(seq::next);
|
||||
}, targetObject).then(seq -> {
|
||||
to = targetObject.get();
|
||||
to.addListener(provider);
|
||||
}).finish();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ public class DebuggerModelServicePlugin extends Plugin
|
|||
|
||||
protected TargetObjectListener forRemoval = new TargetObjectListener() {
|
||||
@Override
|
||||
public void invalidated(TargetObject object, String reason) {
|
||||
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||
synchronized (listenersByModel) {
|
||||
ListenersForRemovalAndFocus listener = listenersByModel.remove(model);
|
||||
if (listener == null) {
|
||||
|
@ -125,7 +125,7 @@ public class DebuggerModelServicePlugin extends Plugin
|
|||
}
|
||||
r.addListener(this.forRemoval);
|
||||
if (!r.isValid()) {
|
||||
forRemoval.invalidated(root, "Who knows?");
|
||||
forRemoval.invalidated(root, root, "Who knows?");
|
||||
}
|
||||
CompletableFuture<? extends TargetFocusScope<?>> findSuitable =
|
||||
DebugModelConventions.findSuitable(TargetFocusScope.tclass, r);
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.junit.Test;
|
|||
|
||||
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceInternal;
|
||||
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceProxyPlugin;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.DebuggerModelFactory;
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
||||
|
@ -35,7 +34,9 @@ import help.screenshot.GhidraScreenShotGenerator;
|
|||
|
||||
public class DebuggerTargetsPluginScreenShots extends GhidraScreenShotGenerator {
|
||||
|
||||
@FactoryDescription(brief = "Demo Debugger", htmlDetails = "A connection for demonstration purposes")
|
||||
@FactoryDescription(
|
||||
brief = "Demo Debugger",
|
||||
htmlDetails = "A connection for demonstration purposes")
|
||||
protected static class ScreenShotDebuggerModelFactory implements DebuggerModelFactory {
|
||||
|
||||
private void nop() {
|
||||
|
@ -63,6 +64,7 @@ public class DebuggerTargetsPluginScreenShots extends GhidraScreenShotGenerator
|
|||
|
||||
public ScreenShotDebuggerObjectModel(String display) {
|
||||
this.display = display;
|
||||
addModelRoot(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -79,11 +81,6 @@ public class DebuggerTargetsPluginScreenShots extends GhidraScreenShotGenerator
|
|||
public AddressFactory getAddressFactory() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> close() {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
}
|
||||
|
||||
DebuggerModelServiceInternal modelService;
|
||||
|
|
|
@ -19,6 +19,7 @@ import java.lang.ref.Cleaner.Cleanable;
|
|||
import java.lang.ref.WeakReference;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
|
@ -166,6 +167,9 @@ public class AsyncReference<T, C> {
|
|||
try {
|
||||
listener.accept(oldVal, newVal, cause);
|
||||
}
|
||||
catch (RejectedExecutionException exc) {
|
||||
Msg.trace(this, "Ignoring rejection", exc);
|
||||
}
|
||||
catch (Throwable exc) {
|
||||
Msg.error(this, "Ignoring exception on async reference listener: ", exc);
|
||||
}
|
||||
|
|
|
@ -123,6 +123,7 @@ public abstract class AbstractAsyncServer<S extends AbstractAsyncServer<S, H>, H
|
|||
}
|
||||
}
|
||||
}
|
||||
group.shutdown();
|
||||
if (err != null) {
|
||||
throw err;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,18 @@
|
|||
*/
|
||||
package ghidra.dbg;
|
||||
|
||||
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
|
||||
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointListener;
|
||||
import ghidra.dbg.target.TargetEventScope.TargetEventScopeListener;
|
||||
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionStateListener;
|
||||
import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener;
|
||||
import ghidra.dbg.target.TargetInterpreter.TargetInterpreterListener;
|
||||
import ghidra.dbg.target.TargetMemory.TargetMemoryListener;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.TargetObject.TargetObjectListener;
|
||||
import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
/**
|
||||
* A listener for events related to the debugger model, usually a connection
|
||||
*
|
||||
|
@ -22,7 +34,19 @@ package ghidra.dbg;
|
|||
* TODO: Most (non-client) models do not implement this. Even the client ones do not implement
|
||||
* {@link #modelStateChanged()}
|
||||
*/
|
||||
public interface DebuggerModelListener {
|
||||
public interface DebuggerModelListener
|
||||
extends TargetObjectListener, TargetAccessibilityListener, TargetBreakpointListener,
|
||||
TargetInterpreterListener, TargetEventScopeListener, TargetExecutionStateListener,
|
||||
TargetFocusScopeListener, TargetMemoryListener, TargetRegisterBankListener {
|
||||
|
||||
/**
|
||||
* An error occurred such that this listener will no longer receive events
|
||||
*
|
||||
* @param t the exception describing the error
|
||||
*/
|
||||
default public void catastrophic(Throwable t) {
|
||||
Msg.error(this, "Catastrophic listener error", t);
|
||||
}
|
||||
|
||||
/**
|
||||
* The model has been successfully opened
|
||||
|
@ -33,6 +57,17 @@ public interface DebuggerModelListener {
|
|||
default public void modelOpened() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The root object has been added to the model
|
||||
*
|
||||
* <p>
|
||||
* This indicates the root is ready, not just {@link #created(TargetObject)}.
|
||||
*
|
||||
* @param root the root object
|
||||
*/
|
||||
default public void rootAdded(TargetObject root) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The model was closed
|
||||
*
|
||||
|
|
|
@ -18,7 +18,7 @@ package ghidra.dbg;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.async.TypeSpec;
|
||||
|
@ -146,9 +146,27 @@ public interface DebuggerObjectModel {
|
|||
/**
|
||||
* Add a listener for model events
|
||||
*
|
||||
* <p>
|
||||
* If requested, the listener is notified of existing objects via an event replay. It will first
|
||||
* replay all the created events in the same order they were originally emitted. Any objects
|
||||
* which have since been invalidated are excluded in the replay. They don't exist anymore, after
|
||||
* all. Next it will replay the attribute- and element-added events in post order. This is an
|
||||
* attempt to ensure an object's dependencies are met by the time the client receives its added
|
||||
* event. This isn't always possible due to cycles, but such cycles are usually informational.
|
||||
*
|
||||
* @param listener the listener
|
||||
* @param replay true to replay object tree events (doesn't include register or memory caches)
|
||||
*/
|
||||
public void addModelListener(DebuggerModelListener listener, boolean replay);
|
||||
|
||||
/**
|
||||
* Add a listener for model events, without replay
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void addModelListener(DebuggerModelListener listener);
|
||||
public default void addModelListener(DebuggerModelListener listener) {
|
||||
addModelListener(listener, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a model event listener
|
||||
|
@ -309,9 +327,18 @@ public interface DebuggerObjectModel {
|
|||
* object represents the debugger itself.
|
||||
*
|
||||
* @return the root
|
||||
* @deprecated use {@link #getModelRoot()} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public CompletableFuture<? extends TargetObject> fetchModelRoot();
|
||||
|
||||
/**
|
||||
* Get the root object of the model
|
||||
*
|
||||
* @return the root
|
||||
*/
|
||||
public TargetObject getModelRoot();
|
||||
|
||||
/**
|
||||
* Fetch the value at the given path
|
||||
*
|
||||
|
@ -362,6 +389,38 @@ public interface DebuggerObjectModel {
|
|||
return fetchModelValue(List.of(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at a given path
|
||||
*
|
||||
* <p>
|
||||
* If the path does not exist, null is returned. Note that an attempt to access the child of a
|
||||
* primitive is the same as accessing a path that does not exist; however, an error will be
|
||||
* logged, since this typically indicates a programming error.
|
||||
*
|
||||
* @param path the path
|
||||
* @return the value
|
||||
*/
|
||||
public default Object getModelValue(List<String> path) {
|
||||
Object cur = getModelRoot();
|
||||
for (String key : path) {
|
||||
if (cur == null) {
|
||||
return null;
|
||||
}
|
||||
if (!(cur instanceof TargetObject)) {
|
||||
Msg.error(this, "Primitive " + cur + " cannot have child '" + key + "'");
|
||||
return null;
|
||||
}
|
||||
TargetObject obj = (TargetObject) cur;
|
||||
if (PathUtils.isIndex(key)) {
|
||||
cur = obj.getCachedElements().get(PathUtils.parseIndex(key));
|
||||
continue;
|
||||
}
|
||||
assert PathUtils.isName(key);
|
||||
cur = obj.getCachedAttribute(key);
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the object with the given path
|
||||
*
|
||||
|
@ -392,11 +451,27 @@ public interface DebuggerObjectModel {
|
|||
|
||||
/**
|
||||
* @see #fetchModelObject(List)
|
||||
* @deprecated Use {@link #getModelObject(List)} instead, or {@link #fetchModelObject(List)} if
|
||||
* a refresh is needed
|
||||
*/
|
||||
@Deprecated
|
||||
public default CompletableFuture<? extends TargetObject> fetchModelObject(List<String> path) {
|
||||
return fetchModelObject(path, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object from the model
|
||||
*
|
||||
* <p>
|
||||
* Note this may return an object which is still being constructed, i.e., between being created
|
||||
* and being added to the model. This differs from {@link #getModelValue(List)}, which will only
|
||||
* return an object after it has been added.
|
||||
*
|
||||
* @param path the path of the object
|
||||
* @return the object
|
||||
*/
|
||||
public TargetObject getModelObject(List<String> path);
|
||||
|
||||
/**
|
||||
* @see #fetchModelObject(List)
|
||||
*/
|
||||
|
@ -498,50 +573,23 @@ public interface DebuggerObjectModel {
|
|||
if (ex == null || DebuggerModelTerminatingException.isIgnorable(ex)) {
|
||||
Msg.warn(origin, message + ": " + ex);
|
||||
}
|
||||
else if (AsyncUtils.unwrapThrowable(ex) instanceof RejectedExecutionException) {
|
||||
Msg.trace(origin, "Ignoring rejection", ex);
|
||||
}
|
||||
else {
|
||||
Msg.error(origin, message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the executor used to invoke client callback routines
|
||||
*
|
||||
* @return the executor
|
||||
*/
|
||||
Executor getClientExecutor();
|
||||
|
||||
/**
|
||||
* Ensure that dependent computations occur on the client executor
|
||||
* Permit all callbacks to be invoked before proceeding
|
||||
*
|
||||
* <p>
|
||||
* This also preserves scheduling order on the executor. Using just
|
||||
* {@link CompletableFuture#thenApplyAsync(java.util.function.Function)} makes no guarantees
|
||||
* about execution order, because that invocation could occur before invocations in the chained
|
||||
* actions. This one instead uses
|
||||
* {@link CompletableFuture#thenCompose(java.util.function.Function)} to schedule a final action
|
||||
* which performs the actual completion via the executor.
|
||||
* This operates by placing the request into the queue itself, so that any event callbacks
|
||||
* queued <em>at the time of the flush invocation</em> are completed first. There are no
|
||||
* guarantees with respect to events which get queued <em>after the flush invocation</em>.
|
||||
*
|
||||
* @param <T> the type of the future value
|
||||
* @param cf the future
|
||||
* @return a future gated via the client executor
|
||||
* @return a future which completes when all queued callbacks have been invoked
|
||||
*/
|
||||
default <T> CompletableFuture<T> gateFuture(CompletableFuture<T> cf) {
|
||||
return cf.thenCompose(this::gateFuture);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that dependent computations occur on the client executor
|
||||
*
|
||||
* <p>
|
||||
* Use as a method reference in a final call to
|
||||
* {@link CompletableFuture#thenCompose(java.util.function.Function)} to ensure the final stage
|
||||
* completes on the client executor.
|
||||
*
|
||||
* @param <T> the type of the future value
|
||||
* @param v the value
|
||||
* @return a future while completes with the given value on the client executor
|
||||
*/
|
||||
default <T> CompletableFuture<T> gateFuture(T v) {
|
||||
return CompletableFuture.supplyAsync(() -> v, getClientExecutor());
|
||||
}
|
||||
CompletableFuture<Void> flushEvents();
|
||||
}
|
||||
|
|
|
@ -15,29 +15,190 @@
|
|||
*/
|
||||
package ghidra.dbg.agent;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.DebuggerModelListener;
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.util.datastruct.ListenerSet;
|
||||
|
||||
public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectModel {
|
||||
protected final Executor clientExecutor = Executors.newSingleThreadExecutor();
|
||||
protected final Object lock = new Object();
|
||||
protected final ExecutorService clientExecutor = Executors.newSingleThreadExecutor();
|
||||
protected final ListenerSet<DebuggerModelListener> listeners =
|
||||
new ListenerSet<>(DebuggerModelListener.class, clientExecutor);
|
||||
|
||||
protected SpiTargetObject root;
|
||||
protected boolean rootAdded;
|
||||
protected CompletableFuture<SpiTargetObject> completedRoot = new CompletableFuture<>();
|
||||
|
||||
// Remember the order of creation events
|
||||
protected final Map<List<String>, SpiTargetObject> creationLog = new LinkedHashMap<>();
|
||||
|
||||
protected void objectCreated(SpiTargetObject object) {
|
||||
synchronized (lock) {
|
||||
creationLog.put(object.getPath(), object);
|
||||
if (object.isRoot()) {
|
||||
if (this.root != null) {
|
||||
throw new IllegalStateException("Already have a root");
|
||||
}
|
||||
this.root = object;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void objectInvalidated(TargetObject object) {
|
||||
synchronized (lock) {
|
||||
creationLog.remove(object);
|
||||
}
|
||||
}
|
||||
|
||||
protected void addModelRoot(SpiTargetObject root) {
|
||||
assert root == this.root;
|
||||
synchronized (lock) {
|
||||
rootAdded = true;
|
||||
}
|
||||
root.getSchema().validateTypeAndInterfaces(root, null, null, root.enforcesStrictSchema());
|
||||
this.completedRoot.completeAsync(() -> root, clientExecutor);
|
||||
listeners.fire.rootAdded(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addModelListener(DebuggerModelListener listener) {
|
||||
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
|
||||
return completedRoot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpiTargetObject getModelRoot() {
|
||||
synchronized (lock) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
protected void replayTreeEvents(DebuggerModelListener listener) {
|
||||
if (root == null) {
|
||||
assert creationLog.isEmpty();
|
||||
return;
|
||||
}
|
||||
for (SpiTargetObject object : creationLog.values()) {
|
||||
listener.created(object);
|
||||
}
|
||||
Set<SpiTargetObject> visited = new HashSet<>();
|
||||
for (SpiTargetObject object : creationLog.values()) {
|
||||
replayAddEvents(listener, object, visited);
|
||||
}
|
||||
if (rootAdded) {
|
||||
listener.rootAdded(root);
|
||||
}
|
||||
}
|
||||
|
||||
protected void replayAddEvents(DebuggerModelListener listener, SpiTargetObject object,
|
||||
Set<SpiTargetObject> visited) {
|
||||
if (!visited.add(object)) {
|
||||
return;
|
||||
}
|
||||
for (Object val : object.getCachedAttributes().values()) {
|
||||
if (!(val instanceof TargetObjectRef)) {
|
||||
continue;
|
||||
}
|
||||
assert val instanceof SpiTargetObject;
|
||||
replayAddEvents(listener, (SpiTargetObject) val, visited);
|
||||
}
|
||||
listener.attributesChanged(object, List.of(), object.getCachedAttributes());
|
||||
for (TargetObjectRef elem : object.getCachedElements().values()) {
|
||||
assert elem instanceof SpiTargetObject;
|
||||
replayAddEvents(listener, (SpiTargetObject) elem, visited);
|
||||
}
|
||||
listener.elementsChanged(object, List.of(), object.getCachedElements());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addModelListener(DebuggerModelListener listener, boolean replay) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
synchronized (lock) {
|
||||
if (replay) {
|
||||
replayTreeEvents(listener);
|
||||
}
|
||||
listeners.add(listener);
|
||||
}
|
||||
}, clientExecutor).exceptionally(ex -> {
|
||||
listener.catastrophic(ex);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeModelListener(DebuggerModelListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that dependent computations occur on the client executor
|
||||
*
|
||||
* <p>
|
||||
* Use as a method reference in a final call to
|
||||
* {@link CompletableFuture#thenCompose(java.util.function.Function)} to ensure the final stage
|
||||
* completes on the client executor.
|
||||
*
|
||||
* @param <T> the type of the future value
|
||||
* @param v the value
|
||||
* @return a future while completes with the given value on the client executor
|
||||
*/
|
||||
public <T> CompletableFuture<T> gateFuture(T v) {
|
||||
//Msg.debug(this, "Gate requested @" + System.identityHashCode(clientExecutor));
|
||||
//Msg.debug(this, " rvalue: " + v);
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
//Msg.debug(this, "Gate completing @" + System.identityHashCode(clientExecutor));
|
||||
//Msg.debug(this, " cvalue: " + v);
|
||||
return v;
|
||||
}, clientExecutor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Executor getClientExecutor() {
|
||||
return clientExecutor;
|
||||
public CompletableFuture<Void> flushEvents() {
|
||||
return gateFuture(null);
|
||||
//return CompletableFuture.supplyAsync(() -> gateFuture((Void) null)).thenCompose(f -> f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> close() {
|
||||
clientExecutor.shutdown();
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
|
||||
public void removeExisting(List<String> path) {
|
||||
TargetObject existing = getModelObject(path);
|
||||
// It had better be. This also checks for null
|
||||
if (existing == null) {
|
||||
return;
|
||||
}
|
||||
TargetObjectRef parent = existing.getParent();
|
||||
if (parent == null) {
|
||||
assert existing == root;
|
||||
throw new IllegalStateException("Cannot replace the root");
|
||||
}
|
||||
if (!path.equals(existing.getPath())) {
|
||||
return; // Is a link
|
||||
}
|
||||
if (parent instanceof DefaultTargetObject<?, ?>) { // It had better be
|
||||
DefaultTargetObject<?, ?> dtoParent = (DefaultTargetObject<?, ?>) parent;
|
||||
if (PathUtils.isIndex(path)) {
|
||||
dtoParent.changeElements(List.of(PathUtils.getIndex(path)), List.of(), "Replaced");
|
||||
}
|
||||
else {
|
||||
assert PathUtils.isName(path);
|
||||
dtoParent.changeAttributes(List.of(PathUtils.getKey(path)), Map.of(), "Replaced");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetObject getModelObject(List<String> path) {
|
||||
synchronized (lock) {
|
||||
return creationLog.get(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture;
|
|||
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.TypedTargetObject;
|
||||
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
|
@ -39,13 +40,20 @@ import ghidra.util.datastruct.ListenerSet;
|
|||
* @param <P> the type of the parent
|
||||
*/
|
||||
public abstract class AbstractTargetObject<P extends TargetObject>
|
||||
implements TargetObject, InvalidatableTargetObjectIf {
|
||||
implements SpiTargetObject {
|
||||
public static interface ProxyFactory<I> {
|
||||
SpiTargetObject createProxy(AbstractTargetObject<?> delegate, I info);
|
||||
}
|
||||
|
||||
protected static final ProxyFactory<Void> THIS_FACTORY = (d, i) -> d;
|
||||
|
||||
protected static final CompletableFuture<Map<String, TargetObject>> COMPLETED_EMPTY_ELEMENTS =
|
||||
CompletableFuture.completedFuture(Map.of());
|
||||
protected static final CompletableFuture<Map<String, Object>> COMPLETED_EMPTY_ATTRIBUTES =
|
||||
CompletableFuture.completedFuture(Map.of());
|
||||
|
||||
protected final DebuggerObjectModel model;
|
||||
protected final AbstractDebuggerObjectModel model;
|
||||
protected final SpiTargetObject proxy;
|
||||
protected final P parent;
|
||||
protected final CompletableFuture<P> completedParent;
|
||||
protected final List<String> path;
|
||||
|
@ -55,12 +63,15 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
|||
|
||||
protected boolean valid = true;
|
||||
|
||||
// TODO: Remove both of these, and just do invocations on model's listeners
|
||||
protected final ListenerSet<TargetObjectListener> listeners;
|
||||
|
||||
public AbstractTargetObject(DebuggerObjectModel model, P parent, String key, String typeHint,
|
||||
public <I> AbstractTargetObject(ProxyFactory<I> proxyFactory, I proxyInfo,
|
||||
AbstractDebuggerObjectModel model, P parent, String key, String typeHint,
|
||||
TargetObjectSchema schema) {
|
||||
this.listeners = new ListenerSet<>(TargetObjectListener.class, model.getClientExecutor());
|
||||
this.listeners = new ListenerSet<>(TargetObjectListener.class, model.clientExecutor);
|
||||
this.model = model;
|
||||
listeners.addChained(model.listeners);
|
||||
this.parent = parent;
|
||||
this.completedParent = CompletableFuture.completedFuture(parent);
|
||||
if (parent == null) {
|
||||
|
@ -69,10 +80,28 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
|||
else {
|
||||
this.path = PathUtils.extend(parent.getPath(), key);
|
||||
}
|
||||
|
||||
model.removeExisting(path);
|
||||
|
||||
this.hash = computeHashCode();
|
||||
this.typeHint = typeHint;
|
||||
|
||||
this.schema = schema;
|
||||
this.proxy = proxyFactory.createProxy(this, proxyInfo);
|
||||
|
||||
fireCreated();
|
||||
}
|
||||
|
||||
public AbstractTargetObject(AbstractDebuggerObjectModel model, P parent, String key,
|
||||
String typeHint, TargetObjectSchema schema) {
|
||||
this(THIS_FACTORY, null, model, parent, key, typeHint, schema);
|
||||
}
|
||||
|
||||
protected void fireCreated() {
|
||||
SpiTargetObject proxy = getProxy();
|
||||
assert proxy != null;
|
||||
model.objectCreated(proxy);
|
||||
listeners.fire.created(proxy);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,14 +111,23 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
|||
* Some implementations may use on a proxy-delegate pattern to implement target objects with
|
||||
* various combinations of supported interfaces. When this pattern is employed, the delegate
|
||||
* will extend {@link DefaultTargetObject}, causing {@code this} to refer to the delegate rather
|
||||
* than the proxy. When invoking listeners, the proxy given by this method is used instead. By
|
||||
* default, it simply returns {@code this}, providing the expected behavior for typical
|
||||
* implementations. The proxy is also used for schema interface validation.
|
||||
* than the proxy. When invoking listeners, the proxy given by this method is used instead. The
|
||||
* proxy is also used for schema interface validation.
|
||||
*
|
||||
* @return the proxy or this
|
||||
*/
|
||||
public TargetObject getProxy() {
|
||||
return this;
|
||||
public SpiTargetObject getProxy() {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public P getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends TypedTargetObject<T>> T as(Class<T> iface) {
|
||||
return DebuggerObjectModel.requireIface(iface, getProxy(), path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,7 +145,8 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
|||
*
|
||||
* @return true to throw exceptions on schema violations.
|
||||
*/
|
||||
protected boolean enforcesStrictSchema() {
|
||||
@Override
|
||||
public boolean enforcesStrictSchema() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -148,6 +187,9 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
|||
|
||||
@Override
|
||||
public void addListener(TargetObjectListener l) {
|
||||
if (!valid) {
|
||||
throw new IllegalStateException("Object is no longer valid: " + getProxy());
|
||||
}
|
||||
listeners.add(l);
|
||||
}
|
||||
|
||||
|
@ -157,7 +199,7 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
|||
}
|
||||
|
||||
@Override
|
||||
public DebuggerObjectModel getModel() {
|
||||
public AbstractDebuggerObjectModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
|
@ -212,38 +254,68 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
|||
return parent;
|
||||
}
|
||||
|
||||
protected void doInvalidate(String reason) {
|
||||
protected void doInvalidate(TargetObject branch, String reason) {
|
||||
valid = false;
|
||||
listeners.fire.invalidated(this, reason);
|
||||
model.objectInvalidated(getProxy());
|
||||
listeners.fire.invalidated(getProxy(), branch, reason);
|
||||
listeners.clear();
|
||||
}
|
||||
|
||||
protected void doInvalidateElements(Collection<?> elems, String reason) {
|
||||
for (Object e : elems) {
|
||||
if (e instanceof InvalidatableTargetObjectIf) {
|
||||
if (e instanceof InvalidatableTargetObjectIf && e instanceof TargetObject) {
|
||||
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) e;
|
||||
obj.invalidateSubtree(reason);
|
||||
obj.invalidateSubtree((TargetObject) e, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void doInvalidateAttributes(Map<String, ?> attrs, String reason) {
|
||||
protected void doInvalidateElements(TargetObject branch, Collection<?> elems, String reason) {
|
||||
for (Object e : elems) {
|
||||
if (e instanceof InvalidatableTargetObjectIf) {
|
||||
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) e;
|
||||
obj.invalidateSubtree(branch, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void doInvalidateAttributes(Map<String, ?> attrs,
|
||||
String reason) {
|
||||
for (Map.Entry<String, ?> ent : attrs.entrySet()) {
|
||||
String name = ent.getKey();
|
||||
Object a = ent.getValue();
|
||||
if (a instanceof InvalidatableTargetObjectIf && a instanceof TargetObject) {
|
||||
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) a;
|
||||
if (!PathUtils.isLink(getPath(), name, obj.getPath())) {
|
||||
obj.invalidateSubtree((TargetObject) a, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void doInvalidateAttributes(TargetObject branch, Map<String, ?> attrs,
|
||||
String reason) {
|
||||
for (Map.Entry<String, ?> ent : attrs.entrySet()) {
|
||||
String name = ent.getKey();
|
||||
Object a = ent.getValue();
|
||||
if (a instanceof InvalidatableTargetObjectIf) {
|
||||
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) a;
|
||||
if (!PathUtils.isLink(getPath(), name, obj.getPath())) {
|
||||
obj.invalidateSubtree(reason);
|
||||
obj.invalidateSubtree(branch, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateSubtree(String reason) {
|
||||
public void invalidateSubtree(TargetObject branch, String reason) {
|
||||
// Pre-ordered traversal
|
||||
doInvalidate(reason);
|
||||
doInvalidateElements(getCachedElements().values(), reason);
|
||||
doInvalidateAttributes(getCachedAttributes(), reason);
|
||||
doInvalidate(branch, reason);
|
||||
doInvalidateElements(branch, getCachedElements().values(), reason);
|
||||
doInvalidateAttributes(branch, getCachedAttributes(), reason);
|
||||
}
|
||||
|
||||
public ListenerSet<TargetObjectListener> getListeners() {
|
||||
return listeners;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,11 +24,11 @@ import ghidra.dbg.target.schema.TargetObjectSchema;
|
|||
public class DefaultTargetModelRoot extends DefaultTargetObject<TargetObject, TargetObject>
|
||||
implements TargetAggregate {
|
||||
|
||||
public DefaultTargetModelRoot(DebuggerObjectModel model, String typeHint) {
|
||||
public DefaultTargetModelRoot(AbstractDebuggerObjectModel model, String typeHint) {
|
||||
this(model, typeHint, EnumerableTargetObjectSchema.OBJECT);
|
||||
}
|
||||
|
||||
public DefaultTargetModelRoot(DebuggerObjectModel model, String typeHint,
|
||||
public DefaultTargetModelRoot(AbstractDebuggerObjectModel model, String typeHint,
|
||||
TargetObjectSchema schema) {
|
||||
super(model, null, null, typeHint, schema);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ import ghidra.dbg.util.CollectionUtils.Delta;
|
|||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.datastruct.ListenerSet;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link TargetObject} suitable for cases where the implementation
|
||||
|
@ -59,7 +58,8 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
* @param key the key (attribute name or element index) of this object
|
||||
* @param typeHint the type hint for this object
|
||||
*/
|
||||
public DefaultTargetObject(DebuggerObjectModel model, P parent, String key, String typeHint) {
|
||||
public DefaultTargetObject(AbstractDebuggerObjectModel model, P parent, String key,
|
||||
String typeHint) {
|
||||
this(model, parent, key, typeHint, parent.getSchema().getChildSchema(key));
|
||||
}
|
||||
|
||||
|
@ -88,14 +88,48 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
* @param typeHint the type hint for this object
|
||||
* @param schema the schema of this object
|
||||
*/
|
||||
public DefaultTargetObject(DebuggerObjectModel model, P parent, String key, String typeHint,
|
||||
public DefaultTargetObject(AbstractDebuggerObjectModel model, P parent, String key,
|
||||
String typeHint, TargetObjectSchema schema) {
|
||||
this(THIS_FACTORY, null, model, parent, key, typeHint, schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new (delegate) default target object
|
||||
*
|
||||
* <p>
|
||||
* This behaves similarly to
|
||||
* {@link #DefaultTargetObject(AbstractDebuggerObjectModel, TargetObject, String, String, TargetObjectSchema)}
|
||||
* when this object is meant to be the delegate of a proxy. The {@code proxyFactory} and
|
||||
* {@code proxyInfo} arguments are necessary to sidestep Java's insistence that the
|
||||
* super-constructor be invoked first. It allows information to be passed straight to the
|
||||
* factory. Using method overrides doesn't work, because the factory method gets called during
|
||||
* construction, before extensions have a chance to initialize fields, on which the proxy
|
||||
* inevitably depends.
|
||||
*
|
||||
* @param proxyFactory a factory to create the proxy, invoked in the super constructor
|
||||
* @param proxyInfo additional information passed to the proxy factory
|
||||
* @param model the model to which the object belongs
|
||||
* @param parent the parent of this object
|
||||
* @param key the key (attribute name or element index) of this object
|
||||
* @param typeHint the type hint for this object
|
||||
* @param schema the schema of this object
|
||||
*/
|
||||
public <I> DefaultTargetObject(ProxyFactory<I> proxyFactory, I proxyInfo,
|
||||
AbstractDebuggerObjectModel model, P parent, String key, String typeHint,
|
||||
TargetObjectSchema schema) {
|
||||
super(model, parent, key, typeHint, schema);
|
||||
changeAttributes(List.of(), List.of(), Map.of(DISPLAY_ATTRIBUTE_NAME,
|
||||
key == null ? "<root>" : key, UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED),
|
||||
super(proxyFactory, proxyInfo, model, parent, key, typeHint, schema);
|
||||
changeAttributes(List.of(), List.of(), Map.ofEntries(
|
||||
Map.entry(DISPLAY_ATTRIBUTE_NAME, key == null ? "<root>" : key),
|
||||
Map.entry(UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED)),
|
||||
"Initialized");
|
||||
}
|
||||
|
||||
public <I> DefaultTargetObject(ProxyFactory<I> proxyFactory, I proxyInfo,
|
||||
AbstractDebuggerObjectModel model, P parent, String key, String typeHint) {
|
||||
this(proxyFactory, proxyInfo, model, parent, key, typeHint,
|
||||
parent.getSchema().getChildSchema(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this object is being observed
|
||||
*
|
||||
|
@ -115,11 +149,20 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
* messaging is involved.
|
||||
*
|
||||
* @return true if there is at least one listener on this object
|
||||
* @deprecated Since the addition of model listeners, everything is always observed
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
protected boolean isObserved() {
|
||||
return !listeners.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> resync(boolean refreshAttributes, boolean refreshElements) {
|
||||
return CompletableFuture.allOf(
|
||||
fetchAttributes(refreshAttributes),
|
||||
fetchElements(refreshElements));
|
||||
}
|
||||
|
||||
/**
|
||||
* The elements for this object need to be updated, optionally invalidating caches
|
||||
*
|
||||
|
@ -159,11 +202,11 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
synchronized (elements) {
|
||||
if (refresh || curElemsRequest == null || curElemsRequest.isCompletedExceptionally() ||
|
||||
getUpdateMode() == TargetUpdateMode.SOLICITED) {
|
||||
curElemsRequest = requestElements(refresh);
|
||||
curElemsRequest = requestElements(refresh).thenCompose(model::gateFuture);
|
||||
}
|
||||
req = curElemsRequest;
|
||||
}
|
||||
return req.thenApply(__ -> getCachedElements()).thenCompose(model::gateFuture);
|
||||
return req.thenApply(__ -> getCachedElements());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -173,7 +216,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
|
||||
@Override
|
||||
public Map<String, E> getCachedElements() {
|
||||
synchronized (elements) {
|
||||
synchronized (model.lock) {
|
||||
return Map.copyOf(elements);
|
||||
}
|
||||
}
|
||||
|
@ -234,7 +277,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
|
||||
private Delta<E, E> setElements(Map<String, E> elements, String reason) {
|
||||
Delta<E, E> delta;
|
||||
synchronized (this.elements) {
|
||||
synchronized (model.lock) {
|
||||
delta = Delta.computeAndSet(this.elements, elements, Delta.SAME);
|
||||
}
|
||||
TargetObjectSchema schemax = getSchema();
|
||||
|
@ -279,7 +322,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
private Delta<E, E> changeElements(Collection<String> remove, Map<String, E> add,
|
||||
String reason) {
|
||||
Delta<E, E> delta;
|
||||
synchronized (elements) {
|
||||
synchronized (model.lock) {
|
||||
delta = Delta.apply(this.elements, remove, add, Delta.SAME);
|
||||
}
|
||||
TargetObjectSchema schemax = getSchema();
|
||||
|
@ -326,18 +369,18 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
synchronized (attributes) {
|
||||
// update_mode does not affect attributes. They always behave as if UNSOLICITED.
|
||||
if (refresh || curAttrsRequest == null || curAttrsRequest.isCompletedExceptionally()) {
|
||||
curAttrsRequest = requestAttributes(refresh);
|
||||
curAttrsRequest = requestAttributes(refresh).thenCompose(model::gateFuture);
|
||||
}
|
||||
req = curAttrsRequest;
|
||||
}
|
||||
return req.thenApply(__ -> {
|
||||
synchronized (attributes) {
|
||||
synchronized (model.lock) {
|
||||
if (schema != null) { // TODO: Remove this. Schema should never be null.
|
||||
schema.validateRequiredAttributes(this, enforcesStrictSchema());
|
||||
}
|
||||
return getCachedAttributes();
|
||||
}
|
||||
}).thenCompose(model::gateFuture);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -347,14 +390,14 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
|
||||
@Override
|
||||
public Map<String, ?> getCachedAttributes() {
|
||||
synchronized (attributes) {
|
||||
synchronized (model.lock) {
|
||||
return Map.copyOf(attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCachedAttribute(String name) {
|
||||
synchronized (attributes) {
|
||||
synchronized (model.lock) {
|
||||
return attributes.get(name);
|
||||
}
|
||||
}
|
||||
|
@ -405,7 +448,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
*/
|
||||
public Delta<?, ?> setAttributes(Map<String, ?> attributes, String reason) {
|
||||
Delta<?, ?> delta;
|
||||
synchronized (this.attributes) {
|
||||
synchronized (model.lock) {
|
||||
delta = Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL);
|
||||
}
|
||||
TargetObjectSchema schemax = getSchema();
|
||||
|
@ -450,7 +493,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
*/
|
||||
public Delta<?, ?> changeAttributes(List<String> remove, Map<String, ?> add, String reason) {
|
||||
Delta<?, ?> delta;
|
||||
synchronized (attributes) {
|
||||
synchronized (model.lock) {
|
||||
delta = Delta.apply(this.attributes, remove, add, Delta.EQUAL);
|
||||
}
|
||||
TargetObjectSchema schemax = getSchema();
|
||||
|
@ -463,8 +506,4 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
|||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
public ListenerSet<TargetObjectListener> getListeners() {
|
||||
return listeners;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ public interface SpiDebuggerObjectModel extends DebuggerObjectModel {
|
|||
return new DefaultTargetObjectRef(this, path);
|
||||
}
|
||||
|
||||
public default CompletableFuture<Object> fetchFreshChild(TargetObject obj, String key) {
|
||||
public static CompletableFuture<Object> fetchFreshChild(TargetObject obj, String key) {
|
||||
if (PathUtils.isIndex(key)) {
|
||||
return obj.fetchElements(true).thenApply(elements -> {
|
||||
return elements.get(PathUtils.parseIndex(key));
|
||||
|
@ -54,7 +54,7 @@ public interface SpiDebuggerObjectModel extends DebuggerObjectModel {
|
|||
});
|
||||
}
|
||||
|
||||
public default CompletableFuture<Object> fetchSuccessorValue(TargetObject obj,
|
||||
public static CompletableFuture<Object> fetchSuccessorValue(TargetObject obj,
|
||||
List<String> path, boolean refresh, boolean followLinks) {
|
||||
if (path.isEmpty()) {
|
||||
return CompletableFuture.completedFuture(obj);
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.dbg.agent;
|
||||
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
|
||||
public interface SpiTargetObject extends TargetObject, InvalidatableTargetObjectIf {
|
||||
@Override
|
||||
AbstractDebuggerObjectModel getModel();
|
||||
|
||||
// TODO:
|
||||
//@Override
|
||||
//Map<String, ? extends SpiTargetObject> getCachedElements();
|
||||
|
||||
boolean enforcesStrictSchema();
|
||||
}
|
|
@ -40,7 +40,11 @@ import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
|
|||
* <p>
|
||||
* Note that it is OK for more than one {@link TargetObjectRef} to refer to the same path. These
|
||||
* objects must override {@link #equals(Object)} and {@link #hashCode()}.
|
||||
*
|
||||
* @deprecated Use {@link TargetObjectPath} for model-bound path manipulation instead. Models should
|
||||
* not longer return nor push stubs, but actual objects.
|
||||
*/
|
||||
@Deprecated
|
||||
public interface TargetObjectRef extends Comparable<TargetObjectRef> {
|
||||
|
||||
/**
|
||||
|
@ -143,7 +147,10 @@ public interface TargetObjectRef extends Comparable<TargetObjectRef> {
|
|||
* Get the actual object
|
||||
*
|
||||
* @return a future which completes with the object
|
||||
* @deprecated Just cast straight to {@link TargetObject}. There should never exist a
|
||||
* {@link TargetObjectRef} that is not already a {@link TargetObject}, anymore.
|
||||
*/
|
||||
@Deprecated
|
||||
public default CompletableFuture<? extends TargetObject> fetch() {
|
||||
return getModel().fetchModelObject(getPath());
|
||||
}
|
||||
|
@ -298,8 +305,20 @@ public interface TargetObjectRef extends Comparable<TargetObjectRef> {
|
|||
* does not exist
|
||||
*/
|
||||
public default CompletableFuture<?> fetchAttribute(String name) {
|
||||
if (!PathUtils.isInvocation(name)) {
|
||||
return fetchAttributes().thenApply(m -> m.get(name));
|
||||
}
|
||||
// TODO: Make a type for the invocation and parse arguments better?
|
||||
Entry<String, String> invocation = PathUtils.parseInvocation(name);
|
||||
return fetchAttribute(invocation.getKey()).thenCompose(obj -> {
|
||||
if (!(obj instanceof TargetMethod<?>)) {
|
||||
throw new DebuggerModelTypeException(invocation.getKey() + " is not a method");
|
||||
}
|
||||
TargetMethod<?> method = (TargetMethod<?>) obj;
|
||||
// Just blindly invoke and let it sort it out
|
||||
return method.invoke(Map.of("arg", invocation.getValue()));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the elements of this object
|
||||
|
|
|
@ -22,6 +22,13 @@ import ghidra.dbg.DebuggerObjectModel;
|
|||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.TypedTargetObject;
|
||||
|
||||
/**
|
||||
* A reference having a known or expected type
|
||||
*
|
||||
* @param <T> the type
|
||||
* @deprecated I don't think this adds any real value.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public interface TypedTargetObjectRef<T extends TargetObject> extends TargetObjectRef {
|
||||
public class CastingTargetObjectRef<T extends TypedTargetObject<T>>
|
||||
implements TypedTargetObjectRef<T> {
|
||||
|
|
|
@ -74,22 +74,26 @@ public interface TargetConsole<T extends TargetConsole<T>> extends TypedTargetOb
|
|||
*/
|
||||
default void consoleOutput(TargetObject console, Channel channel, byte[] data) {
|
||||
}
|
||||
}
|
||||
|
||||
public interface TargetTextConsoleListener extends TargetConsoleListener {
|
||||
/**
|
||||
* The console has produced output
|
||||
*
|
||||
* @implNote Overriding this method is not a substitute for overriding
|
||||
* {@link #consoleOutput(TargetObject, Channel, byte[])}. Some models may invoke
|
||||
* this {@code String} variant as a convenience, which by default, invokes the
|
||||
* {@code byte[]} variant, but models are only expected to invoke the
|
||||
* {@code byte[]} variant. A client may override this method simply to avoid
|
||||
* back-and-forth conversions between {@code String}s and {@code byte[]}s.
|
||||
*
|
||||
* @param console the console producing the output
|
||||
* @param channel identifies the "output stream", stdout or stderr
|
||||
* @param text the output text
|
||||
*/
|
||||
default void consoleOutput(TargetObject console, Channel channel, String text) {
|
||||
consoleOutput(console, channel, text.getBytes(CHARSET));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
default void consoleOutput(TargetObject console, Channel channel, byte[] data) {
|
||||
consoleOutput(console, channel, new String(data, CHARSET));
|
||||
}
|
||||
public interface TargetTextConsoleListener extends TargetConsoleListener {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -529,6 +529,30 @@ public interface TargetObject extends TargetObjectRef {
|
|||
return CompletableFuture.completedFuture(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the children of this object
|
||||
*
|
||||
* <p>
|
||||
* This is necessary when {@link #getUpdateMode()} is {@link TargetUpdateMode#SOLICITED}. It is
|
||||
* also useful when the user believes things are out of sync. This causes the model to update
|
||||
* its attributes and/or elements. If either of the {@code refresh} parameters are set, the
|
||||
* model should be aggressive in ensuring its caches are up to date.
|
||||
*
|
||||
* @param refreshAttributes ask the model to refresh attributes, querying the debugger if needed
|
||||
* @param refreshElements as the model to refresh elements, querying the debugger if needed
|
||||
* @return a future which completes when the children are updated.
|
||||
*/
|
||||
CompletableFuture<Void> resync(boolean refreshAttributes, boolean refreshElements);
|
||||
|
||||
/**
|
||||
* Refresh the elements of this object
|
||||
*
|
||||
* @return a future which completes when the children are updated.
|
||||
*/
|
||||
default CompletableFuture<Void> resync() {
|
||||
return resync(false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the (usually opaque) identifier that the underlying connection uses for this object
|
||||
*
|
||||
|
@ -585,6 +609,14 @@ public interface TargetObject extends TargetObjectRef {
|
|||
return getCachedAttributes().get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
default CompletableFuture<?> fetchAttribute(String name) {
|
||||
if (PathUtils.isInvocation(name) && getCachedAttributes().containsKey(name)) {
|
||||
return CompletableFuture.completedFuture(getCachedAttributes().get(name));
|
||||
}
|
||||
return TargetObjectRef.super.fetchAttribute(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the named attribute to the given type, if possible
|
||||
*
|
||||
|
@ -675,13 +707,19 @@ public interface TargetObject extends TargetObjectRef {
|
|||
}
|
||||
|
||||
public interface TargetObjectListener {
|
||||
|
||||
/**
|
||||
* The object's display string has changed
|
||||
* The object was created
|
||||
*
|
||||
* @param object the object
|
||||
* @param display the new display string
|
||||
* <p>
|
||||
* This can only be received by listening on the model. While the created object can now
|
||||
* appear in other callbacks, it should not be used aside from those callbacks, until it is
|
||||
* added to its parent. Until that time, the object may not adhere to the schema, since its
|
||||
* children are still being initialized.
|
||||
*
|
||||
* @param object the newly-created object
|
||||
*/
|
||||
default void displayChanged(TargetObject object, String display) {
|
||||
default void created(TargetObject object) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -704,9 +742,19 @@ public interface TargetObject extends TargetObjectRef {
|
|||
* mistakenly applied to the replacement or its successors.
|
||||
*
|
||||
* @param object the now-invalid object
|
||||
* @param branch the root of the sub-tree being invalidated
|
||||
* @param reason an informational, human-consumable reason, if applicable
|
||||
*/
|
||||
default void invalidated(TargetObject object, String reason) {
|
||||
default void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The object's display string has changed
|
||||
*
|
||||
* @param object the object
|
||||
* @param display the new display string
|
||||
*/
|
||||
default void displayChanged(TargetObject object, String display) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -760,7 +808,11 @@ public interface TargetObject extends TargetObjectRef {
|
|||
|
||||
/**
|
||||
* An adapter which automatically gets new children from the model
|
||||
*
|
||||
* @deprecated {@link TargetObjectRef} is being deprecated, so this is no longer necessary. Just
|
||||
* cast refs to {@link TargetObject}
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public interface TargetObjectFetchingListener extends TargetObjectListener {
|
||||
@Override
|
||||
default void elementsChanged(TargetObject parent, Collection<String> removed,
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.dbg.target;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.dbg.util.PathUtils.PathComparator;
|
||||
|
||||
public class TargetObjectPath implements Comparable<TargetObjectPath> {
|
||||
protected final DebuggerObjectModel model;
|
||||
protected final List<String> keyList;
|
||||
protected final int hash;
|
||||
|
||||
public TargetObjectPath(DebuggerObjectModel model, List<String> keyList) {
|
||||
this.model = model;
|
||||
this.keyList = keyList;
|
||||
this.hash = Objects.hash(model, keyList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof TargetObjectPath)) {
|
||||
return false;
|
||||
}
|
||||
TargetObjectPath that = (TargetObjectPath) obj;
|
||||
return this.getModel() == that.getModel() &&
|
||||
Objects.equals(this.getKeyList(), that.getKeyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(TargetObjectPath that) {
|
||||
if (this == that) {
|
||||
return 0;
|
||||
}
|
||||
DebuggerObjectModel thisModel = this.getModel();
|
||||
DebuggerObjectModel thatModel = that.getModel();
|
||||
if (thisModel != thatModel) {
|
||||
if (thisModel == null) {
|
||||
return -1;
|
||||
}
|
||||
if (thatModel == null) {
|
||||
return 1;
|
||||
}
|
||||
int result = thisModel.toString().compareTo(thatModel.toString());
|
||||
if (result == 0) {
|
||||
return Integer.compare(
|
||||
System.identityHashCode(thisModel),
|
||||
System.identityHashCode(thatModel));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return PathComparator.KEYED.compare(this.getKeyList(), that.getKeyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("<%s in %s>", toPathString(), model);
|
||||
}
|
||||
|
||||
public DebuggerObjectModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public List<String> getKeyList() {
|
||||
return keyList;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return PathUtils.getKey(keyList);
|
||||
}
|
||||
|
||||
public String index() {
|
||||
return PathUtils.getIndex(keyList);
|
||||
}
|
||||
|
||||
public boolean isRoot() {
|
||||
return keyList.isEmpty();
|
||||
}
|
||||
|
||||
public CompletableFuture<TargetObject> fetch() {
|
||||
return model.fetchModelObject(getKeyList()).thenApply(obj -> obj);
|
||||
}
|
||||
|
||||
public String toPathString() {
|
||||
return PathUtils.toString(keyList);
|
||||
}
|
||||
|
||||
public TargetObjectPath parent() {
|
||||
List<String> pkl = PathUtils.parent(keyList);
|
||||
return pkl == null ? null : new TargetObjectPath(model, pkl);
|
||||
}
|
||||
|
||||
public TargetObjectPath successor(List<String> subKeyList) {
|
||||
return new TargetObjectPath(model, PathUtils.extend(keyList, subKeyList));
|
||||
}
|
||||
|
||||
public TargetObjectPath successor(String... subKeyList) {
|
||||
return successor(Arrays.asList(subKeyList));
|
||||
}
|
||||
}
|
|
@ -516,6 +516,24 @@ public enum PathUtils {
|
|||
return !Objects.equals(extend(parentPath, name), attributePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given element is a link.
|
||||
*
|
||||
* <p>
|
||||
* Consider an object {@code O} with an element {@code [1]}. {@code [1]}'s value is a link, iff
|
||||
* its path does <em>not</em> match that generated by extending {@code O}'s path with
|
||||
* {@code [1]}'s key.
|
||||
*
|
||||
* @param parentPath the path of the parent object of the given element
|
||||
* @param index the index of the given element
|
||||
* @param elementPath the canonical path of the element
|
||||
* @return true if the value is a link (i.e., it's object has a different path)
|
||||
*/
|
||||
public static boolean isElementLink(List<String> parentPath, String index,
|
||||
List<String> elementPath) {
|
||||
return !Objects.equals(index(parentPath, index), elementPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given attribute should be displayed.
|
||||
*
|
||||
|
|
|
@ -18,17 +18,20 @@ package ghidra.dbg.agent;
|
|||
import static ghidra.lifecycle.Unfinished.TODO;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.junit.Test;
|
||||
|
||||
import generic.Unique;
|
||||
import ghidra.async.AsyncTestUtils;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.DebuggerModelListener;
|
||||
import ghidra.dbg.attributes.TargetObjectRef;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.TargetRegisterBank;
|
||||
import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener;
|
||||
import ghidra.dbg.util.*;
|
||||
import ghidra.dbg.util.AttributesChangedListener.AttributesChangedInvocation;
|
||||
import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation;
|
||||
|
@ -39,16 +42,38 @@ import ghidra.program.model.address.AddressSpace;
|
|||
public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
||||
|
||||
public static class FakeTargetObject extends DefaultTargetObject<TargetObject, TargetObject> {
|
||||
public FakeTargetObject(DebuggerObjectModel model, TargetObject parent, String name) {
|
||||
public FakeTargetObject(AbstractDebuggerObjectModel model, TargetObject parent,
|
||||
String name) {
|
||||
super(model, parent, name, "Fake");
|
||||
}
|
||||
}
|
||||
|
||||
public static class FakeTargetRegisterBank<T extends FakeTargetRegisterBank<T>>
|
||||
extends FakeTargetObject implements TargetRegisterBank<T> {
|
||||
|
||||
public FakeTargetRegisterBank(AbstractDebuggerObjectModel model, TargetObject parent,
|
||||
String name) {
|
||||
super(model, parent, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<? extends Map<String, byte[]>> readRegistersNamed(
|
||||
Collection<String> names) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> writeRegistersNamed(Map<String, byte[]> values) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Functionally identical to a Fake, but intrinsically different
|
||||
*/
|
||||
public static class PhonyTargetObject extends DefaultTargetObject<TargetObject, TargetObject> {
|
||||
public PhonyTargetObject(DebuggerObjectModel model, TargetObject parent, String name) {
|
||||
public PhonyTargetObject(AbstractDebuggerObjectModel model, TargetObject parent,
|
||||
String name) {
|
||||
super(model, parent, name, "Phony");
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +81,10 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
|||
public static class FakeDebuggerObjectModel extends AbstractDebuggerObjectModel {
|
||||
DefaultTargetModelRoot root = new DefaultTargetModelRoot(this, "Root");
|
||||
|
||||
public FakeDebuggerObjectModel() {
|
||||
addModelRoot(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
|
||||
return CompletableFuture.completedFuture(root);
|
||||
|
@ -70,16 +99,11 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
|||
public AddressSpace getAddressSpace(String name) {
|
||||
return TODO();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> close() {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
}
|
||||
|
||||
static class OffThreadTargetObject extends DefaultTargetObject<TargetObject, TargetObject> {
|
||||
public OffThreadTargetObject(DebuggerObjectModel model, TargetObject parent, String name,
|
||||
String typeHint) {
|
||||
public OffThreadTargetObject(AbstractDebuggerObjectModel model, TargetObject parent,
|
||||
String name, String typeHint) {
|
||||
super(model, parent, name, typeHint);
|
||||
}
|
||||
|
||||
|
@ -149,59 +173,63 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
|||
fakeA.addListener(invL);
|
||||
|
||||
PhonyTargetObject phonyA = new PhonyTargetObject(model, model.root, "[A]");
|
||||
|
||||
// mere creation causes removal of old
|
||||
waitOn(elemL.count.waitValue(1));
|
||||
ElementsChangedInvocation changed1 = Unique.assertOne(elemL.invocations);
|
||||
assertSame(model.root, changed1.parent);
|
||||
assertEquals(Set.of("A"), changed1.removed);
|
||||
assertTrue(changed1.added.isEmpty());
|
||||
waitOn(invL.count.waitValue(1));
|
||||
InvalidatedInvocation invalidated = Unique.assertOne(invL.invocations);
|
||||
assertSame(fakeA, invalidated.object);
|
||||
|
||||
elemL.clear();
|
||||
invL.clear();
|
||||
model.root.setElements(List.of(phonyA), "Replace");
|
||||
|
||||
assertSame(phonyA, waitOn(model.fetchModelObject("[A]")));
|
||||
assertFalse(fakeA.isValid());
|
||||
|
||||
ElementsChangedInvocation changed = Unique.assertOne(elemL.invocations);
|
||||
assertSame(model.root, changed.parent);
|
||||
assertSame(phonyA, Unique.assertOne(changed.added.values()));
|
||||
|
||||
InvalidatedInvocation invalidated = Unique.assertOne(invL.invocations);
|
||||
assertSame(fakeA, invalidated.object);
|
||||
assertEquals("Replace", invalidated.reason);
|
||||
ElementsChangedInvocation changed2 = Unique.assertOne(elemL.invocations);
|
||||
assertSame(model.root, changed2.parent);
|
||||
assertSame(phonyA, Unique.assertOne(changed2.added.values()));
|
||||
assertTrue(changed2.removed.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeReplacement() throws Throwable {
|
||||
AttributesChangedListener attrL = new AttributesChangedListener();
|
||||
InvalidatedListener invL = new InvalidatedListener();
|
||||
|
||||
FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A");
|
||||
model.root.setAttributes(Map.of("A", fakeA), "Init");
|
||||
|
||||
String str1 = new String("EqualStrings");
|
||||
String str2 = new String("EqualStrings");
|
||||
model.root.setAttributes(Map.of("a", str1), "Init");
|
||||
model.root.addListener(attrL);
|
||||
fakeA.addListener(invL);
|
||||
|
||||
PhonyTargetObject phonyA = new PhonyTargetObject(model, model.root, "A");
|
||||
model.root.setAttributes(Map.of("A", phonyA), "Replace");
|
||||
// Note: mere object creation will cause "prior removal"
|
||||
// We'll do this test just with primitives
|
||||
// Should not cause replacement, since they're equal
|
||||
model.root.setAttributes(Map.of("a", str2), "Replace");
|
||||
waitOn(model.clientExecutor);
|
||||
|
||||
// Object-valued attribute replacement requires prior removal
|
||||
assertSame(fakeA, waitOn(model.fetchModelObject("A")));
|
||||
assertSame(str1, waitOn(model.fetchModelValue("a")));
|
||||
assertEquals(0, attrL.invocations.size());
|
||||
assertEquals(0, invL.invocations.size());
|
||||
|
||||
// Now, with prior removal
|
||||
// TODO: Should I permit custom equality check?
|
||||
model.root.setAttributes(Map.of(), "Clear");
|
||||
model.root.setAttributes(Map.of("A", phonyA), "Replace");
|
||||
model.root.setAttributes(Map.of("a", str2), "Replace");
|
||||
waitOn(model.clientExecutor);
|
||||
|
||||
assertEquals(2, attrL.invocations.size());
|
||||
AttributesChangedInvocation changed = attrL.invocations.get(0);
|
||||
assertEquals(model.root, changed.parent);
|
||||
assertSame("A", Unique.assertOne(changed.removed));
|
||||
assertEquals("a", Unique.assertOne(changed.removed));
|
||||
assertEquals(0, changed.added.size());
|
||||
changed = attrL.invocations.get(1);
|
||||
assertEquals(model.root, changed.parent);
|
||||
assertSame(phonyA, Unique.assertOne(changed.added.values()));
|
||||
assertSame(str2, Unique.assertOne(changed.added.values()));
|
||||
assertEquals(0, changed.removed.size());
|
||||
|
||||
InvalidatedInvocation invalidated = Unique.assertOne(invL.invocations);
|
||||
assertSame(fakeA, invalidated.object);
|
||||
assertEquals("Clear", invalidated.reason);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -223,4 +251,83 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
|||
|
||||
waitOn(invL.count.waitValue(3));
|
||||
}
|
||||
|
||||
public static class EventRecordingListener implements DebuggerModelListener {
|
||||
List<Pair<String, TargetObject>> record = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void created(TargetObject object) {
|
||||
record.add(new ImmutablePair<>("created", object));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void elementsChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ? extends TargetObjectRef> added) {
|
||||
for (TargetObjectRef elem : added.values()) {
|
||||
record.add(new ImmutablePair<>("addedElem", (TargetObject) elem));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attributesChanged(TargetObject parent, Collection<String> removed,
|
||||
Map<String, ?> added) {
|
||||
for (Object attr : added.values()) {
|
||||
if (attr instanceof TargetObject) {
|
||||
record.add(new ImmutablePair<>("addedAttr", (TargetObject) attr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registersUpdated(TargetRegisterBank<?> bank, Map<String, byte[]> updates) {
|
||||
record.add(new ImmutablePair<>("registersUpdated", bank));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreationAndModelListenerWithoutReplay() throws Throwable {
|
||||
EventRecordingListener listener = new EventRecordingListener();
|
||||
model.addModelListener(listener, false);
|
||||
waitOn(model.clientExecutor);
|
||||
|
||||
FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A");
|
||||
FakeTargetRegisterBank<?> fakeA1rb = new FakeTargetRegisterBank<>(model, fakeA, "[1]");
|
||||
fakeA1rb.listeners.fire(TargetRegisterBankListener.class)
|
||||
.registersUpdated(fakeA1rb, Map.of());
|
||||
fakeA.setElements(List.of(fakeA1rb), "Init");
|
||||
model.root.setAttributes(List.of(fakeA), Map.of(), "Init");
|
||||
|
||||
waitOn(model.clientExecutor);
|
||||
|
||||
assertEquals(List.of(
|
||||
new ImmutablePair<>("created", fakeA),
|
||||
new ImmutablePair<>("created", fakeA1rb),
|
||||
new ImmutablePair<>("registersUpdated", fakeA1rb),
|
||||
new ImmutablePair<>("addedElem", fakeA1rb),
|
||||
new ImmutablePair<>("addedAttr", fakeA)),
|
||||
listener.record);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddListenerWithReplay() throws Throwable {
|
||||
|
||||
FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A");
|
||||
FakeTargetRegisterBank<?> fakeA1rb = new FakeTargetRegisterBank<>(model, fakeA, "[1]");
|
||||
fakeA1rb.listeners.fire(TargetRegisterBankListener.class)
|
||||
.registersUpdated(fakeA1rb, Map.of());
|
||||
fakeA.setElements(List.of(fakeA1rb), "Init");
|
||||
model.root.setAttributes(List.of(fakeA), Map.of(), "Init");
|
||||
EventRecordingListener listener = new EventRecordingListener();
|
||||
model.addModelListener(listener, true);
|
||||
|
||||
waitOn(model.clientExecutor);
|
||||
|
||||
assertEquals(List.of(
|
||||
new ImmutablePair<>("created", model.root),
|
||||
new ImmutablePair<>("created", fakeA),
|
||||
new ImmutablePair<>("created", fakeA1rb),
|
||||
new ImmutablePair<>("addedElem", fakeA1rb),
|
||||
new ImmutablePair<>("addedAttr", fakeA)),
|
||||
listener.record);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,8 +46,13 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel {
|
|||
this("Session");
|
||||
}
|
||||
|
||||
public Executor getClientExecutor() {
|
||||
return clientExecutor;
|
||||
}
|
||||
|
||||
public TestDebuggerObjectModel(String rootHint) {
|
||||
this.session = new TestTargetSession(this, rootHint);
|
||||
addModelRoot(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -67,8 +72,8 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel {
|
|||
|
||||
@Override
|
||||
public CompletableFuture<Void> close() {
|
||||
session.invalidateSubtree("Model closed");
|
||||
return future(null);
|
||||
session.invalidateSubtree(session, "Model closed");
|
||||
return super.close().thenCompose(__ -> future(null));
|
||||
}
|
||||
|
||||
public TestTargetProcess addProcess(int pid) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -22,9 +22,7 @@ import java.util.concurrent.CompletableFuture;
|
|||
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.agent.DefaultTargetModelRoot;
|
||||
import ghidra.dbg.agent.DefaultTargetObject;
|
||||
import ghidra.dbg.agent.*;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
|
@ -53,7 +51,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
|
||||
@TargetObjectSchemaInfo
|
||||
static class TestAnnotatedTargetRootPlain extends DefaultTargetModelRoot {
|
||||
public TestAnnotatedTargetRootPlain(DebuggerObjectModel model, String typeHint) {
|
||||
public TestAnnotatedTargetRootPlain(AbstractDebuggerObjectModel model, String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +69,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
|
||||
@TargetObjectSchemaInfo(elements = @TargetElementType(type = Void.class))
|
||||
static class TestAnnotatedTargetRootNoElems extends DefaultTargetModelRoot {
|
||||
public TestAnnotatedTargetRootNoElems(DebuggerObjectModel model, String typeHint) {
|
||||
public TestAnnotatedTargetRootNoElems(AbstractDebuggerObjectModel model, String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
}
|
||||
|
@ -92,15 +90,15 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
static class TestAnnotatedTargetProcessStub
|
||||
extends DefaultTargetObject<TargetObject, TargetObject>
|
||||
implements TargetProcess<TestAnnotatedTargetProcessStub> {
|
||||
public TestAnnotatedTargetProcessStub(DebuggerObjectModel model, TargetObject parent,
|
||||
String key, String typeHint) {
|
||||
public TestAnnotatedTargetProcessStub(AbstractDebuggerObjectModel model,
|
||||
TargetObject parent, String key, String typeHint) {
|
||||
super(model, parent, key, typeHint);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetObjectSchemaInfo(name = "Root")
|
||||
static class TestAnnotatedTargetRootOverriddenFetchElems extends DefaultTargetModelRoot {
|
||||
public TestAnnotatedTargetRootOverriddenFetchElems(DebuggerObjectModel model,
|
||||
public TestAnnotatedTargetRootOverriddenFetchElems(AbstractDebuggerObjectModel model,
|
||||
String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
|
@ -131,7 +129,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
@TargetObjectSchemaInfo(name = "ProcessContainer")
|
||||
static class TestAnnotatedProcessContainer
|
||||
extends DefaultTargetObject<TestAnnotatedTargetProcessStub, TargetObject> {
|
||||
public TestAnnotatedProcessContainer(DebuggerObjectModel model, TargetObject parent,
|
||||
public TestAnnotatedProcessContainer(AbstractDebuggerObjectModel model, TargetObject parent,
|
||||
String key, String typeHint) {
|
||||
super(model, parent, key, typeHint);
|
||||
}
|
||||
|
@ -154,15 +152,15 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
static class TestAnnotatedTargetProcessParam<T>
|
||||
extends DefaultTargetObject<TargetObject, TargetObject>
|
||||
implements TargetProcess<TestAnnotatedTargetProcessParam<T>> {
|
||||
public TestAnnotatedTargetProcessParam(DebuggerObjectModel model, TargetObject parent,
|
||||
String key, String typeHint) {
|
||||
public TestAnnotatedTargetProcessParam(AbstractDebuggerObjectModel model,
|
||||
TargetObject parent, String key, String typeHint) {
|
||||
super(model, parent, key, typeHint);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetObjectSchemaInfo
|
||||
static class TestAnnotatedTargetRootWithAnnotatedAttrs extends DefaultTargetModelRoot {
|
||||
public TestAnnotatedTargetRootWithAnnotatedAttrs(DebuggerObjectModel model,
|
||||
public TestAnnotatedTargetRootWithAnnotatedAttrs(AbstractDebuggerObjectModel model,
|
||||
String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
|
@ -209,7 +207,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
@TargetElementType(index = "reserved", type = Void.class)
|
||||
})
|
||||
static class TestAnnotatedTargetRootWithListedAttrs extends DefaultTargetModelRoot {
|
||||
public TestAnnotatedTargetRootWithListedAttrs(DebuggerObjectModel model,
|
||||
public TestAnnotatedTargetRootWithListedAttrs(AbstractDebuggerObjectModel model,
|
||||
String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
|
@ -243,7 +241,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
@TargetObjectSchemaInfo
|
||||
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadType extends DefaultTargetModelRoot {
|
||||
|
||||
public TestAnnotatedTargetRootWithAnnotatedAttrsBadType(DebuggerObjectModel model,
|
||||
public TestAnnotatedTargetRootWithAnnotatedAttrsBadType(AbstractDebuggerObjectModel model,
|
||||
String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
|
@ -273,7 +271,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
static class TestAnnotatedTargetRootWithAnnotatedAttrsNonUnique<T extends Dummy & TargetProcess<T> & TargetInterpreter<T>>
|
||||
extends DefaultTargetModelRoot {
|
||||
|
||||
public TestAnnotatedTargetRootWithAnnotatedAttrsNonUnique(DebuggerObjectModel model,
|
||||
public TestAnnotatedTargetRootWithAnnotatedAttrsNonUnique(AbstractDebuggerObjectModel model,
|
||||
String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
|
@ -294,7 +292,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
static class TestAnnotatedTargetRootWithElemsNonUnique<T extends Dummy & TargetProcess<T> & TargetInterpreter<T>>
|
||||
extends DefaultTargetModelRoot {
|
||||
|
||||
public TestAnnotatedTargetRootWithElemsNonUnique(DebuggerObjectModel model,
|
||||
public TestAnnotatedTargetRootWithElemsNonUnique(AbstractDebuggerObjectModel model,
|
||||
String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
|
@ -314,7 +312,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
|
||||
@TargetObjectSchemaInfo
|
||||
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadName extends DefaultTargetModelRoot {
|
||||
public TestAnnotatedTargetRootWithAnnotatedAttrsBadName(DebuggerObjectModel model,
|
||||
public TestAnnotatedTargetRootWithAnnotatedAttrsBadName(AbstractDebuggerObjectModel model,
|
||||
String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
|
@ -333,7 +331,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
|
||||
@TargetObjectSchemaInfo
|
||||
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadGetter extends DefaultTargetModelRoot {
|
||||
public TestAnnotatedTargetRootWithAnnotatedAttrsBadGetter(DebuggerObjectModel model,
|
||||
public TestAnnotatedTargetRootWithAnnotatedAttrsBadGetter(AbstractDebuggerObjectModel model,
|
||||
String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
|
@ -353,7 +351,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
@TargetObjectSchemaInfo(
|
||||
attributes = @TargetAttributeType(name = "some_attr", type = NotAPrimitive.class))
|
||||
static class TestAnnotatedTargetRootWithListedAttrsBadType extends DefaultTargetModelRoot {
|
||||
public TestAnnotatedTargetRootWithListedAttrsBadType(DebuggerObjectModel model,
|
||||
public TestAnnotatedTargetRootWithListedAttrsBadType(AbstractDebuggerObjectModel model,
|
||||
String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import java.util.concurrent.ExecutionException;
|
|||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.agent.*;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.TargetObject.TargetUpdateMode;
|
||||
|
@ -61,12 +60,6 @@ public class TargetObjectSchemaValidationTest {
|
|||
fail();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> close() {
|
||||
fail();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
|
@ -107,29 +100,29 @@ public class TargetObjectSchemaValidationTest {
|
|||
}
|
||||
|
||||
static class ValidatedModelRoot extends DefaultTargetModelRoot {
|
||||
public ValidatedModelRoot(DebuggerObjectModel model, String typeHint,
|
||||
public ValidatedModelRoot(AbstractDebuggerObjectModel model, String typeHint,
|
||||
TargetObjectSchema schema) {
|
||||
super(model, typeHint, schema);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean enforcesStrictSchema() {
|
||||
public boolean enforcesStrictSchema() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static class ValidatedObject extends DefaultTargetObject<TargetObject, TargetObject> {
|
||||
public ValidatedObject(DebuggerObjectModel model, TargetObject parent, String key,
|
||||
public ValidatedObject(AbstractDebuggerObjectModel model, TargetObject parent, String key,
|
||||
TargetObjectSchema schema) {
|
||||
super(model, parent, key, "Object", schema);
|
||||
}
|
||||
|
||||
public ValidatedObject(DebuggerObjectModel model, TargetObject parent, String key) {
|
||||
public ValidatedObject(AbstractDebuggerObjectModel model, TargetObject parent, String key) {
|
||||
super(model, parent, key, "Object");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean enforcesStrictSchema() {
|
||||
public boolean enforcesStrictSchema() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,13 +15,12 @@
|
|||
*/
|
||||
package ghidra.dbg.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import ghidra.async.AsyncReference;
|
||||
|
||||
public abstract class AbstractInvocationListener<T> {
|
||||
public final List<T> invocations = new ArrayList<>();
|
||||
public final LinkedList<T> invocations = new LinkedList<>();
|
||||
public final AsyncReference<Integer, Void> count = new AsyncReference<>(0);
|
||||
|
||||
protected void record(T rec) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -23,10 +23,12 @@ public class InvalidatedListener extends
|
|||
AbstractInvocationListener<InvalidatedInvocation> implements TargetObjectListener {
|
||||
public static class InvalidatedInvocation {
|
||||
public final TargetObject object;
|
||||
public final TargetObject branch;
|
||||
public final String reason;
|
||||
|
||||
public InvalidatedInvocation(TargetObject object, String reason) {
|
||||
public InvalidatedInvocation(TargetObject object, TargetObject branch, String reason) {
|
||||
this.object = object;
|
||||
this.branch = branch;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
|
@ -37,7 +39,7 @@ public class InvalidatedListener extends
|
|||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(TargetObject object, String reason) {
|
||||
record(new InvalidatedInvocation(object, reason));
|
||||
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||
record(new InvalidatedInvocation(object, branch, reason));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package ghidra.util.datastruct;
|
|||
import java.lang.reflect.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import com.google.common.cache.*;
|
||||
|
@ -110,15 +111,27 @@ public class ListenerMap<K, P, V extends P> {
|
|||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
Collection<V> listenersVolatile = map.values();
|
||||
//Msg.debug(this, "Queuing invocation: " + method.getName() + " @" +
|
||||
// System.identityHashCode(executor));
|
||||
Collection<V> listenersVolatile;
|
||||
Set<ListenerMap<?, ? extends P, ?>> chainedVolatile;
|
||||
synchronized (lock) {
|
||||
listenersVolatile = map.values();
|
||||
chainedVolatile = chained;
|
||||
}
|
||||
for (V l : listenersVolatile) {
|
||||
if (!ext.isAssignableFrom(l.getClass())) {
|
||||
continue;
|
||||
}
|
||||
executor.execute(() -> {
|
||||
//Msg.debug(this,
|
||||
// "Invoking: " + method.getName() + " @" + System.identityHashCode(executor));
|
||||
try {
|
||||
method.invoke(l, args);
|
||||
}
|
||||
catch (RejectedExecutionException e) {
|
||||
Msg.trace(this, "Listener invocation rejected", e);
|
||||
}
|
||||
catch (InvocationTargetException e) {
|
||||
Throwable cause = e.getCause();
|
||||
reportError(l, cause);
|
||||
|
@ -128,13 +141,30 @@ public class ListenerMap<K, P, V extends P> {
|
|||
}
|
||||
});
|
||||
}
|
||||
for (ListenerMap<?, ? extends P, ?> c : chained) {
|
||||
// Invocation will check if assignable
|
||||
@SuppressWarnings("unchecked")
|
||||
T l = ((ListenerMap<?, P, ?>) c).fire(ext);
|
||||
try {
|
||||
method.invoke(l, args);
|
||||
}
|
||||
catch (InvocationTargetException e) {
|
||||
Throwable cause = e.getCause();
|
||||
reportError(l, cause);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
reportError(l, e);
|
||||
}
|
||||
}
|
||||
return null; // TODO: Assumes void return type
|
||||
}
|
||||
}
|
||||
|
||||
private final Object lock = new Object();
|
||||
private final Class<P> iface;
|
||||
private final Executor executor;
|
||||
private Map<K, V> map = createMap();
|
||||
private Set<ListenerMap<?, ? extends P, ?>> chained = new LinkedHashSet<>();
|
||||
|
||||
/**
|
||||
* A proxy which passes invocations to each value of this map
|
||||
|
@ -210,6 +240,7 @@ public class ListenerMap<K, P, V extends P> {
|
|||
}
|
||||
|
||||
public V put(K key, V val) {
|
||||
synchronized (lock) {
|
||||
if (map.get(key) == val) {
|
||||
return val;
|
||||
}
|
||||
|
@ -219,19 +250,23 @@ public class ListenerMap<K, P, V extends P> {
|
|||
map = newMap;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public void putAll(ListenerMap<? extends K, P, ? extends V> that) {
|
||||
synchronized (lock) {
|
||||
Map<K, V> newMap = createMap();
|
||||
newMap.putAll(map);
|
||||
newMap.putAll(that.map);
|
||||
map = newMap;
|
||||
}
|
||||
}
|
||||
|
||||
public V get(K key) {
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
public V remove(K key) {
|
||||
synchronized (lock) {
|
||||
if (!map.containsKey(key)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -241,11 +276,47 @@ public class ListenerMap<K, P, V extends P> {
|
|||
map = newMap;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
synchronized (lock) {
|
||||
if (map.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
map = createMap();
|
||||
}
|
||||
}
|
||||
|
||||
public void addChained(ListenerMap<?, ? extends P, ?> map) {
|
||||
synchronized (lock) {
|
||||
if (chained.contains(map)) {
|
||||
return;
|
||||
}
|
||||
Set<ListenerMap<?, ? extends P, ?>> newChained = new LinkedHashSet<>();
|
||||
newChained.addAll(chained);
|
||||
newChained.add(map);
|
||||
chained = newChained;
|
||||
}
|
||||
}
|
||||
|
||||
public void removeChained(ListenerMap<?, ?, ?> map) {
|
||||
synchronized (lock) {
|
||||
if (!chained.contains(map)) {
|
||||
return;
|
||||
}
|
||||
Set<ListenerMap<?, ? extends P, ?>> newChained = new LinkedHashSet<>();
|
||||
newChained.addAll(chained);
|
||||
newChained.remove(map);
|
||||
chained = newChained;
|
||||
}
|
||||
}
|
||||
|
||||
public void clearChained() {
|
||||
synchronized (lock) {
|
||||
if (chained.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
chained = new LinkedHashSet<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,4 +107,16 @@ public class ListenerSet<E> {
|
|||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
|
||||
public void addChained(ListenerSet<? extends E> set) {
|
||||
map.addChained(set.map);
|
||||
}
|
||||
|
||||
public void removeChained(ListenerSet<?> set) {
|
||||
map.removeChained(set.map);
|
||||
}
|
||||
|
||||
public void clearChained() {
|
||||
map.clearChained();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue