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>>
|
public interface DbgModelTargetEnvironment<T extends TargetEnvironment<T>>
|
||||||
extends DbgModelTargetObject, TargetEnvironment<T> {
|
extends DbgModelTargetObject, TargetEnvironment<T> {
|
||||||
|
|
||||||
public void refresh();
|
public void refreshInternal();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public default String getArchitecture() {
|
public default String getArchitecture() {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import ghidra.dbg.target.TargetEnvironment;
|
||||||
public interface DbgModelTargetEnvironmentEx
|
public interface DbgModelTargetEnvironmentEx
|
||||||
extends DbgModelTargetObject, TargetEnvironment<DbgModelTargetEnvironmentEx> {
|
extends DbgModelTargetObject, TargetEnvironment<DbgModelTargetEnvironmentEx> {
|
||||||
|
|
||||||
public void refresh();
|
public void refreshInternal();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -23,11 +23,13 @@ import agent.dbgeng.dbgeng.DebugClient.DebugStatus;
|
||||||
import agent.dbgeng.manager.impl.DbgManagerImpl;
|
import agent.dbgeng.manager.impl.DbgManagerImpl;
|
||||||
import agent.dbgeng.model.AbstractDbgModel;
|
import agent.dbgeng.model.AbstractDbgModel;
|
||||||
import ghidra.dbg.agent.InvalidatableTargetObjectIf;
|
import ghidra.dbg.agent.InvalidatableTargetObjectIf;
|
||||||
|
import ghidra.dbg.agent.SpiTargetObject;
|
||||||
import ghidra.dbg.target.TargetObject;
|
import ghidra.dbg.target.TargetObject;
|
||||||
|
import ghidra.dbg.target.TargetObject.TargetObjectListener;
|
||||||
import ghidra.dbg.util.CollectionUtils.Delta;
|
import ghidra.dbg.util.CollectionUtils.Delta;
|
||||||
import ghidra.util.datastruct.ListenerSet;
|
import ghidra.util.datastruct.ListenerSet;
|
||||||
|
|
||||||
public interface DbgModelTargetObject extends TargetObject, InvalidatableTargetObjectIf {
|
public interface DbgModelTargetObject extends SpiTargetObject, InvalidatableTargetObjectIf {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbstractDbgModel getModel();
|
public AbstractDbgModel getModel();
|
||||||
|
|
|
@ -60,6 +60,7 @@ public class DbgModelImpl extends AbstractDbgModel {
|
||||||
s.add();
|
s.add();
|
||||||
DbgModelTargetSessionContainer sessions = root.sessions;
|
DbgModelTargetSessionContainer sessions = root.sessions;
|
||||||
this.session = (DbgModelTargetSessionImpl) sessions.getTargetSession(s);
|
this.session = (DbgModelTargetSessionImpl) sessions.getTargetSession(s);
|
||||||
|
addModelRoot(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -110,7 +111,7 @@ public class DbgModelImpl extends AbstractDbgModel {
|
||||||
public CompletableFuture<Void> close() {
|
public CompletableFuture<Void> close() {
|
||||||
try {
|
try {
|
||||||
terminate();
|
terminate();
|
||||||
return CompletableFuture.completedFuture(null);
|
return super.close();
|
||||||
}
|
}
|
||||||
catch (Throwable t) {
|
catch (Throwable t) {
|
||||||
return CompletableFuture.failedFuture(t);
|
return CompletableFuture.failedFuture(t);
|
||||||
|
|
|
@ -56,8 +56,8 @@ public class DbgModelTargetObjectImpl extends DefaultTargetObject<TargetObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doInvalidate(String reason) {
|
protected void doInvalidate(TargetObject branch, String reason) {
|
||||||
super.doInvalidate(reason);
|
super.doInvalidate(branch, reason);
|
||||||
getManager().removeStateListener(accessListener);
|
getManager().removeStateListener(accessListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,14 +30,29 @@ import ghidra.dbg.target.*;
|
||||||
import ghidra.dbg.target.schema.*;
|
import ghidra.dbg.target.schema.*;
|
||||||
import ghidra.dbg.util.PathUtils;
|
import ghidra.dbg.util.PathUtils;
|
||||||
|
|
||||||
@TargetObjectSchemaInfo(name = "Debugger", elements = { //
|
@TargetObjectSchemaInfo(
|
||||||
|
name = "Debugger",
|
||||||
|
elements = { //
|
||||||
@TargetElementType(type = Void.class) //
|
@TargetElementType(type = Void.class) //
|
||||||
}, attributes = { //
|
},
|
||||||
@TargetAttributeType(name = "Available", type = DbgModelTargetAvailableContainerImpl.class, required = true, fixed = true), //
|
attributes = { //
|
||||||
@TargetAttributeType(name = "Connectors", type = DbgModelTargetConnectorContainerImpl.class, required = true, fixed = true), //
|
@TargetAttributeType(
|
||||||
@TargetAttributeType(name = "Sessions", type = DbgModelTargetSessionContainerImpl.class, required = true, fixed = true), //
|
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) //
|
@TargetAttributeType(type = Void.class) //
|
||||||
})
|
})
|
||||||
public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot
|
public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot
|
||||||
implements DbgModelTargetRoot {
|
implements DbgModelTargetRoot {
|
||||||
|
|
||||||
|
@ -136,12 +151,6 @@ public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot
|
||||||
), reason.desc());
|
), reason.desc());
|
||||||
}
|
}
|
||||||
|
|
||||||
//@Override
|
|
||||||
public void refresh() {
|
|
||||||
// TODO ???
|
|
||||||
System.err.println("root:refresh");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TargetAccessibility getAccessibility() {
|
public TargetAccessibility getAccessibility() {
|
||||||
return accessibility;
|
return accessibility;
|
||||||
|
|
|
@ -23,12 +23,18 @@ import agent.dbgeng.model.iface2.DbgModelTargetSession;
|
||||||
import agent.dbgeng.model.iface2.DbgModelTargetSessionAttributes;
|
import agent.dbgeng.model.iface2.DbgModelTargetSessionAttributes;
|
||||||
import ghidra.dbg.target.schema.*;
|
import ghidra.dbg.target.schema.*;
|
||||||
|
|
||||||
@TargetObjectSchemaInfo(name = "SessionAttributes", elements = { //
|
@TargetObjectSchemaInfo(
|
||||||
|
name = "SessionAttributes",
|
||||||
|
elements = { //
|
||||||
@TargetElementType(type = Void.class) //
|
@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) //
|
@TargetAttributeType(type = Void.class) //
|
||||||
})
|
})
|
||||||
public class DbgModelTargetSessionAttributesImpl extends DbgModelTargetObjectImpl
|
public class DbgModelTargetSessionAttributesImpl extends DbgModelTargetObjectImpl
|
||||||
implements DbgModelTargetSessionAttributes {
|
implements DbgModelTargetSessionAttributes {
|
||||||
|
|
||||||
|
@ -75,8 +81,8 @@ public class DbgModelTargetSessionAttributesImpl extends DbgModelTargetObjectImp
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refresh() {
|
public void refreshInternal() {
|
||||||
machineAttributes.refresh();
|
machineAttributes.refreshInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,15 +60,15 @@ public class DbgModelTargetSessionAttributesMachineImpl extends DbgModelTargetOb
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sessionAdded(DbgSession session, DbgCause cause) {
|
public void sessionAdded(DbgSession session, DbgCause cause) {
|
||||||
refresh();
|
refreshInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void processAdded(DbgProcess process, DbgCause cause) {
|
public void processAdded(DbgProcess process, DbgCause cause) {
|
||||||
refresh();
|
refreshInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refresh() {
|
public void refreshInternal() {
|
||||||
DebugControl control = getManager().getControl();
|
DebugControl control = getManager().getControl();
|
||||||
int processorType = control.getActualProcessorType();
|
int processorType = control.getActualProcessorType();
|
||||||
if (processorType < 0) {
|
if (processorType < 0) {
|
||||||
|
|
|
@ -16,20 +16,27 @@
|
||||||
package agent.dbgmodel.model.impl;
|
package agent.dbgmodel.model.impl;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import org.jdom.JDOMException;
|
import org.jdom.JDOMException;
|
||||||
|
|
||||||
import agent.dbgeng.manager.impl.DbgManagerImpl;
|
import agent.dbgeng.manager.impl.DbgManagerImpl;
|
||||||
import agent.dbgeng.model.AbstractDbgModel;
|
import agent.dbgeng.model.AbstractDbgModel;
|
||||||
|
import agent.dbgeng.model.iface2.DbgModelTargetObject;
|
||||||
import agent.dbgeng.model.iface2.DbgModelTargetSession;
|
import agent.dbgeng.model.iface2.DbgModelTargetSession;
|
||||||
import agent.dbgmodel.manager.DbgManager2Impl;
|
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.TargetObject;
|
||||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||||
import ghidra.dbg.target.schema.XmlSchemaContext;
|
import ghidra.dbg.target.schema.XmlSchemaContext;
|
||||||
import ghidra.program.model.address.*;
|
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.
|
// 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
|
// The model must convert to and from Ghidra's address space names
|
||||||
protected static final String SPACE_NAME = "ram";
|
protected static final String SPACE_NAME = "ram";
|
||||||
|
@ -66,6 +73,15 @@ public class DbgModel2Impl extends AbstractDbgModel {
|
||||||
//System.out.println(XmlSchemaContext.serialize(SCHEMA_CTX));
|
//System.out.println(XmlSchemaContext.serialize(SCHEMA_CTX));
|
||||||
this.root = new DbgModel2TargetRootImpl(this, ROOT_SCHEMA);
|
this.root = new DbgModel2TargetRootImpl(this, ROOT_SCHEMA);
|
||||||
this.completedRoot = CompletableFuture.completedFuture(root);
|
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
|
@Override
|
||||||
|
@ -116,7 +132,7 @@ public class DbgModel2Impl extends AbstractDbgModel {
|
||||||
public CompletableFuture<Void> close() {
|
public CompletableFuture<Void> close() {
|
||||||
try {
|
try {
|
||||||
terminate();
|
terminate();
|
||||||
return CompletableFuture.completedFuture(null);
|
return super.close();
|
||||||
}
|
}
|
||||||
catch (Throwable t) {
|
catch (Throwable t) {
|
||||||
return CompletableFuture.failedFuture(t);
|
return CompletableFuture.failedFuture(t);
|
||||||
|
|
|
@ -59,8 +59,6 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
|
||||||
|
|
||||||
protected String DBG_PROMPT = "(kd2)"; // Used by DbgModelTargetEnvironment
|
protected String DBG_PROMPT = "(kd2)"; // Used by DbgModelTargetEnvironment
|
||||||
|
|
||||||
protected boolean fireAttributesChanged = false;
|
|
||||||
|
|
||||||
protected static String indexObject(ModelObject obj) {
|
protected static String indexObject(ModelObject obj) {
|
||||||
return obj.getSearchKey();
|
return obj.getSearchKey();
|
||||||
}
|
}
|
||||||
|
@ -84,6 +82,12 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
|
||||||
super(model, parent, name, typeHint, schema);
|
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
|
@Override
|
||||||
public DbgModel2Impl getModel() {
|
public DbgModel2Impl getModel() {
|
||||||
return (DbgModel2Impl) super.getModel();
|
return (DbgModel2Impl) super.getModel();
|
||||||
|
@ -127,7 +131,6 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Void> requestAttributes(boolean refresh) {
|
public CompletableFuture<Void> requestAttributes(boolean refresh) {
|
||||||
fireAttributesChanged = true;
|
|
||||||
Map<String, Object> nmap = new HashMap<>();
|
Map<String, Object> nmap = new HashMap<>();
|
||||||
return requestNativeAttributes().thenCompose(map -> {
|
return requestNativeAttributes().thenCompose(map -> {
|
||||||
synchronized (attributes) {
|
synchronized (attributes) {
|
||||||
|
@ -418,13 +421,10 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
|
||||||
schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
|
schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
|
||||||
}
|
}
|
||||||
doInvalidateAttributes(delta.removed, reason);
|
doInvalidateAttributes(delta.removed, reason);
|
||||||
if (parent == null && !delta.isEmpty()) {
|
if (!delta.isEmpty()) {
|
||||||
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||||
return delta;
|
return delta;
|
||||||
}
|
}
|
||||||
if (fireAttributesChanged && !delta.isEmpty()) {
|
|
||||||
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
|
||||||
}
|
|
||||||
return delta;
|
return delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,14 +439,9 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject<TargetObject,
|
||||||
schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
|
schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema());
|
||||||
}
|
}
|
||||||
doInvalidateAttributes(delta.removed, reason);
|
doInvalidateAttributes(delta.removed, reason);
|
||||||
if (fireAttributesChanged && !delta.isEmpty()) {
|
if (!delta.isEmpty()) {
|
||||||
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added);
|
||||||
}
|
}
|
||||||
return delta;
|
return delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean enforcesStrictSchema() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,6 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot
|
||||||
}
|
}
|
||||||
if (doFire) {
|
if (doFire) {
|
||||||
this.focus = sel;
|
this.focus = sel;
|
||||||
fireAttributesChanged = true;
|
|
||||||
changeAttributes(List.of(), List.of(), Map.of( //
|
changeAttributes(List.of(), List.of(), Map.of( //
|
||||||
TargetFocusScope.FOCUS_ATTRIBUTE_NAME, focus //
|
TargetFocusScope.FOCUS_ATTRIBUTE_NAME, focus //
|
||||||
), "Focus changed");
|
), "Focus changed");
|
||||||
|
@ -492,12 +491,6 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//@Override
|
|
||||||
public void refresh() {
|
|
||||||
// TODO ???
|
|
||||||
System.err.println("root:refresh");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TargetAccessibility getAccessibility() {
|
public TargetAccessibility getAccessibility() {
|
||||||
return accessibility;
|
return accessibility;
|
||||||
|
|
|
@ -28,13 +28,11 @@ import agent.dbgeng.model.iface2.*;
|
||||||
import agent.dbgmodel.dbgmodel.main.ModelObject;
|
import agent.dbgmodel.dbgmodel.main.ModelObject;
|
||||||
import agent.dbgmodel.jna.dbgmodel.DbgModelNative.ModelObjectKind;
|
import agent.dbgmodel.jna.dbgmodel.DbgModelNative.ModelObjectKind;
|
||||||
import ghidra.async.AsyncUtils;
|
import ghidra.async.AsyncUtils;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
|
||||||
import ghidra.dbg.attributes.TargetObjectRef;
|
import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.target.*;
|
import ghidra.dbg.target.*;
|
||||||
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
|
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
|
||||||
import ghidra.dbg.util.PathUtils;
|
import ghidra.dbg.util.PathUtils;
|
||||||
import ghidra.util.datastruct.ListenerSet;
|
import ghidra.util.datastruct.ListenerSet;
|
||||||
import utilities.util.ProxyUtilities;
|
|
||||||
|
|
||||||
public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl implements //
|
public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl implements //
|
||||||
DbgModelTargetAccessConditioned<DelegateDbgModel2TargetObject>, //
|
DbgModelTargetAccessConditioned<DelegateDbgModel2TargetObject>, //
|
||||||
|
@ -160,7 +158,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
||||||
mixins.add(mixin);
|
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();
|
protected static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||||
|
@ -170,8 +168,6 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
||||||
protected final ProxyState state;
|
protected final ProxyState state;
|
||||||
protected final Cleanable cleanable;
|
protected final Cleanable cleanable;
|
||||||
|
|
||||||
private final DbgModelTargetObject proxy;
|
|
||||||
|
|
||||||
private boolean breakpointEnabled;
|
private boolean breakpointEnabled;
|
||||||
private final ListenerSet<TargetBreakpointAction> breakpointActions =
|
private final ListenerSet<TargetBreakpointAction> breakpointActions =
|
||||||
new ListenerSet<>(TargetBreakpointAction.class) {
|
new ListenerSet<>(TargetBreakpointAction.class) {
|
||||||
|
@ -189,15 +185,12 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
||||||
|
|
||||||
public DelegateDbgModel2TargetObject(DbgModel2Impl model, DbgModelTargetObject parent,
|
public DelegateDbgModel2TargetObject(DbgModel2Impl model, DbgModelTargetObject parent,
|
||||||
String key, ModelObject modelObject, List<Class<? extends TargetObject>> mixins) {
|
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.state = new ProxyState(model, modelObject);
|
||||||
this.cleanable = CLEANER.register(this, state);
|
this.cleanable = CLEANER.register(this, state);
|
||||||
|
|
||||||
getManager().addStateListener(accessListener);
|
getManager().addStateListener(accessListener);
|
||||||
|
|
||||||
mixins.add(DbgModel2TargetProxy.class);
|
|
||||||
this.proxy =
|
|
||||||
ProxyUtilities.composeOnDelegate(DbgModelTargetObject.class, this, mixins, LOOKUP);
|
|
||||||
if (proxy instanceof DbgEventsListener) {
|
if (proxy instanceof DbgEventsListener) {
|
||||||
model.getManager().addEventsListener((DbgEventsListener) proxy);
|
model.getManager().addEventsListener((DbgEventsListener) proxy);
|
||||||
}
|
}
|
||||||
|
@ -216,11 +209,6 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
||||||
return delegate;
|
return delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T extends TypedTargetObject<T>> T as(Class<T> iface) {
|
|
||||||
return DebuggerObjectModel.requireIface(iface, proxy, getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
public CompletableFuture<? extends DelegateDbgModel2TargetObject> fetch() {
|
public CompletableFuture<? extends DelegateDbgModel2TargetObject> fetch() {
|
||||||
|
@ -228,8 +216,8 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TargetObject getProxy() {
|
public DbgModelTargetObject getProxy() {
|
||||||
return proxy;
|
return (DbgModelTargetObject) proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@ -311,7 +299,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (proxy instanceof DbgModelTargetRegister || proxy instanceof DbgModelTargetStackFrame) {
|
if (proxy instanceof DbgModelTargetRegister || proxy instanceof DbgModelTargetStackFrame) {
|
||||||
DbgThread thread = proxy.getParentThread().getThread();
|
DbgThread thread = getProxy().getParentThread().getThread();
|
||||||
if (thread.equals(getManager().getEventThread())) {
|
if (thread.equals(getManager().getEventThread())) {
|
||||||
requestAttributes(true);
|
requestAttributes(true);
|
||||||
}
|
}
|
||||||
|
@ -354,6 +342,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public DelegateDbgModel2TargetObject getDelegate() {
|
public DelegateDbgModel2TargetObject getDelegate() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -388,6 +377,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp
|
||||||
this.breakpointEnabled = enabled;
|
this.breakpointEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public ListenerSet<TargetBreakpointAction> getActions() {
|
public ListenerSet<TargetBreakpointAction> getActions() {
|
||||||
return breakpointActions;
|
return breakpointActions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
|
||||||
this.completedSession = CompletableFuture.completedFuture(session);
|
this.completedSession = CompletableFuture.completedFuture(session);
|
||||||
|
|
||||||
gdb.addStateListener(gdbExitListener);
|
gdb.addStateListener(gdbExitListener);
|
||||||
|
addModelRoot(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -136,7 +137,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
|
||||||
|
|
||||||
public void terminate() throws IOException {
|
public void terminate() throws IOException {
|
||||||
listeners.fire.modelClosed(DebuggerModelClosedReason.NORMAL);
|
listeners.fire.modelClosed(DebuggerModelClosedReason.NORMAL);
|
||||||
session.invalidateSubtree("GDB is terminating");
|
session.invalidateSubtree(session, "GDB is terminating");
|
||||||
gdb.terminate();
|
gdb.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +155,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
|
||||||
public CompletableFuture<Void> close() {
|
public CompletableFuture<Void> close() {
|
||||||
try {
|
try {
|
||||||
terminate();
|
terminate();
|
||||||
return AsyncUtils.NIL;
|
return super.close();
|
||||||
}
|
}
|
||||||
catch (Throwable t) {
|
catch (Throwable t) {
|
||||||
return CompletableFuture.failedFuture(t);
|
return CompletableFuture.failedFuture(t);
|
||||||
|
|
|
@ -59,7 +59,7 @@ public class GdbModelTargetEnvironment
|
||||||
VISIBLE_ENDIAN_ATTRIBUTE_NAME, endian,
|
VISIBLE_ENDIAN_ATTRIBUTE_NAME, endian,
|
||||||
UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED),
|
UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED),
|
||||||
"Initialized");
|
"Initialized");
|
||||||
refresh();
|
refreshInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<Void> refreshArchitecture() {
|
protected CompletableFuture<Void> refreshArchitecture() {
|
||||||
|
@ -156,7 +156,7 @@ public class GdbModelTargetEnvironment
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<Void> refresh() {
|
protected CompletableFuture<Void> refreshInternal() {
|
||||||
AsyncFence fence = new AsyncFence();
|
AsyncFence fence = new AsyncFence();
|
||||||
fence.include(refreshArchitecture());
|
fence.include(refreshArchitecture());
|
||||||
fence.include(refreshOS());
|
fence.include(refreshOS());
|
||||||
|
|
|
@ -220,9 +220,9 @@ public class GdbModelTargetInferior
|
||||||
|
|
||||||
protected CompletableFuture<Void> inferiorStarted(Long pid) {
|
protected CompletableFuture<Void> inferiorStarted(Long pid) {
|
||||||
AsyncFence fence = new AsyncFence();
|
AsyncFence fence = new AsyncFence();
|
||||||
fence.include(modules.refresh());
|
fence.include(modules.refreshInternal());
|
||||||
fence.include(registers.refresh());
|
fence.include(registers.resync());
|
||||||
fence.include(environment.refresh());
|
fence.include(environment.refreshInternal());
|
||||||
return fence.ready().thenAccept(__ -> {
|
return fence.ready().thenAccept(__ -> {
|
||||||
if (pid != null) {
|
if (pid != null) {
|
||||||
changeAttributes(List.of(), Map.of( //
|
changeAttributes(List.of(), Map.of( //
|
||||||
|
|
|
@ -29,12 +29,14 @@ import ghidra.dbg.target.TargetExecutionStateful;
|
||||||
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
|
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
|
||||||
import ghidra.dbg.target.schema.TargetAttributeType;
|
import ghidra.dbg.target.schema.TargetAttributeType;
|
||||||
import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
|
import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
|
||||||
import ghidra.util.Msg;
|
|
||||||
import ghidra.util.datastruct.WeakValueHashMap;
|
import ghidra.util.datastruct.WeakValueHashMap;
|
||||||
|
|
||||||
@TargetObjectSchemaInfo(name = "InferiorContainer", attributes = {
|
@TargetObjectSchemaInfo(
|
||||||
|
name = "InferiorContainer",
|
||||||
|
attributes = {
|
||||||
@TargetAttributeType(type = Void.class)
|
@TargetAttributeType(type = Void.class)
|
||||||
}, canonicalContainer = true)
|
},
|
||||||
|
canonicalContainer = true)
|
||||||
public class GdbModelTargetInferiorContainer
|
public class GdbModelTargetInferiorContainer
|
||||||
extends DefaultTargetObject<GdbModelTargetInferior, GdbModelTargetSession>
|
extends DefaultTargetObject<GdbModelTargetInferior, GdbModelTargetSession>
|
||||||
implements GdbEventsListenerAdapter {
|
implements GdbEventsListenerAdapter {
|
||||||
|
@ -70,7 +72,7 @@ public class GdbModelTargetInferiorContainer
|
||||||
" started " + inf.getExecutable() + " pid=" + inf.getPid(),
|
" started " + inf.getExecutable() + " pid=" + inf.getPid(),
|
||||||
List.of(inferior));
|
List.of(inferior));
|
||||||
}).exceptionally(ex -> {
|
}).exceptionally(ex -> {
|
||||||
Msg.error(this, "Could not notify inferior started", ex);
|
impl.reportError(this, "Could not notify inferior started", ex);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,9 +32,12 @@ import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
|
||||||
import ghidra.lifecycle.Internal;
|
import ghidra.lifecycle.Internal;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
@TargetObjectSchemaInfo(name = "ModuleContainer", attributes = {
|
@TargetObjectSchemaInfo(
|
||||||
|
name = "ModuleContainer",
|
||||||
|
attributes = {
|
||||||
@TargetAttributeType(type = Void.class)
|
@TargetAttributeType(type = Void.class)
|
||||||
}, canonicalContainer = true)
|
},
|
||||||
|
canonicalContainer = true)
|
||||||
public class GdbModelTargetModuleContainer
|
public class GdbModelTargetModuleContainer
|
||||||
extends DefaultTargetObject<GdbModelTargetModule, GdbModelTargetInferior>
|
extends DefaultTargetObject<GdbModelTargetModule, GdbModelTargetInferior>
|
||||||
implements TargetModuleContainer<GdbModelTargetModuleContainer> {
|
implements TargetModuleContainer<GdbModelTargetModuleContainer> {
|
||||||
|
@ -118,7 +121,7 @@ public class GdbModelTargetModuleContainer
|
||||||
return modulesByName.get(name);
|
return modulesByName.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<?> refresh() {
|
public CompletableFuture<?> refreshInternal() {
|
||||||
if (!isObserved()) {
|
if (!isObserved()) {
|
||||||
return AsyncUtils.NIL;
|
return AsyncUtils.NIL;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,14 @@ import ghidra.dbg.target.schema.*;
|
||||||
import ghidra.dbg.util.PathUtils;
|
import ghidra.dbg.util.PathUtils;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
@TargetObjectSchemaInfo(name = "Session", elements = {
|
@TargetObjectSchemaInfo(
|
||||||
|
name = "Session",
|
||||||
|
elements = {
|
||||||
@TargetElementType(type = Void.class)
|
@TargetElementType(type = Void.class)
|
||||||
}, attributes = {
|
},
|
||||||
|
attributes = {
|
||||||
@TargetAttributeType(type = Void.class)
|
@TargetAttributeType(type = Void.class)
|
||||||
})
|
})
|
||||||
public class GdbModelTargetSession extends DefaultTargetModelRoot implements //
|
public class GdbModelTargetSession extends DefaultTargetModelRoot implements //
|
||||||
TargetAccessConditioned<GdbModelTargetSession>,
|
TargetAccessConditioned<GdbModelTargetSession>,
|
||||||
TargetAttacher<GdbModelTargetSession>,
|
TargetAttacher<GdbModelTargetSession>,
|
||||||
|
@ -91,12 +94,18 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements //
|
||||||
return inferiors;
|
return inferiors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetAttributeType(name = GdbModelTargetAvailableContainer.NAME, required = true, fixed = true)
|
@TargetAttributeType(
|
||||||
|
name = GdbModelTargetAvailableContainer.NAME,
|
||||||
|
required = true,
|
||||||
|
fixed = true)
|
||||||
public GdbModelTargetAvailableContainer getAvailable() {
|
public GdbModelTargetAvailableContainer getAvailable() {
|
||||||
return available;
|
return available;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetAttributeType(name = GdbModelTargetBreakpointContainer.NAME, required = true, fixed = true)
|
@TargetAttributeType(
|
||||||
|
name = GdbModelTargetBreakpointContainer.NAME,
|
||||||
|
required = true,
|
||||||
|
fixed = true)
|
||||||
public GdbModelTargetBreakpointContainer getBreakpoints() {
|
public GdbModelTargetBreakpointContainer getBreakpoints() {
|
||||||
return breakpoints;
|
return breakpoints;
|
||||||
}
|
}
|
||||||
|
@ -135,7 +144,6 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements //
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
listeners.fire(TargetInterpreterListener.class).consoleOutput(this, dbgChannel, out);
|
listeners.fire(TargetInterpreterListener.class).consoleOutput(this, dbgChannel, out);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -435,9 +435,9 @@ public abstract class AbstractModelForGdbTest
|
||||||
AllTargetObjectListenerAdapter l = new AllTargetObjectListenerAdapter() {
|
AllTargetObjectListenerAdapter l = new AllTargetObjectListenerAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void consoleOutput(TargetObject interpreter, Channel channel,
|
public void consoleOutput(TargetObject interpreter, Channel channel,
|
||||||
String out) {
|
byte[] out) {
|
||||||
Msg.debug(this, "Got " + channel + " output: " + 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) {
|
catch (AssertionError e) {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"Client implementation sent an invalid request: " +
|
"Client implementation sent an invalid request: " +
|
||||||
"BAD_REQUEST: Unrecognized request: ERROR_REQUEST",
|
"EC_BAD_REQUEST: Unrecognized request: ERROR_REQUEST",
|
||||||
e.getMessage());
|
e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,27 +18,24 @@ package ghidra.dbg.gadp.client;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.invoke.MethodHandle;
|
import java.lang.invoke.MethodHandle;
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
import java.lang.ref.Cleaner.Cleanable;
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.agent.DefaultTargetObject;
|
||||||
import ghidra.dbg.attributes.TargetObjectRef;
|
|
||||||
import ghidra.dbg.gadp.GadpRegistry;
|
import ghidra.dbg.gadp.GadpRegistry;
|
||||||
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
|
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
|
||||||
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp.EventNotification.EvtCase;
|
import ghidra.dbg.gadp.protocol.Gadp.EventNotification.EvtCase;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.memory.CachedMemory;
|
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.TargetAccessibility;
|
||||||
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
|
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
|
||||||
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
|
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
|
||||||
|
import ghidra.dbg.target.TargetObject;
|
||||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||||
import ghidra.dbg.util.CollectionUtils.Delta;
|
import ghidra.dbg.util.CollectionUtils.Delta;
|
||||||
import ghidra.dbg.util.PathUtils;
|
|
||||||
import ghidra.program.model.address.AddressSpace;
|
import ghidra.program.model.address.AddressSpace;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.datastruct.ListenerSet;
|
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
|
* 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 abstract static class GadpHandlerMap<A extends Annotation, K> {
|
||||||
protected final Class<A> annotationType;
|
protected final Class<A> annotationType;
|
||||||
protected final Class<?>[] paramClasses;
|
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 MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||||
protected static final Map<Set<Class<? extends TargetObject>>, GadpEventHandlerMap> EVENT_HANDLER_MAPS_BY_COMPOSITION =
|
protected static final Map<Set<Class<? extends TargetObject>>, GadpEventHandlerMap> EVENT_HANDLER_MAPS_BY_COMPOSITION =
|
||||||
new HashMap<>();
|
new HashMap<>();
|
||||||
protected static final Map<Set<Class<? extends TargetObject>>, GadpAttributeChangeCallbackMap> ATTRIBUTE_CHANGE_CALLBACKS_MAPS_BY_COMPOSITION =
|
protected static final Map<Set<Class<? extends TargetObject>>, GadpAttributeChangeCallbackMap> ATTRIBUTE_CHANGE_CALLBACKS_MAPS_BY_COMPOSITION =
|
||||||
new HashMap<>();
|
new HashMap<>();
|
||||||
|
|
||||||
protected static GadpClientTargetObject makeModelProxy(GadpClient client, List<String> path,
|
protected static GadpClientTargetObject makeModelProxy(GadpClient client,
|
||||||
String typeHint, List<String> ifaceNames) {
|
GadpClientTargetObject parent, String key, String typeHint, List<String> ifaceNames) {
|
||||||
List<Class<? extends TargetObject>> ifaces = TargetObject.getInterfacesByName(ifaceNames);
|
List<Class<? extends TargetObject>> ifaces = TargetObject.getInterfacesByName(ifaceNames);
|
||||||
List<Class<? extends TargetObject>> mixins = GadpRegistry.getMixins(ifaces);
|
List<Class<? extends TargetObject>> mixins = GadpRegistry.getMixins(ifaces);
|
||||||
return new DelegateGadpClientTargetObject(client, path, typeHint, ifaceNames, ifaces,
|
TargetObjectSchema schema =
|
||||||
mixins).proxy;
|
parent == null ? client.getRootSchema() : parent.getSchema().getChildSchema(key);
|
||||||
|
return new DelegateGadpClientTargetObject(client, parent, key, typeHint, schema, ifaceNames,
|
||||||
|
ifaces, mixins).getProxy();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final ProxyState state;
|
private final GadpClient client;
|
||||||
protected final int hash;
|
|
||||||
protected final Cleanable cleanable;
|
|
||||||
|
|
||||||
private final GadpClientTargetObject proxy;
|
|
||||||
private TargetObjectSchema schema; // lazily evaluated
|
|
||||||
private final String typeHint;
|
|
||||||
private final List<String> ifaceNames;
|
private final List<String> ifaceNames;
|
||||||
private final List<Class<? extends TargetObject>> ifaces;
|
private final List<Class<? extends TargetObject>> ifaces;
|
||||||
private final GadpEventHandlerMap eventHandlers;
|
private final GadpEventHandlerMap eventHandlers;
|
||||||
private final GadpAttributeChangeCallbackMap attributeChangeCallbacks;
|
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<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 Map<String, byte[]> regCache = null; // Becomes active if this is a TargtRegisterBank
|
||||||
protected ListenerSet<TargetBreakpointAction> actions = null; // Becomes active is this is a TargetBreakpointSpec
|
protected ListenerSet<TargetBreakpointAction> actions = null; // Becomes active is this is a TargetBreakpointSpec
|
||||||
|
|
||||||
public DelegateGadpClientTargetObject(GadpClient client, List<String> path, String typeHint,
|
public DelegateGadpClientTargetObject(GadpClient client, GadpClientTargetObject parent,
|
||||||
List<String> ifaceNames, List<Class<? extends TargetObject>> ifaces,
|
String key, String typeHint, TargetObjectSchema schema, List<String> ifaceNames,
|
||||||
|
List<Class<? extends TargetObject>> ifaces,
|
||||||
List<Class<? extends TargetObject>> mixins) {
|
List<Class<? extends TargetObject>> mixins) {
|
||||||
this.listeners = new ListenerSet<>(TargetObjectListener.class, client.getClientExecutor());
|
super(client, mixins, client, parent, key, typeHint, schema);
|
||||||
this.state = new ProxyState(client, path);
|
this.client = client;
|
||||||
this.hash = computeHashCode();
|
|
||||||
this.cleanable = GadpClient.CLEANER.register(this, state);
|
|
||||||
|
|
||||||
this.proxy = ProxyUtilities.composeOnDelegate(GadpClientTargetObject.class,
|
|
||||||
this, mixins, MethodHandles.lookup());
|
|
||||||
this.typeHint = typeHint;
|
|
||||||
this.ifaceNames = ifaceNames;
|
this.ifaceNames = ifaceNames;
|
||||||
this.ifaces = ifaces;
|
this.ifaces = ifaces;
|
||||||
|
|
||||||
|
@ -229,48 +191,14 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
||||||
GadpAttributeChangeCallbackMap::new);
|
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
|
@Override
|
||||||
public GadpClient getModel() {
|
public GadpClient getModel() {
|
||||||
return state.client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getProtocolID() {
|
public GadpClientTargetObject getProxy() {
|
||||||
return state.path;
|
return (GadpClientTargetObject) super.getProxy();
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -279,104 +207,32 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Class<? extends TargetObject>> getInterfaces() {
|
public Collection<? extends Class<? extends TargetObject>> getInterfaces() {
|
||||||
return ifaces;
|
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
|
@Override
|
||||||
public CompletableFuture<? extends TargetObject> fetch() {
|
public CompletableFuture<? extends TargetObject> fetch() {
|
||||||
return CompletableFuture.completedFuture(proxy);
|
return CompletableFuture.completedFuture(getProxy());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<?> fetchAttribute(String name) {
|
public CompletableFuture<Void> resync(boolean attributes, boolean elements) {
|
||||||
if (!PathUtils.isInvocation(name)) {
|
return client.sendChecked(Gadp.ResyncRequest.newBuilder()
|
||||||
return GadpClientTargetObject.super.fetchAttribute(name);
|
.setPath(GadpValueUtils.makePath(path))
|
||||||
}
|
.setAttributes(attributes)
|
||||||
return state.client.fetchModelValue(PathUtils.extend(state.path, name));
|
.setElements(elements),
|
||||||
|
Gadp.ResyncReply.getDefaultInstance()).thenApply(rep -> null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addListener(TargetObjectListener l) {
|
protected CompletableFuture<Void> requestAttributes(boolean refresh) {
|
||||||
listeners.add(l);
|
return resync(refresh, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeListener(TargetObjectListener l) {
|
protected CompletableFuture<Void> requestElements(boolean refresh) {
|
||||||
listeners.remove(l);
|
return resync(false, refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -384,34 +240,25 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateWithInfo(Gadp.ModelObjectInfo info) {
|
public void updateWithDeltas(Gadp.ModelObjectDelta deltaE, Gadp.ModelObjectDelta deltaA) {
|
||||||
Map<String, TargetObjectRef> elements =
|
Map<String, GadpClientTargetObject> elementsAdded =
|
||||||
GadpValueUtils.getElementMap(this, info.getElementIndexList());
|
GadpValueUtils.getElementMap(this, deltaE.getAddedList());
|
||||||
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());
|
|
||||||
Map<String, Object> attributesAdded =
|
Map<String, Object> attributesAdded =
|
||||||
GadpValueUtils.getAttributeMap(this, delta.getAttributeAddedList());
|
GadpValueUtils.getAttributeMap(this, deltaA.getAddedList());
|
||||||
|
|
||||||
Delta<TargetObjectRef, TargetObjectRef> deltaE =
|
changeElements(deltaE.getRemovedList(), List.of(), elementsAdded, "Updated");
|
||||||
updateElements(Delta.create(delta.getIndexRemovedList(), elementsAdded));
|
Delta<?, ?> attrDelta =
|
||||||
Delta<Object, Object> deltaA =
|
changeAttributes(deltaA.getRemovedList(), attributesAdded, "Updated");
|
||||||
updateAttributes(Delta.create(delta.getAttributeRemovedList(), attributesAdded));
|
for (String name : attrDelta.getKeysRemoved()) {
|
||||||
fireElementsChanged(deltaE);
|
handleAttributeChange(name, null);
|
||||||
fireAttributesChanged(deltaA);
|
}
|
||||||
|
for (Map.Entry<String, ?> a : attrDelta.added.entrySet()) {
|
||||||
|
handleAttributeChange(a.getKey(), a.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void handleEvent(Gadp.EventNotification notify) {
|
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
|
* @param value the new value of the attribute
|
||||||
*/
|
*/
|
||||||
protected void handleAttributeChange(String name, Object value) {
|
protected void handleAttributeChange(String name, Object value) {
|
||||||
attributeChangeCallbacks.handle(proxy, name, value);
|
attributeChangeCallbacks.handle(getProxy(), 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void assertValid() {
|
protected void assertValid() {
|
||||||
if (!state.valid) {
|
if (!valid) {
|
||||||
throw new IllegalStateException("Object is no longer valid: " + toString());
|
throw new IllegalStateException("Object is no longer valid: " + toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -505,13 +302,13 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
||||||
public synchronized CompletableFuture<Void> invalidateCaches() {
|
public synchronized CompletableFuture<Void> invalidateCaches() {
|
||||||
assertValid();
|
assertValid();
|
||||||
doClearCaches();
|
doClearCaches();
|
||||||
return state.client.sendChecked(Gadp.CacheInvalidateRequest.newBuilder()
|
return client.sendChecked(Gadp.CacheInvalidateRequest.newBuilder()
|
||||||
.setPath(GadpValueUtils.makePath(state.path)),
|
.setPath(GadpValueUtils.makePath(path)),
|
||||||
Gadp.CacheInvalidateReply.getDefaultInstance()).thenApply(rep -> null);
|
Gadp.CacheInvalidateReply.getDefaultInstance()).thenApply(rep -> null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected synchronized CachedMemory getMemoryCache(AddressSpace space) {
|
protected synchronized CachedMemory getMemoryCache(AddressSpace space) {
|
||||||
GadpClientTargetMemory memory = (GadpClientTargetMemory) proxy;
|
GadpClientTargetMemory memory = (GadpClientTargetMemory) getProxy();
|
||||||
if (memCache == null) {
|
if (memCache == null) {
|
||||||
memCache = new HashMap<>();
|
memCache = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
@ -552,4 +349,10 @@ public class DelegateGadpClientTargetObject implements GadpClientTargetObject {
|
||||||
}
|
}
|
||||||
return actions;
|
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.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.ref.Cleaner;
|
|
||||||
import java.nio.channels.*;
|
import java.nio.channels.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.jdom.JDOMException;
|
import org.jdom.JDOMException;
|
||||||
|
@ -33,29 +31,29 @@ import com.google.protobuf.Message;
|
||||||
import com.google.protobuf.ProtocolStringList;
|
import com.google.protobuf.ProtocolStringList;
|
||||||
|
|
||||||
import ghidra.async.*;
|
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.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.error.*;
|
import ghidra.dbg.error.*;
|
||||||
import ghidra.dbg.gadp.GadpVersion;
|
import ghidra.dbg.gadp.GadpVersion;
|
||||||
import ghidra.dbg.gadp.error.*;
|
import ghidra.dbg.gadp.error.*;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp.*;
|
import ghidra.dbg.gadp.protocol.Gadp.ObjectCreatedEvent;
|
||||||
import ghidra.dbg.gadp.util.*;
|
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;
|
||||||
import ghidra.dbg.target.TargetObject.TargetUpdateMode;
|
|
||||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||||
import ghidra.dbg.target.schema.XmlSchemaContext;
|
import ghidra.dbg.target.schema.XmlSchemaContext;
|
||||||
import ghidra.dbg.util.PathUtils;
|
import ghidra.dbg.util.PathUtils;
|
||||||
import ghidra.dbg.util.PathUtils.PathComparator;
|
|
||||||
import ghidra.lifecycle.Internal;
|
|
||||||
import ghidra.program.model.address.*;
|
import ghidra.program.model.address.*;
|
||||||
import ghidra.util.*;
|
import ghidra.util.*;
|
||||||
import ghidra.util.datastruct.ListenerSet;
|
|
||||||
import ghidra.util.datastruct.WeakValueTreeMap;
|
|
||||||
import ghidra.util.exception.DuplicateNameException;
|
import ghidra.util.exception.DuplicateNameException;
|
||||||
|
import utilities.util.ProxyUtilities;
|
||||||
|
|
||||||
public class GadpClient implements DebuggerObjectModel {
|
public class GadpClient extends AbstractDebuggerObjectModel
|
||||||
protected static final Cleaner CLEANER = Cleaner.create();
|
implements ProxyFactory<List<Class<? extends TargetObject>>> {
|
||||||
|
|
||||||
protected static final int WARN_OUTSTANDING_REQUESTS = 10000;
|
protected static final int WARN_OUTSTANDING_REQUESTS = 10000;
|
||||||
// TODO: More sophisticated cache management
|
// TODO: More sophisticated cache management
|
||||||
|
@ -264,6 +262,7 @@ public class GadpClient implements DebuggerObjectModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final String description;
|
protected final String description;
|
||||||
|
protected final AsynchronousByteChannel byteChannel;
|
||||||
protected final AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> messageChannel;
|
protected final AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> messageChannel;
|
||||||
|
|
||||||
protected AsyncReference<ChannelState, DebuggerModelClosedReason> channelState =
|
protected AsyncReference<ChannelState, DebuggerModelClosedReason> channelState =
|
||||||
|
@ -272,44 +271,32 @@ public class GadpClient implements DebuggerObjectModel {
|
||||||
protected XmlSchemaContext schemaContext;
|
protected XmlSchemaContext schemaContext;
|
||||||
protected TargetObjectSchema rootSchema;
|
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 =
|
protected final TriConsumer<ChannelState, ChannelState, DebuggerModelClosedReason> listenerForChannelState =
|
||||||
this::channelStateChanged;
|
this::channelStateChanged;
|
||||||
protected final MessagePairingCache messageMatcher = new MessagePairingCache();
|
protected final MessagePairingCache messageMatcher = new MessagePairingCache();
|
||||||
protected final AtomicInteger sequencer = new AtomicInteger();
|
protected final AtomicInteger sequencer = new AtomicInteger();
|
||||||
|
|
||||||
protected final NavigableMap<List<String>, GadpClientTargetObject> modelProxies =
|
protected final Map<List<String>, GadpClientTargetObject> modelProxies = new HashMap<>();
|
||||||
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 GadpAddressFactory factory = new GadpAddressFactory();
|
protected final GadpAddressFactory factory = new GadpAddressFactory();
|
||||||
|
|
||||||
{
|
{
|
||||||
channelState.addChangeListener(listenerForChannelState);
|
channelState.addChangeListener(listenerForChannelState);
|
||||||
|
|
||||||
valueRequests.forgetValues((p, v) -> true);
|
|
||||||
elemsRequests.forgetValues(this::forgetElementsRequests);
|
|
||||||
attrsRequests.forgetValues(this::forgetAttributesRequests);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GadpClient(String description, AsynchronousByteChannel channel) {
|
public GadpClient(String description, AsynchronousByteChannel channel) {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
|
this.byteChannel = channel;
|
||||||
this.messageChannel = createMessageChannel(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(
|
protected AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> createMessageChannel(
|
||||||
AsynchronousByteChannel channel) {
|
AsynchronousByteChannel channel) {
|
||||||
return new AsyncProtobufMessageChannel<>(channel);
|
return new AsyncProtobufMessageChannel<>(channel);
|
||||||
|
@ -323,16 +310,16 @@ public class GadpClient implements DebuggerObjectModel {
|
||||||
protected void channelStateChanged(ChannelState old, ChannelState set,
|
protected void channelStateChanged(ChannelState old, ChannelState set,
|
||||||
DebuggerModelClosedReason reason) {
|
DebuggerModelClosedReason reason) {
|
||||||
if (old == ChannelState.NEGOTIATING && set == ChannelState.ACTIVE) {
|
if (old == ChannelState.NEGOTIATING && set == ChannelState.ACTIVE) {
|
||||||
listenersClient.fire.modelOpened();
|
listeners.fire.modelOpened();
|
||||||
}
|
}
|
||||||
else if (old == ChannelState.ACTIVE && set == ChannelState.CLOSED) {
|
else if (old == ChannelState.ACTIVE && set == ChannelState.CLOSED) {
|
||||||
listenersClient.fire.modelClosed(reason);
|
listeners.fire.modelClosed(reason);
|
||||||
List<GadpClientTargetObject> copy;
|
List<GadpClientTargetObject> copy;
|
||||||
synchronized (modelProxies) {
|
synchronized (lock) {
|
||||||
copy = List.copyOf(modelProxies.values());
|
copy = List.copyOf(modelProxies.values());
|
||||||
}
|
}
|
||||||
for (GadpClientTargetObject proxy : copy) {
|
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,
|
protected <M extends Message> CompletableFuture<M> sendChecked(Message.Builder req,
|
||||||
M exampleRep) {
|
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() {
|
protected void receiveLoop() {
|
||||||
|
@ -380,9 +370,7 @@ public class GadpClient implements DebuggerObjectModel {
|
||||||
}
|
}
|
||||||
messageChannel.read(Gadp.RootMessage::parseFrom).handle(loop::consume);
|
messageChannel.read(Gadp.RootMessage::parseFrom).handle(loop::consume);
|
||||||
}, TypeSpec.cls(Gadp.RootMessage.class), (msg, loop) -> {
|
}, TypeSpec.cls(Gadp.RootMessage.class), (msg, loop) -> {
|
||||||
loop.repeat(); // All async loop to continue while we process
|
CompletableFuture.runAsync(() -> {
|
||||||
|
|
||||||
try {
|
|
||||||
Gadp.EventNotification notify =
|
Gadp.EventNotification notify =
|
||||||
MSG_HELPER.expect(msg, Gadp.EventNotification.getDefaultInstance());
|
MSG_HELPER.expect(msg, Gadp.EventNotification.getDefaultInstance());
|
||||||
if (notify != null) {
|
if (notify != null) {
|
||||||
|
@ -391,10 +379,11 @@ public class GadpClient implements DebuggerObjectModel {
|
||||||
else {
|
else {
|
||||||
messageMatcher.fulfill(msg.getSequence(), msg);
|
messageMatcher.fulfill(msg.getSequence(), msg);
|
||||||
}
|
}
|
||||||
}
|
}, clientExecutor).exceptionally(ex -> {
|
||||||
catch (Throwable e) {
|
Msg.error(this, "Error processing message: ", ex);
|
||||||
Msg.error(this, "Error processing message: " + msg, e);
|
return null;
|
||||||
}
|
});
|
||||||
|
loop.repeat(); // All async loop to continue while we process
|
||||||
}).exceptionally(exc -> {
|
}).exceptionally(exc -> {
|
||||||
exc = AsyncUtils.unwrapThrowable(exc);
|
exc = AsyncUtils.unwrapThrowable(exc);
|
||||||
if (exc instanceof NotYetConnectedException) {
|
if (exc instanceof NotYetConnectedException) {
|
||||||
|
@ -409,6 +398,9 @@ public class GadpClient implements DebuggerObjectModel {
|
||||||
else if (exc instanceof CancelledKeyException) {
|
else if (exc instanceof CancelledKeyException) {
|
||||||
Msg.error(this, "Channel key is cancelled. Probably closed");
|
Msg.error(this, "Channel key is cancelled. Probably closed");
|
||||||
}
|
}
|
||||||
|
else if (exc instanceof RejectedExecutionException) {
|
||||||
|
Msg.trace(this, "Ignoring rejection", exc);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
Msg.error(this, "Receive failed for an unknown reason", exc);
|
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) {
|
protected void processNotification(Gadp.EventNotification notify) {
|
||||||
|
if (!byteChannel.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ProtocolStringList path = notify.getPath().getEList();
|
ProtocolStringList path = notify.getPath().getEList();
|
||||||
GadpClientTargetObject obj = getCachedProxy(path);
|
//Msg.debug(this, "Processing notification: " + path + " " + notify.getEvtCase());
|
||||||
if (obj == null) {
|
if (notify.hasObjectCreatedEvent()) {
|
||||||
if (!hasPendingRequest(path)) {
|
notify.getObjectCreatedEvent();
|
||||||
/**
|
// AbstractTargetObject invokes created event
|
||||||
* For pending, I guess we just miss the event. NB: If it was a model event, then
|
createProxy(path, notify.getObjectCreatedEvent());
|
||||||
* the pending subscribe reply ought to already reflect the update we're ignoring
|
return;
|
||||||
* here.
|
}
|
||||||
*/
|
if (notify.hasRootAddedEvent()) {
|
||||||
Msg.error(this, "Server sent notification for non-cached object: " + notify);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
GadpClientTargetObject obj = getProxy(path, true);
|
||||||
|
if (obj == null) {
|
||||||
|
return; // Error already logged
|
||||||
|
}
|
||||||
obj.getDelegate().handleEvent(notify);
|
obj.getDelegate().handleEvent(notify);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,16 +443,6 @@ public class GadpClient implements DebuggerObjectModel {
|
||||||
return description + " via GADP (" + channelState.get().name().toLowerCase() + ")";
|
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() {
|
public CompletableFuture<Void> connect() {
|
||||||
Gadp.ConnectRequest.Builder req = GadpVersion.makeRequest();
|
Gadp.ConnectRequest.Builder req = GadpVersion.makeRequest();
|
||||||
if (channelState.get() != ChannelState.INACTIVE) {
|
if (channelState.get() != ChannelState.INACTIVE) {
|
||||||
|
@ -493,8 +487,13 @@ public class GadpClient implements DebuggerObjectModel {
|
||||||
public CompletableFuture<Void> close() {
|
public CompletableFuture<Void> close() {
|
||||||
try {
|
try {
|
||||||
messageChannel.close();
|
messageChannel.close();
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
channelState.set(ChannelState.CLOSED, DebuggerModelClosedReason.normal());
|
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) {
|
catch (IOException e) {
|
||||||
return CompletableFuture.failedFuture(e);
|
return CompletableFuture.failedFuture(e);
|
||||||
|
@ -528,292 +527,47 @@ public class GadpClient implements DebuggerObjectModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected GadpClientTargetObject getCachedProxy(List<String> path) {
|
protected GadpClientTargetObject getProxy(List<String> path, boolean internal) {
|
||||||
synchronized (modelProxies) {
|
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);
|
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);
|
List<String> parentPath = PathUtils.parent(path);
|
||||||
|
GadpClientTargetObject parent;
|
||||||
if (parentPath == null) {
|
if (parentPath == null) {
|
||||||
return null;
|
parent = null;
|
||||||
}
|
}
|
||||||
GadpClientTargetObject parent = getCachedProxy(parentPath);
|
else {
|
||||||
|
parent = getProxy(parentPath, true);
|
||||||
if (parent == null) {
|
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,
|
GadpClientTargetObject proxy = DelegateGadpClientTargetObject.makeModelProxy(this,
|
||||||
path, info.getTypeHint(), info.getInterfaceList());
|
parent, PathUtils.getKey(path), evt.getTypeHint(), evt.getInterfaceList());
|
||||||
cacheInParent(path, proxy);
|
modelProxies.put(path, proxy);
|
||||||
return proxy;
|
return proxy;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Internal
|
protected void removeProxy(List<String> path, String reason) {
|
||||||
public TargetObjectRef getProxyOrStub(List<String> path) {
|
synchronized (lock) {
|
||||||
GadpClientTargetObject cached = getCachedProxy(path);
|
modelProxies.remove(path);
|
||||||
if (cached != null) {
|
|
||||||
return cached;
|
|
||||||
}
|
}
|
||||||
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
|
@Override
|
||||||
|
@ -823,22 +577,28 @@ public class GadpClient implements DebuggerObjectModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TargetObjectRef createRef(List<String> path) {
|
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
|
@Override
|
||||||
public void invalidateAllLocalCaches() {
|
public void invalidateAllLocalCaches() {
|
||||||
List<GadpClientTargetObject> copy;
|
List<GadpClientTargetObject> copy;
|
||||||
synchronized (modelProxies) {
|
synchronized (lock) {
|
||||||
copy = List.copyOf(modelProxies.values());
|
copy = List.copyOf(modelProxies.values());
|
||||||
}
|
}
|
||||||
for (GadpClientTargetObject proxy : copy) {
|
for (GadpClientTargetObject proxy : copy) {
|
||||||
proxy.getDelegate().doClearCaches();
|
proxy.getDelegate().doClearCaches();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Executor getClientExecutor() {
|
|
||||||
return clientExecutor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,8 @@ public interface GadpClientTargetAccessConditioned
|
||||||
|
|
||||||
@GadpAttributeChangeCallback(ACCESSIBLE_ATTRIBUTE_NAME)
|
@GadpAttributeChangeCallback(ACCESSIBLE_ATTRIBUTE_NAME)
|
||||||
default void handleAccessibleChanged(Object accessible) {
|
default void handleAccessibleChanged(Object accessible) {
|
||||||
getDelegate().listeners.fire(TargetAccessibilityListener.class)
|
getDelegate().getListeners()
|
||||||
|
.fire(TargetAccessibilityListener.class)
|
||||||
.accessibilityChanged(this, fromObj(accessible));
|
.accessibilityChanged(this, fromObj(accessible));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import ghidra.dbg.attributes.TargetObjectRef;
|
import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.attributes.TypedTargetObjectRef;
|
import ghidra.dbg.attributes.TypedTargetObjectRef;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetAttachable;
|
import ghidra.dbg.target.TargetAttachable;
|
||||||
import ghidra.dbg.target.TargetAttacher;
|
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.client.annot.GadpEventHandler;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp.Path;
|
import ghidra.dbg.gadp.protocol.Gadp.Path;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.*;
|
import ghidra.dbg.target.*;
|
||||||
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
|
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction;
|
||||||
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
|
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
|
||||||
|
@ -64,19 +63,20 @@ public interface GadpClientTargetBreakpointContainer extends GadpClientTargetObj
|
||||||
@GadpEventHandler(Gadp.EventNotification.EvtCase.BREAK_HIT_EVENT)
|
@GadpEventHandler(Gadp.EventNotification.EvtCase.BREAK_HIT_EVENT)
|
||||||
default void handleBreakHitEvent(Gadp.EventNotification notification) {
|
default void handleBreakHitEvent(Gadp.EventNotification notification) {
|
||||||
Gadp.BreakHitEvent evt = notification.getBreakHitEvent();
|
Gadp.BreakHitEvent evt = notification.getBreakHitEvent();
|
||||||
TargetObjectRef trapped = getModel().getProxyOrStub(evt.getTrapped().getEList());
|
TargetObjectRef trapped = getModel().getProxy(evt.getTrapped().getEList(), true);
|
||||||
Path framePath = evt.getFrame();
|
Path framePath = evt.getFrame();
|
||||||
TypedTargetObjectRef<? extends TargetStackFrame<?>> frame =
|
TypedTargetObjectRef<? extends TargetStackFrame<?>> frame =
|
||||||
framePath == null || framePath.getECount() == 0 ? null
|
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();
|
Path specPath = evt.getSpec();
|
||||||
TypedTargetObjectRef<? extends TargetBreakpointSpec<?>> spec = specPath == null ? null
|
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();
|
Path bptPath = evt.getEffective();
|
||||||
TypedTargetObjectRef<? extends TargetBreakpointLocation<?>> breakpoint = bptPath == null
|
TypedTargetObjectRef<? extends TargetBreakpointLocation<?>> breakpoint = bptPath == null
|
||||||
? null
|
? null
|
||||||
: getModel().getProxyOrStub(bptPath.getEList()).as(TargetBreakpointLocation.tclass);
|
: getModel().getProxy(bptPath.getEList(), true).as(TargetBreakpointLocation.tclass);
|
||||||
getDelegate().listeners.fire(TargetBreakpointListener.class)
|
getDelegate().getListeners()
|
||||||
|
.fire(TargetBreakpointListener.class)
|
||||||
.breakpointHit(this, trapped, frame, spec, breakpoint);
|
.breakpointHit(this, trapped, frame, spec, breakpoint);
|
||||||
if (spec instanceof GadpClientTargetBreakpointSpec) {
|
if (spec instanceof GadpClientTargetBreakpointSpec) {
|
||||||
// If I don't have a cached proxy, then I don't have any listeners
|
// 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.client.annot.GadpAttributeChangeCallback;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetBreakpointSpec;
|
import ghidra.dbg.target.TargetBreakpointSpec;
|
||||||
import ghidra.dbg.util.ValueUtils;
|
import ghidra.dbg.util.ValueUtils;
|
||||||
import ghidra.util.datastruct.ListenerSet;
|
import ghidra.util.datastruct.ListenerSet;
|
||||||
|
@ -65,7 +64,8 @@ public interface GadpClientTargetBreakpointSpec
|
||||||
|
|
||||||
@GadpAttributeChangeCallback(ENABLED_ATTRIBUTE_NAME)
|
@GadpAttributeChangeCallback(ENABLED_ATTRIBUTE_NAME)
|
||||||
default void handleEnabledChanged(Object enabled) {
|
default void handleEnabledChanged(Object enabled) {
|
||||||
getDelegate().listeners.fire(TargetBreakpointSpecListener.class)
|
getDelegate().getListeners()
|
||||||
|
.fire(TargetBreakpointSpecListener.class)
|
||||||
.breakpointToggled(this, enabledFromObj(enabled));
|
.breakpointToggled(this, enabledFromObj(enabled));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetConsole;
|
import ghidra.dbg.target.TargetConsole;
|
||||||
|
|
||||||
public interface GadpClientTargetConsole
|
public interface GadpClientTargetConsole
|
||||||
|
|
|
@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetDeletable;
|
import ghidra.dbg.target.TargetDeletable;
|
||||||
|
|
||||||
public interface GadpClientTargetDeletable
|
public interface GadpClientTargetDeletable
|
||||||
|
|
|
@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetDetachable;
|
import ghidra.dbg.target.TargetDetachable;
|
||||||
|
|
||||||
public interface GadpClientTargetDetachable
|
public interface GadpClientTargetDetachable
|
||||||
|
|
|
@ -20,7 +20,6 @@ import java.util.List;
|
||||||
import ghidra.dbg.attributes.TypedTargetObjectRef;
|
import ghidra.dbg.attributes.TypedTargetObjectRef;
|
||||||
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetEventScope;
|
import ghidra.dbg.target.TargetEventScope;
|
||||||
import ghidra.dbg.target.TargetThread;
|
import ghidra.dbg.target.TargetThread;
|
||||||
|
|
||||||
|
@ -32,12 +31,13 @@ public interface GadpClientTargetEventScope
|
||||||
Gadp.Path threadPath = evt.getEventThread();
|
Gadp.Path threadPath = evt.getEventThread();
|
||||||
TypedTargetObjectRef<? extends TargetThread<?>> thread =
|
TypedTargetObjectRef<? extends TargetThread<?>> thread =
|
||||||
threadPath == null || threadPath.getECount() == 0 ? null
|
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());
|
TargetEventType type = GadpValueUtils.getTargetEventType(evt.getType());
|
||||||
String description = evt.getDescription();
|
String description = evt.getDescription();
|
||||||
List<Object> parameters =
|
List<Object> parameters =
|
||||||
GadpValueUtils.getValues(getModel(), evt.getParametersList());
|
GadpValueUtils.getValues(getModel(), evt.getParametersList());
|
||||||
getDelegate().listeners.fire(TargetEventScopeListener.class)
|
getDelegate().getListeners()
|
||||||
|
.fire(TargetEventScopeListener.class)
|
||||||
.event(this, thread, type, description, parameters);
|
.event(this, thread, type, description, parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,8 @@ public interface GadpClientTargetExecutionStateful
|
||||||
|
|
||||||
@GadpAttributeChangeCallback(STATE_ATTRIBUTE_NAME)
|
@GadpAttributeChangeCallback(STATE_ATTRIBUTE_NAME)
|
||||||
default void handleStateChanged(Object state) {
|
default void handleStateChanged(Object state) {
|
||||||
getDelegate().listeners.fire(TargetExecutionStateListener.class)
|
getDelegate().getListeners()
|
||||||
|
.fire(TargetExecutionStateListener.class)
|
||||||
.executionStateChanged(this, stateFromObj(state));
|
.executionStateChanged(this, stateFromObj(state));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.error.DebuggerIllegalArgumentException;
|
import ghidra.dbg.error.DebuggerIllegalArgumentException;
|
||||||
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
|
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetFocusScope;
|
import ghidra.dbg.target.TargetFocusScope;
|
||||||
import ghidra.dbg.util.PathUtils;
|
import ghidra.dbg.util.PathUtils;
|
||||||
import ghidra.dbg.util.ValueUtils;
|
import ghidra.dbg.util.ValueUtils;
|
||||||
|
@ -51,7 +50,8 @@ public interface GadpClientTargetFocusScope
|
||||||
|
|
||||||
@GadpAttributeChangeCallback(FOCUS_ATTRIBUTE_NAME)
|
@GadpAttributeChangeCallback(FOCUS_ATTRIBUTE_NAME)
|
||||||
default void handleFocusChanged(Object focus) {
|
default void handleFocusChanged(Object focus) {
|
||||||
getDelegate().listeners.fire(TargetFocusScopeListener.class)
|
getDelegate().getListeners()
|
||||||
|
.fire(TargetFocusScopeListener.class)
|
||||||
.focusChanged(this, refFromObj(focus));
|
.focusChanged(this, refFromObj(focus));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
|
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetInterpreter;
|
import ghidra.dbg.target.TargetInterpreter;
|
||||||
import ghidra.dbg.util.ValueUtils;
|
import ghidra.dbg.util.ValueUtils;
|
||||||
|
|
||||||
|
@ -53,7 +52,8 @@ public interface GadpClientTargetInterpreter
|
||||||
|
|
||||||
@GadpAttributeChangeCallback(PROMPT_ATTRIBUTE_NAME)
|
@GadpAttributeChangeCallback(PROMPT_ATTRIBUTE_NAME)
|
||||||
default void handlePromptChanged(Object prompt) {
|
default void handlePromptChanged(Object prompt) {
|
||||||
getDelegate().listeners.fire(TargetInterpreterListener.class)
|
getDelegate().getListeners()
|
||||||
|
.fire(TargetInterpreterListener.class)
|
||||||
.promptChanged(this, promptFromObj(prompt));
|
.promptChanged(this, promptFromObj(prompt));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetInterruptible;
|
import ghidra.dbg.target.TargetInterruptible;
|
||||||
|
|
||||||
public interface GadpClientTargetInterruptible
|
public interface GadpClientTargetInterruptible
|
||||||
|
|
|
@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetKillable;
|
import ghidra.dbg.target.TargetKillable;
|
||||||
|
|
||||||
public interface GadpClientTargetKillable
|
public interface GadpClientTargetKillable
|
||||||
|
|
|
@ -19,7 +19,6 @@ import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetLauncher;
|
import ghidra.dbg.target.TargetLauncher;
|
||||||
import ghidra.dbg.target.TargetMethod;
|
import ghidra.dbg.target.TargetMethod;
|
||||||
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
||||||
|
|
|
@ -22,7 +22,6 @@ import com.google.protobuf.ByteString;
|
||||||
import ghidra.dbg.error.DebuggerMemoryAccessException;
|
import ghidra.dbg.error.DebuggerMemoryAccessException;
|
||||||
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.memory.MemoryReader;
|
import ghidra.dbg.memory.MemoryReader;
|
||||||
import ghidra.dbg.memory.MemoryWriter;
|
import ghidra.dbg.memory.MemoryWriter;
|
||||||
import ghidra.dbg.target.TargetMemory;
|
import ghidra.dbg.target.TargetMemory;
|
||||||
|
@ -91,7 +90,7 @@ public interface GadpClientTargetMemory
|
||||||
byte[] data = evt.getContent().toByteArray();
|
byte[] data = evt.getContent().toByteArray();
|
||||||
DelegateGadpClientTargetObject delegate = getDelegate();
|
DelegateGadpClientTargetObject delegate = getDelegate();
|
||||||
delegate.getMemoryCache(address.getAddressSpace()).updateMemory(address.getOffset(), data);
|
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)
|
@GadpEventHandler(Gadp.EventNotification.EvtCase.MEMORY_ERROR_EVENT)
|
||||||
|
@ -100,7 +99,8 @@ public interface GadpClientTargetMemory
|
||||||
AddressRange range = GadpValueUtils.getAddressRange(getModel(), evt.getRange());
|
AddressRange range = GadpValueUtils.getAddressRange(getModel(), evt.getRange());
|
||||||
String message = evt.getMessage();
|
String message = evt.getMessage();
|
||||||
// Errors are not cached, but recorded in trace
|
// Errors are not cached, but recorded in trace
|
||||||
getDelegate().listeners.fire(TargetMemoryListener.class)
|
getDelegate().getListeners()
|
||||||
|
.fire(TargetMemoryListener.class)
|
||||||
.memoryReadError(this, range, new DebuggerMemoryAccessException(message));
|
.memoryReadError(this, range, new DebuggerMemoryAccessException(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetMethod;
|
import ghidra.dbg.target.TargetMethod;
|
||||||
|
|
||||||
public interface GadpClientTargetMethod
|
public interface GadpClientTargetMethod
|
||||||
|
|
|
@ -15,46 +15,36 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.dbg.gadp.client;
|
package ghidra.dbg.gadp.client;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.lang.invoke.MethodHandles;
|
||||||
import java.util.List;
|
import java.lang.invoke.MethodHandles.Lookup;
|
||||||
|
|
||||||
|
import ghidra.dbg.agent.SpiTargetObject;
|
||||||
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
|
import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback;
|
||||||
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
import ghidra.dbg.gadp.client.annot.GadpEventHandler;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.target.TargetConsole.Channel;
|
import ghidra.dbg.target.TargetConsole.Channel;
|
||||||
import ghidra.dbg.target.TargetConsole.TargetConsoleListener;
|
import ghidra.dbg.target.TargetConsole.TargetConsoleListener;
|
||||||
import ghidra.dbg.target.TargetObject;
|
|
||||||
import ghidra.dbg.util.ValueUtils;
|
import ghidra.dbg.util.ValueUtils;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
public interface GadpClientTargetObject extends TargetObject {
|
public interface GadpClientTargetObject extends SpiTargetObject {
|
||||||
|
Lookup LOOKUP = MethodHandles.lookup();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
GadpClient getModel();
|
GadpClient getModel();
|
||||||
|
|
||||||
@Override
|
|
||||||
List<String> getProtocolID();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String getTypeHint();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Collection<String> getInterfaceNames();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Collection<Class<? extends TargetObject>> getInterfaces();
|
|
||||||
|
|
||||||
DelegateGadpClientTargetObject getDelegate();
|
DelegateGadpClientTargetObject getDelegate();
|
||||||
|
|
||||||
@GadpEventHandler(Gadp.EventNotification.EvtCase.MODEL_OBJECT_EVENT)
|
@GadpEventHandler(Gadp.EventNotification.EvtCase.MODEL_OBJECT_EVENT)
|
||||||
default void handleModelObjectEvent(Gadp.EventNotification notification) {
|
default void handleModelObjectEvent(Gadp.EventNotification notification) {
|
||||||
Gadp.ModelObjectEvent evt = notification.getModelObjectEvent();
|
Gadp.ModelObjectEvent evt = notification.getModelObjectEvent();
|
||||||
getDelegate().updateWithDelta(evt.getDelta());
|
getDelegate().updateWithDeltas(evt.getElementDelta(), evt.getAttributeDelta());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GadpEventHandler(Gadp.EventNotification.EvtCase.OBJECT_INVALIDATE_EVENT)
|
@GadpEventHandler(Gadp.EventNotification.EvtCase.OBJECT_INVALIDATE_EVENT)
|
||||||
default void handleObjectInvalidateEvent(Gadp.EventNotification notification) {
|
default void handleObjectInvalidateEvent(Gadp.EventNotification notification) {
|
||||||
Gadp.ObjectInvalidateEvent evt = notification.getObjectInvalidateEvent();
|
Gadp.ObjectInvalidateEvent evt = notification.getObjectInvalidateEvent();
|
||||||
getDelegate().doInvalidateSubtree(evt.getReason());
|
getDelegate().doInvalidate(this, evt.getReason());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GadpEventHandler(Gadp.EventNotification.EvtCase.CACHE_INVALIDATE_EVENT)
|
@GadpEventHandler(Gadp.EventNotification.EvtCase.CACHE_INVALIDATE_EVENT)
|
||||||
|
@ -69,7 +59,7 @@ public interface GadpClientTargetObject extends TargetObject {
|
||||||
|
|
||||||
@GadpAttributeChangeCallback(DISPLAY_ATTRIBUTE_NAME)
|
@GadpAttributeChangeCallback(DISPLAY_ATTRIBUTE_NAME)
|
||||||
default void handleDisplayChanged(Object display) {
|
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
|
// 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();
|
int channelIndex = evt.getChannel();
|
||||||
Channel[] allChannels = Channel.values();
|
Channel[] allChannels = Channel.values();
|
||||||
if (0 <= channelIndex && channelIndex < allChannels.length) {
|
if (0 <= channelIndex && channelIndex < allChannels.length) {
|
||||||
getDelegate().listeners.fire(TargetConsoleListener.class)
|
getDelegate().getListeners()
|
||||||
|
.fire(TargetConsoleListener.class)
|
||||||
.consoleOutput(this, allChannels[channelIndex], evt.getData().toByteArray());
|
.consoleOutput(this, allChannels[channelIndex], evt.getData().toByteArray());
|
||||||
}
|
}
|
||||||
else {
|
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.client.annot.GadpEventHandler;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetRegisterBank;
|
import ghidra.dbg.target.TargetRegisterBank;
|
||||||
|
|
||||||
public interface GadpClientTargetRegisterBank
|
public interface GadpClientTargetRegisterBank
|
||||||
|
@ -84,6 +83,8 @@ public interface GadpClientTargetRegisterBank
|
||||||
Map<String, byte[]> updates = GadpValueUtils.getRegisterValueMap(evt.getValueList());
|
Map<String, byte[]> updates = GadpValueUtils.getRegisterValueMap(evt.getValueList());
|
||||||
DelegateGadpClientTargetObject delegate = getDelegate();
|
DelegateGadpClientTargetObject delegate = getDelegate();
|
||||||
delegate.getRegisterCache().putAll(updates);
|
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 java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetResumable;
|
import ghidra.dbg.target.TargetResumable;
|
||||||
|
|
||||||
public interface GadpClientTargetResumable
|
public interface GadpClientTargetResumable
|
||||||
|
|
|
@ -18,7 +18,6 @@ package ghidra.dbg.gadp.client;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetSteppable;
|
import ghidra.dbg.target.TargetSteppable;
|
||||||
|
|
||||||
public interface GadpClientTargetSteppable
|
public interface GadpClientTargetSteppable
|
||||||
|
|
|
@ -13,9 +13,9 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.*;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
@ -26,11 +26,8 @@ import com.google.protobuf.ByteString;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.DebuggerObjectModel;
|
||||||
import ghidra.dbg.attributes.*;
|
import ghidra.dbg.attributes.*;
|
||||||
import ghidra.dbg.attributes.TargetObjectRefList.DefaultTargetObjectRefList;
|
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;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp.ModelObjectDelta;
|
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.TargetAttachKind;
|
||||||
import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet;
|
import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet;
|
||||||
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet;
|
import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet;
|
||||||
|
@ -309,28 +306,22 @@ public enum GadpValueUtils {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Gadp.ModelObjectInfo makeInfo(TargetObject obj) {
|
public static Gadp.ModelObjectDelta makeElementDelta(List<String> parentPath,
|
||||||
ModelObjectInfo.Builder builder = Gadp.ModelObjectInfo.newBuilder()
|
Delta<?, ?> delta) {
|
||||||
.setPath(GadpValueUtils.makePath(obj.getPath()))
|
ModelObjectDelta.Builder builder = Gadp.ModelObjectDelta.newBuilder()
|
||||||
.setTypeHint(obj.getTypeHint())
|
.addAllRemoved(delta.getKeysRemoved());
|
||||||
.addAllInterface(GadpRegistry.getInterfaceNames(obj));
|
for (Entry<String, ?> ent : delta.added.entrySet()) {
|
||||||
|
builder.addAdded(makeIndexedValue(parentPath, ent));
|
||||||
builder.addAllElementIndex(obj.getCachedElements().keySet());
|
|
||||||
for (Entry<String, ?> ent : obj.getCachedAttributes().entrySet()) {
|
|
||||||
builder.addAttribute(makeAttribute(obj, ent));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Gadp.ModelObjectDelta makeDelta(TargetObject parent,
|
public static Gadp.ModelObjectDelta makeAttributeDelta(List<String> parentPath,
|
||||||
Delta<?, ? extends TargetObjectRef> deltaE, Delta<?, ?> deltaA) {
|
Delta<?, ?> delta) {
|
||||||
ModelObjectDelta.Builder builder = Gadp.ModelObjectDelta.newBuilder()
|
ModelObjectDelta.Builder builder = Gadp.ModelObjectDelta.newBuilder()
|
||||||
.addAllIndexRemoved(deltaE.getKeysRemoved())
|
.addAllRemoved(delta.getKeysRemoved());
|
||||||
.addAllIndexAdded(deltaE.added.keySet())
|
for (Entry<String, ?> ent : delta.added.entrySet()) {
|
||||||
.addAllAttributeRemoved(deltaA.getKeysRemoved());
|
builder.addAdded(makeNamedValue(parentPath, ent));
|
||||||
for (Entry<String, ?> ent : deltaA.added.entrySet()) {
|
|
||||||
builder.addAttributeAdded(makeAttribute(parent, ent));
|
|
||||||
}
|
}
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
@ -649,18 +640,25 @@ public enum GadpValueUtils {
|
||||||
return b.build();
|
return b.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Gadp.Argument makeArgument(Map.Entry<String, ?> argument) {
|
public static Gadp.NamedValue makeNamedValue(Map.Entry<String, ?> ent) {
|
||||||
return Gadp.Argument.newBuilder()
|
return makeNamedValue(null, ent);
|
||||||
.setName(argument.getKey())
|
}
|
||||||
.setValue(makeValue(null, argument.getValue()))
|
|
||||||
|
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();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Gadp.Attribute makeAttribute(TargetObject parent, Map.Entry<String, ?> ent) {
|
public static Gadp.NamedValue makeIndexedValue(List<String> parentPath,
|
||||||
return Gadp.Attribute.newBuilder()
|
Map.Entry<String, ?> ent) {
|
||||||
|
List<String> path = parentPath == null ? null : PathUtils.index(parentPath, ent.getKey());
|
||||||
|
return Gadp.NamedValue.newBuilder()
|
||||||
.setName(ent.getKey())
|
.setName(ent.getKey())
|
||||||
.setValue(
|
.setValue(makeValue(path, ent.getValue()))
|
||||||
makeValue(PathUtils.extend(parent.getPath(), ent.getKey()), ent.getValue()))
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -712,14 +710,11 @@ public enum GadpValueUtils {
|
||||||
case PARAMETERS_VALUE:
|
case PARAMETERS_VALUE:
|
||||||
return getParameters(model, value.getParametersValue());
|
return getParameters(model, value.getParametersValue());
|
||||||
case PATH_VALUE:
|
case PATH_VALUE:
|
||||||
return model.createRef(value.getPathValue().getEList());
|
return model.getModelObject(value.getPathValue().getEList());
|
||||||
case PATH_LIST_VALUE:
|
case PATH_LIST_VALUE:
|
||||||
return getRefList(model, value.getPathListValue());
|
return getRefList(model, value.getPathListValue());
|
||||||
case OBJECT_INFO:
|
|
||||||
Msg.error(GadpValueUtils.class, "ObjectInfo requires special treatment:" + value);
|
|
||||||
return model.createRef(path);
|
|
||||||
case OBJECT_STUB:
|
case OBJECT_STUB:
|
||||||
return model.createRef(path);
|
return model.getModelObject(path);
|
||||||
case TYPE_VALUE:
|
case TYPE_VALUE:
|
||||||
return getValueType(value.getTypeValue());
|
return getValueType(value.getTypeValue());
|
||||||
case SPEC_NOT_SET:
|
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()),
|
return getValue(object.getModel(), PathUtils.extend(object.getPath(), attr.getName()),
|
||||||
attr.getValue());
|
attr.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<String, Object> getAttributeMap(TargetObject object,
|
public static GadpClientTargetObject getElementValue(GadpClientTargetObject object,
|
||||||
List<Gadp.Attribute> list) {
|
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<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
for (Gadp.Attribute attr : list) {
|
for (Gadp.NamedValue attr : list) {
|
||||||
if (result.put(attr.getName(),
|
Object val = GadpValueUtils.getAttributeValue(object, attr);
|
||||||
GadpValueUtils.getAttributeValue(object, attr)) != null) {
|
if (result.put(attr.getName(), val) != null) {
|
||||||
Msg.warn(GadpValueUtils.class, "Received duplicate attribute: " + attr);
|
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());
|
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()
|
return arguments.entrySet()
|
||||||
.stream()
|
.stream()
|
||||||
.map(ent -> makeArgument(ent))
|
.map(ent -> makeNamedValue(ent))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<String, TargetObjectRef> getElementMap(TargetObject parent,
|
public static Map<String, GadpClientTargetObject> getElementMap(GadpClientTargetObject parent,
|
||||||
List<String> indices) {
|
List<Gadp.NamedValue> list) {
|
||||||
Map<String, TargetObjectRef> result = new LinkedHashMap<>();
|
Map<String, GadpClientTargetObject> result = new LinkedHashMap<>();
|
||||||
for (String index : indices) {
|
for (Gadp.NamedValue elem : list) {
|
||||||
result.put(index,
|
GadpClientTargetObject val = GadpValueUtils.getElementValue(parent, elem);
|
||||||
parent.getModel().createRef(PathUtils.index(parent.getPath(), index)));
|
if (val == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (result.put(elem.getName(), val) != null) {
|
||||||
|
Msg.warn(GadpValueUtils.class, "Received duplicate element: " + elem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<String, ?> getArguments(DebuggerObjectModel model,
|
public static Map<String, ?> getArguments(DebuggerObjectModel model,
|
||||||
List<Gadp.Argument> arguments) {
|
List<Gadp.NamedValue> arguments) {
|
||||||
return arguments.stream()
|
return arguments.stream()
|
||||||
.collect(
|
.collect(
|
||||||
Collectors.toMap(a -> a.getName(), a -> getValue(model, null, a.getValue())));
|
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.error.GadpErrorException;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.program.model.address.*;
|
import ghidra.program.model.address.*;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
public abstract class AbstractGadpServer
|
public abstract class AbstractGadpServer
|
||||||
extends AbstractAsyncServer<AbstractGadpServer, GadpClientHandler>
|
extends AbstractAsyncServer<AbstractGadpServer, GadpClientHandler>
|
||||||
|
@ -86,4 +87,13 @@ public abstract class AbstractGadpServer
|
||||||
public void setExitOnClosed(boolean exitOnClosed) {
|
public void setExitOnClosed(boolean exitOnClosed) {
|
||||||
this.exitOnClosed = 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;
|
package ghidra.dbg.gadp.server;
|
||||||
|
|
||||||
|
import java.nio.channels.AsynchronousByteChannel;
|
||||||
import java.nio.channels.AsynchronousSocketChannel;
|
import java.nio.channels.AsynchronousSocketChannel;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
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.comm.service.AbstractAsyncClientHandler;
|
||||||
|
import ghidra.dbg.DebuggerModelListener;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.DebuggerObjectModel;
|
||||||
import ghidra.dbg.attributes.TargetObjectRef;
|
import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.attributes.TypedTargetObjectRef;
|
import ghidra.dbg.attributes.TypedTargetObjectRef;
|
||||||
import ghidra.dbg.error.*;
|
import ghidra.dbg.error.*;
|
||||||
import ghidra.dbg.gadp.GadpVersion;
|
import ghidra.dbg.gadp.GadpVersion;
|
||||||
import ghidra.dbg.gadp.client.GadpClientTargetAttachable;
|
import ghidra.dbg.gadp.client.GadpClientTargetAttachable;
|
||||||
|
import ghidra.dbg.gadp.client.GadpValueUtils;
|
||||||
import ghidra.dbg.gadp.error.GadpErrorException;
|
import ghidra.dbg.gadp.error.GadpErrorException;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp.ErrorCode;
|
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.AsyncProtobufMessageChannel;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.*;
|
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.TargetBreakpointSpec.TargetBreakpointKind;
|
||||||
import ghidra.dbg.target.TargetConsole.Channel;
|
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.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.TargetObjectSchema;
|
||||||
import ghidra.dbg.target.schema.XmlSchemaContext;
|
import ghidra.dbg.target.schema.XmlSchemaContext;
|
||||||
import ghidra.dbg.util.CollectionUtils.Delta;
|
import ghidra.dbg.util.CollectionUtils.Delta;
|
||||||
|
@ -88,42 +81,81 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class ListenerForEvents
|
protected class ListenerForEvents implements DebuggerModelListener, TargetTextConsoleListener {
|
||||||
implements TargetObjectListener, TargetAccessibilityListener, TargetBreakpointListener,
|
@Override
|
||||||
TargetEventScopeListener, TargetExecutionStateListener, TargetFocusScopeListener,
|
public void created(TargetObject object) {
|
||||||
TargetInterpreterListener, TargetMemoryListener, TargetRegisterBankListener {
|
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
|
@Override
|
||||||
public void attributesChanged(TargetObject parent, Collection<String> removed,
|
public void attributesChanged(TargetObject parent, Collection<String> removed,
|
||||||
Map<String, ?> added) {
|
Map<String, ?> added) {
|
||||||
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: Can elements and attributes be combined into one message?
|
// 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);
|
.exceptionally(GadpClientHandler::errorSendNotify);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void elementsChanged(TargetObject parent, Collection<String> removed,
|
public void elementsChanged(TargetObject parent, Collection<String> removed,
|
||||||
Map<String, ? extends TargetObjectRef> added) {
|
Map<String, ? extends TargetObjectRef> added) {
|
||||||
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: Can elements and attributes be combined into one message?
|
// 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);
|
.exceptionally(GadpClientHandler::errorSendNotify);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidated(TargetObject object, String reason) {
|
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||||
if (!unsubscribeSubtree(object)) {
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (object != branch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
channel.write(Gadp.RootMessage.newBuilder()
|
channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||||
.setPath(GadpValueUtils.makePath(object.getPath()))
|
.setPath(GadpValueUtils.makePath(object.getPath()))
|
||||||
.setObjectInvalidateEvent(
|
.setObjectInvalidateEvent(
|
||||||
ObjectInvalidateEvent.newBuilder().setReason(reason)))
|
Gadp.ObjectInvalidateEvent.newBuilder().setReason(reason)))
|
||||||
.build()).exceptionally(GadpClientHandler::errorSendNotify);
|
.build()).exceptionally(GadpClientHandler::errorSendNotify);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void consoleOutput(TargetObject console, Channel c, byte[] data) {
|
public void consoleOutput(TargetObject console, Channel c, byte[] data) {
|
||||||
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (c == null || console == null) {
|
if (c == null || console == null) {
|
||||||
Msg.warn(this, "Why is console or channel null in consoleOutput callback?");
|
Msg.warn(this, "Why is console or channel null in consoleOutput callback?");
|
||||||
return;
|
return;
|
||||||
|
@ -139,6 +171,9 @@ public class GadpClientHandler
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void consoleOutput(TargetObject console, Channel c, String out) {
|
public void consoleOutput(TargetObject console, Channel c, String out) {
|
||||||
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
consoleOutput(console, c, out.getBytes(TargetConsole.CHARSET));
|
consoleOutput(console, c, out.getBytes(TargetConsole.CHARSET));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,6 +182,9 @@ public class GadpClientHandler
|
||||||
TypedTargetObjectRef<? extends TargetStackFrame<?>> frame,
|
TypedTargetObjectRef<? extends TargetStackFrame<?>> frame,
|
||||||
TypedTargetObjectRef<? extends TargetBreakpointSpec<?>> spec,
|
TypedTargetObjectRef<? extends TargetBreakpointSpec<?>> spec,
|
||||||
TypedTargetObjectRef<? extends TargetBreakpointLocation<?>> breakpoint) {
|
TypedTargetObjectRef<? extends TargetBreakpointLocation<?>> breakpoint) {
|
||||||
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Gadp.BreakHitEvent.Builder evt = Gadp.BreakHitEvent.newBuilder()
|
Gadp.BreakHitEvent.Builder evt = Gadp.BreakHitEvent.newBuilder()
|
||||||
.setTrapped(GadpValueUtils.makePath(trapped.getPath()))
|
.setTrapped(GadpValueUtils.makePath(trapped.getPath()))
|
||||||
.setSpec(GadpValueUtils.makePath(spec.getPath()))
|
.setSpec(GadpValueUtils.makePath(spec.getPath()))
|
||||||
|
@ -163,6 +201,9 @@ public class GadpClientHandler
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidateCacheRequested(TargetObject object) {
|
public void invalidateCacheRequested(TargetObject object) {
|
||||||
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
channel.write(Gadp.RootMessage.newBuilder()
|
channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||||
.setPath(GadpValueUtils.makePath(object.getPath()))
|
.setPath(GadpValueUtils.makePath(object.getPath()))
|
||||||
|
@ -174,6 +215,9 @@ public class GadpClientHandler
|
||||||
@Override
|
@Override
|
||||||
public void memoryReadError(TargetMemory<?> memory, AddressRange range,
|
public void memoryReadError(TargetMemory<?> memory, AddressRange range,
|
||||||
DebuggerMemoryAccessException e) {
|
DebuggerMemoryAccessException e) {
|
||||||
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: Ignore those generated by this client
|
// TODO: Ignore those generated by this client
|
||||||
channel.write(Gadp.RootMessage.newBuilder()
|
channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||||
|
@ -186,6 +230,9 @@ public class GadpClientHandler
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void memoryUpdated(TargetMemory<?> memory, Address address, byte[] data) {
|
public void memoryUpdated(TargetMemory<?> memory, Address address, byte[] data) {
|
||||||
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: Ignore those generated by this client
|
// TODO: Ignore those generated by this client
|
||||||
channel.write(Gadp.RootMessage.newBuilder()
|
channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||||
|
@ -198,6 +245,9 @@ public class GadpClientHandler
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registersUpdated(TargetRegisterBank<?> bank, Map<String, byte[]> updates) {
|
public void registersUpdated(TargetRegisterBank<?> bank, Map<String, byte[]> updates) {
|
||||||
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: Ignore those generated by this client
|
// TODO: Ignore those generated by this client
|
||||||
channel.write(Gadp.RootMessage.newBuilder()
|
channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||||
|
@ -211,6 +261,9 @@ public class GadpClientHandler
|
||||||
public void event(TargetEventScope<?> object,
|
public void event(TargetEventScope<?> object,
|
||||||
TypedTargetObjectRef<? extends TargetThread<?>> eventThread, TargetEventType type,
|
TypedTargetObjectRef<? extends TargetThread<?>> eventThread, TargetEventType type,
|
||||||
String description, List<Object> parameters) {
|
String description, List<Object> parameters) {
|
||||||
|
if (!sock.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Gadp.TargetEvent.Builder evt = Gadp.TargetEvent.newBuilder();
|
Gadp.TargetEvent.Builder evt = Gadp.TargetEvent.newBuilder();
|
||||||
if (eventThread != null) {
|
if (eventThread != null) {
|
||||||
evt.setEventThread(GadpValueUtils.makePath(eventThread.getPath()));
|
evt.setEventThread(GadpValueUtils.makePath(eventThread.getPath()));
|
||||||
|
@ -235,12 +288,16 @@ public class GadpClientHandler
|
||||||
protected final AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> channel;
|
protected final AsyncProtobufMessageChannel<Gadp.RootMessage, Gadp.RootMessage> channel;
|
||||||
protected final ListenerForEvents listenerForEvents = new ListenerForEvents();
|
protected final ListenerForEvents listenerForEvents = new ListenerForEvents();
|
||||||
// Keeps strong references and tells level of subscription
|
// Keeps strong references and tells level of subscription
|
||||||
protected final NavigableSet<TargetObject> subscriptions = new TreeSet<>();
|
|
||||||
|
|
||||||
public GadpClientHandler(AbstractGadpServer server, AsynchronousSocketChannel sock) {
|
public GadpClientHandler(AbstractGadpServer server, AsynchronousSocketChannel sock) {
|
||||||
super(server, sock);
|
super(server, sock);
|
||||||
model = server.model;
|
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
|
@Override
|
||||||
|
@ -256,6 +313,7 @@ public class GadpClientHandler
|
||||||
loop.repeat();
|
loop.repeat();
|
||||||
try {
|
try {
|
||||||
processMessage(msg).exceptionally(e -> {
|
processMessage(msg).exceptionally(e -> {
|
||||||
|
e.printStackTrace();
|
||||||
replyError(msg, e).exceptionally(ee -> {
|
replyError(msg, e).exceptionally(ee -> {
|
||||||
Msg.error(this, "Could not send error reply: " + ee);
|
Msg.error(this, "Could not send error reply: " + ee);
|
||||||
return null;
|
return null;
|
||||||
|
@ -346,6 +404,8 @@ public class GadpClientHandler
|
||||||
return processFocus(msg.getSequence(), msg.getFocusRequest());
|
return processFocus(msg.getSequence(), msg.getFocusRequest());
|
||||||
case INTERRUPT_REQUEST:
|
case INTERRUPT_REQUEST:
|
||||||
return processInterrupt(msg.getSequence(), msg.getInterruptRequest());
|
return processInterrupt(msg.getSequence(), msg.getInterruptRequest());
|
||||||
|
case INVOKE_REQUEST:
|
||||||
|
return processInvoke(msg.getSequence(), msg.getInvokeRequest());
|
||||||
case KILL_REQUEST:
|
case KILL_REQUEST:
|
||||||
return processKill(msg.getSequence(), msg.getKillRequest());
|
return processKill(msg.getSequence(), msg.getKillRequest());
|
||||||
case LAUNCH_REQUEST:
|
case LAUNCH_REQUEST:
|
||||||
|
@ -358,14 +418,12 @@ public class GadpClientHandler
|
||||||
return processRegisterRead(msg.getSequence(), msg.getRegisterReadRequest());
|
return processRegisterRead(msg.getSequence(), msg.getRegisterReadRequest());
|
||||||
case REGISTER_WRITE_REQUEST:
|
case REGISTER_WRITE_REQUEST:
|
||||||
return processRegisterWrite(msg.getSequence(), msg.getRegisterWriteRequest());
|
return processRegisterWrite(msg.getSequence(), msg.getRegisterWriteRequest());
|
||||||
|
case RESYNC_REQUEST:
|
||||||
|
return processResync(msg.getSequence(), msg.getResyncRequest());
|
||||||
case RESUME_REQUEST:
|
case RESUME_REQUEST:
|
||||||
return processResume(msg.getSequence(), msg.getResumeRequest());
|
return processResume(msg.getSequence(), msg.getResumeRequest());
|
||||||
case STEP_REQUEST:
|
case STEP_REQUEST:
|
||||||
return processStep(msg.getSequence(), msg.getStepRequest());
|
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:
|
default:
|
||||||
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
|
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
|
||||||
"Unrecognized request: " + msg.getMsgCase());
|
"Unrecognized request: " + msg.getMsgCase());
|
||||||
|
@ -379,13 +437,16 @@ public class GadpClientHandler
|
||||||
"No listed version is supported");
|
"No listed version is supported");
|
||||||
}
|
}
|
||||||
TargetObjectSchema rootSchema = model.getRootSchema();
|
TargetObjectSchema rootSchema = model.getRootSchema();
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
CompletableFuture<Integer> send = channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
.setConnectReply(Gadp.ConnectReply.newBuilder()
|
.setConnectReply(Gadp.ConnectReply.newBuilder()
|
||||||
.setVersion(ver)
|
.setVersion(ver)
|
||||||
.setSchemaContext(XmlSchemaContext.serialize(rootSchema.getContext()))
|
.setSchemaContext(XmlSchemaContext.serialize(rootSchema.getContext()))
|
||||||
.setRootSchema(rootSchema.getName().toString()))
|
.setRootSchema(rootSchema.getName().toString()))
|
||||||
.build());
|
.build());
|
||||||
|
return send.thenAccept(__ -> {
|
||||||
|
model.addModelListener(listenerForEvents, true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processPing(int seqno, Gadp.PingRequest req) {
|
protected CompletableFuture<?> processPing(int seqno, Gadp.PingRequest req) {
|
||||||
|
@ -408,99 +469,27 @@ public class GadpClientHandler
|
||||||
return ref;
|
return ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void changeSubscription(TargetObject obj, boolean subscribed) {
|
protected <T extends TargetObjectRef> CompletableFuture<Integer> sendDelta(
|
||||||
synchronized (subscriptions) {
|
List<String> parentPath, Delta<TargetObjectRef, T> deltaE, Delta<?, ?> deltaA) {
|
||||||
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
|
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||||
.setPath(GadpValueUtils.makePath(parent.getPath()))
|
.setPath(GadpValueUtils.makePath(parentPath))
|
||||||
.setModelObjectEvent(Gadp.ModelObjectEvent.newBuilder()
|
.setModelObjectEvent(Gadp.ModelObjectEvent.newBuilder()
|
||||||
.setDelta(GadpValueUtils.makeDelta(parent, deltaE, deltaA))))
|
.setElementDelta(
|
||||||
|
GadpValueUtils.makeElementDelta(parentPath, deltaE))
|
||||||
|
.setAttributeDelta(
|
||||||
|
GadpValueUtils.makeElementDelta(parentPath, deltaA))))
|
||||||
.build());
|
.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) {
|
protected CompletableFuture<?> processInvoke(int seqno, Gadp.InvokeRequest req) {
|
||||||
List<String> path = req.getPath().getEList();
|
List<String> path = req.getPath().getEList();
|
||||||
return model.fetchModelObject(path).thenCompose(obj -> {
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
TargetMethod<?> method =
|
TargetMethod<?> method =
|
||||||
DebuggerObjectModel.requireIface(TargetMethod.class, obj, path);
|
DebuggerObjectModel.requireIface(TargetMethod.class, obj, path);
|
||||||
return method.invoke(GadpValueUtils.getArguments(model, req.getArgumentList()));
|
return method.invoke(GadpValueUtils.getArguments(model, req.getArgumentList()));
|
||||||
|
}).thenCompose(result -> {
|
||||||
|
return model.flushEvents().thenApply(__ -> result);
|
||||||
}).thenCompose(result -> {
|
}).thenCompose(result -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.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) {
|
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 -> {
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
TargetAttacher<?> attacher =
|
TargetAttacher<?> attacher =
|
||||||
DebuggerObjectModel.requireIface(TargetAttacher.class, obj, path);
|
DebuggerObjectModel.requireIface(TargetAttacher.class, obj, path);
|
||||||
|
@ -527,6 +530,8 @@ public class GadpClientHandler
|
||||||
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
|
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
|
||||||
"Unrecognized attach specification:" + req);
|
"Unrecognized attach specification:" + req);
|
||||||
}
|
}
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -536,9 +541,10 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processBreakCreate(int seqno, Gadp.BreakCreateRequest req) {
|
protected CompletableFuture<?> processBreakCreate(int seqno, Gadp.BreakCreateRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetBreakpointContainer<?> breaks = DebuggerObjectModel
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
.requireIface(TargetBreakpointContainer.class, obj, req.getPath().getEList());
|
TargetBreakpointContainer<?> breaks =
|
||||||
|
DebuggerObjectModel.requireIface(TargetBreakpointContainer.class, obj, path);
|
||||||
Set<TargetBreakpointKind> kinds = GadpValueUtils.getBreakKindSet(req.getKinds());
|
Set<TargetBreakpointKind> kinds = GadpValueUtils.getBreakKindSet(req.getKinds());
|
||||||
switch (req.getSpecCase()) {
|
switch (req.getSpecCase()) {
|
||||||
case EXPRESSION:
|
case EXPRESSION:
|
||||||
|
@ -550,6 +556,8 @@ public class GadpClientHandler
|
||||||
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
|
throw new GadpErrorException(Gadp.ErrorCode.EC_BAD_REQUEST,
|
||||||
"Unrecognized breakpoint specification: " + req);
|
"Unrecognized breakpoint specification: " + req);
|
||||||
}
|
}
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -559,10 +567,13 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processBreakToggle(int seqno, Gadp.BreakToggleRequest req) {
|
protected CompletableFuture<?> processBreakToggle(int seqno, Gadp.BreakToggleRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetBreakpointSpec<?> spec = DebuggerObjectModel
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
.requireIface(TargetBreakpointSpec.tclass, obj, req.getPath().getEList());
|
TargetBreakpointSpec<?> spec =
|
||||||
|
DebuggerObjectModel.requireIface(TargetBreakpointSpec.tclass, obj, path);
|
||||||
return spec.toggle(req.getEnabled());
|
return spec.toggle(req.getEnabled());
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -572,10 +583,13 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processDelete(int seqno, Gadp.DeleteRequest req) {
|
protected CompletableFuture<?> processDelete(int seqno, Gadp.DeleteRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetDeletable<?> del = DebuggerObjectModel.requireIface(TargetDeletable.tclass, obj,
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
req.getPath().getEList());
|
TargetDeletable<?> del =
|
||||||
|
DebuggerObjectModel.requireIface(TargetDeletable.tclass, obj, path);
|
||||||
return del.delete();
|
return del.delete();
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -585,10 +599,13 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processDetach(int seqno, Gadp.DetachRequest req) {
|
protected CompletableFuture<?> processDetach(int seqno, Gadp.DetachRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetDetachable<?> det = DebuggerObjectModel.requireIface(TargetDetachable.tclass, obj,
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
req.getPath().getEList());
|
TargetDetachable<?> det =
|
||||||
|
DebuggerObjectModel.requireIface(TargetDetachable.tclass, obj, path);
|
||||||
return det.detach();
|
return det.detach();
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -598,13 +615,16 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processExecute(int seqno, Gadp.ExecuteRequest req) {
|
protected CompletableFuture<?> processExecute(int seqno, Gadp.ExecuteRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetInterpreter<?> interpreter = DebuggerObjectModel
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
.requireIface(TargetInterpreter.tclass, obj, req.getPath().getEList());
|
TargetInterpreter<?> interpreter =
|
||||||
|
DebuggerObjectModel.requireIface(TargetInterpreter.tclass, obj, path);
|
||||||
if (req.getCapture()) {
|
if (req.getCapture()) {
|
||||||
return interpreter.executeCapture(req.getCommand());
|
return interpreter.executeCapture(req.getCommand());
|
||||||
}
|
}
|
||||||
return interpreter.execute(req.getCommand()).thenApply(__ -> "");
|
return interpreter.execute(req.getCommand()).thenApply(__ -> "");
|
||||||
|
}).thenCompose(out -> {
|
||||||
|
return model.flushEvents().thenApply(__ -> out);
|
||||||
}).thenCompose(out -> {
|
}).thenCompose(out -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -614,10 +634,13 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processFocus(int seqno, Gadp.FocusRequest req) {
|
protected CompletableFuture<?> processFocus(int seqno, Gadp.FocusRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetFocusScope<?> scope = DebuggerObjectModel.requireIface(TargetFocusScope.tclass,
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
obj, req.getPath().getEList());
|
TargetFocusScope<?> scope =
|
||||||
|
DebuggerObjectModel.requireIface(TargetFocusScope.tclass, obj, path);
|
||||||
return scope.requestFocus(model.createRef(req.getFocus().getEList()));
|
return scope.requestFocus(model.createRef(req.getFocus().getEList()));
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -627,10 +650,13 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processInterrupt(int seqno, Gadp.InterruptRequest req) {
|
protected CompletableFuture<?> processInterrupt(int seqno, Gadp.InterruptRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetInterruptible<?> interruptible = DebuggerObjectModel
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
.requireIface(TargetInterruptible.tclass, obj, req.getPath().getEList());
|
TargetInterruptible<?> interruptible =
|
||||||
|
DebuggerObjectModel.requireIface(TargetInterruptible.tclass, obj, path);
|
||||||
return interruptible.interrupt();
|
return interruptible.interrupt();
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -641,9 +667,11 @@ public class GadpClientHandler
|
||||||
|
|
||||||
protected CompletableFuture<?> processCacheInvalidate(int seqno,
|
protected CompletableFuture<?> processCacheInvalidate(int seqno,
|
||||||
Gadp.CacheInvalidateRequest req) {
|
Gadp.CacheInvalidateRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
return DebuggerObjectModel.requireNonNull(obj, req.getPath().getEList())
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
.invalidateCaches();
|
return DebuggerObjectModel.requireNonNull(obj, path).invalidateCaches();
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -653,10 +681,13 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processKill(int seqno, Gadp.KillRequest req) {
|
protected CompletableFuture<?> processKill(int seqno, Gadp.KillRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetKillable<?> killable = DebuggerObjectModel.requireIface(TargetKillable.class, obj,
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
req.getPath().getEList());
|
TargetKillable<?> killable =
|
||||||
|
DebuggerObjectModel.requireIface(TargetKillable.class, obj, path);
|
||||||
return killable.kill();
|
return killable.kill();
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -666,11 +697,17 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processLaunch(int seqno, Gadp.LaunchRequest req) {
|
protected CompletableFuture<?> processLaunch(int seqno, Gadp.LaunchRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetLauncher<?> launcher = DebuggerObjectModel.requireIface(TargetLauncher.class, obj,
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
req.getPath().getEList());
|
Msg.debug(this, "Launching: " + Thread.currentThread());
|
||||||
|
TargetLauncher<?> launcher =
|
||||||
|
DebuggerObjectModel.requireIface(TargetLauncher.class, obj, path);
|
||||||
return launcher.launch(GadpValueUtils.getArguments(model, req.getArgumentList()));
|
return launcher.launch(GadpValueUtils.getArguments(model, req.getArgumentList()));
|
||||||
}).thenCompose(__ -> {
|
}).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()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
.setLaunchReply(Gadp.LaunchReply.getDefaultInstance())
|
.setLaunchReply(Gadp.LaunchReply.getDefaultInstance())
|
||||||
|
@ -679,11 +716,14 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processMemoryRead(int seqno, Gadp.MemoryReadRequest req) {
|
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 =
|
TargetMemory<?> memory =
|
||||||
DebuggerObjectModel.requireIface(TargetMemory.class, obj, req.getPath().getEList());
|
DebuggerObjectModel.requireIface(TargetMemory.class, obj, path);
|
||||||
AddressRange range = GadpValueUtils.getAddressRange(memory.getModel(), req.getRange());
|
AddressRange range = GadpValueUtils.getAddressRange(memory.getModel(), req.getRange());
|
||||||
return memory.readMemory(range.getMinAddress(), (int) range.getLength());
|
return memory.readMemory(range.getMinAddress(), (int) range.getLength());
|
||||||
|
}).thenCompose(data -> {
|
||||||
|
return model.flushEvents().thenApply(__ -> data);
|
||||||
}).thenCompose(data -> {
|
}).thenCompose(data -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -694,12 +734,15 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processMemoryWrite(int seqno, Gadp.MemoryWriteRequest req) {
|
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 =
|
TargetMemory<?> memory =
|
||||||
DebuggerObjectModel.requireIface(TargetMemory.class, obj, req.getPath().getEList());
|
DebuggerObjectModel.requireIface(TargetMemory.class, obj, path);
|
||||||
Address start = GadpValueUtils.getAddress(memory.getModel(), req.getStart());
|
Address start = GadpValueUtils.getAddress(memory.getModel(), req.getStart());
|
||||||
// TODO: Spare a copy by specifying a ByteBuffer variant of writeMemory?
|
// TODO: Spare a copy by specifying a ByteBuffer variant of writeMemory?
|
||||||
return memory.writeMemory(start, req.getContent().toByteArray());
|
return memory.writeMemory(start, req.getContent().toByteArray());
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -709,10 +752,13 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processRegisterRead(int seqno, Gadp.RegisterReadRequest req) {
|
protected CompletableFuture<?> processRegisterRead(int seqno, Gadp.RegisterReadRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetRegisterBank<?> bank = DebuggerObjectModel.requireIface(TargetRegisterBank.class,
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
obj, req.getPath().getEList());
|
TargetRegisterBank<?> bank =
|
||||||
|
DebuggerObjectModel.requireIface(TargetRegisterBank.class, obj, path);
|
||||||
return bank.readRegistersNamed(req.getNameList());
|
return bank.readRegistersNamed(req.getNameList());
|
||||||
|
}).thenCompose(data -> {
|
||||||
|
return model.flushEvents().thenApply(__ -> data);
|
||||||
}).thenCompose(data -> {
|
}).thenCompose(data -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -723,14 +769,17 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processRegisterWrite(int seqno, Gadp.RegisterWriteRequest req) {
|
protected CompletableFuture<?> processRegisterWrite(int seqno, Gadp.RegisterWriteRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetRegisterBank<?> bank = DebuggerObjectModel.requireIface(TargetRegisterBank.class,
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
obj, req.getPath().getEList());
|
TargetRegisterBank<?> bank =
|
||||||
|
DebuggerObjectModel.requireIface(TargetRegisterBank.class, obj, path);
|
||||||
Map<String, byte[]> values = new LinkedHashMap<>();
|
Map<String, byte[]> values = new LinkedHashMap<>();
|
||||||
for (Gadp.RegisterValue rv : req.getValueList()) {
|
for (Gadp.RegisterValue rv : req.getValueList()) {
|
||||||
values.put(rv.getName(), rv.getContent().toByteArray());
|
values.put(rv.getName(), rv.getContent().toByteArray());
|
||||||
}
|
}
|
||||||
return bank.writeRegistersNamed(values);
|
return bank.writeRegistersNamed(values);
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -740,10 +789,13 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processResume(int seqno, Gadp.ResumeRequest req) {
|
protected CompletableFuture<?> processResume(int seqno, Gadp.ResumeRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetResumable<?> resumable = DebuggerObjectModel.requireIface(TargetResumable.class,
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
obj, req.getPath().getEList());
|
TargetResumable<?> resumable =
|
||||||
|
DebuggerObjectModel.requireIface(TargetResumable.class, obj, path);
|
||||||
return resumable.resume();
|
return resumable.resume();
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
@ -753,10 +805,13 @@ public class GadpClientHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<?> processStep(int seqno, Gadp.StepRequest req) {
|
protected CompletableFuture<?> processStep(int seqno, Gadp.StepRequest req) {
|
||||||
return model.fetchModelObject(req.getPath().getEList()).thenCompose(obj -> {
|
List<String> path = req.getPath().getEList();
|
||||||
TargetSteppable<?> steppable = DebuggerObjectModel.requireIface(TargetSteppable.class,
|
return model.fetchModelObject(path).thenCompose(obj -> {
|
||||||
obj, req.getPath().getEList());
|
TargetSteppable<?> steppable =
|
||||||
|
DebuggerObjectModel.requireIface(TargetSteppable.class, obj, path);
|
||||||
return steppable.step(GadpValueUtils.getStepKind(req.getKind()));
|
return steppable.step(GadpValueUtils.getStepKind(req.getKind()));
|
||||||
|
}).thenCompose(__ -> {
|
||||||
|
return model.flushEvents();
|
||||||
}).thenCompose(__ -> {
|
}).thenCompose(__ -> {
|
||||||
return channel.write(Gadp.RootMessage.newBuilder()
|
return channel.write(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
|
|
|
@ -210,11 +210,6 @@ message ParameterList {
|
||||||
repeated Parameter parameter = 1;
|
repeated Parameter parameter = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Argument {
|
|
||||||
string name = 1;
|
|
||||||
Value value = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Value {
|
message Value {
|
||||||
oneof spec {
|
oneof spec {
|
||||||
bool bool_value = 1;
|
bool bool_value = 1;
|
||||||
|
@ -235,7 +230,6 @@ message Value {
|
||||||
UpdateMode update_mode_value = 16;
|
UpdateMode update_mode_value = 16;
|
||||||
Path path_value = 17;
|
Path path_value = 17;
|
||||||
PathList path_list_value = 18;
|
PathList path_list_value = 18;
|
||||||
ModelObjectInfo object_info = 19;
|
|
||||||
ModelObjectStub object_stub = 20;
|
ModelObjectStub object_stub = 20;
|
||||||
ParameterList parameters_value = 21;
|
ParameterList parameters_value = 21;
|
||||||
ValueType type_value = 22;
|
ValueType type_value = 22;
|
||||||
|
@ -243,7 +237,7 @@ message Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message Attribute {
|
message NamedValue {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
Value value = 2;
|
Value value = 2;
|
||||||
}
|
}
|
||||||
|
@ -251,46 +245,19 @@ message Attribute {
|
||||||
message ModelObjectStub {
|
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 {
|
message ModelObjectDelta {
|
||||||
repeated string index_removed = 2;
|
repeated string removed = 1;
|
||||||
repeated string index_added = 3;
|
repeated NamedValue added = 2;
|
||||||
// TODO: indices_moved?
|
|
||||||
repeated string attribute_removed = 4;
|
|
||||||
repeated Attribute attribute_added = 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ModelObjectEvent {
|
message ModelObjectEvent {
|
||||||
ModelObjectDelta delta = 1;
|
ModelObjectDelta element_delta = 1;
|
||||||
|
ModelObjectDelta attribute_delta = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
message ObjectCreatedEvent {
|
||||||
* This message both retrieves the requested object(s) and (un)subscribes to further changes
|
string type_hint = 2;
|
||||||
*
|
repeated string interface = 3;
|
||||||
* 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 ObjectInvalidateEvent {
|
message ObjectInvalidateEvent {
|
||||||
|
@ -299,7 +266,7 @@ message ObjectInvalidateEvent {
|
||||||
|
|
||||||
message LaunchRequest {
|
message LaunchRequest {
|
||||||
Path path = 1;
|
Path path = 1;
|
||||||
repeated Argument argument = 2;
|
repeated NamedValue argument = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LaunchReply {
|
message LaunchReply {
|
||||||
|
@ -486,13 +453,22 @@ message FocusReply {
|
||||||
|
|
||||||
message InvokeRequest {
|
message InvokeRequest {
|
||||||
Path path = 1;
|
Path path = 1;
|
||||||
repeated Argument argument = 2;
|
repeated NamedValue argument = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message InvokeReply {
|
message InvokeReply {
|
||||||
Value result = 1;
|
Value result = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ResyncRequest {
|
||||||
|
Path path = 1;
|
||||||
|
bool attributes = 2;
|
||||||
|
bool elements = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ResyncReply {
|
||||||
|
}
|
||||||
|
|
||||||
enum TargetEventType {
|
enum TargetEventType {
|
||||||
EV_STOPPED = 0;
|
EV_STOPPED = 0;
|
||||||
EV_RUNNING = 1;
|
EV_RUNNING = 1;
|
||||||
|
@ -515,6 +491,9 @@ message TargetEvent {
|
||||||
repeated Value parameters = 4;
|
repeated Value parameters = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RootAddedEvent {
|
||||||
|
}
|
||||||
|
|
||||||
message EventNotification {
|
message EventNotification {
|
||||||
Path path = 1;
|
Path path = 1;
|
||||||
oneof evt {
|
oneof evt {
|
||||||
|
@ -525,9 +504,11 @@ message EventNotification {
|
||||||
ConsoleOutputEvent console_output_event = 312;
|
ConsoleOutputEvent console_output_event = 312;
|
||||||
MemoryUpdateEvent memory_update_event = 317;
|
MemoryUpdateEvent memory_update_event = 317;
|
||||||
MemoryErrorEvent memory_error_event = 417;
|
MemoryErrorEvent memory_error_event = 417;
|
||||||
|
ObjectCreatedEvent object_created_event = 324;
|
||||||
ObjectInvalidateEvent object_invalidate_event = 323;
|
ObjectInvalidateEvent object_invalidate_event = 323;
|
||||||
RegisterUpdateEvent register_update_event = 322;
|
RegisterUpdateEvent register_update_event = 322;
|
||||||
TargetEvent target_event = 330;
|
TargetEvent target_event = 330;
|
||||||
|
RootAddedEvent root_added_event = 326;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -600,10 +581,10 @@ message RootMessage {
|
||||||
StepRequest step_request = 119;
|
StepRequest step_request = 119;
|
||||||
StepReply step_reply = 219;
|
StepReply step_reply = 219;
|
||||||
|
|
||||||
SubscribeRequest subscribe_request = 104;
|
|
||||||
SubscribeReply subscribe_reply = 204;
|
|
||||||
|
|
||||||
InvokeRequest invoke_request = 105;
|
InvokeRequest invoke_request = 105;
|
||||||
InvokeReply invoke_reply = 205;
|
InvokeReply invoke_reply = 205;
|
||||||
|
|
||||||
|
ResyncRequest resync_request = 125;
|
||||||
|
ResyncReply resync_reply = 225;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.dbg.gadp;
|
package ghidra.dbg.gadp;
|
||||||
|
|
||||||
import static ghidra.lifecycle.Unfinished.TODO;
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -27,7 +26,6 @@ import java.util.*;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
|
@ -39,21 +37,23 @@ import generic.ID;
|
||||||
import generic.Unique;
|
import generic.Unique;
|
||||||
import ghidra.async.*;
|
import ghidra.async.*;
|
||||||
import ghidra.dbg.DebugModelConventions;
|
import ghidra.dbg.DebugModelConventions;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.DebuggerModelListener;
|
||||||
import ghidra.dbg.agent.*;
|
import ghidra.dbg.agent.*;
|
||||||
import ghidra.dbg.attributes.TargetObjectRef;
|
import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.attributes.TargetStringList;
|
import ghidra.dbg.attributes.TargetStringList;
|
||||||
|
import ghidra.dbg.gadp.GadpClientServerTest.EventListener.CallEntry;
|
||||||
import ghidra.dbg.gadp.client.GadpClient;
|
import ghidra.dbg.gadp.client.GadpClient;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
|
import ghidra.dbg.gadp.protocol.Gadp.RootMessage;
|
||||||
import ghidra.dbg.gadp.server.AbstractGadpServer;
|
import ghidra.dbg.gadp.server.AbstractGadpServer;
|
||||||
|
import ghidra.dbg.gadp.server.GadpClientHandler;
|
||||||
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
|
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
|
||||||
import ghidra.dbg.target.*;
|
import ghidra.dbg.target.*;
|
||||||
import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener;
|
import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener;
|
||||||
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
|
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
|
||||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||||
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
||||||
import ghidra.dbg.target.TargetObject.TargetObjectFetchingListener;
|
import ghidra.dbg.target.TargetObject.*;
|
||||||
import ghidra.dbg.target.TargetObject.TargetObjectListener;
|
|
||||||
import ghidra.dbg.target.schema.TargetAttributeType;
|
import ghidra.dbg.target.schema.TargetAttributeType;
|
||||||
import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
|
import ghidra.dbg.target.schema.TargetObjectSchemaInfo;
|
||||||
import ghidra.dbg.util.*;
|
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 {
|
protected static <T> T waitOn(CompletableFuture<T> future) throws Throwable {
|
||||||
try {
|
try {
|
||||||
return future.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
|
return future.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
|
||||||
|
@ -251,12 +293,17 @@ public class GadpClientServerTest {
|
||||||
protected final TestGadpTargetAvailableLinkContainer links =
|
protected final TestGadpTargetAvailableLinkContainer links =
|
||||||
new TestGadpTargetAvailableLinkContainer(this);
|
new TestGadpTargetAvailableLinkContainer(this);
|
||||||
|
|
||||||
public TestGadpTargetSession(FakeDebuggerObjectModel model) {
|
public TestGadpTargetSession(TestGadpObjectModel model) {
|
||||||
super(model, "Session");
|
super(model, "Session");
|
||||||
|
|
||||||
changeAttributes(List.of(), List.of(available, processes), Map.of(), "Initialized");
|
changeAttributes(List.of(), List.of(available, processes), Map.of(), "Initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractDebuggerObjectModel getModel() {
|
||||||
|
return super.getModel();
|
||||||
|
}
|
||||||
|
|
||||||
@TargetAttributeType(name = "Available", required = true, fixed = true)
|
@TargetAttributeType(name = "Available", required = true, fixed = true)
|
||||||
public TestGadpTargetAvailableContainer getAvailable() {
|
public TestGadpTargetAvailableContainer getAvailable() {
|
||||||
return available;
|
return available;
|
||||||
|
@ -292,24 +339,14 @@ public class GadpClientServerTest {
|
||||||
public class TestTargetObject<E extends TargetObject, P extends TargetObject>
|
public class TestTargetObject<E extends TargetObject, P extends TargetObject>
|
||||||
extends DefaultTargetObject<E, P> {
|
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);
|
super(model, parent, key, typeHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<?> fetchAttribute(String name) {
|
public AbstractDebuggerObjectModel getModel() {
|
||||||
if (!PathUtils.isInvocation(name)) {
|
return super.getModel();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -334,13 +371,11 @@ public class GadpClientServerTest {
|
||||||
false, TargetStringList.of(), "Others to greet",
|
false, TargetStringList.of(), "Others to greet",
|
||||||
"List of other people to greet individually")));
|
"List of other people to greet individually")));
|
||||||
|
|
||||||
public class TestTargetMethod extends TestTargetObject<TargetObject, TargetObject>
|
public class TestGadpTargetMethod extends TestTargetObject<TargetObject, TestTargetObject<?, ?>>
|
||||||
implements TargetMethod<TestTargetMethod> {
|
implements TargetMethod<TestGadpTargetMethod> {
|
||||||
private Function<String, ?> method;
|
|
||||||
|
|
||||||
public TestTargetMethod(TargetObject parent, String key, Function<String, ?> method) {
|
public TestGadpTargetMethod(TestTargetObject<?, ?> parent, String key) {
|
||||||
super(parent.getModel(), parent, key, "Method");
|
super(parent.getModel(), parent, key, "Method");
|
||||||
this.method = method;
|
|
||||||
|
|
||||||
setAttributes(Map.of(
|
setAttributes(Map.of(
|
||||||
PARAMETERS_ATTRIBUTE_NAME, PARAMS,
|
PARAMETERS_ATTRIBUTE_NAME, PARAMS,
|
||||||
|
@ -348,16 +383,16 @@ public class GadpClientServerTest {
|
||||||
"Initialized");
|
"Initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object testInvoke(String paramsExpr) {
|
|
||||||
return method.apply(paramsExpr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Object> invoke(Map<String, ?> arguments) {
|
public CompletableFuture<Object> invoke(Map<String, ?> arguments) {
|
||||||
TestMethodInvocation invocation = new TestMethodInvocation(arguments);
|
TestMethodInvocation invocation = new TestMethodInvocation(arguments);
|
||||||
invocations.offer(invocation);
|
invocations.offer(invocation);
|
||||||
invocationCount.set(invocations.size(), null);
|
return invocation.thenApply(obj -> {
|
||||||
return invocation;
|
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
|
public class TestGadpTargetAvailableContainer
|
||||||
extends TestTargetObject<TestGadpTargetAvailable, TestGadpTargetSession> {
|
extends TestTargetObject<TestGadpTargetAvailable, TestGadpTargetSession> {
|
||||||
public char punct = '!';
|
|
||||||
|
|
||||||
public TestGadpTargetAvailableContainer(TestGadpTargetSession session) {
|
public TestGadpTargetAvailableContainer(TestGadpTargetSession session) {
|
||||||
super(session.getModel(), session, "Available", "AvailableContainer");
|
super(session.getModel(), session, "Available", "AvailableContainer");
|
||||||
|
|
||||||
setAttributes(List.of(
|
setAttributes(List.of(
|
||||||
new TestTargetMethod(this, "greet", p -> "Hello, " + p + punct)),
|
new TestGadpTargetMethod(this, "greet")),
|
||||||
Map.of(), "Initialized");
|
Map.of(), "Initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -417,7 +451,7 @@ public class GadpClientServerTest {
|
||||||
String cmd) {
|
String cmd) {
|
||||||
super(available.getModel(), available, PathUtils.makeKey(PathUtils.makeIndex(pid)),
|
super(available.getModel(), available, PathUtils.makeKey(PathUtils.makeIndex(pid)),
|
||||||
"Available");
|
"Available");
|
||||||
setAttributes(List.of(), Map.of(
|
changeAttributes(List.of(), Map.of(
|
||||||
"pid", pid,
|
"pid", pid,
|
||||||
"cmd", cmd //
|
"cmd", cmd //
|
||||||
), "Initialized");
|
), "Initialized");
|
||||||
|
@ -435,38 +469,62 @@ public class GadpClientServerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Refactor with other Fake and Test. Probably put in Framework-Debugging....
|
public class BlankObjectModel extends AbstractDebuggerObjectModel {
|
||||||
public class FakeDebuggerObjectModel extends AbstractDebuggerObjectModel {
|
|
||||||
private final TestGadpTargetSession session = new TestGadpTargetSession(this);
|
|
||||||
|
|
||||||
private final AddressSpace ram =
|
private final AddressSpace ram =
|
||||||
new GenericAddressSpace("RAM", 64, AddressSpace.TYPE_RAM, 0);
|
new GenericAddressSpace("RAM", 64, AddressSpace.TYPE_RAM, 0);
|
||||||
private final AddressFactory factory =
|
private final AddressFactory factory =
|
||||||
new DefaultAddressFactory(new AddressSpace[] { ram });
|
new DefaultAddressFactory(new AddressSpace[] { ram });
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
|
|
||||||
return CompletableFuture.completedFuture(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AddressFactory getAddressFactory() {
|
public AddressFactory getAddressFactory() {
|
||||||
return factory;
|
return factory;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
// TODO: Refactor with other Fake and Test. Probably put in Framework-Debugging....
|
||||||
public CompletableFuture<Void> close() {
|
public class TestGadpObjectModel extends BlankObjectModel {
|
||||||
return AsyncUtils.NIL;
|
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 {
|
public class TestGadpServer extends AbstractGadpServer {
|
||||||
@SuppressWarnings("hiding")
|
final TestGadpObjectModel model;
|
||||||
final FakeDebuggerObjectModel model;
|
|
||||||
|
|
||||||
public TestGadpServer(SocketAddress addr) throws IOException {
|
public TestGadpServer(SocketAddress addr) throws IOException {
|
||||||
super(new FakeDebuggerObjectModel(), addr);
|
this(new TestGadpObjectModel(true), addr);
|
||||||
this.model = (FakeDebuggerObjectModel) getModel();
|
}
|
||||||
|
|
||||||
|
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;
|
TestGadpServer server;
|
||||||
|
|
||||||
public ServerRunner() throws IOException {
|
public ServerRunner() throws IOException {
|
||||||
server = new TestGadpServer(new InetSocketAddress("localhost", 0));
|
server = createServer(new InetSocketAddress("localhost", 0));
|
||||||
server.launchAsyncService();
|
server.launchAsyncService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
|
||||||
|
return new TestGadpServer(addr);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws Exception {
|
public void close() throws Exception {
|
||||||
server.terminate();
|
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> {
|
class TestMethodInvocation extends CompletableFuture<Object> {
|
||||||
final Map<String, ?> args;
|
final Map<String, ?> args;
|
||||||
|
|
||||||
|
@ -493,12 +566,41 @@ public class GadpClientServerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Deque<Map<String, ?>> launches = new LinkedList<>();
|
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
|
@Test
|
||||||
public void testConnectDisconnect() throws Throwable {
|
public void testConnectDisconnect() throws Throwable {
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
|
|
||||||
|
@ -514,7 +616,7 @@ public class GadpClientServerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFetchModelValue() throws Throwable {
|
public void testFetchModelValue() throws Throwable {
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
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
|
@Test
|
||||||
public void testFetchModelValueCached() throws Throwable {
|
public void testFetchModelValueCached() throws Throwable {
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
MonitoredAsyncByteChannel monitored = new MonitoredAsyncByteChannel(socket);
|
MonitoredAsyncByteChannel monitored = new MonitoredAsyncByteChannel(socket);
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", monitored);
|
GadpClient client = new GadpClient("Test", monitored);
|
||||||
|
@ -552,7 +684,8 @@ public class GadpClientServerTest {
|
||||||
monitored.reset();
|
monitored.reset();
|
||||||
Object cmd = waitOn(client.fetchModelValue(PathUtils.parse("Available[1].cmd")));
|
Object cmd = waitOn(client.fetchModelValue(PathUtils.parse("Available[1].cmd")));
|
||||||
assertEquals("echo", 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());
|
waitOn(client.close());
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
@ -562,7 +695,7 @@ public class GadpClientServerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInvoke() throws Throwable {
|
public void testInvoke() throws Throwable {
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -579,7 +712,7 @@ public class GadpClientServerTest {
|
||||||
CompletableFuture<Object> future = method.invoke(Map.of(
|
CompletableFuture<Object> future = method.invoke(Map.of(
|
||||||
"whom", "GADP",
|
"whom", "GADP",
|
||||||
"others", TargetStringList.of("Alice", "Bob")));
|
"others", TargetStringList.of("Alice", "Bob")));
|
||||||
waitOn(invocationCount.waitValue(1));
|
waitOn(invocations.count.waitValue(1));
|
||||||
TestMethodInvocation invocation = invocations.poll();
|
TestMethodInvocation invocation = invocations.poll();
|
||||||
assertEquals(Map.of(
|
assertEquals(Map.of(
|
||||||
"whom", "GADP",
|
"whom", "GADP",
|
||||||
|
@ -598,7 +731,7 @@ public class GadpClientServerTest {
|
||||||
@Test
|
@Test
|
||||||
public void testInvokeMethodCached() throws Throwable {
|
public void testInvokeMethodCached() throws Throwable {
|
||||||
// TODO: Disambiguate / deconflict these two "method" cases
|
// TODO: Disambiguate / deconflict these two "method" cases
|
||||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
try (AsynchronousSocketChannel socket = socketChannel();
|
||||||
ServerRunner runner = new ServerRunner()) {
|
ServerRunner runner = new ServerRunner()) {
|
||||||
MonitoredAsyncByteChannel monitored = new MonitoredAsyncByteChannel(socket);
|
MonitoredAsyncByteChannel monitored = new MonitoredAsyncByteChannel(socket);
|
||||||
GadpClient client = new GadpClient("Test", monitored);
|
GadpClient client = new GadpClient("Test", monitored);
|
||||||
|
@ -616,13 +749,24 @@ public class GadpClientServerTest {
|
||||||
TargetObjectRef methodRef = (TargetObjectRef) attrs.get("greet");
|
TargetObjectRef methodRef = (TargetObjectRef) attrs.get("greet");
|
||||||
TargetMethod<?> method = waitOn(methodRef.as(TargetMethod.tclass).fetch());
|
TargetMethod<?> method = waitOn(methodRef.as(TargetMethod.tclass).fetch());
|
||||||
assertNotNull(method);
|
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)")));
|
assertEquals("Hello, World!", waitOn(avail.fetchAttribute("greet(World)")));
|
||||||
runner.server.model.session.available.punct = '?'; // No effect before flush
|
assertEquals(0, invocations.count.get().intValue());
|
||||||
assertEquals("Hello, World!", waitOn(avail.fetchAttribute("greet(World)")));
|
|
||||||
|
|
||||||
// Flush the cache
|
// Flush the cache
|
||||||
waitOn(avail.fetchAttributes(true));
|
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());
|
waitOn(client.close());
|
||||||
}
|
}
|
||||||
|
@ -630,7 +774,7 @@ public class GadpClientServerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListRoot() throws Throwable {
|
public void testListRoot() throws Throwable {
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -657,7 +801,7 @@ public class GadpClientServerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLaunch() throws Throwable {
|
public void testLaunch() throws Throwable {
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -691,7 +835,7 @@ public class GadpClientServerTest {
|
||||||
invocations.add(new ElementsChangedInvocation(parent, removed, added));
|
invocations.add(new ElementsChangedInvocation(parent, removed, added));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
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()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -757,7 +901,7 @@ public class GadpClientServerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSubscribeNoSuchPath() throws Throwable {
|
public void testSubscribeNoSuchPath() throws Throwable {
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -774,7 +918,7 @@ public class GadpClientServerTest {
|
||||||
public void testSubscribeLaunchForChildrenChanged() throws Throwable {
|
public void testSubscribeLaunchForChildrenChanged() throws Throwable {
|
||||||
ElementsChangedListener elemL = new ElementsChangedListener();
|
ElementsChangedListener elemL = new ElementsChangedListener();
|
||||||
|
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
|
|
||||||
|
@ -811,7 +955,7 @@ public class GadpClientServerTest {
|
||||||
ElementsChangedListener elemL = new ElementsChangedListener();
|
ElementsChangedListener elemL = new ElementsChangedListener();
|
||||||
InvalidatedListener invL = new InvalidatedListener();
|
InvalidatedListener invL = new InvalidatedListener();
|
||||||
|
|
||||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
try (AsynchronousSocketChannel socket = socketChannel();
|
||||||
ServerRunner runner = new ServerRunner()) {
|
ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -836,7 +980,7 @@ public class GadpClientServerTest {
|
||||||
), "Changed");
|
), "Changed");
|
||||||
|
|
||||||
waitOn(invL.count.waitValue(2));
|
waitOn(invL.count.waitValue(2));
|
||||||
waitOn(elemL.count.waitValue(1));
|
waitOn(elemL.count.waitValue(2));
|
||||||
|
|
||||||
for (TargetObject a : avail1.values()) {
|
for (TargetObject a : avail1.values()) {
|
||||||
assertFalse(a.isValid());
|
assertFalse(a.isValid());
|
||||||
|
@ -848,17 +992,22 @@ public class GadpClientServerTest {
|
||||||
assertEquals(1, avail2.size());
|
assertEquals(1, avail2.size());
|
||||||
assertEquals("cat", avail2.get("1").getCachedAttribute("cmd"));
|
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);
|
ElementsChangedInvocation changed = Unique.assertOne(elemL.invocations);
|
||||||
assertSame(availCont, changed.parent);
|
assertSame(availCont, changed.parent);
|
||||||
// Use equals here, since the listener only gets the ref
|
assertEquals(Set.of("1"), changed.added.keySet());
|
||||||
assertEquals(avail2.get("1"), Unique.assertOne(changed.added.values()));
|
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()
|
Map<ID<TargetObject>, String> actualInv = invL.invocations.stream()
|
||||||
.collect(Collectors.toMap(ii -> ID.of(ii.object), ii -> ii.reason));
|
.collect(Collectors.toMap(ii -> ID.of(ii.object), ii -> ii.reason));
|
||||||
Map<ID<TargetObject>, String> expectedInv =
|
Map<ID<TargetObject>, String> expectedInv = Map.ofEntries(
|
||||||
avail1.values()
|
Map.entry(ID.of(avail1.get("1")), "Replaced"),
|
||||||
.stream()
|
Map.entry(ID.of(avail1.get("2")), "Changed"));
|
||||||
.collect(Collectors.toMap(o -> ID.of(o), o -> "Changed"));
|
|
||||||
assertEquals(expectedInv, actualInv);
|
assertEquals(expectedInv, actualInv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -867,7 +1016,7 @@ public class GadpClientServerTest {
|
||||||
public void testReplaceAttribute() throws Throwable {
|
public void testReplaceAttribute() throws Throwable {
|
||||||
AttributesChangedListener attrL = new AttributesChangedListener();
|
AttributesChangedListener attrL = new AttributesChangedListener();
|
||||||
|
|
||||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
try (AsynchronousSocketChannel socket = socketChannel();
|
||||||
ServerRunner runner = new ServerRunner()) {
|
ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -877,32 +1026,36 @@ public class GadpClientServerTest {
|
||||||
TargetObject echoAvail =
|
TargetObject echoAvail =
|
||||||
waitOn(client.fetchModelObject(PathUtils.parse("Available[1]")));
|
waitOn(client.fetchModelObject(PathUtils.parse("Available[1]")));
|
||||||
echoAvail.addListener(attrL);
|
echoAvail.addListener(attrL);
|
||||||
assertEquals(Map.of(
|
assertEquals(Map.ofEntries(
|
||||||
"pid", 1,
|
Map.entry("pid", 1),
|
||||||
"cmd", "echo" //
|
Map.entry("cmd", "echo"),
|
||||||
), waitOn(echoAvail.fetchAttributes()));
|
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||||
|
Map.entry("_display", "[1]")),
|
||||||
|
waitOn(echoAvail.fetchAttributes()));
|
||||||
|
|
||||||
TestGadpTargetAvailable ssEchoAvail =
|
TestGadpTargetAvailable ssEchoAvail =
|
||||||
runner.server.model.session.available.getCachedElements().get("1");
|
runner.server.model.session.available.getCachedElements().get("1");
|
||||||
|
|
||||||
ssEchoAvail.setAttributes(Map.of(
|
ssEchoAvail.changeAttributes(List.of("pid"), Map.ofEntries(
|
||||||
"cmd", "echo",
|
Map.entry("cmd", "echo"),
|
||||||
"args", "Hello, World!" //
|
Map.entry("args", "Hello, World!")),
|
||||||
), "Changed");
|
"Changed");
|
||||||
|
|
||||||
waitOn(attrL.count.waitValue(1));
|
waitOn(attrL.count.waitValue(1));
|
||||||
|
|
||||||
assertEquals(Map.of(
|
assertEquals(Map.ofEntries(
|
||||||
"cmd", "echo",
|
Map.entry("cmd", "echo"),
|
||||||
"args", "Hello, World!" //
|
Map.entry("args", "Hello, World!"),
|
||||||
), echoAvail.getCachedAttributes());
|
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||||
|
Map.entry("_display", "[1]")),
|
||||||
|
echoAvail.getCachedAttributes());
|
||||||
|
|
||||||
AttributesChangedInvocation changed = Unique.assertOne(attrL.invocations);
|
AttributesChangedInvocation changed = Unique.assertOne(attrL.invocations);
|
||||||
assertSame(echoAvail, changed.parent);
|
assertSame(echoAvail, changed.parent);
|
||||||
assertEquals(Set.of("pid"), Set.copyOf(changed.removed));
|
assertEquals(Set.of("pid"), Set.copyOf(changed.removed));
|
||||||
assertEquals(Map.of(
|
assertEquals(Map.ofEntries(
|
||||||
"args", "Hello, World!" //
|
Map.entry("args", "Hello, World!")),
|
||||||
), changed.added);
|
changed.added);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -910,7 +1063,7 @@ public class GadpClientServerTest {
|
||||||
public void testSubtreeInvalidationDeduped() throws Throwable {
|
public void testSubtreeInvalidationDeduped() throws Throwable {
|
||||||
InvalidatedListener invL = new InvalidatedListener();
|
InvalidatedListener invL = new InvalidatedListener();
|
||||||
|
|
||||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
try (AsynchronousSocketChannel socket = socketChannel();
|
||||||
ServerRunner runner = new ServerRunner()) {
|
ServerRunner runner = new ServerRunner()) {
|
||||||
MonitoredGadpClient client = new MonitoredGadpClient("Test", socket);
|
MonitoredGadpClient client = new MonitoredGadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -951,7 +1104,7 @@ public class GadpClientServerTest {
|
||||||
public void testNoEventsAfterInvalidated() throws Throwable {
|
public void testNoEventsAfterInvalidated() throws Throwable {
|
||||||
AttributesChangedListener attrL = new AttributesChangedListener();
|
AttributesChangedListener attrL = new AttributesChangedListener();
|
||||||
|
|
||||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
try (AsynchronousSocketChannel socket = socketChannel();
|
||||||
ServerRunner runner = new ServerRunner()) {
|
ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -960,18 +1113,23 @@ public class GadpClientServerTest {
|
||||||
|
|
||||||
TargetObject echoAvail =
|
TargetObject echoAvail =
|
||||||
waitOn(client.fetchModelObject(PathUtils.parse("Available[1]")));
|
waitOn(client.fetchModelObject(PathUtils.parse("Available[1]")));
|
||||||
|
// TODO: This comes back null too often...
|
||||||
echoAvail.addListener(attrL);
|
echoAvail.addListener(attrL);
|
||||||
assertEquals(Map.of(
|
assertEquals(Map.ofEntries(
|
||||||
"pid", 1,
|
Map.entry("pid", 1),
|
||||||
"cmd", "echo" //
|
Map.entry("cmd", "echo"),
|
||||||
), waitOn(echoAvail.fetchAttributes()));
|
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||||
|
Map.entry("_display", "[1]")),
|
||||||
|
waitOn(echoAvail.fetchAttributes()));
|
||||||
|
|
||||||
TargetObject ddAvail = waitOn(client.fetchModelObject(PathUtils.parse("Available[2]")));
|
TargetObject ddAvail = waitOn(client.fetchModelObject(PathUtils.parse("Available[2]")));
|
||||||
ddAvail.addListener(attrL);
|
ddAvail.addListener(attrL);
|
||||||
assertEquals(Map.of(
|
assertEquals(Map.ofEntries(
|
||||||
"pid", 2,
|
Map.entry("pid", 2),
|
||||||
"cmd", "dd" //
|
Map.entry("cmd", "dd"),
|
||||||
), waitOn(ddAvail.fetchAttributes()));
|
Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED),
|
||||||
|
Map.entry("_display", "[2]")),
|
||||||
|
waitOn(ddAvail.fetchAttributes()));
|
||||||
|
|
||||||
// NB: copy
|
// NB: copy
|
||||||
Map<String, TestGadpTargetAvailable> ssAvail =
|
Map<String, TestGadpTargetAvailable> ssAvail =
|
||||||
|
@ -980,9 +1138,8 @@ public class GadpClientServerTest {
|
||||||
runner.server.model.session.available.changeElements(List.of("1"), List.of(), Map.of(),
|
runner.server.model.session.available.changeElements(List.of("1"), List.of(), Map.of(),
|
||||||
"1 is Gone");
|
"1 is Gone");
|
||||||
// Should produce nothing
|
// Should produce nothing
|
||||||
(ssAvail.get("1")).setAttributes(List.of(), Map.of(
|
(ssAvail.get("1")).changeAttributes(List.of(), Map.ofEntries(
|
||||||
"cmd", "echo",
|
Map.entry("args", "Hello, World!")),
|
||||||
"args", "Hello, World!"),
|
|
||||||
"Changed");
|
"Changed");
|
||||||
// Produce something, so we know we didn't get the other thing
|
// Produce something, so we know we didn't get the other thing
|
||||||
(ssAvail.get("2")).changeAttributes(List.of(), List.of(), Map.of(
|
(ssAvail.get("2")).changeAttributes(List.of(), List.of(), Map.of(
|
||||||
|
@ -1002,7 +1159,7 @@ public class GadpClientServerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testProxyWithLinkedElementsCanonicalFirst() throws Throwable {
|
public void testProxyWithLinkedElementsCanonicalFirst() throws Throwable {
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -1024,7 +1181,7 @@ public class GadpClientServerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testProxyWithLinkedElementsLinkFirst() throws Throwable {
|
public void testProxyWithLinkedElementsLinkFirst() throws Throwable {
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -1033,9 +1190,10 @@ public class GadpClientServerTest {
|
||||||
runner.server.model.session.addLinks();
|
runner.server.model.session.addLinks();
|
||||||
TargetObjectRef linkRef =
|
TargetObjectRef linkRef =
|
||||||
(TargetObjectRef) waitOn(client.fetchModelValue(PathUtils.parse("Links[1]")));
|
(TargetObjectRef) waitOn(client.fetchModelValue(PathUtils.parse("Links[1]")));
|
||||||
assertFalse(linkRef instanceof TargetObject);
|
assertTrue(linkRef instanceof TargetObject);
|
||||||
TargetObject link =
|
TargetObject link =
|
||||||
waitOn(client.fetchModelObject(PathUtils.parse("Links[1]")));
|
waitOn(client.fetchModelObject(PathUtils.parse("Links[1]")));
|
||||||
|
assertSame(linkRef, link);
|
||||||
TargetObject canonical =
|
TargetObject canonical =
|
||||||
waitOn(client.fetchModelObject(PathUtils.parse("Available[2]")));
|
waitOn(client.fetchModelObject(PathUtils.parse("Available[2]")));
|
||||||
assertSame(canonical, link);
|
assertSame(canonical, link);
|
||||||
|
@ -1049,7 +1207,7 @@ public class GadpClientServerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFetchModelValueFollowsLink() throws Throwable {
|
public void testFetchModelValueFollowsLink() throws Throwable {
|
||||||
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
try (ServerRunner runner = new ServerRunner()) {
|
try (ServerRunner runner = new ServerRunner()) {
|
||||||
GadpClient client = new GadpClient("Test", socket);
|
GadpClient client = new GadpClient("Test", socket);
|
||||||
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
|
@ -1064,64 +1222,221 @@ public class GadpClientServerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
public static class EventListener implements DebuggerModelListener {
|
||||||
@Ignore("TODO")
|
public static class CallEntry {
|
||||||
public void testCachingWithLinks() throws Throwable {
|
public final String methodName;
|
||||||
|
public final List<Object> args;
|
||||||
|
|
||||||
try (AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
|
public CallEntry(String methodName, List<Object> args) {
|
||||||
ServerRunner runner = new ServerRunner()) {
|
this.methodName = methodName;
|
||||||
MonitoredGadpClient client = new MonitoredGadpClient("Test", socket);
|
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,
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
runner.server.getLocalAddress()));
|
runner.server.getLocalAddress()));
|
||||||
waitOn(client.connect());
|
waitOn(client.connect());
|
||||||
runner.server.model.session.addLinks();
|
|
||||||
|
|
||||||
client.getMessageChannel().clear();
|
DefaultTargetObject<?, ?> root =
|
||||||
assertEquals("echo", waitOn(client.fetchModelValue(PathUtils.parse("Links[2].cmd"))));
|
new DefaultTargetModelRoot(runner.server.model, "Root");
|
||||||
assertEquals(2, client.getMessageChannel().record.size());
|
DefaultTargetObject<?, ?> a =
|
||||||
assertEquals(Gadp.RootMessage.MsgCase.SUBSCRIBE_REQUEST,
|
new DefaultTargetObject<>(runner.server.model, root, "a", "A");
|
||||||
client.getMessageChannel().record.get(0).assertSent().getMsgCase());
|
a.changeAttributes(List.of(), Map.of("test", 6), "Because");
|
||||||
assertEquals(Gadp.RootMessage.MsgCase.SUBSCRIBE_REPLY,
|
root.changeAttributes(List.of(), List.of(a), Map.of(), "Because");
|
||||||
client.getMessageChannel().record.get(1).assertReceived().getMsgCase());
|
runner.server.model.addModelRoot(root);
|
||||||
|
waitOn(client.fetchModelRoot());
|
||||||
|
|
||||||
// Since I don't have the parent, as usual, no cache
|
assertEquals(List.of(
|
||||||
client.getMessageChannel().clear();
|
new CallEntry("created", List.of(
|
||||||
assertEquals("echo", waitOn(client.fetchModelValue(PathUtils.parse("Links[2].cmd"))));
|
client.getModelRoot())),
|
||||||
assertEquals(2, client.getMessageChannel().record.size());
|
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
|
||||||
* Now, fetch Links[2] first, and repeat the experiment It should still not use the
|
public void testConnectBetweenRootCreatedAndAdded() throws Throwable {
|
||||||
* cache, because we won't know if Links[2] still points to Available[1]. Technically,
|
AsynchronousSocketChannel socket = socketChannel();
|
||||||
* Available[1] is what will be cached. Index 2 of Links will not be cached until/if
|
try (ServerRunner runner = new ServerRunner() {
|
||||||
* Links is fetched.
|
@Override
|
||||||
*/
|
protected TestGadpServer createServer(SocketAddress addr) throws IOException {
|
||||||
client.getMessageChannel().clear();
|
return new TestGadpServer(new TestGadpObjectModel(false), addr);
|
||||||
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());
|
GadpClient client = new PrintingGadpClient("Test", socket);
|
||||||
assertNotNull(avail1);
|
EventListener listener = new EventListener();
|
||||||
client.getMessageChannel().clear();
|
client.addModelListener(listener, true);
|
||||||
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());
|
|
||||||
|
|
||||||
// Now, fetch Links, and its elements to ensure it is cached
|
DefaultTargetObject<?, ?> root =
|
||||||
TargetObject links = waitOn(client.fetchModelObject(PathUtils.parse("Links")));
|
new DefaultTargetModelRoot(runner.server.model, "Root");
|
||||||
assertSame(avail1, waitOn(links.fetchElement("2")));
|
DefaultTargetObject<?, ?> a =
|
||||||
|
new DefaultTargetObject<>(runner.server.model, root, "a", "A");
|
||||||
|
a.changeAttributes(List.of(), Map.of("test", 6), "Because");
|
||||||
|
|
||||||
client.getMessageChannel().clear();
|
waitOn(AsyncUtils.completable(TypeSpec.VOID, socket::connect,
|
||||||
assertSame(avail1, waitOn(links.fetchElement("2")));
|
runner.server.getLocalAddress()));
|
||||||
assertEquals(2, client.getMessageChannel().record.size());
|
waitOn(client.connect());
|
||||||
assertEquals("Links[2]", PathUtils.toString(client.getMessageChannel().record.get(0)
|
|
||||||
.assertSent()
|
|
||||||
.getSubscribeRequest()
|
|
||||||
.getPath()
|
|
||||||
.getEList()));
|
|
||||||
|
|
||||||
TODO();
|
root.changeAttributes(List.of(), List.of(a), Map.of(), "Because");
|
||||||
waitOn(client.close());
|
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;
|
package ghidra.dbg.gadp.client;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
@ -23,7 +24,8 @@ import java.net.SocketAddress;
|
||||||
import java.nio.BufferUnderflowException;
|
import java.nio.BufferUnderflowException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.*;
|
import java.nio.channels.*;
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -36,7 +38,6 @@ import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.gadp.GadpVersion;
|
import ghidra.dbg.gadp.GadpVersion;
|
||||||
import ghidra.dbg.gadp.protocol.Gadp;
|
import ghidra.dbg.gadp.protocol.Gadp;
|
||||||
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
|
import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel;
|
||||||
import ghidra.dbg.gadp.util.GadpValueUtils;
|
|
||||||
import ghidra.dbg.target.TargetObject;
|
import ghidra.dbg.target.TargetObject;
|
||||||
import ghidra.dbg.util.ElementsChangedListener;
|
import ghidra.dbg.util.ElementsChangedListener;
|
||||||
import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation;
|
import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation;
|
||||||
|
@ -90,6 +91,10 @@ public class GadpClientTest {
|
||||||
this.cli = srv.accept();
|
this.cli = srv.accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void nextSeq() {
|
||||||
|
seqno++;
|
||||||
|
}
|
||||||
|
|
||||||
public void expect(Gadp.RootMessage msg) throws IOException {
|
public void expect(Gadp.RootMessage msg) throws IOException {
|
||||||
Msg.debug(this, "Expecting: " + msg);
|
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()
|
expect(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
.setConnectRequest(GadpVersion.makeRequest())
|
.setConnectRequest(GadpVersion.makeRequest())
|
||||||
.build());
|
.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()
|
send(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
.setConnectReply(Gadp.ConnectReply.newBuilder()
|
.setConnectReply(Gadp.ConnectReply.newBuilder()
|
||||||
|
@ -145,89 +151,163 @@ public class GadpClientTest {
|
||||||
.setSchemaContext("<context/>")
|
.setSchemaContext("<context/>")
|
||||||
.setRootSchema("OBJECT"))
|
.setRootSchema("OBJECT"))
|
||||||
.build());
|
.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()
|
expect(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
.setPingRequest(Gadp.PingRequest.newBuilder()
|
.setPingRequest(Gadp.PingRequest.newBuilder()
|
||||||
.setContent(HELLO_WORLD))
|
.setContent(content))
|
||||||
.build());
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendReplyPing(String content) throws IOException {
|
||||||
send(Gadp.RootMessage.newBuilder()
|
send(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
.setPingReply(Gadp.PingReply.newBuilder()
|
.setPingReply(Gadp.PingReply.newBuilder()
|
||||||
.setContent(HELLO_WORLD))
|
.setContent(content))
|
||||||
.build());
|
.build());
|
||||||
seqno++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleSubscribeValue(List<String> path, Object value)
|
public void handlePing() throws IOException {
|
||||||
throws Exception {
|
expectRequestPing(HELLO_WORLD);
|
||||||
expect(Gadp.RootMessage.newBuilder()
|
sendReplyPing(HELLO_WORLD);
|
||||||
.setSequence(seqno)
|
nextSeq();
|
||||||
.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 handleFetchObject(List<String> path) throws Exception {
|
public Gadp.ModelObjectDelta.Builder makeAttributeDelta(List<String> parentPath,
|
||||||
expect(Gadp.RootMessage.newBuilder()
|
Map<String, Object> primitives, Map<String, List<String>> objects) {
|
||||||
.setSequence(seqno)
|
Gadp.ModelObjectDelta.Builder delta = Gadp.ModelObjectDelta.newBuilder();
|
||||||
.setSubscribeRequest(Gadp.SubscribeRequest.newBuilder()
|
if (primitives != null) {
|
||||||
.setPath(GadpValueUtils.makePath(path))
|
for (Map.Entry<String, ?> ent : primitives.entrySet()) {
|
||||||
.setSubscribe(true))
|
delta.addAdded(GadpValueUtils.makeNamedValue(parentPath, ent));
|
||||||
.build());
|
}
|
||||||
Gadp.SubscribeReply.Builder reply = Gadp.SubscribeReply.newBuilder()
|
}
|
||||||
|
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()
|
.setValue(Gadp.Value.newBuilder()
|
||||||
.setObjectInfo(Gadp.ModelObjectInfo.newBuilder()
|
.setPathValue(GadpValueUtils.makePath(ent.getValue()))));
|
||||||
.setPath(GadpValueUtils.makePath(path))));
|
}
|
||||||
send(Gadp.RootMessage.newBuilder()
|
else {
|
||||||
.setSequence(seqno)
|
delta.addAdded(Gadp.NamedValue.newBuilder()
|
||||||
.setSubscribeReply(reply)
|
.setName(ent.getKey())
|
||||||
.build());
|
.setValue(Gadp.Value.newBuilder()
|
||||||
seqno++;
|
.setObjectStub(Gadp.ModelObjectStub.getDefaultInstance())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleFetchElements(List<String> path, Collection<String> indices)
|
public Gadp.ModelObjectDelta.Builder makeElementDelta(List<String> parentPath,
|
||||||
throws Exception {
|
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()
|
expect(Gadp.RootMessage.newBuilder()
|
||||||
.setSequence(seqno)
|
.setSequence(seqno)
|
||||||
.setSubscribeRequest(Gadp.SubscribeRequest.newBuilder()
|
.setResyncRequest(Gadp.ResyncRequest.newBuilder()
|
||||||
.setPath(GadpValueUtils.makePath(path))
|
.setPath(GadpValueUtils.makePath(path))
|
||||||
.setSubscribe(true)
|
.setAttributes(refreshAttributes)
|
||||||
.setFetchElements(true))
|
.setElements(refreshElements))
|
||||||
.build());
|
.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)
|
public void sendNotifyObjects(List<String> parentPath, Map<String, List<String>> elements,
|
||||||
throws Exception {
|
Map<String, List<String>> attrObjects, Map<String, Object> attrPrimitives)
|
||||||
Gadp.ModelObjectEvent.Builder evt = Gadp.ModelObjectEvent.newBuilder();
|
throws IOException {
|
||||||
evt.setDelta(Gadp.ModelObjectDelta.newBuilder()
|
|
||||||
.addAllIndexAdded(indicesAdded));
|
|
||||||
send(Gadp.RootMessage.newBuilder()
|
send(Gadp.RootMessage.newBuilder()
|
||||||
|
.setSequence(seqno)
|
||||||
.setEventNotification(Gadp.EventNotification.newBuilder()
|
.setEventNotification(Gadp.EventNotification.newBuilder()
|
||||||
.setPath(GadpValueUtils.makePath(path))
|
.setPath(GadpValueUtils.makePath(parentPath))
|
||||||
.setModelObjectEvent(evt))
|
.setModelObjectEvent(Gadp.ModelObjectEvent.newBuilder()
|
||||||
|
.setAttributeDelta(
|
||||||
|
makeAttributeDelta(parentPath, attrPrimitives,
|
||||||
|
attrObjects))
|
||||||
|
.setElementDelta(makeElementDelta(parentPath, elements))))
|
||||||
.build());
|
.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) {
|
protected void dumpBuffer(ByteBuffer buf) {
|
||||||
|
@ -313,29 +393,29 @@ public class GadpClientTest {
|
||||||
srv.handleConnect(GadpVersion.VER1);
|
srv.handleConnect(GadpVersion.VER1);
|
||||||
waitOn(gadpConnect);
|
waitOn(gadpConnect);
|
||||||
|
|
||||||
|
List<String> parentPath = PathUtils.parse("Parent");
|
||||||
CompletableFuture<? extends TargetObject> fetchParent =
|
CompletableFuture<? extends TargetObject> fetchParent =
|
||||||
client.fetchModelObject(PathUtils.parse("Parent"));
|
client.fetchModelObject(parentPath);
|
||||||
srv.handleFetchObject(PathUtils.parse("Parent"));
|
srv.notifyAddRoot();
|
||||||
|
srv.sendNotifyObjectCreated(parentPath, List.of(), "Parent");
|
||||||
|
srv.handleResyncAttributes(List.of(), false, Map.of("Parent", parentPath), null);
|
||||||
TargetObject parent = waitOn(fetchParent);
|
TargetObject parent = waitOn(fetchParent);
|
||||||
parent.addListener(elemL);
|
parent.addListener(elemL);
|
||||||
|
|
||||||
CompletableFuture<? extends Map<String, ? extends TargetObjectRef>> fetchElements =
|
CompletableFuture<? extends Map<String, ? extends TargetObjectRef>> fetchElements =
|
||||||
parent.fetchElements();
|
parent.fetchElements();
|
||||||
srv.handleFetchElements(PathUtils.parse("Parent"), List.of());
|
srv.handleResyncElements(parentPath, false, Map.of());
|
||||||
assertEquals(Map.of(), waitOn(fetchElements));
|
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));
|
waitOn(elemL.count.waitValue(1));
|
||||||
ElementsChangedInvocation changed = Unique.assertOne(elemL.invocations);
|
ElementsChangedInvocation changed = Unique.assertOne(elemL.invocations);
|
||||||
assertEquals(parent, changed.parent);
|
assertEquals(parent, changed.parent);
|
||||||
TargetObjectRef childRef = Unique.assertOne(changed.added.values());
|
TargetObjectRef childRef = Unique.assertOne(changed.added.values());
|
||||||
assertEquals(PathUtils.parse("Parent[0]"), childRef.getPath());
|
assertTrue(childRef instanceof GadpClientTargetObject);
|
||||||
|
assertEquals(elem0Path, 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());
|
|
||||||
}
|
}
|
||||||
assertEquals(1, elemL.count.get().intValue()); // After connection is closed
|
assertEquals(1, elemL.count.get().intValue()); // After connection is closed
|
||||||
}
|
}
|
||||||
|
@ -354,16 +434,18 @@ public class GadpClientTest {
|
||||||
CompletableFuture<Void> gadpConnect = client.connect();
|
CompletableFuture<Void> gadpConnect = client.connect();
|
||||||
srv.handleConnect(GadpVersion.VER1);
|
srv.handleConnect(GadpVersion.VER1);
|
||||||
waitOn(gadpConnect);
|
waitOn(gadpConnect);
|
||||||
|
CompletableFuture<?> cfRoot = client.fetchModelRoot();
|
||||||
|
srv.notifyAddRoot();
|
||||||
|
waitOn(cfRoot);
|
||||||
|
|
||||||
CompletableFuture<?> fetchVal1 = client.fetchModelValue(PathUtils.parse("value"));
|
CompletableFuture<?> fetchVal1 = client.fetchModelValue(PathUtils.parse("value"));
|
||||||
CompletableFuture<?> fetchVal2 = 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(fetchVal1));
|
||||||
assertEquals(HELLO_WORLD, waitOn(fetchVal2));
|
assertEquals(HELLO_WORLD, waitOn(fetchVal2));
|
||||||
|
|
||||||
// Because parent not cached, it should send another request
|
CompletableFuture<?> fetchVal3 = client.fetchModelValue(PathUtils.parse("value"), true);
|
||||||
CompletableFuture<?> fetchVal3 = client.fetchModelValue(PathUtils.parse("value"));
|
srv.handleResyncAttributes(List.of(), true, null, Map.of("value", "Hi"));
|
||||||
srv.handleSubscribeValue(PathUtils.parse("value"), "Hi");
|
|
||||||
assertEquals("Hi", waitOn(fetchVal3));
|
assertEquals("Hi", waitOn(fetchVal3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import com.sun.jdi.*;
|
import com.sun.jdi.*;
|
||||||
|
|
||||||
import ghidra.async.AsyncUtils;
|
|
||||||
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
||||||
import ghidra.dbg.jdi.manager.JdiManager;
|
import ghidra.dbg.jdi.manager.JdiManager;
|
||||||
import ghidra.dbg.target.TargetObject;
|
import ghidra.dbg.target.TargetObject;
|
||||||
|
@ -64,6 +63,7 @@ public class JdiModelImpl extends AbstractDebuggerObjectModel {
|
||||||
|
|
||||||
Address start = ram.getAddress(0L);
|
Address start = ram.getAddress(0L);
|
||||||
this.defaultRange = new AddressRangeImpl(start, start.add(BLOCK_SIZE));
|
this.defaultRange = new AddressRangeImpl(start, start.add(BLOCK_SIZE));
|
||||||
|
addModelRoot(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -86,7 +86,7 @@ public class JdiModelImpl extends AbstractDebuggerObjectModel {
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Void> close() {
|
public CompletableFuture<Void> close() {
|
||||||
jdi.terminate();
|
jdi.terminate();
|
||||||
return AsyncUtils.NIL;
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public JdiModelTargetRoot getRoot() {
|
public JdiModelTargetRoot getRoot() {
|
||||||
|
@ -227,6 +227,7 @@ public class JdiModelImpl extends AbstractDebuggerObjectModel {
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public AddressFactory getAddressFactory() {
|
public AddressFactory getAddressFactory() {
|
||||||
return addressFactory;
|
return addressFactory;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ public class JdiModelTargetClassContainer extends JdiModelTargetObjectImpl {
|
||||||
return getClassesByName().get(name);
|
return getClassesByName().get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<?> refresh() {
|
public CompletableFuture<?> refreshInternal() {
|
||||||
if (!isObserved()) {
|
if (!isObserved()) {
|
||||||
return AsyncUtils.NIL;
|
return AsyncUtils.NIL;
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ public class JdiModelTargetConnectorContainer extends JdiModelTargetObjectImpl {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<?> refresh() {
|
public CompletableFuture<?> refreshInternal() {
|
||||||
if (!isObserved()) {
|
if (!isObserved()) {
|
||||||
return AsyncUtils.NIL;
|
return AsyncUtils.NIL;
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,7 @@ public class JdiModelTargetModuleContainer extends JdiModelTargetObjectImpl
|
||||||
return modulesByName.get(name);
|
return modulesByName.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<?> refresh() {
|
public CompletableFuture<?> refreshInternal() {
|
||||||
if (!isObserved()) {
|
if (!isObserved()) {
|
||||||
return AsyncUtils.NIL;
|
return AsyncUtils.NIL;
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ public class JdiModelTargetObjectImpl extends
|
||||||
private boolean modified;
|
private boolean modified;
|
||||||
|
|
||||||
public JdiModelTargetObjectImpl(JdiModelTargetObject parent, String id) {
|
public JdiModelTargetObjectImpl(JdiModelTargetObject parent, String id) {
|
||||||
super(parent.getModel(), parent, id, "Object");
|
super(parent.getModelImpl(), parent, id, "Object");
|
||||||
this.impl = parent.getModelImpl();
|
this.impl = parent.getModelImpl();
|
||||||
this.mirror = (Mirror) parent.getObject();
|
this.mirror = (Mirror) parent.getObject();
|
||||||
this.object = null;
|
this.object = null;
|
||||||
|
@ -65,7 +65,7 @@ public class JdiModelTargetObjectImpl extends
|
||||||
|
|
||||||
public JdiModelTargetObjectImpl(JdiModelTargetObject parent, String id, Object object,
|
public JdiModelTargetObjectImpl(JdiModelTargetObject parent, String id, Object object,
|
||||||
boolean isElement) {
|
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.impl = parent.getModelImpl();
|
||||||
this.mirror = object instanceof Mirror ? (Mirror) object : null;
|
this.mirror = object instanceof Mirror ? (Mirror) object : null;
|
||||||
this.object = object;
|
this.object = object;
|
||||||
|
@ -88,7 +88,7 @@ public class JdiModelTargetObjectImpl extends
|
||||||
}
|
}
|
||||||
|
|
||||||
public JdiModelTargetObjectImpl(JdiModelTargetSectionContainer parent) {
|
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.impl = parent.getModelImpl();
|
||||||
this.mirror = parent.mirror;
|
this.mirror = parent.mirror;
|
||||||
this.display = "NULL_SPACE";
|
this.display = "NULL_SPACE";
|
||||||
|
|
|
@ -398,7 +398,7 @@ public class JdiModelTargetVM extends JdiModelTargetObjectImpl implements //
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refresh() {
|
public void refreshInternal() {
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import ghidra.dbg.target.TargetEnvironment;
|
||||||
public interface JdiModelTargetEnvironment<T extends TargetEnvironment<T>>
|
public interface JdiModelTargetEnvironment<T extends TargetEnvironment<T>>
|
||||||
extends JdiModelTargetObject, TargetEnvironment<T> {
|
extends JdiModelTargetObject, TargetEnvironment<T> {
|
||||||
|
|
||||||
public void refresh();
|
public void refreshInternal();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public default String getArchitecture() {
|
public default String getArchitecture() {
|
||||||
|
|
|
@ -47,27 +47,33 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
|
||||||
*/
|
*/
|
||||||
protected class ForInterpreterListener implements TargetInterpreterListener {
|
protected class ForInterpreterListener implements TargetInterpreterListener {
|
||||||
@Override
|
@Override
|
||||||
public void consoleOutput(TargetObject console, Channel channel, String out) {
|
public void consoleOutput(TargetObject console, Channel channel, byte[] out) {
|
||||||
// NB: yes, this is lame... The InterpreterPanel's repositionScrollPane
|
OutputStream os;
|
||||||
// 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 += " ";
|
|
||||||
switch (channel) {
|
switch (channel) {
|
||||||
case STDOUT:
|
case STDOUT:
|
||||||
if (outWriter == null) {
|
os = stdOut;
|
||||||
return;
|
|
||||||
}
|
|
||||||
outWriter.print(out);
|
|
||||||
outWriter.flush();
|
|
||||||
break;
|
break;
|
||||||
case STDERR:
|
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;
|
return;
|
||||||
}
|
}
|
||||||
errWriter.print(out);
|
/**
|
||||||
errWriter.flush();
|
* NB: yes, the extra space is lame... The InterpreterPanel's repositionScrollPane
|
||||||
break;
|
* 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
|
@Override
|
||||||
public void invalidated(TargetObject object, String reason) {
|
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||||
Swing.runLater(() -> {
|
Swing.runLater(() -> {
|
||||||
if (object == targetConsole) { // Redundant
|
if (object == targetConsole) { // Redundant
|
||||||
if (pinned) {
|
if (pinned) {
|
||||||
|
@ -107,8 +113,8 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
|
||||||
protected Thread thread;
|
protected Thread thread;
|
||||||
protected InterpreterConsole guiConsole;
|
protected InterpreterConsole guiConsole;
|
||||||
protected BufferedReader inReader;
|
protected BufferedReader inReader;
|
||||||
protected PrintWriter outWriter;
|
protected OutputStream stdOut;
|
||||||
protected PrintWriter errWriter;
|
protected OutputStream stdErr;
|
||||||
|
|
||||||
protected ToggleDockingAction actionPin;
|
protected ToggleDockingAction actionPin;
|
||||||
protected boolean pinned = false;
|
protected boolean pinned = false;
|
||||||
|
@ -146,8 +152,8 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
|
||||||
InterpreterComponentProvider provider = (InterpreterComponentProvider) guiConsole;
|
InterpreterComponentProvider provider = (InterpreterComponentProvider) guiConsole;
|
||||||
provider.setSubTitle(targetConsole.getDisplay());
|
provider.setSubTitle(targetConsole.getDisplay());
|
||||||
|
|
||||||
setErrWriter(guiConsole.getErrWriter());
|
setStdErr(guiConsole.getStdErr());
|
||||||
setOutWriter(guiConsole.getOutWriter());
|
setStdOut(guiConsole.getStdOut());
|
||||||
setStdIn(guiConsole.getStdin());
|
setStdIn(guiConsole.getStdin());
|
||||||
|
|
||||||
createActions();
|
createActions();
|
||||||
|
@ -161,12 +167,12 @@ public abstract class AbstractDebuggerWrappedConsoleConnection<T extends TargetO
|
||||||
guiConsole.addAction(actionPin);
|
guiConsole.addAction(actionPin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOutWriter(PrintWriter outWriter) {
|
public void setStdOut(OutputStream stdOut) {
|
||||||
this.outWriter = outWriter;
|
this.stdOut = stdOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setErrWriter(PrintWriter errWriter) {
|
public void setStdErr(OutputStream stdErr) {
|
||||||
this.errWriter = errWriter;
|
this.stdErr = stdErr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStdIn(InputStream stdIn) {
|
public void setStdIn(InputStream stdIn) {
|
||||||
|
|
|
@ -46,22 +46,15 @@ import ghidra.app.plugin.core.debug.gui.objects.components.*;
|
||||||
import ghidra.app.services.*;
|
import ghidra.app.services.*;
|
||||||
import ghidra.async.AsyncUtils;
|
import ghidra.async.AsyncUtils;
|
||||||
import ghidra.async.TypeSpec;
|
import ghidra.async.TypeSpec;
|
||||||
import ghidra.dbg.DebugModelConventions;
|
import ghidra.dbg.*;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
|
||||||
import ghidra.dbg.attributes.TargetObjectRef;
|
import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.error.DebuggerMemoryAccessException;
|
import ghidra.dbg.error.DebuggerMemoryAccessException;
|
||||||
import ghidra.dbg.target.*;
|
import ghidra.dbg.target.*;
|
||||||
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibility;
|
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibility;
|
||||||
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener;
|
|
||||||
import ghidra.dbg.target.TargetConsole.Channel;
|
import ghidra.dbg.target.TargetConsole.Channel;
|
||||||
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
|
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.TargetLauncher.TargetCmdLineLauncher;
|
||||||
import ghidra.dbg.target.TargetMemory.TargetMemoryListener;
|
|
||||||
import ghidra.dbg.target.TargetObject.TargetObjectFetchingListener;
|
import ghidra.dbg.target.TargetObject.TargetObjectFetchingListener;
|
||||||
import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener;
|
|
||||||
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
|
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
|
||||||
import ghidra.dbg.util.PathUtils;
|
import ghidra.dbg.util.PathUtils;
|
||||||
import ghidra.framework.options.AutoOptions;
|
import ghidra.framework.options.AutoOptions;
|
||||||
|
@ -80,9 +73,9 @@ import ghidra.util.table.GhidraTable;
|
||||||
import resources.ResourceManager;
|
import resources.ResourceManager;
|
||||||
|
|
||||||
public class DebuggerObjectsProvider extends ComponentProviderAdapter implements //AllTargetObjectListenerAdapter,
|
public class DebuggerObjectsProvider extends ComponentProviderAdapter implements //AllTargetObjectListenerAdapter,
|
||||||
TargetObjectFetchingListener, TargetAccessibilityListener, TargetExecutionStateListener,
|
TargetObjectFetchingListener, //
|
||||||
TargetFocusScopeListener, TargetInterpreterListener, TargetMemoryListener,
|
DebuggerModelListener, //
|
||||||
TargetRegisterBankListener, ObjectContainerListener {
|
ObjectContainerListener {
|
||||||
|
|
||||||
public static final String PATH_JOIN_CHAR = ".";
|
public static final String PATH_JOIN_CHAR = ".";
|
||||||
//private static final String AUTOUPDATE_ATTRIBUTE_NAME = "autoupdate";
|
//private static final String AUTOUPDATE_ATTRIBUTE_NAME = "autoupdate";
|
||||||
|
@ -310,6 +303,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
|
||||||
|
|
||||||
public void setModel(DebuggerObjectModel model) {
|
public void setModel(DebuggerObjectModel model) {
|
||||||
currentModel = model;
|
currentModel = model;
|
||||||
|
currentModel.addModelListener(this, true);
|
||||||
refresh();
|
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) {
|
public void stopRecording(TargetObject targetObject) {
|
||||||
// TODO: Do `this.recorder = ...` on every object selection change?
|
// TODO: Do `this.recorder = ...` on every object selection change?
|
||||||
TraceRecorder rec = modelService.getRecorderForSuccessor(targetObject);
|
TraceRecorder rec = modelService.getRecorderForSuccessor(targetObject);
|
||||||
|
@ -1681,7 +1667,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
|
||||||
public void elementsChangedObjects(TargetObject parent, Collection<String> removed,
|
public void elementsChangedObjects(TargetObject parent, Collection<String> removed,
|
||||||
Map<String, ? extends TargetObject> added) {
|
Map<String, ? extends TargetObject> added) {
|
||||||
//System.err.println("local EC: " + parent);
|
//System.err.println("local EC: " + parent);
|
||||||
ObjectContainer container = getContainerByPath(parent.getPath());
|
ObjectContainer container = parent == null ? null : getContainerByPath(parent.getPath());
|
||||||
if (container != null) {
|
if (container != null) {
|
||||||
container.augmentElements(removed, added);
|
container.augmentElements(removed, added);
|
||||||
boolean visibleChange = false;
|
boolean visibleChange = false;
|
||||||
|
@ -1702,7 +1688,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements
|
||||||
public void attributesChangedObjects(TargetObject parent, Collection<String> removed,
|
public void attributesChangedObjects(TargetObject parent, Collection<String> removed,
|
||||||
Map<String, ?> added) {
|
Map<String, ?> added) {
|
||||||
//System.err.println("local AC: " + parent + ":" + removed + ":" + 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) {
|
if (container != null) {
|
||||||
container.augmentAttributes(removed, added);
|
container.augmentAttributes(removed, added);
|
||||||
boolean visibleChange = false;
|
boolean visibleChange = false;
|
||||||
|
|
|
@ -15,23 +15,17 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.app.plugin.core.debug.gui.objects;
|
package ghidra.app.plugin.core.debug.gui.objects;
|
||||||
|
|
||||||
import static ghidra.async.AsyncUtils.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
import org.jdom.Element;
|
import org.jdom.Element;
|
||||||
|
|
||||||
import ghidra.async.AsyncFence;
|
|
||||||
import ghidra.async.TypeSpec;
|
|
||||||
import ghidra.dbg.DebugModelConventions;
|
import ghidra.dbg.DebugModelConventions;
|
||||||
import ghidra.dbg.attributes.TargetObjectRef;
|
import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.target.TargetObject;
|
import ghidra.dbg.target.TargetObject;
|
||||||
import ghidra.dbg.target.TargetProcess;
|
import ghidra.dbg.target.TargetProcess;
|
||||||
import ghidra.dbg.util.PathUtils;
|
import ghidra.dbg.util.PathUtils;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.datastruct.ListenerSet;
|
|
||||||
import ghidra.util.xml.XmlUtilities;
|
import ghidra.util.xml.XmlUtilities;
|
||||||
|
|
||||||
public class ObjectContainer implements Comparable {
|
public class ObjectContainer implements Comparable {
|
||||||
|
@ -43,9 +37,6 @@ public class ObjectContainer implements Comparable {
|
||||||
private final Map<String, Object> attributeMap = new LinkedHashMap<>();
|
private final Map<String, Object> attributeMap = new LinkedHashMap<>();
|
||||||
private Set<ObjectContainer> currentChildren = new TreeSet<>();
|
private Set<ObjectContainer> currentChildren = new TreeSet<>();
|
||||||
|
|
||||||
public final ListenerSet<ObjectContainerListener> listeners =
|
|
||||||
new ListenerSet<>(ObjectContainerListener.class);
|
|
||||||
|
|
||||||
private boolean immutable;
|
private boolean immutable;
|
||||||
private boolean visible = true;
|
private boolean visible = true;
|
||||||
private boolean isSubscribed = false;
|
private boolean isSubscribed = false;
|
||||||
|
@ -171,29 +162,14 @@ public class ObjectContainer implements Comparable {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public CompletableFuture<ObjectContainer> getOffspring() {
|
public CompletableFuture<ObjectContainer> getOffspring() {
|
||||||
if (targetObjectRef == null) {
|
if (targetObject == null) {
|
||||||
return null;
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
AtomicReference<TargetObject> to = new AtomicReference<>();
|
return targetObject.resync(true, true).thenApply(__ -> {
|
||||||
AtomicReference<Map<String, ? extends TargetObject>> elements = new AtomicReference<>();
|
rebuildContainers(targetObject.getCachedElements(), targetObject.getCachedAttributes());
|
||||||
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());
|
|
||||||
propagateProvider(provider);
|
propagateProvider(provider);
|
||||||
seq.exit(this);
|
return this;
|
||||||
}).finish();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void checkAutoRecord() {
|
protected void checkAutoRecord() {
|
||||||
|
@ -375,10 +351,6 @@ public class ObjectContainer implements Comparable {
|
||||||
this.provider = newProvider;
|
this.provider = newProvider;
|
||||||
provider.addTargetToMap(this);
|
provider.addTargetToMap(this);
|
||||||
}
|
}
|
||||||
this.addListener(provider);
|
|
||||||
//if (targetObject != null && !currentChildren.isEmpty()) {
|
|
||||||
// targetObject.addListener(provider);
|
|
||||||
//}
|
|
||||||
for (ObjectContainer c : currentChildren) {
|
for (ObjectContainer c : currentChildren) {
|
||||||
c.propagateProvider(provider);
|
c.propagateProvider(provider);
|
||||||
}
|
}
|
||||||
|
@ -534,14 +506,6 @@ public class ObjectContainer implements Comparable {
|
||||||
this.immutable = immutable;
|
this.immutable = immutable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addListener(ObjectContainerListener listener) {
|
|
||||||
listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeListener(ObjectContainerListener listener) {
|
|
||||||
listeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isVisible() {
|
public boolean isVisible() {
|
||||||
return visible;
|
return visible;
|
||||||
}
|
}
|
||||||
|
@ -561,18 +525,10 @@ public class ObjectContainer implements Comparable {
|
||||||
|
|
||||||
public void subscribe() {
|
public void subscribe() {
|
||||||
isSubscribed = true;
|
isSubscribed = true;
|
||||||
if (targetObject != null && provider != null) {
|
|
||||||
targetObject.addListener(provider);
|
|
||||||
provider.addListener(targetObject);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unsubscribe() {
|
public void unsubscribe() {
|
||||||
isSubscribed = false;
|
isSubscribed = false;
|
||||||
targetObject.removeListener(provider);
|
|
||||||
if (provider.isAutorecord()) {
|
|
||||||
//provider.stopRecording(targetObject);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isModified() {
|
public boolean isModified() {
|
||||||
|
|
|
@ -127,7 +127,6 @@ public class ImportFromFactsAction extends ImportExportAsAction {
|
||||||
if (root != null) {
|
if (root != null) {
|
||||||
ObjectContainer c = p.getRoot();
|
ObjectContainer c = p.getRoot();
|
||||||
c.setTargetObject(root);
|
c.setTargetObject(root);
|
||||||
root.addListener(p);
|
|
||||||
provider.update(c);
|
provider.update(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,6 @@ public class ImportFromXMLAction extends ImportExportAsAction {
|
||||||
DummyTargetObject to = xmlToObject(p, root, path);
|
DummyTargetObject to = xmlToObject(p, root, path);
|
||||||
ObjectContainer c = p.getRoot();
|
ObjectContainer c = p.getRoot();
|
||||||
c.setTargetObject(to);
|
c.setTargetObject(to);
|
||||||
to.addListener(p);
|
|
||||||
provider.update(c);
|
provider.update(c);
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import ghidra.async.AsyncUtils;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.DebuggerObjectModel;
|
||||||
import ghidra.dbg.attributes.TargetObjectRef;
|
import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.target.TargetObject;
|
import ghidra.dbg.target.TargetObject;
|
||||||
|
@ -156,6 +157,11 @@ public class DummyTargetObject implements TargetObject {
|
||||||
return kind;
|
return kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> resync(boolean attributes, boolean elements) {
|
||||||
|
return AsyncUtils.NIL;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<? extends Map<String, ? extends TargetObject>> fetchElements() {
|
public CompletableFuture<? extends Map<String, ? extends TargetObject>> fetchElements() {
|
||||||
// Why not completedFuture(elements)?
|
// Why not completedFuture(elements)?
|
||||||
|
|
|
@ -34,7 +34,6 @@ public class ObjectAttributeRow {
|
||||||
ref.fetch().handle(seq::next);
|
ref.fetch().handle(seq::next);
|
||||||
}, targetObject).then(seq -> {
|
}, targetObject).then(seq -> {
|
||||||
to = targetObject.get();
|
to = targetObject.get();
|
||||||
to.addListener(provider);
|
|
||||||
}).finish();
|
}).finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,17 +38,10 @@ public class ObjectElementRow {
|
||||||
ref.fetch().handle(seq::next);
|
ref.fetch().handle(seq::next);
|
||||||
}, targetObject).then(seq -> {
|
}, targetObject).then(seq -> {
|
||||||
to = targetObject.get();
|
to = targetObject.get();
|
||||||
to.addListener(provider);
|
|
||||||
to.fetchAttributes(true).handle(seq::next);
|
to.fetchAttributes(true).handle(seq::next);
|
||||||
//to.getAttributes().thenAccept(v -> map = v);
|
//to.getAttributes().thenAccept(v -> map = v);
|
||||||
}, attributes).then(seq -> {
|
}, attributes).then(seq -> {
|
||||||
map = attributes.get();
|
map = attributes.get();
|
||||||
for (Object obj : map.values()) {
|
|
||||||
if (obj instanceof TargetObject) {
|
|
||||||
TargetObject attr = (TargetObject) obj;
|
|
||||||
attr.addListener(provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).finish();
|
}).finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ public class DebuggerModelServicePlugin extends Plugin
|
||||||
|
|
||||||
protected TargetObjectListener forRemoval = new TargetObjectListener() {
|
protected TargetObjectListener forRemoval = new TargetObjectListener() {
|
||||||
@Override
|
@Override
|
||||||
public void invalidated(TargetObject object, String reason) {
|
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||||
synchronized (listenersByModel) {
|
synchronized (listenersByModel) {
|
||||||
ListenersForRemovalAndFocus listener = listenersByModel.remove(model);
|
ListenersForRemovalAndFocus listener = listenersByModel.remove(model);
|
||||||
if (listener == null) {
|
if (listener == null) {
|
||||||
|
@ -125,7 +125,7 @@ public class DebuggerModelServicePlugin extends Plugin
|
||||||
}
|
}
|
||||||
r.addListener(this.forRemoval);
|
r.addListener(this.forRemoval);
|
||||||
if (!r.isValid()) {
|
if (!r.isValid()) {
|
||||||
forRemoval.invalidated(root, "Who knows?");
|
forRemoval.invalidated(root, root, "Who knows?");
|
||||||
}
|
}
|
||||||
CompletableFuture<? extends TargetFocusScope<?>> findSuitable =
|
CompletableFuture<? extends TargetFocusScope<?>> findSuitable =
|
||||||
DebugModelConventions.findSuitable(TargetFocusScope.tclass, r);
|
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.DebuggerModelServiceInternal;
|
||||||
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceProxyPlugin;
|
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceProxyPlugin;
|
||||||
import ghidra.async.AsyncUtils;
|
|
||||||
import ghidra.dbg.DebuggerModelFactory;
|
import ghidra.dbg.DebuggerModelFactory;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.DebuggerObjectModel;
|
||||||
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
||||||
|
@ -35,7 +34,9 @@ import help.screenshot.GhidraScreenShotGenerator;
|
||||||
|
|
||||||
public class DebuggerTargetsPluginScreenShots extends 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 {
|
protected static class ScreenShotDebuggerModelFactory implements DebuggerModelFactory {
|
||||||
|
|
||||||
private void nop() {
|
private void nop() {
|
||||||
|
@ -63,6 +64,7 @@ public class DebuggerTargetsPluginScreenShots extends GhidraScreenShotGenerator
|
||||||
|
|
||||||
public ScreenShotDebuggerObjectModel(String display) {
|
public ScreenShotDebuggerObjectModel(String display) {
|
||||||
this.display = display;
|
this.display = display;
|
||||||
|
addModelRoot(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -79,11 +81,6 @@ public class DebuggerTargetsPluginScreenShots extends GhidraScreenShotGenerator
|
||||||
public AddressFactory getAddressFactory() {
|
public AddressFactory getAddressFactory() {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Void> close() {
|
|
||||||
return AsyncUtils.NIL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DebuggerModelServiceInternal modelService;
|
DebuggerModelServiceInternal modelService;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import java.lang.ref.Cleaner.Cleanable;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
@ -166,6 +167,9 @@ public class AsyncReference<T, C> {
|
||||||
try {
|
try {
|
||||||
listener.accept(oldVal, newVal, cause);
|
listener.accept(oldVal, newVal, cause);
|
||||||
}
|
}
|
||||||
|
catch (RejectedExecutionException exc) {
|
||||||
|
Msg.trace(this, "Ignoring rejection", exc);
|
||||||
|
}
|
||||||
catch (Throwable exc) {
|
catch (Throwable exc) {
|
||||||
Msg.error(this, "Ignoring exception on async reference listener: ", 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) {
|
if (err != null) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
@ -523,7 +523,7 @@ public enum DebugModelConventions {
|
||||||
protected abstract boolean checkDescend(TargetObjectRef ref);
|
protected abstract boolean checkDescend(TargetObjectRef ref);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidated(TargetObject object, String reason) {
|
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||||
runNotInSwing(this, () -> doInvalidated(object, reason), "invalidated");
|
runNotInSwing(this, () -> doInvalidated(object, reason), "invalidated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,18 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.dbg;
|
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
|
* 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
|
* TODO: Most (non-client) models do not implement this. Even the client ones do not implement
|
||||||
* {@link #modelStateChanged()}
|
* {@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
|
* The model has been successfully opened
|
||||||
|
@ -33,6 +57,17 @@ public interface DebuggerModelListener {
|
||||||
default public void modelOpened() {
|
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
|
* The model was closed
|
||||||
*
|
*
|
||||||
|
|
|
@ -18,7 +18,7 @@ package ghidra.dbg;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
|
||||||
import ghidra.async.AsyncUtils;
|
import ghidra.async.AsyncUtils;
|
||||||
import ghidra.async.TypeSpec;
|
import ghidra.async.TypeSpec;
|
||||||
|
@ -146,9 +146,27 @@ public interface DebuggerObjectModel {
|
||||||
/**
|
/**
|
||||||
* Add a listener for model events
|
* 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
|
* @param listener the listener
|
||||||
*/
|
*/
|
||||||
public void addModelListener(DebuggerModelListener listener);
|
public default void addModelListener(DebuggerModelListener listener) {
|
||||||
|
addModelListener(listener, false);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a model event listener
|
* Remove a model event listener
|
||||||
|
@ -309,9 +327,18 @@ public interface DebuggerObjectModel {
|
||||||
* object represents the debugger itself.
|
* object represents the debugger itself.
|
||||||
*
|
*
|
||||||
* @return the root
|
* @return the root
|
||||||
|
* @deprecated use {@link #getModelRoot()} instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
public CompletableFuture<? extends TargetObject> fetchModelRoot();
|
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
|
* Fetch the value at the given path
|
||||||
*
|
*
|
||||||
|
@ -362,6 +389,38 @@ public interface DebuggerObjectModel {
|
||||||
return fetchModelValue(List.of(path));
|
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
|
* Fetch the object with the given path
|
||||||
*
|
*
|
||||||
|
@ -392,11 +451,27 @@ public interface DebuggerObjectModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see #fetchModelObject(List)
|
* @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) {
|
public default CompletableFuture<? extends TargetObject> fetchModelObject(List<String> path) {
|
||||||
return fetchModelObject(path, false);
|
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)
|
* @see #fetchModelObject(List)
|
||||||
*/
|
*/
|
||||||
|
@ -498,50 +573,23 @@ public interface DebuggerObjectModel {
|
||||||
if (ex == null || DebuggerModelTerminatingException.isIgnorable(ex)) {
|
if (ex == null || DebuggerModelTerminatingException.isIgnorable(ex)) {
|
||||||
Msg.warn(origin, message + ": " + ex);
|
Msg.warn(origin, message + ": " + ex);
|
||||||
}
|
}
|
||||||
|
else if (AsyncUtils.unwrapThrowable(ex) instanceof RejectedExecutionException) {
|
||||||
|
Msg.trace(origin, "Ignoring rejection", ex);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
Msg.error(origin, message, ex);
|
Msg.error(origin, message, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the executor used to invoke client callback routines
|
* Permit all callbacks to be invoked before proceeding
|
||||||
*
|
|
||||||
* @return the executor
|
|
||||||
*/
|
|
||||||
Executor getClientExecutor();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure that dependent computations occur on the client executor
|
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* This also preserves scheduling order on the executor. Using just
|
* This operates by placing the request into the queue itself, so that any event callbacks
|
||||||
* {@link CompletableFuture#thenApplyAsync(java.util.function.Function)} makes no guarantees
|
* queued <em>at the time of the flush invocation</em> are completed first. There are no
|
||||||
* about execution order, because that invocation could occur before invocations in the chained
|
* guarantees with respect to events which get queued <em>after the flush invocation</em>.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* @param <T> the type of the future value
|
* @return a future which completes when all queued callbacks have been invoked
|
||||||
* @param cf the future
|
|
||||||
* @return a future gated via the client executor
|
|
||||||
*/
|
*/
|
||||||
default <T> CompletableFuture<T> gateFuture(CompletableFuture<T> cf) {
|
CompletableFuture<Void> flushEvents();
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,29 +15,190 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.dbg.agent;
|
package ghidra.dbg.agent;
|
||||||
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.*;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
import ghidra.async.AsyncUtils;
|
||||||
import ghidra.dbg.DebuggerModelListener;
|
import ghidra.dbg.DebuggerModelListener;
|
||||||
|
import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
|
import ghidra.dbg.target.TargetObject;
|
||||||
|
import ghidra.dbg.util.PathUtils;
|
||||||
import ghidra.util.datastruct.ListenerSet;
|
import ghidra.util.datastruct.ListenerSet;
|
||||||
|
|
||||||
public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectModel {
|
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 =
|
protected final ListenerSet<DebuggerModelListener> listeners =
|
||||||
new ListenerSet<>(DebuggerModelListener.class, clientExecutor);
|
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
|
@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);
|
listeners.add(listener);
|
||||||
}
|
}
|
||||||
|
}, clientExecutor).exceptionally(ex -> {
|
||||||
|
listener.catastrophic(ex);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeModelListener(DebuggerModelListener listener) {
|
public void removeModelListener(DebuggerModelListener listener) {
|
||||||
listeners.remove(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
|
@Override
|
||||||
public Executor getClientExecutor() {
|
public CompletableFuture<Void> flushEvents() {
|
||||||
return clientExecutor;
|
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.DebuggerObjectModel;
|
||||||
import ghidra.dbg.target.TargetObject;
|
import ghidra.dbg.target.TargetObject;
|
||||||
|
import ghidra.dbg.target.TypedTargetObject;
|
||||||
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
|
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
|
||||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||||
import ghidra.dbg.util.PathUtils;
|
import ghidra.dbg.util.PathUtils;
|
||||||
|
@ -39,13 +40,20 @@ import ghidra.util.datastruct.ListenerSet;
|
||||||
* @param <P> the type of the parent
|
* @param <P> the type of the parent
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractTargetObject<P extends TargetObject>
|
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 =
|
protected static final CompletableFuture<Map<String, TargetObject>> COMPLETED_EMPTY_ELEMENTS =
|
||||||
CompletableFuture.completedFuture(Map.of());
|
CompletableFuture.completedFuture(Map.of());
|
||||||
protected static final CompletableFuture<Map<String, Object>> COMPLETED_EMPTY_ATTRIBUTES =
|
protected static final CompletableFuture<Map<String, Object>> COMPLETED_EMPTY_ATTRIBUTES =
|
||||||
CompletableFuture.completedFuture(Map.of());
|
CompletableFuture.completedFuture(Map.of());
|
||||||
|
|
||||||
protected final DebuggerObjectModel model;
|
protected final AbstractDebuggerObjectModel model;
|
||||||
|
protected final SpiTargetObject proxy;
|
||||||
protected final P parent;
|
protected final P parent;
|
||||||
protected final CompletableFuture<P> completedParent;
|
protected final CompletableFuture<P> completedParent;
|
||||||
protected final List<String> path;
|
protected final List<String> path;
|
||||||
|
@ -55,12 +63,15 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
||||||
|
|
||||||
protected boolean valid = true;
|
protected boolean valid = true;
|
||||||
|
|
||||||
|
// TODO: Remove both of these, and just do invocations on model's listeners
|
||||||
protected final ListenerSet<TargetObjectListener> 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) {
|
TargetObjectSchema schema) {
|
||||||
this.listeners = new ListenerSet<>(TargetObjectListener.class, model.getClientExecutor());
|
this.listeners = new ListenerSet<>(TargetObjectListener.class, model.clientExecutor);
|
||||||
this.model = model;
|
this.model = model;
|
||||||
|
listeners.addChained(model.listeners);
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.completedParent = CompletableFuture.completedFuture(parent);
|
this.completedParent = CompletableFuture.completedFuture(parent);
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
|
@ -69,10 +80,28 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
||||||
else {
|
else {
|
||||||
this.path = PathUtils.extend(parent.getPath(), key);
|
this.path = PathUtils.extend(parent.getPath(), key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model.removeExisting(path);
|
||||||
|
|
||||||
this.hash = computeHashCode();
|
this.hash = computeHashCode();
|
||||||
this.typeHint = typeHint;
|
this.typeHint = typeHint;
|
||||||
|
|
||||||
this.schema = schema;
|
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
|
* 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
|
* 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
|
* 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
|
* than the proxy. When invoking listeners, the proxy given by this method is used instead. The
|
||||||
* default, it simply returns {@code this}, providing the expected behavior for typical
|
* proxy is also used for schema interface validation.
|
||||||
* implementations. The proxy is also used for schema interface validation.
|
|
||||||
*
|
*
|
||||||
* @return the proxy or this
|
* @return the proxy or this
|
||||||
*/
|
*/
|
||||||
public TargetObject getProxy() {
|
public SpiTargetObject getProxy() {
|
||||||
return this;
|
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.
|
* @return true to throw exceptions on schema violations.
|
||||||
*/
|
*/
|
||||||
protected boolean enforcesStrictSchema() {
|
@Override
|
||||||
|
public boolean enforcesStrictSchema() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +187,9 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addListener(TargetObjectListener l) {
|
public void addListener(TargetObjectListener l) {
|
||||||
|
if (!valid) {
|
||||||
|
throw new IllegalStateException("Object is no longer valid: " + getProxy());
|
||||||
|
}
|
||||||
listeners.add(l);
|
listeners.add(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +199,7 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DebuggerObjectModel getModel() {
|
public AbstractDebuggerObjectModel getModel() {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,38 +254,68 @@ public abstract class AbstractTargetObject<P extends TargetObject>
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void doInvalidate(String reason) {
|
protected void doInvalidate(TargetObject branch, String reason) {
|
||||||
valid = false;
|
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) {
|
protected void doInvalidateElements(Collection<?> elems, String reason) {
|
||||||
for (Object e : elems) {
|
for (Object e : elems) {
|
||||||
if (e instanceof InvalidatableTargetObjectIf) {
|
if (e instanceof InvalidatableTargetObjectIf && e instanceof TargetObject) {
|
||||||
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) e;
|
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()) {
|
for (Map.Entry<String, ?> ent : attrs.entrySet()) {
|
||||||
String name = ent.getKey();
|
String name = ent.getKey();
|
||||||
Object a = ent.getValue();
|
Object a = ent.getValue();
|
||||||
if (a instanceof InvalidatableTargetObjectIf) {
|
if (a instanceof InvalidatableTargetObjectIf) {
|
||||||
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) a;
|
InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) a;
|
||||||
if (!PathUtils.isLink(getPath(), name, obj.getPath())) {
|
if (!PathUtils.isLink(getPath(), name, obj.getPath())) {
|
||||||
obj.invalidateSubtree(reason);
|
obj.invalidateSubtree(branch, reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidateSubtree(String reason) {
|
public void invalidateSubtree(TargetObject branch, String reason) {
|
||||||
// Pre-ordered traversal
|
// Pre-ordered traversal
|
||||||
doInvalidate(reason);
|
doInvalidate(branch, reason);
|
||||||
doInvalidateElements(getCachedElements().values(), reason);
|
doInvalidateElements(branch, getCachedElements().values(), reason);
|
||||||
doInvalidateAttributes(getCachedAttributes(), 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>
|
public class DefaultTargetModelRoot extends DefaultTargetObject<TargetObject, TargetObject>
|
||||||
implements TargetAggregate {
|
implements TargetAggregate {
|
||||||
|
|
||||||
public DefaultTargetModelRoot(DebuggerObjectModel model, String typeHint) {
|
public DefaultTargetModelRoot(AbstractDebuggerObjectModel model, String typeHint) {
|
||||||
this(model, typeHint, EnumerableTargetObjectSchema.OBJECT);
|
this(model, typeHint, EnumerableTargetObjectSchema.OBJECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefaultTargetModelRoot(DebuggerObjectModel model, String typeHint,
|
public DefaultTargetModelRoot(AbstractDebuggerObjectModel model, String typeHint,
|
||||||
TargetObjectSchema schema) {
|
TargetObjectSchema schema) {
|
||||||
super(model, null, null, typeHint, 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;
|
||||||
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
|
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.datastruct.ListenerSet;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A default implementation of {@link TargetObject} suitable for cases where the implementation
|
* 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 key the key (attribute name or element index) of this object
|
||||||
* @param typeHint the type hint for 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));
|
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 typeHint the type hint for this object
|
||||||
* @param schema the schema of 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) {
|
TargetObjectSchema schema) {
|
||||||
super(model, parent, key, typeHint, schema);
|
super(proxyFactory, proxyInfo, model, parent, key, typeHint, schema);
|
||||||
changeAttributes(List.of(), List.of(), Map.of(DISPLAY_ATTRIBUTE_NAME,
|
changeAttributes(List.of(), List.of(), Map.ofEntries(
|
||||||
key == null ? "<root>" : key, UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED),
|
Map.entry(DISPLAY_ATTRIBUTE_NAME, key == null ? "<root>" : key),
|
||||||
|
Map.entry(UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED)),
|
||||||
"Initialized");
|
"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
|
* Check if this object is being observed
|
||||||
*
|
*
|
||||||
|
@ -115,11 +149,20 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
||||||
* messaging is involved.
|
* messaging is involved.
|
||||||
*
|
*
|
||||||
* @return true if there is at least one listener on this object
|
* @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() {
|
protected boolean isObserved() {
|
||||||
return !listeners.isEmpty();
|
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
|
* 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) {
|
synchronized (elements) {
|
||||||
if (refresh || curElemsRequest == null || curElemsRequest.isCompletedExceptionally() ||
|
if (refresh || curElemsRequest == null || curElemsRequest.isCompletedExceptionally() ||
|
||||||
getUpdateMode() == TargetUpdateMode.SOLICITED) {
|
getUpdateMode() == TargetUpdateMode.SOLICITED) {
|
||||||
curElemsRequest = requestElements(refresh);
|
curElemsRequest = requestElements(refresh).thenCompose(model::gateFuture);
|
||||||
}
|
}
|
||||||
req = curElemsRequest;
|
req = curElemsRequest;
|
||||||
}
|
}
|
||||||
return req.thenApply(__ -> getCachedElements()).thenCompose(model::gateFuture);
|
return req.thenApply(__ -> getCachedElements());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -173,7 +216,7 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, E> getCachedElements() {
|
public Map<String, E> getCachedElements() {
|
||||||
synchronized (elements) {
|
synchronized (model.lock) {
|
||||||
return Map.copyOf(elements);
|
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) {
|
private Delta<E, E> setElements(Map<String, E> elements, String reason) {
|
||||||
Delta<E, E> delta;
|
Delta<E, E> delta;
|
||||||
synchronized (this.elements) {
|
synchronized (model.lock) {
|
||||||
delta = Delta.computeAndSet(this.elements, elements, Delta.SAME);
|
delta = Delta.computeAndSet(this.elements, elements, Delta.SAME);
|
||||||
}
|
}
|
||||||
TargetObjectSchema schemax = getSchema();
|
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,
|
private Delta<E, E> changeElements(Collection<String> remove, Map<String, E> add,
|
||||||
String reason) {
|
String reason) {
|
||||||
Delta<E, E> delta;
|
Delta<E, E> delta;
|
||||||
synchronized (elements) {
|
synchronized (model.lock) {
|
||||||
delta = Delta.apply(this.elements, remove, add, Delta.SAME);
|
delta = Delta.apply(this.elements, remove, add, Delta.SAME);
|
||||||
}
|
}
|
||||||
TargetObjectSchema schemax = getSchema();
|
TargetObjectSchema schemax = getSchema();
|
||||||
|
@ -326,18 +369,18 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
||||||
synchronized (attributes) {
|
synchronized (attributes) {
|
||||||
// update_mode does not affect attributes. They always behave as if UNSOLICITED.
|
// update_mode does not affect attributes. They always behave as if UNSOLICITED.
|
||||||
if (refresh || curAttrsRequest == null || curAttrsRequest.isCompletedExceptionally()) {
|
if (refresh || curAttrsRequest == null || curAttrsRequest.isCompletedExceptionally()) {
|
||||||
curAttrsRequest = requestAttributes(refresh);
|
curAttrsRequest = requestAttributes(refresh).thenCompose(model::gateFuture);
|
||||||
}
|
}
|
||||||
req = curAttrsRequest;
|
req = curAttrsRequest;
|
||||||
}
|
}
|
||||||
return req.thenApply(__ -> {
|
return req.thenApply(__ -> {
|
||||||
synchronized (attributes) {
|
synchronized (model.lock) {
|
||||||
if (schema != null) { // TODO: Remove this. Schema should never be null.
|
if (schema != null) { // TODO: Remove this. Schema should never be null.
|
||||||
schema.validateRequiredAttributes(this, enforcesStrictSchema());
|
schema.validateRequiredAttributes(this, enforcesStrictSchema());
|
||||||
}
|
}
|
||||||
return getCachedAttributes();
|
return getCachedAttributes();
|
||||||
}
|
}
|
||||||
}).thenCompose(model::gateFuture);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -347,14 +390,14 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, ?> getCachedAttributes() {
|
public Map<String, ?> getCachedAttributes() {
|
||||||
synchronized (attributes) {
|
synchronized (model.lock) {
|
||||||
return Map.copyOf(attributes);
|
return Map.copyOf(attributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getCachedAttribute(String name) {
|
public Object getCachedAttribute(String name) {
|
||||||
synchronized (attributes) {
|
synchronized (model.lock) {
|
||||||
return attributes.get(name);
|
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) {
|
public Delta<?, ?> setAttributes(Map<String, ?> attributes, String reason) {
|
||||||
Delta<?, ?> delta;
|
Delta<?, ?> delta;
|
||||||
synchronized (this.attributes) {
|
synchronized (model.lock) {
|
||||||
delta = Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL);
|
delta = Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL);
|
||||||
}
|
}
|
||||||
TargetObjectSchema schemax = getSchema();
|
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) {
|
public Delta<?, ?> changeAttributes(List<String> remove, Map<String, ?> add, String reason) {
|
||||||
Delta<?, ?> delta;
|
Delta<?, ?> delta;
|
||||||
synchronized (attributes) {
|
synchronized (model.lock) {
|
||||||
delta = Delta.apply(this.attributes, remove, add, Delta.EQUAL);
|
delta = Delta.apply(this.attributes, remove, add, Delta.EQUAL);
|
||||||
}
|
}
|
||||||
TargetObjectSchema schemax = getSchema();
|
TargetObjectSchema schemax = getSchema();
|
||||||
|
@ -463,8 +506,4 @@ public class DefaultTargetObject<E extends TargetObject, P extends TargetObject>
|
||||||
}
|
}
|
||||||
return delta;
|
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
|
* {@link DefaultTargetObject#setElements(Collection, String)} will automatically invoke this
|
||||||
* method when they detect object removal.
|
* 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
|
* @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);
|
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)) {
|
if (PathUtils.isIndex(key)) {
|
||||||
return obj.fetchElements(true).thenApply(elements -> {
|
return obj.fetchElements(true).thenApply(elements -> {
|
||||||
return elements.get(PathUtils.parseIndex(key));
|
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) {
|
List<String> path, boolean refresh, boolean followLinks) {
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return CompletableFuture.completedFuture(obj);
|
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>
|
* <p>
|
||||||
* Note that it is OK for more than one {@link TargetObjectRef} to refer to the same path. These
|
* 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()}.
|
* 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> {
|
public interface TargetObjectRef extends Comparable<TargetObjectRef> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,7 +147,10 @@ public interface TargetObjectRef extends Comparable<TargetObjectRef> {
|
||||||
* Get the actual object
|
* Get the actual object
|
||||||
*
|
*
|
||||||
* @return a future which completes with the 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() {
|
public default CompletableFuture<? extends TargetObject> fetch() {
|
||||||
return getModel().fetchModelObject(getPath());
|
return getModel().fetchModelObject(getPath());
|
||||||
}
|
}
|
||||||
|
@ -298,8 +305,20 @@ public interface TargetObjectRef extends Comparable<TargetObjectRef> {
|
||||||
* does not exist
|
* does not exist
|
||||||
*/
|
*/
|
||||||
public default CompletableFuture<?> fetchAttribute(String name) {
|
public default CompletableFuture<?> fetchAttribute(String name) {
|
||||||
|
if (!PathUtils.isInvocation(name)) {
|
||||||
return fetchAttributes().thenApply(m -> m.get(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
|
* Fetch all the elements of this object
|
||||||
|
|
|
@ -22,6 +22,13 @@ import ghidra.dbg.DebuggerObjectModel;
|
||||||
import ghidra.dbg.target.TargetObject;
|
import ghidra.dbg.target.TargetObject;
|
||||||
import ghidra.dbg.target.TypedTargetObject;
|
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 interface TypedTargetObjectRef<T extends TargetObject> extends TargetObjectRef {
|
||||||
public class CastingTargetObjectRef<T extends TypedTargetObject<T>>
|
public class CastingTargetObjectRef<T extends TypedTargetObject<T>>
|
||||||
implements TypedTargetObjectRef<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) {
|
default void consoleOutput(TargetObject console, Channel channel, byte[] data) {
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public interface TargetTextConsoleListener extends TargetConsoleListener {
|
|
||||||
/**
|
/**
|
||||||
* The console has produced output
|
* 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 console the console producing the output
|
||||||
* @param channel identifies the "output stream", stdout or stderr
|
* @param channel identifies the "output stream", stdout or stderr
|
||||||
* @param text the output text
|
* @param text the output text
|
||||||
*/
|
*/
|
||||||
default void consoleOutput(TargetObject console, Channel channel, String text) {
|
default void consoleOutput(TargetObject console, Channel channel, String text) {
|
||||||
|
consoleOutput(console, channel, text.getBytes(CHARSET));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public interface TargetTextConsoleListener extends TargetConsoleListener {
|
||||||
default void consoleOutput(TargetObject console, Channel channel, byte[] data) {
|
|
||||||
consoleOutput(console, channel, new String(data, CHARSET));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -529,6 +529,30 @@ public interface TargetObject extends TargetObjectRef {
|
||||||
return CompletableFuture.completedFuture(this);
|
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
|
* 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);
|
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
|
* Cast the named attribute to the given type, if possible
|
||||||
*
|
*
|
||||||
|
@ -675,13 +707,19 @@ public interface TargetObject extends TargetObjectRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface TargetObjectListener {
|
public interface TargetObjectListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The object's display string has changed
|
* The object was created
|
||||||
*
|
*
|
||||||
* @param object the object
|
* <p>
|
||||||
* @param display the new display string
|
* This can only be received by listening on the model. While the created object can now
|
||||||
|
* appear in other callbacks, it should not be used aside from those callbacks, until it is
|
||||||
|
* added to its parent. Until that time, the object may not adhere to the schema, since its
|
||||||
|
* children are still being initialized.
|
||||||
|
*
|
||||||
|
* @param object the newly-created object
|
||||||
*/
|
*/
|
||||||
default void displayChanged(TargetObject object, String display) {
|
default void created(TargetObject object) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -704,9 +742,19 @@ public interface TargetObject extends TargetObjectRef {
|
||||||
* mistakenly applied to the replacement or its successors.
|
* mistakenly applied to the replacement or its successors.
|
||||||
*
|
*
|
||||||
* @param object the now-invalid object
|
* @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
|
* @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
|
* 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 {
|
public interface TargetObjectFetchingListener extends TargetObjectListener {
|
||||||
@Override
|
@Override
|
||||||
default void elementsChanged(TargetObject parent, Collection<String> removed,
|
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);
|
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.
|
* Check whether a given attribute should be displayed.
|
||||||
*
|
*
|
||||||
|
|
|
@ -18,17 +18,20 @@ package ghidra.dbg.agent;
|
||||||
import static ghidra.lifecycle.Unfinished.TODO;
|
import static ghidra.lifecycle.Unfinished.TODO;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import generic.Unique;
|
import generic.Unique;
|
||||||
import ghidra.async.AsyncTestUtils;
|
import ghidra.async.AsyncTestUtils;
|
||||||
import ghidra.async.AsyncUtils;
|
import ghidra.dbg.DebuggerModelListener;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.attributes.TargetObjectRef;
|
||||||
import ghidra.dbg.target.TargetObject;
|
import ghidra.dbg.target.TargetObject;
|
||||||
|
import ghidra.dbg.target.TargetRegisterBank;
|
||||||
|
import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener;
|
||||||
import ghidra.dbg.util.*;
|
import ghidra.dbg.util.*;
|
||||||
import ghidra.dbg.util.AttributesChangedListener.AttributesChangedInvocation;
|
import ghidra.dbg.util.AttributesChangedListener.AttributesChangedInvocation;
|
||||||
import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation;
|
import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation;
|
||||||
|
@ -39,16 +42,38 @@ import ghidra.program.model.address.AddressSpace;
|
||||||
public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
||||||
|
|
||||||
public static class FakeTargetObject extends DefaultTargetObject<TargetObject, TargetObject> {
|
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");
|
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
|
* Functionally identical to a Fake, but intrinsically different
|
||||||
*/
|
*/
|
||||||
public static class PhonyTargetObject extends DefaultTargetObject<TargetObject, TargetObject> {
|
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");
|
super(model, parent, name, "Phony");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +81,10 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
||||||
public static class FakeDebuggerObjectModel extends AbstractDebuggerObjectModel {
|
public static class FakeDebuggerObjectModel extends AbstractDebuggerObjectModel {
|
||||||
DefaultTargetModelRoot root = new DefaultTargetModelRoot(this, "Root");
|
DefaultTargetModelRoot root = new DefaultTargetModelRoot(this, "Root");
|
||||||
|
|
||||||
|
public FakeDebuggerObjectModel() {
|
||||||
|
addModelRoot(root);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
|
public CompletableFuture<? extends TargetObject> fetchModelRoot() {
|
||||||
return CompletableFuture.completedFuture(root);
|
return CompletableFuture.completedFuture(root);
|
||||||
|
@ -70,16 +99,11 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
||||||
public AddressSpace getAddressSpace(String name) {
|
public AddressSpace getAddressSpace(String name) {
|
||||||
return TODO();
|
return TODO();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Void> close() {
|
|
||||||
return AsyncUtils.NIL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static class OffThreadTargetObject extends DefaultTargetObject<TargetObject, TargetObject> {
|
static class OffThreadTargetObject extends DefaultTargetObject<TargetObject, TargetObject> {
|
||||||
public OffThreadTargetObject(DebuggerObjectModel model, TargetObject parent, String name,
|
public OffThreadTargetObject(AbstractDebuggerObjectModel model, TargetObject parent,
|
||||||
String typeHint) {
|
String name, String typeHint) {
|
||||||
super(model, parent, name, typeHint);
|
super(model, parent, name, typeHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,59 +173,63 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
||||||
fakeA.addListener(invL);
|
fakeA.addListener(invL);
|
||||||
|
|
||||||
PhonyTargetObject phonyA = new PhonyTargetObject(model, model.root, "[A]");
|
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");
|
model.root.setElements(List.of(phonyA), "Replace");
|
||||||
|
|
||||||
assertSame(phonyA, waitOn(model.fetchModelObject("[A]")));
|
assertSame(phonyA, waitOn(model.fetchModelObject("[A]")));
|
||||||
assertFalse(fakeA.isValid());
|
assertFalse(fakeA.isValid());
|
||||||
|
|
||||||
ElementsChangedInvocation changed = Unique.assertOne(elemL.invocations);
|
ElementsChangedInvocation changed2 = Unique.assertOne(elemL.invocations);
|
||||||
assertSame(model.root, changed.parent);
|
assertSame(model.root, changed2.parent);
|
||||||
assertSame(phonyA, Unique.assertOne(changed.added.values()));
|
assertSame(phonyA, Unique.assertOne(changed2.added.values()));
|
||||||
|
assertTrue(changed2.removed.isEmpty());
|
||||||
InvalidatedInvocation invalidated = Unique.assertOne(invL.invocations);
|
|
||||||
assertSame(fakeA, invalidated.object);
|
|
||||||
assertEquals("Replace", invalidated.reason);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAttributeReplacement() throws Throwable {
|
public void testAttributeReplacement() throws Throwable {
|
||||||
AttributesChangedListener attrL = new AttributesChangedListener();
|
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);
|
model.root.addListener(attrL);
|
||||||
fakeA.addListener(invL);
|
|
||||||
|
|
||||||
PhonyTargetObject phonyA = new PhonyTargetObject(model, model.root, "A");
|
// Note: mere object creation will cause "prior removal"
|
||||||
model.root.setAttributes(Map.of("A", phonyA), "Replace");
|
// 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);
|
waitOn(model.clientExecutor);
|
||||||
|
|
||||||
// Object-valued attribute replacement requires prior removal
|
assertSame(str1, waitOn(model.fetchModelValue("a")));
|
||||||
assertSame(fakeA, waitOn(model.fetchModelObject("A")));
|
|
||||||
assertEquals(0, attrL.invocations.size());
|
assertEquals(0, attrL.invocations.size());
|
||||||
assertEquals(0, invL.invocations.size());
|
|
||||||
|
|
||||||
// Now, with prior removal
|
// Now, with prior removal
|
||||||
// TODO: Should I permit custom equality check?
|
// TODO: Should I permit custom equality check?
|
||||||
model.root.setAttributes(Map.of(), "Clear");
|
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);
|
waitOn(model.clientExecutor);
|
||||||
|
|
||||||
assertEquals(2, attrL.invocations.size());
|
assertEquals(2, attrL.invocations.size());
|
||||||
AttributesChangedInvocation changed = attrL.invocations.get(0);
|
AttributesChangedInvocation changed = attrL.invocations.get(0);
|
||||||
assertEquals(model.root, changed.parent);
|
assertEquals(model.root, changed.parent);
|
||||||
assertSame("A", Unique.assertOne(changed.removed));
|
assertEquals("a", Unique.assertOne(changed.removed));
|
||||||
assertEquals(0, changed.added.size());
|
assertEquals(0, changed.added.size());
|
||||||
changed = attrL.invocations.get(1);
|
changed = attrL.invocations.get(1);
|
||||||
assertEquals(model.root, changed.parent);
|
assertEquals(model.root, changed.parent);
|
||||||
assertSame(phonyA, Unique.assertOne(changed.added.values()));
|
assertSame(str2, Unique.assertOne(changed.added.values()));
|
||||||
assertEquals(0, changed.removed.size());
|
assertEquals(0, changed.removed.size());
|
||||||
|
|
||||||
InvalidatedInvocation invalidated = Unique.assertOne(invL.invocations);
|
|
||||||
assertSame(fakeA, invalidated.object);
|
|
||||||
assertEquals("Clear", invalidated.reason);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -223,4 +251,83 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils {
|
||||||
|
|
||||||
waitOn(invL.count.waitValue(3));
|
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");
|
this("Session");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Executor getClientExecutor() {
|
||||||
|
return clientExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
public TestDebuggerObjectModel(String rootHint) {
|
public TestDebuggerObjectModel(String rootHint) {
|
||||||
this.session = new TestTargetSession(this, rootHint);
|
this.session = new TestTargetSession(this, rootHint);
|
||||||
|
addModelRoot(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -67,8 +72,8 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Void> close() {
|
public CompletableFuture<Void> close() {
|
||||||
session.invalidateSubtree("Model closed");
|
session.invalidateSubtree(session, "Model closed");
|
||||||
return future(null);
|
return super.close().thenCompose(__ -> future(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestTargetProcess addProcess(int pid) {
|
public TestTargetProcess addProcess(int pid) {
|
||||||
|
|
|
@ -15,9 +15,9 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.dbg.model;
|
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
|
@Override
|
||||||
TestDebuggerObjectModel getModel();
|
TestDebuggerObjectModel getModel();
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,7 @@ import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.agent.*;
|
||||||
import ghidra.dbg.agent.DefaultTargetModelRoot;
|
|
||||||
import ghidra.dbg.agent.DefaultTargetObject;
|
|
||||||
import ghidra.dbg.target.*;
|
import ghidra.dbg.target.*;
|
||||||
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
|
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
|
||||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||||
|
@ -53,7 +51,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
|
|
||||||
@TargetObjectSchemaInfo
|
@TargetObjectSchemaInfo
|
||||||
static class TestAnnotatedTargetRootPlain extends DefaultTargetModelRoot {
|
static class TestAnnotatedTargetRootPlain extends DefaultTargetModelRoot {
|
||||||
public TestAnnotatedTargetRootPlain(DebuggerObjectModel model, String typeHint) {
|
public TestAnnotatedTargetRootPlain(AbstractDebuggerObjectModel model, String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,7 +69,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
|
|
||||||
@TargetObjectSchemaInfo(elements = @TargetElementType(type = Void.class))
|
@TargetObjectSchemaInfo(elements = @TargetElementType(type = Void.class))
|
||||||
static class TestAnnotatedTargetRootNoElems extends DefaultTargetModelRoot {
|
static class TestAnnotatedTargetRootNoElems extends DefaultTargetModelRoot {
|
||||||
public TestAnnotatedTargetRootNoElems(DebuggerObjectModel model, String typeHint) {
|
public TestAnnotatedTargetRootNoElems(AbstractDebuggerObjectModel model, String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,15 +90,15 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
static class TestAnnotatedTargetProcessStub
|
static class TestAnnotatedTargetProcessStub
|
||||||
extends DefaultTargetObject<TargetObject, TargetObject>
|
extends DefaultTargetObject<TargetObject, TargetObject>
|
||||||
implements TargetProcess<TestAnnotatedTargetProcessStub> {
|
implements TargetProcess<TestAnnotatedTargetProcessStub> {
|
||||||
public TestAnnotatedTargetProcessStub(DebuggerObjectModel model, TargetObject parent,
|
public TestAnnotatedTargetProcessStub(AbstractDebuggerObjectModel model,
|
||||||
String key, String typeHint) {
|
TargetObject parent, String key, String typeHint) {
|
||||||
super(model, parent, key, typeHint);
|
super(model, parent, key, typeHint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetObjectSchemaInfo(name = "Root")
|
@TargetObjectSchemaInfo(name = "Root")
|
||||||
static class TestAnnotatedTargetRootOverriddenFetchElems extends DefaultTargetModelRoot {
|
static class TestAnnotatedTargetRootOverriddenFetchElems extends DefaultTargetModelRoot {
|
||||||
public TestAnnotatedTargetRootOverriddenFetchElems(DebuggerObjectModel model,
|
public TestAnnotatedTargetRootOverriddenFetchElems(AbstractDebuggerObjectModel model,
|
||||||
String typeHint) {
|
String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
|
@ -131,7 +129,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
@TargetObjectSchemaInfo(name = "ProcessContainer")
|
@TargetObjectSchemaInfo(name = "ProcessContainer")
|
||||||
static class TestAnnotatedProcessContainer
|
static class TestAnnotatedProcessContainer
|
||||||
extends DefaultTargetObject<TestAnnotatedTargetProcessStub, TargetObject> {
|
extends DefaultTargetObject<TestAnnotatedTargetProcessStub, TargetObject> {
|
||||||
public TestAnnotatedProcessContainer(DebuggerObjectModel model, TargetObject parent,
|
public TestAnnotatedProcessContainer(AbstractDebuggerObjectModel model, TargetObject parent,
|
||||||
String key, String typeHint) {
|
String key, String typeHint) {
|
||||||
super(model, parent, key, typeHint);
|
super(model, parent, key, typeHint);
|
||||||
}
|
}
|
||||||
|
@ -154,15 +152,15 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
static class TestAnnotatedTargetProcessParam<T>
|
static class TestAnnotatedTargetProcessParam<T>
|
||||||
extends DefaultTargetObject<TargetObject, TargetObject>
|
extends DefaultTargetObject<TargetObject, TargetObject>
|
||||||
implements TargetProcess<TestAnnotatedTargetProcessParam<T>> {
|
implements TargetProcess<TestAnnotatedTargetProcessParam<T>> {
|
||||||
public TestAnnotatedTargetProcessParam(DebuggerObjectModel model, TargetObject parent,
|
public TestAnnotatedTargetProcessParam(AbstractDebuggerObjectModel model,
|
||||||
String key, String typeHint) {
|
TargetObject parent, String key, String typeHint) {
|
||||||
super(model, parent, key, typeHint);
|
super(model, parent, key, typeHint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetObjectSchemaInfo
|
@TargetObjectSchemaInfo
|
||||||
static class TestAnnotatedTargetRootWithAnnotatedAttrs extends DefaultTargetModelRoot {
|
static class TestAnnotatedTargetRootWithAnnotatedAttrs extends DefaultTargetModelRoot {
|
||||||
public TestAnnotatedTargetRootWithAnnotatedAttrs(DebuggerObjectModel model,
|
public TestAnnotatedTargetRootWithAnnotatedAttrs(AbstractDebuggerObjectModel model,
|
||||||
String typeHint) {
|
String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
|
@ -209,7 +207,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
@TargetElementType(index = "reserved", type = Void.class)
|
@TargetElementType(index = "reserved", type = Void.class)
|
||||||
})
|
})
|
||||||
static class TestAnnotatedTargetRootWithListedAttrs extends DefaultTargetModelRoot {
|
static class TestAnnotatedTargetRootWithListedAttrs extends DefaultTargetModelRoot {
|
||||||
public TestAnnotatedTargetRootWithListedAttrs(DebuggerObjectModel model,
|
public TestAnnotatedTargetRootWithListedAttrs(AbstractDebuggerObjectModel model,
|
||||||
String typeHint) {
|
String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
|
@ -243,7 +241,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
@TargetObjectSchemaInfo
|
@TargetObjectSchemaInfo
|
||||||
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadType extends DefaultTargetModelRoot {
|
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadType extends DefaultTargetModelRoot {
|
||||||
|
|
||||||
public TestAnnotatedTargetRootWithAnnotatedAttrsBadType(DebuggerObjectModel model,
|
public TestAnnotatedTargetRootWithAnnotatedAttrsBadType(AbstractDebuggerObjectModel model,
|
||||||
String typeHint) {
|
String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
|
@ -273,7 +271,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
static class TestAnnotatedTargetRootWithAnnotatedAttrsNonUnique<T extends Dummy & TargetProcess<T> & TargetInterpreter<T>>
|
static class TestAnnotatedTargetRootWithAnnotatedAttrsNonUnique<T extends Dummy & TargetProcess<T> & TargetInterpreter<T>>
|
||||||
extends DefaultTargetModelRoot {
|
extends DefaultTargetModelRoot {
|
||||||
|
|
||||||
public TestAnnotatedTargetRootWithAnnotatedAttrsNonUnique(DebuggerObjectModel model,
|
public TestAnnotatedTargetRootWithAnnotatedAttrsNonUnique(AbstractDebuggerObjectModel model,
|
||||||
String typeHint) {
|
String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
|
@ -294,7 +292,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
static class TestAnnotatedTargetRootWithElemsNonUnique<T extends Dummy & TargetProcess<T> & TargetInterpreter<T>>
|
static class TestAnnotatedTargetRootWithElemsNonUnique<T extends Dummy & TargetProcess<T> & TargetInterpreter<T>>
|
||||||
extends DefaultTargetModelRoot {
|
extends DefaultTargetModelRoot {
|
||||||
|
|
||||||
public TestAnnotatedTargetRootWithElemsNonUnique(DebuggerObjectModel model,
|
public TestAnnotatedTargetRootWithElemsNonUnique(AbstractDebuggerObjectModel model,
|
||||||
String typeHint) {
|
String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
|
@ -314,7 +312,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
|
|
||||||
@TargetObjectSchemaInfo
|
@TargetObjectSchemaInfo
|
||||||
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadName extends DefaultTargetModelRoot {
|
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadName extends DefaultTargetModelRoot {
|
||||||
public TestAnnotatedTargetRootWithAnnotatedAttrsBadName(DebuggerObjectModel model,
|
public TestAnnotatedTargetRootWithAnnotatedAttrsBadName(AbstractDebuggerObjectModel model,
|
||||||
String typeHint) {
|
String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
|
@ -333,7 +331,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
|
|
||||||
@TargetObjectSchemaInfo
|
@TargetObjectSchemaInfo
|
||||||
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadGetter extends DefaultTargetModelRoot {
|
static class TestAnnotatedTargetRootWithAnnotatedAttrsBadGetter extends DefaultTargetModelRoot {
|
||||||
public TestAnnotatedTargetRootWithAnnotatedAttrsBadGetter(DebuggerObjectModel model,
|
public TestAnnotatedTargetRootWithAnnotatedAttrsBadGetter(AbstractDebuggerObjectModel model,
|
||||||
String typeHint) {
|
String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
|
@ -353,7 +351,7 @@ public class AnnotatedTargetObjectSchemaTest {
|
||||||
@TargetObjectSchemaInfo(
|
@TargetObjectSchemaInfo(
|
||||||
attributes = @TargetAttributeType(name = "some_attr", type = NotAPrimitive.class))
|
attributes = @TargetAttributeType(name = "some_attr", type = NotAPrimitive.class))
|
||||||
static class TestAnnotatedTargetRootWithListedAttrsBadType extends DefaultTargetModelRoot {
|
static class TestAnnotatedTargetRootWithListedAttrsBadType extends DefaultTargetModelRoot {
|
||||||
public TestAnnotatedTargetRootWithListedAttrsBadType(DebuggerObjectModel model,
|
public TestAnnotatedTargetRootWithListedAttrsBadType(AbstractDebuggerObjectModel model,
|
||||||
String typeHint) {
|
String typeHint) {
|
||||||
super(model, typeHint);
|
super(model, typeHint);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import java.util.concurrent.ExecutionException;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
|
||||||
import ghidra.dbg.agent.*;
|
import ghidra.dbg.agent.*;
|
||||||
import ghidra.dbg.target.*;
|
import ghidra.dbg.target.*;
|
||||||
import ghidra.dbg.target.TargetObject.TargetUpdateMode;
|
import ghidra.dbg.target.TargetObject.TargetUpdateMode;
|
||||||
|
@ -61,12 +60,6 @@ public class TargetObjectSchemaValidationTest {
|
||||||
fail();
|
fail();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Void> close() {
|
|
||||||
fail();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -107,29 +100,29 @@ public class TargetObjectSchemaValidationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ValidatedModelRoot extends DefaultTargetModelRoot {
|
static class ValidatedModelRoot extends DefaultTargetModelRoot {
|
||||||
public ValidatedModelRoot(DebuggerObjectModel model, String typeHint,
|
public ValidatedModelRoot(AbstractDebuggerObjectModel model, String typeHint,
|
||||||
TargetObjectSchema schema) {
|
TargetObjectSchema schema) {
|
||||||
super(model, typeHint, schema);
|
super(model, typeHint, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean enforcesStrictSchema() {
|
public boolean enforcesStrictSchema() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ValidatedObject extends DefaultTargetObject<TargetObject, TargetObject> {
|
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) {
|
TargetObjectSchema schema) {
|
||||||
super(model, parent, key, "Object", 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");
|
super(model, parent, key, "Object");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean enforcesStrictSchema() {
|
public boolean enforcesStrictSchema() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.dbg.util;
|
package ghidra.dbg.util;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import ghidra.async.AsyncReference;
|
import ghidra.async.AsyncReference;
|
||||||
|
|
||||||
public abstract class AbstractInvocationListener<T> {
|
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);
|
public final AsyncReference<Integer, Void> count = new AsyncReference<>(0);
|
||||||
|
|
||||||
protected void record(T rec) {
|
protected void record(T rec) {
|
||||||
|
|
|
@ -59,7 +59,7 @@ public interface AllTargetObjectListenerAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
default void consoleOutput(TargetObject console, Channel channel, String out) {
|
default void consoleOutput(TargetObject console, Channel channel, byte[] out) {
|
||||||
//fail();
|
//fail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,10 +23,12 @@ public class InvalidatedListener extends
|
||||||
AbstractInvocationListener<InvalidatedInvocation> implements TargetObjectListener {
|
AbstractInvocationListener<InvalidatedInvocation> implements TargetObjectListener {
|
||||||
public static class InvalidatedInvocation {
|
public static class InvalidatedInvocation {
|
||||||
public final TargetObject object;
|
public final TargetObject object;
|
||||||
|
public final TargetObject branch;
|
||||||
public final String reason;
|
public final String reason;
|
||||||
|
|
||||||
public InvalidatedInvocation(TargetObject object, String reason) {
|
public InvalidatedInvocation(TargetObject object, TargetObject branch, String reason) {
|
||||||
this.object = object;
|
this.object = object;
|
||||||
|
this.branch = branch;
|
||||||
this.reason = reason;
|
this.reason = reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +39,7 @@ public class InvalidatedListener extends
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidated(TargetObject object, String reason) {
|
public void invalidated(TargetObject object, TargetObject branch, String reason) {
|
||||||
record(new InvalidatedInvocation(object, reason));
|
record(new InvalidatedInvocation(object, branch, reason));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package ghidra.util.datastruct;
|
||||||
import java.lang.reflect.*;
|
import java.lang.reflect.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import com.google.common.cache.*;
|
import com.google.common.cache.*;
|
||||||
|
@ -110,15 +111,27 @@ public class ListenerMap<K, P, V extends P> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
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) {
|
for (V l : listenersVolatile) {
|
||||||
if (!ext.isAssignableFrom(l.getClass())) {
|
if (!ext.isAssignableFrom(l.getClass())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
|
//Msg.debug(this,
|
||||||
|
// "Invoking: " + method.getName() + " @" + System.identityHashCode(executor));
|
||||||
try {
|
try {
|
||||||
method.invoke(l, args);
|
method.invoke(l, args);
|
||||||
}
|
}
|
||||||
|
catch (RejectedExecutionException e) {
|
||||||
|
Msg.trace(this, "Listener invocation rejected", e);
|
||||||
|
}
|
||||||
catch (InvocationTargetException e) {
|
catch (InvocationTargetException e) {
|
||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
reportError(l, cause);
|
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
|
return null; // TODO: Assumes void return type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final Object lock = new Object();
|
||||||
private final Class<P> iface;
|
private final Class<P> iface;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private Map<K, V> map = createMap();
|
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
|
* 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) {
|
public V put(K key, V val) {
|
||||||
|
synchronized (lock) {
|
||||||
if (map.get(key) == val) {
|
if (map.get(key) == val) {
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
@ -219,19 +250,23 @@ public class ListenerMap<K, P, V extends P> {
|
||||||
map = newMap;
|
map = newMap;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void putAll(ListenerMap<? extends K, P, ? extends V> that) {
|
public void putAll(ListenerMap<? extends K, P, ? extends V> that) {
|
||||||
|
synchronized (lock) {
|
||||||
Map<K, V> newMap = createMap();
|
Map<K, V> newMap = createMap();
|
||||||
newMap.putAll(map);
|
newMap.putAll(map);
|
||||||
newMap.putAll(that.map);
|
newMap.putAll(that.map);
|
||||||
map = newMap;
|
map = newMap;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public V get(K key) {
|
public V get(K key) {
|
||||||
return map.get(key);
|
return map.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public V remove(K key) {
|
public V remove(K key) {
|
||||||
|
synchronized (lock) {
|
||||||
if (!map.containsKey(key)) {
|
if (!map.containsKey(key)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -241,11 +276,47 @@ public class ListenerMap<K, P, V extends P> {
|
||||||
map = newMap;
|
map = newMap;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
|
synchronized (lock) {
|
||||||
if (map.isEmpty()) {
|
if (map.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
map = createMap();
|
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() {
|
public void clear() {
|
||||||
map.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
|
* 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
|
* 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
|
* wouldn't work when the return value itself wraps or will provide the delegate (e.g.,
|
||||||
* future).
|
* a future).
|
||||||
*/
|
*/
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue