diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest b/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest index fc6bcc9c98..3faa1826dd 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest +++ b/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest @@ -4,5 +4,6 @@ .project||NONE||reviewed||END| Module.manifest||GHIDRA||||END| build.gradle||GHIDRA||||END| +hs_err_pid5696.mdmp||GHIDRA||||END| src/javaprovider/def/javaprovider.def||GHIDRA||||END| src/javaprovider/rc/javaprovider.rc||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/garbage/AbstractModelForDbgTest.java similarity index 95% rename from Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgTest.java rename to Ghidra/Debug/Debugger-agent-dbgeng/garbage/AbstractModelForDbgTest.java index 0514b23551..6a04551ff4 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgTest.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/garbage/AbstractModelForDbgTest.java @@ -19,6 +19,7 @@ import static agent.dbgeng.testutil.DummyProc.runProc; import static ghidra.lifecycle.Unfinished.TODO; import static org.junit.Assert.*; +import java.lang.invoke.MethodHandles; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.*; @@ -31,22 +32,17 @@ import agent.dbgeng.dbgeng.DbgEngTest; import agent.dbgeng.model.iface1.DbgModelTargetLauncher; import agent.dbgeng.testutil.DummyProc; import ghidra.async.*; -import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.*; import ghidra.dbg.DebugModelConventions.AllRequiredAccess; -import ghidra.dbg.DebuggerObjectModel; -import ghidra.dbg.attributes.TargetObjectList; import ghidra.dbg.error.DebuggerModelNoSuchPathException; import ghidra.dbg.error.DebuggerModelTypeException; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetConsole.Channel; -import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener; import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; -import ghidra.dbg.target.TargetObject.TargetObjectListener; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.XmlSchemaContext; -import ghidra.dbg.util.AllTargetObjectListenerAdapter; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.Address; import ghidra.test.AbstractGhidraHeadlessIntegrationTest; @@ -623,12 +619,13 @@ public abstract class AbstractModelForDbgTest AtomicReference access = new AtomicReference<>(); AsyncReference lastOut = new AsyncReference<>(); - AllTargetObjectListenerAdapter l = new AllTargetObjectListenerAdapter() { + DebuggerModelListener l = new DebuggerModelListener() { @Override public void consoleOutput(TargetObject interpreter, Channel channel, - String out) { - Msg.debug(this, "Got " + channel + " output: " + out); - lastOut.set(out, null); + byte[] out) { + String str = new String(out); + Msg.debug(this, "Got " + channel + " output: " + str); + lastOut.set(str, null); } }; @@ -663,15 +660,16 @@ public abstract class AbstractModelForDbgTest AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); - AllTargetObjectListenerAdapter l = new AllTargetObjectListenerAdapter() { + DebuggerModelListener l = new DebuggerModelListener() { @Override public void consoleOutput(TargetObject interpreter, Channel channel, String out) { - Msg.debug(this, "Got " + channel + " output: " + out); - if (!out.contains("test")) { + String str = new String(out); + Msg.debug(this, "Got " + channel + " output: " + str); + if (!str.contains("test")) { return; } - offThread.catching(() -> fail("Unexpected output:" + out)); + offThread.catching(() -> fail("Unexpected output:" + str)); } }; @@ -706,7 +704,7 @@ public abstract class AbstractModelForDbgTest AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); - AtomicReference breaks = new AtomicReference<>(); + AtomicReference breaks = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); @@ -721,7 +719,7 @@ public abstract class AbstractModelForDbgTest access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding breakpoint container..."); - DebugModelConventions.findSuitable(TargetBreakpointContainer.class, root.get()) + DebugModelConventions.findSuitable(TargetBreakpointSpecContainer.class, root.get()) .handle(seq::next); }, breaks).then(seq -> { Msg.debug(this, "Got: " + breaks); @@ -744,7 +742,7 @@ public abstract class AbstractModelForDbgTest AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference launcher = new AtomicReference<>(); - AtomicReference breaks = new AtomicReference<>(); + AtomicReference breaks = new AtomicReference<>(); AtomicReference loc = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { @@ -770,13 +768,13 @@ public abstract class AbstractModelForDbgTest .handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Finding breakpoint container..."); - DebugModelConventions.findSuitable(TargetBreakpointContainer.class, root.get()) + DebugModelConventions.findSuitable(TargetBreakpointSpecContainer.class, root.get()) .handle(seq::next); }, breaks).then(seq -> { Msg.debug(this, "Placing breakpoint..."); breaks.get() .placeBreakpoint("0x7ff7d52c8987", - Set.of(TargetBreakpointKind.SOFTWARE)) + Set.of(TargetBreakpointKind.SW_EXECUTE)) .handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting breakpoint specs..."); @@ -794,9 +792,6 @@ public abstract class AbstractModelForDbgTest loc.set(es.iterator().next()); Address addr = loc.get().getAddress(); Msg.debug(this, "Got address: " + addr); - TargetObjectList list = loc.get().getAffects(); - Msg.debug(this, "Got affects: " + list); - assertEquals(1, list.size()); seq.exit(); }).finish()); } @@ -809,7 +804,7 @@ public abstract class AbstractModelForDbgTest AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); - AtomicReference breaks = new AtomicReference<>(); + AtomicReference breaks = new AtomicReference<>(); AtomicReference launcher = new AtomicReference<>(); AtomicReference obj = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { @@ -838,13 +833,13 @@ public abstract class AbstractModelForDbgTest access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding breakpoint container..."); - DebugModelConventions.findSuitable(TargetBreakpointContainer.class, root.get()) + DebugModelConventions.findSuitable(TargetBreakpointSpecContainer.class, root.get()) .handle(seq::next); }, breaks).then(seq -> { Msg.debug(this, "Placing breakpoint..."); breaks.get() .placeBreakpoint("0x7ff7d52c8987", - Set.of(TargetBreakpointKind.SOFTWARE)) + Set.of(TargetBreakpointKind.SW_EXECUTE)) .handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting Process 1..."); @@ -970,20 +965,21 @@ public abstract class AbstractModelForDbgTest AsyncReference, Void> focusProcPath = new AsyncReference<>(); AsyncReference processCount = new AsyncReference<>(); - TargetObjectListener procListener = new TargetObjectListener() { + DebuggerModelListener procListener = new DebuggerModelListener() { @Override public void elementsChanged(TargetObject parent, Collection removed, Map added) { processCount.set(processes.get().getCachedElements().size(), null); } }; - TargetFocusScopeListener focusListener = new TargetFocusScopeListener() { - @Override - public void focusChanged(TargetFocusScope object, TargetObject focused) { - // Truncate the path to the parent process - focusProcPath.set(focused.getPath().subList(0, 2), null); - } - }; + DebuggerModelListener focusListener = + new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + @AttributeCallback(TargetFocusScope.FOCUS_ATTRIBUTE_NAME) + public void focusChanged(TargetObject object, TargetObject focused) { + // Truncate the path to the parent process + focusProcPath.set(focused.getPath().subList(0, 2), null); + } + }; waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/GadpForDbgTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/garbage/GadpForDbgTest.java similarity index 100% rename from Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/GadpForDbgTest.java rename to Ghidra/Debug/Debugger-agent-dbgeng/garbage/GadpForDbgTest.java diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/ModelForDbgTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/garbage/ModelForDbgTest.java similarity index 100% rename from Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/ModelForDbgTest.java rename to Ghidra/Debug/Debugger-agent-dbgeng/garbage/ModelForDbgTest.java diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/DbgEngInJvmDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/DbgEngInJvmDebuggerModelFactory.java index bc7b058fb5..676275268b 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/DbgEngInJvmDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/DbgEngInJvmDebuggerModelFactory.java @@ -24,16 +24,18 @@ import ghidra.dbg.util.ConfigurableFactory.FactoryDescription; import ghidra.util.classfinder.ExtensionPointProperties; /** - * Note this is in the testing source because it's not meant to be shipped in the release.... That - * may change if it proves stable, though, no? + * Note this is in the testing source because it's not meant to be shipped in + * the release.... That may change if it proves stable, though, no? */ @FactoryDescription( // - brief = "IN-VM MS dbgeng local debugger", // - htmlDetails = "Launch a dbgeng session in this same JVM" // + brief = "IN-VM MS dbgeng local debugger", // + htmlDetails = "Launch a dbgeng session in this same JVM" // ) @ExtensionPointProperties(priority = 80) public class DbgEngInJvmDebuggerModelFactory implements LocalDebuggerModelFactory { + // TODO remoteTransport option? + @Override public CompletableFuture build() { DbgModelImpl model = new DbgModelImpl(); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugBreakpoint.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugBreakpoint.java index a2e17b418e..8f02d92262 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugBreakpoint.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugBreakpoint.java @@ -108,10 +108,23 @@ public interface DebugBreakpoint { void setFlags(BreakFlags... flags); - long getOffset(); + /** + * Get the location on target that triggers the breakpoint + * + *

+ * If the breakpoint is deferred, this will return {@code null}. In that case, use + * {@link #getOffsetExpression()}. + * + * @return the offset, or {@code null} + */ + Long getOffset(); void setOffset(long offset); + String getOffsetExpression(); + + void setOffsetExpression(String expression); + BreakDataParameters getDataParameters(); void setDataParameters(BreakDataParameters params); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugControl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugControl.java index 25dc3acbf0..125dee70f6 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugControl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugControl.java @@ -254,6 +254,7 @@ public interface DebugControl extends DebugControlReentrant { /** * Shortcut to retrieve all breakpoints for the current process. * + *

* Uses {@link #getNumberBreakpoints()} and {@link #getBreakpointByIndex(int)} to enumerate all * breakpoints for the current process. * @@ -287,10 +288,54 @@ public interface DebugControl extends DebugControlReentrant { */ DebugBreakpoint getBreakpointById(int id); + /** + * Add a (resolved) breakpoint with the given type and desired id + * + *

+ * This is equivalent, in part, to the {@code bp} command. + * + * @param type the type + * @param desiredId the desired id + * @return the breakpoint, disabled at offset 0 + */ DebugBreakpoint addBreakpoint(BreakType type, int desiredId); + /** + * Add a (resolved) breakpoint with the given type and any id + * + *

+ * This is equivalent, in part, to the {@code bp} command. + * + * @param type the type + * @return the breakpoint, disable at offset 0 + */ DebugBreakpoint addBreakpoint(BreakType type); + /** + * Add an unresolved breakpoint with the given type and desired id + * + *

+ * This is equivalent, in part, to the {@code bu} command. See the MSDN for a comparison of + * {@code bu} and {@code bp}. + * + * @param type the type + * @param desiredId the desired id + * @return the breakpoint, disabled at offset 0 + */ + DebugBreakpoint addBreakpoint2(BreakType type, int desiredId); + + /** + * Add an unresolved breakpoint with the given type and any id + * + *

+ * This is equivalent, in part, to the {@code bu} command. See the MSDN for a comparison of + * {@code bu} and {@code bp}. + * + * @param desiredId the desired id + * @return the breakpoint, disabled at offset 0 + */ + DebugBreakpoint addBreakpoint2(BreakType type); + void waitForEvent(int timeout); DebugEventInformation getLastEventInformation(); @@ -311,5 +356,4 @@ public interface DebugControl extends DebugControlReentrant { int getExecutingProcessorType(); int getDebuggeeType(); - } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugModuleInfo.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugModuleInfo.java index a0b378fe49..58973e5813 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugModuleInfo.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugModuleInfo.java @@ -26,18 +26,18 @@ public class DebugModuleInfo { public final long imageFileHandle; public final long baseOffset; public final int moduleSize; - public final String moduleName; - public final String imageName; public final int checkSum; public final int timeDateStamp; + private String moduleName; + private String imageName; public DebugModuleInfo(long imageFileHandle, long baseOffset, int moduleSize, String moduleName, String imageName, int checkSum, int timeDateStamp) { this.imageFileHandle = imageFileHandle; this.baseOffset = baseOffset; this.moduleSize = moduleSize; - this.moduleName = moduleName; - this.imageName = imageName; + this.setModuleName(moduleName); + this.setImageName(imageName); this.checkSum = checkSum; this.timeDateStamp = timeDateStamp; // TODO: Convert to DateTime? } @@ -45,4 +45,20 @@ public class DebugModuleInfo { public String toString() { return Long.toHexString(baseOffset); } + + public String getModuleName() { + return moduleName; + } + + public void setModuleName(String moduleName) { + this.moduleName = moduleName; + } + + public String getImageName() { + return imageName; + } + + public void setImageName(String imageName) { + this.imageName = imageName; + } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugRegisters.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugRegisters.java index 717f449134..6574f72987 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugRegisters.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/dbgeng/DebugRegisters.java @@ -134,7 +134,7 @@ public interface DebugRegisters { */ default DebugValue getValueByName(String name) { int indexByName = getIndexByName(name); - if (indexByName > 0) { + if (indexByName >= 0) { return getValue(indexByName); } return null; diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/gadp/impl/AbstractClientThreadExecutor.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/gadp/impl/AbstractClientThreadExecutor.java index 324f7e3330..24cc191006 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/gadp/impl/AbstractClientThreadExecutor.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/gadp/impl/AbstractClientThreadExecutor.java @@ -31,6 +31,7 @@ import ghidra.util.Msg; /** * A single-threaded executor which creates and exclusively accesses the {@code dbgeng.dll} client. * + *

* The executor also has a priority mechanism, so that callbacks may register follow-on handlers * which take precedence over other tasks in the queue (which could trigger additional callbacks). * This is required since certain operation are not allowed during normal callback processing. For @@ -102,6 +103,7 @@ public abstract class AbstractClientThreadExecutor extends AbstractExecutorServi * we can always create a new thread and client, using the existing client's reentrant * methods. * + *

* As stated in the MSDN, this thread repeatedly calls {@code DispatchEvents} in order to * receive callbacks regarding events caused by other clients. If, however, an wait is * registered, or the current engine state indicates that a wait is proper, the thread calls @@ -196,6 +198,7 @@ public abstract class AbstractClientThreadExecutor extends AbstractExecutorServi /** * Schedule a task with a given priority. * + *

* Smaller priority values indicate earlier execution. The default priority is * {@link #DEFAULT_PRIORITY}. * @@ -224,6 +227,7 @@ public abstract class AbstractClientThreadExecutor extends AbstractExecutorServi /** * Schedule a task with the given priority, taking a reference to the client. * + *

* This is a convenience which spares a call to {@link #getClient()}. See * {@link #execute(int, Runnable)} about priority. * diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/breakpoint/DebugBreakpointImpl1.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/breakpoint/DebugBreakpointImpl1.java index 0e3b288d26..85d14959d3 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/breakpoint/DebugBreakpointImpl1.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/breakpoint/DebugBreakpointImpl1.java @@ -15,13 +15,16 @@ */ package agent.dbgeng.impl.dbgeng.breakpoint; +import com.sun.jna.Native; +import com.sun.jna.platform.win32.Kernel32; import com.sun.jna.platform.win32.WinDef.*; +import com.sun.jna.platform.win32.WinNT.HRESULT; import com.sun.jna.platform.win32.COM.COMUtils; import com.sun.jna.ptr.PointerByReference; import agent.dbgeng.dbgeng.DbgEng; -import agent.dbgeng.dbgeng.DebugClient; import agent.dbgeng.dbgeng.DbgEng.OpaqueCleanable; +import agent.dbgeng.dbgeng.DebugClient; import agent.dbgeng.impl.dbgeng.client.DebugClientInternal; import agent.dbgeng.impl.dbgeng.control.DebugControlInternal; import agent.dbgeng.jna.dbgeng.WinNTExtra.Machine; @@ -124,9 +127,14 @@ public class DebugBreakpointImpl1 implements DebugBreakpointInternal { } @Override - public long getOffset() { + public Long getOffset() { ULONGLONGByReference pullOffset = new ULONGLONGByReference(); - COMUtils.checkRC(jnaBreakpoint.GetOffset(pullOffset)); + HRESULT getOffset = jnaBreakpoint.GetOffset(pullOffset); + if (getOffset.longValue() == Kernel32.E_NOINTERFACE) { + // Per MSDN, this means the placement is deferred + return null; + } + COMUtils.checkRC(getOffset); return pullOffset.getValue().longValue(); } @@ -136,6 +144,21 @@ public class DebugBreakpointImpl1 implements DebugBreakpointInternal { COMUtils.checkRC(jnaBreakpoint.SetOffset(ullOffset)); } + @Override + public String getOffsetExpression() { + ULONGByReference pulExpressionSize = new ULONGByReference(); + COMUtils.checkRC(jnaBreakpoint.GetOffsetExpression(null, new ULONG(0), pulExpressionSize)); + byte[] buffer = new byte[pulExpressionSize.getValue().intValue()]; + COMUtils.checkRC( + jnaBreakpoint.GetOffsetExpression(buffer, pulExpressionSize.getValue(), null)); + return Native.toString(buffer); + } + + @Override + public void setOffsetExpression(String expression) { + COMUtils.checkRC(jnaBreakpoint.SetOffsetExpression(expression)); + } + @Override public BreakDataParameters getDataParameters() { ULONGByReference pulSize = new ULONGByReference(); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/breakpoint/DebugBreakpointImpl2.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/breakpoint/DebugBreakpointImpl2.java index eb8e3de7f6..f24d5f2899 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/breakpoint/DebugBreakpointImpl2.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/breakpoint/DebugBreakpointImpl2.java @@ -15,6 +15,12 @@ */ package agent.dbgeng.impl.dbgeng.breakpoint; +import com.sun.jna.Native; +import com.sun.jna.WString; +import com.sun.jna.platform.win32.WinDef.ULONG; +import com.sun.jna.platform.win32.WinDef.ULONGByReference; +import com.sun.jna.platform.win32.COM.COMUtils; + import agent.dbgeng.jna.dbgeng.breakpoint.IDebugBreakpoint2; public class DebugBreakpointImpl2 extends DebugBreakpointImpl1 { @@ -25,4 +31,20 @@ public class DebugBreakpointImpl2 extends DebugBreakpointImpl1 { super(jnaBreakpoint); this.jnaBreakpoint = jnaBreakpoint; } + + @Override + public String getOffsetExpression() { + ULONGByReference pulExpressionSize = new ULONGByReference(); + COMUtils.checkRC( + jnaBreakpoint.GetOffsetExpressionWide(null, new ULONG(0), pulExpressionSize)); + char[] buffer = new char[pulExpressionSize.getValue().intValue()]; + COMUtils.checkRC( + jnaBreakpoint.GetOffsetExpressionWide(buffer, pulExpressionSize.getValue(), null)); + return Native.toString(buffer); + } + + @Override + public void setOffsetExpression(String expression) { + COMUtils.checkRC(jnaBreakpoint.SetOffsetExpressionWide(new WString(expression))); + } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/control/DebugControlImpl1.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/control/DebugControlImpl1.java index 36b3c0d97b..ac8936f9e2 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/control/DebugControlImpl1.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/control/DebugControlImpl1.java @@ -15,6 +15,8 @@ */ package agent.dbgeng.impl.dbgeng.control; +import javax.help.UnsupportedOperationException; + import com.sun.jna.Native; import com.sun.jna.platform.win32.WinDef.*; import com.sun.jna.platform.win32.WinError; @@ -211,6 +213,16 @@ public class DebugControlImpl1 implements DebugControlInternal { return doAddBreakpoint(type, DbgEngUtil.DEBUG_ANY_ID); } + @Override + public DebugBreakpoint addBreakpoint2(BreakType type) { + throw new UnsupportedOperationException(); + } + + @Override + public DebugBreakpoint addBreakpoint2(BreakType type, int desiredId) { + throw new UnsupportedOperationException(); + } + @Override public void removeBreakpoint(IDebugBreakpoint comBpt) { COMUtils.checkRC(jnaControl.RemoveBreakpoint(comBpt)); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/control/DebugControlImpl4.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/control/DebugControlImpl4.java index 50da82b8e5..30ddba27e2 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/control/DebugControlImpl4.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/impl/dbgeng/control/DebugControlImpl4.java @@ -19,13 +19,18 @@ import com.sun.jna.Native; import com.sun.jna.WString; import com.sun.jna.platform.win32.WinDef.ULONG; import com.sun.jna.platform.win32.WinDef.ULONGByReference; - -import agent.dbgeng.dbgeng.DebugValue.DebugValueType; -import agent.dbgeng.jna.dbgeng.DbgEngNative.DEBUG_VALUE; -import agent.dbgeng.jna.dbgeng.control.IDebugControl4; - import com.sun.jna.platform.win32.COM.COMUtils; +import com.sun.jna.ptr.PointerByReference; +import agent.dbgeng.dbgeng.DebugBreakpoint; +import agent.dbgeng.dbgeng.DebugBreakpoint.BreakType; +import agent.dbgeng.dbgeng.DebugValue.DebugValueType; +import agent.dbgeng.impl.dbgeng.DbgEngUtil; +import agent.dbgeng.impl.dbgeng.breakpoint.DebugBreakpointInternal; +import agent.dbgeng.jna.dbgeng.DbgEngNative.DEBUG_VALUE; +import agent.dbgeng.jna.dbgeng.breakpoint.IDebugBreakpoint; +import agent.dbgeng.jna.dbgeng.breakpoint.WrapIDebugBreakpoint; +import agent.dbgeng.jna.dbgeng.control.IDebugControl4; import ghidra.comm.util.BitmaskSet; public class DebugControlImpl4 extends DebugControlImpl3 { @@ -92,4 +97,26 @@ public class DebugControlImpl4 extends DebugControlImpl3 { public void returnInput(String input) { COMUtils.checkRC(jnaControl.ReturnInputWide(new WString(input))); } + + public DebugBreakpoint doAddBreakpoint2(BreakType type, ULONG ulDesiredId) { + ULONG ulType = new ULONG(type.ordinal()); + PointerByReference ppBp = new PointerByReference(); + COMUtils.checkRC(jnaControl.AddBreakpoint2(ulType, ulDesiredId, ppBp)); + IDebugBreakpoint Bp = new WrapIDebugBreakpoint(ppBp.getValue()); + DebugBreakpoint bpt = + DebugBreakpointInternal.tryPreferredInterfaces(this, Bp::QueryInterface); + // AddRef or no? Probably not. + return bpt; + } + + @Override + public DebugBreakpoint addBreakpoint2(BreakType type, int desiredId) { + return doAddBreakpoint2(type, new ULONG(desiredId)); + } + + @Override + public DebugBreakpoint addBreakpoint2(BreakType type) { + return doAddBreakpoint2(type, DbgEngUtil.DEBUG_ANY_ID); + } + } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/breakpoint/WrapIDebugBreakpoint2.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/breakpoint/WrapIDebugBreakpoint2.java index a43f456ae9..e68cd80426 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/breakpoint/WrapIDebugBreakpoint2.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/breakpoint/WrapIDebugBreakpoint2.java @@ -51,6 +51,6 @@ public class WrapIDebugBreakpoint2 extends WrapIDebugBreakpoint implements IDebu @Override public HRESULT SetOffsetExpressionWide(WString Expression) { - return _invokeHR(VTIndices2.SET_COMMAND_WIDE, getPointer(), Expression); + return _invokeHR(VTIndices2.SET_OFFSET_EXPRESSION_WIDE, getPointer(), Expression); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/control/IDebugControl4.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/control/IDebugControl4.java index 48e3b54e1a..255e24d059 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/control/IDebugControl4.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/control/IDebugControl4.java @@ -20,6 +20,7 @@ import com.sun.jna.platform.win32.Guid.IID; import com.sun.jna.platform.win32.WinDef.ULONG; import com.sun.jna.platform.win32.WinDef.ULONGByReference; import com.sun.jna.platform.win32.WinNT.HRESULT; +import com.sun.jna.ptr.PointerByReference; import agent.dbgeng.jna.dbgeng.DbgEngNative.DEBUG_VALUE; import agent.dbgeng.jna.dbgeng.UnknownWithUtils.VTableIndex; @@ -91,6 +92,8 @@ public interface IDebugControl4 extends IDebugControl3 { } } + HRESULT AddBreakpoint2(ULONG Type, ULONG DesiredId, PointerByReference Bp); + HRESULT ReturnInputWide(WString Buffer); HRESULT OutputWide(ULONG Mask, WString Format, Object... objects); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/control/WrapIDebugControl4.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/control/WrapIDebugControl4.java index 7446371a3b..9526616cda 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/control/WrapIDebugControl4.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/jna/dbgeng/control/WrapIDebugControl4.java @@ -21,6 +21,7 @@ import com.sun.jna.*; import com.sun.jna.platform.win32.WinDef.ULONG; import com.sun.jna.platform.win32.WinDef.ULONGByReference; import com.sun.jna.platform.win32.WinNT.HRESULT; +import com.sun.jna.ptr.PointerByReference; import agent.dbgeng.jna.dbgeng.DbgEngNative.DEBUG_VALUE; @@ -35,6 +36,11 @@ public class WrapIDebugControl4 extends WrapIDebugControl3 implements IDebugCont super(pvInstance); } + @Override + public HRESULT AddBreakpoint2(ULONG Type, ULONG DesiredId, PointerByReference Bp) { + return _invokeHR(VTIndices4.ADD_BREAKPOINT2, getPointer(), Type, DesiredId, Bp); + } + @Override public HRESULT ReturnInputWide(WString Buffer) { return _invokeHR(VTIndices4.RETURN_INPUT_WIDE, getPointer(), Buffer); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgReason.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgReason.java index 9a8e5bbf27..2776874605 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgReason.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgReason.java @@ -32,8 +32,7 @@ public interface DbgReason { @Override public String desc() { - // TODO Auto-generated method stub - return null; + return "Unknown"; } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgState.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgState.java index 179fa6dafe..745b935099 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgState.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/DbgState.java @@ -74,6 +74,15 @@ public enum DbgState { public boolean isAlive() { return false; } + }, + /** + * Dbg or the process has exited + */ + SESSION_EXIT { + @Override + public boolean isAlive() { + return false; + } }; public abstract boolean isAlive(); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/breakpoint/DbgBreakpointInfo.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/breakpoint/DbgBreakpointInfo.java index 1ac4e7fe7c..6fdb5b8625 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/breakpoint/DbgBreakpointInfo.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/breakpoint/DbgBreakpointInfo.java @@ -15,11 +15,10 @@ */ package agent.dbgeng.manager.breakpoint; -import java.util.*; +import java.util.Objects; import agent.dbgeng.dbgeng.DebugBreakpoint; import agent.dbgeng.dbgeng.DebugBreakpoint.*; -import agent.dbgeng.dbgeng.DebugProcessId; import agent.dbgeng.manager.DbgProcess; import agent.dbgeng.manager.DbgThread; import ghidra.comm.util.BitmaskSet; @@ -39,8 +38,9 @@ public class DbgBreakpointInfo { private final long number; private boolean enabled; - private final String location; - private final List locations; + private Long offset; + private String expression; + //private final List locations; /** * Construct Dbg breakpoint information @@ -64,28 +64,28 @@ public class DbgBreakpointInfo { } public DbgBreakpointInfo(DebugBreakpoint bp, DbgProcess process, DbgThread thread) { - this.bpt = bp; + this.setBreakpoint(bp); this.proc = process; this.eventThread = thread; this.number = bpt.getId(); this.bptType = bpt.getType(); this.flags = bpt.getFlags(); - //this.parameters = bpt.getDataParameters(); - //this.access = parameters.access; - //this.size = parameters.size; - this.location = Long.toHexString(bpt.getOffset()); - List locs = new ArrayList<>(); - List ids = new ArrayList<>(); - ids.add(proc.getId()); - locs.add(new DbgBreakpointLocation(bpt.getId(), 1, true, location, ids)); - this.locations = Collections.unmodifiableList(locs); + if (bpt.getType().breakType.equals(BreakType.DATA)) { + this.parameters = bpt.getDataParameters(); + } + this.access = parameters.access; + this.size = parameters.size; + this.offset = bpt.getOffset(); + this.expression = bpt.getOffsetExpression(); } @Override public int hashCode() { - return Objects.hash(number, bptType, getFlags(), location, enabled, access, size); + return Objects.hash(number, bptType, getFlags(), /*location,*/ enabled, access, getSize(), + offset, expression); } + @Override public String toString() { return Integer.toHexString(bpt.getId()); } @@ -110,10 +110,16 @@ public class DbgBreakpointInfo { if (this.getFlags() != that.getFlags()) { return false; } - if (this.size != that.size) { + if (this.getSize() != that.getSize()) { return false; } - if (!Objects.equals(this.location, that.location)) { + /*if (!Objects.equals(this.location, that.location)) { + return false; + }*/ + if (!Objects.equals(this.expression, that.expression)) { + return false; + } + if (!Objects.equals(this.offset, that.offset)) { return false; } if (this.enabled != that.enabled) { @@ -173,12 +179,12 @@ public class DbgBreakpointInfo { } /** - * Get the location of the breakpoint + * Get the offset expression of the breakpoint * * @return the location */ - public String getLocation() { - return location; + public String getExpression() { + return expression; } /** @@ -200,12 +206,16 @@ public class DbgBreakpointInfo { } /** - * Assuming the location is an address, get it as a long + * Get the offset of this breakpoint * - * @return the address + *

+ * Note if the offset was given as an expression, but it hasn't been resolved, this will return + * {@code null}. + * + * @return the offset, or {@code null} */ - public long addrAsLong() { - return Long.parseUnsignedLong(location, 16); + public Long getOffset() { + return offset; } /** @@ -238,15 +248,16 @@ public class DbgBreakpointInfo { /** * Get a list of resolved addresses * + *

* The effective locations may change for a variety of reasons. Most notable, a new module may * be loaded, having location(s) that match the desired location of this breakpoint. The binary * addresses within will become new effective locations of this breakpoint. * * @return the list of locations at the time the breakpoint information was captured */ - public List getLocations() { + /*public List getLocations() { return locations; - } + }*/ public DbgBreakpointInfo withEnabled(@SuppressWarnings("hiding") boolean enabled) { if (isEnabled() == enabled) { @@ -271,7 +282,20 @@ public class DbgBreakpointInfo { return eventThread; } - public long getAddressAsLong() { - return locations.get(0).addrAsLong(); + public void setBreakpoint(DebugBreakpoint bpt) { + this.bpt = bpt; + this.bptType = bpt.getType(); + this.flags = bpt.getFlags(); + this.offset = bpt.getOffset(); + this.expression = bpt.getOffsetExpression(); + if (bptType.breakType.equals(BreakType.DATA)) { + BreakDataParameters p = bpt.getDataParameters(); + this.access = p.access; + this.size = p.size; + } } + + /*public long getAddressAsLong() { + return locations.get(0).addrAsLong(); + }*/ // getOffset instead } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgDetachCommand.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgDetachCommand.java index 28f00427b8..43fbad9c24 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgDetachCommand.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgDetachCommand.java @@ -19,10 +19,8 @@ import java.util.ArrayList; import java.util.Collection; import agent.dbgeng.dbgeng.DebugClient; -import agent.dbgeng.manager.DbgEvent; +import agent.dbgeng.manager.DbgCause; import agent.dbgeng.manager.DbgProcess; -import agent.dbgeng.manager.evt.AbstractDbgCompletedCommandEvent; -import agent.dbgeng.manager.evt.DbgThreadExitedEvent; import agent.dbgeng.manager.impl.*; /** @@ -44,6 +42,7 @@ public class DbgDetachCommand extends AbstractDbgCommand { manager.fireThreadExited(t.getId(), process, pending); t.remove(); } + manager.getEventListeners().fire.processRemoved(process.getId(), DbgCause.Causes.UNCLAIMED); return null; } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgInsertBreakpointCommand.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgInsertBreakpointCommand.java index 7019ee6863..be91d29556 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgInsertBreakpointCommand.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgInsertBreakpointCommand.java @@ -15,11 +15,9 @@ */ package agent.dbgeng.manager.cmd; -import java.util.ArrayList; -import java.util.List; - -import agent.dbgeng.dbgeng.*; +import agent.dbgeng.dbgeng.DebugBreakpoint; import agent.dbgeng.dbgeng.DebugBreakpoint.*; +import agent.dbgeng.dbgeng.DebugControl; import agent.dbgeng.manager.breakpoint.*; import agent.dbgeng.manager.impl.DbgManagerImpl; import ghidra.comm.util.BitmaskSet; @@ -28,41 +26,33 @@ import ghidra.comm.util.BitmaskSet; * Implementation of {@link DbgBreakpointInsertions#insertBreakpoint(String)} */ public class DbgInsertBreakpointCommand extends AbstractDbgCommand { - private List locations; + //private List locations; private final DbgBreakpointType type; private DbgBreakpointInfo bkpt; private int len; + private final String expression; + private final Long loc; public DbgInsertBreakpointCommand(DbgManagerImpl manager, String expression, DbgBreakpointType type) { super(manager); - locations = new ArrayList<>(); - DebugSymbols symbols = manager.getSymbols(); - List ids = symbols.getSymbolIdsByName(expression); - if (ids.isEmpty()) { - locations.add(Long.decode(expression)); - } - else { - for (DebugSymbolId id : ids) { - DebugSymbolEntry entry = symbols.getSymbolEntry(id); - locations.add(entry.offset); - } - } this.type = type; + this.expression = expression; + this.loc = null; } public DbgInsertBreakpointCommand(DbgManagerImpl manager, long loc, int len, DbgBreakpointType type) { super(manager); - locations = new ArrayList<>(); - locations.add(loc); this.len = len; this.type = type; + this.expression = null; + this.loc = loc; } @Override public DbgBreakpointInfo complete(DbgPendingCommand pending) { - manager.doBreakpointCreated(bkpt, pending); + //manager.doBreakpointCreated(bkpt, pending); return bkpt; } @@ -73,8 +63,8 @@ public class DbgInsertBreakpointCommand extends AbstractDbgCommand access = BitmaskSet.of(BreakAccess.EXECUTE); if (type.equals(DbgBreakpointType.ACCESS_WATCHPOINT)) { @@ -92,9 +82,14 @@ public class DbgInsertBreakpointCommand extends AbstractDbgCommand(cur)) { if (updatedThreadIds.contains(id)) { diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgListModulesCommand.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgListModulesCommand.java index 5f00478ad9..3b435cc0da 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgListModulesCommand.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgListModulesCommand.java @@ -18,6 +18,7 @@ package agent.dbgeng.manager.cmd; import java.util.*; import agent.dbgeng.dbgeng.*; +import agent.dbgeng.dbgeng.DebugModule.DebugModuleName; import agent.dbgeng.manager.DbgModule; import agent.dbgeng.manager.impl.*; import ghidra.util.Msg; @@ -62,6 +63,10 @@ public class DbgListModulesCommand extends AbstractDbgCommand> { + protected final DbgProcessImpl process; + protected final DbgModuleImpl module; + + private Map symbolEntries = new HashMap<>(); + + public DbgListSymbolsCommand(DbgManagerImpl manager, DbgProcessImpl process, + DbgModuleImpl module) { + super(manager); + this.process = process; + this.module = module; + } + + @Override + public Map complete(DbgPendingCommand pending) { + Map symbolMap = new HashMap<>(); + for (Entry entry : symbolEntries.entrySet()) { + DebugSymbolEntry value = entry.getValue(); + DbgMinimalSymbol minSymbol = new DbgMinimalSymbol(entry.getKey().symbolIndex, + value.typeId, value.name, value.offset, value.size, value.tag, value.moduleBase); + symbolMap.put(entry.getKey().toString(), minSymbol); + } + return symbolMap; + } + + @Override + public void invoke() { + DebugSystemObjects so = manager.getSystemObjects(); + so.setCurrentProcessId(process.getId()); + DebugSymbols symbols = manager.getSymbols(); + + for (DebugSymbolName symbol : symbols.iterateSymbolMatches(module.getName() + "!*")) { + List symbolIdsByName = symbols.getSymbolIdsByName(symbol.name); + for (DebugSymbolId symbolId : symbolIdsByName) { + DebugSymbolEntry symbolEntry = symbols.getSymbolEntry(symbolId); + symbolEntries.put(symbolId, symbolEntry); + } + } + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgListThreadsCommand.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgListThreadsCommand.java index 8087c2645d..73c84bdd74 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgListThreadsCommand.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/cmd/DbgListThreadsCommand.java @@ -20,7 +20,8 @@ import java.util.*; import agent.dbgeng.dbgeng.DebugSystemObjects; import agent.dbgeng.dbgeng.DebugThreadId; import agent.dbgeng.manager.DbgThread; -import agent.dbgeng.manager.impl.*; +import agent.dbgeng.manager.impl.DbgManagerImpl; +import agent.dbgeng.manager.impl.DbgProcessImpl; import ghidra.util.Msg; public class DbgListThreadsCommand extends AbstractDbgCommand> { @@ -45,8 +46,7 @@ public class DbgListThreadsCommand extends AbstractDbgCommand(cur)) { if (updatedThreadIds.contains(id)) { diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgDebugEventCallbacksAdapter.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgDebugEventCallbacksAdapter.java index 4bcb83f365..9e93a3a209 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgDebugEventCallbacksAdapter.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgDebugEventCallbacksAdapter.java @@ -34,28 +34,35 @@ public class DbgDebugEventCallbacksAdapter extends DebugEventCallbacksAdapter { this.manager = manager; } + protected DebugStatus checkInterrupt(DebugStatus normal) { + if (manager.getControl().getInterrupt()) { + return DebugStatus.BREAK; + } + return normal; + } + @Override public DebugStatus breakpoint(DebugBreakpoint bp) { Msg.info(this, "***Breakpoint: " + bp.getId()); - return manager.processEvent(new DbgBreakpointEvent(bp)); + return checkInterrupt(manager.processEvent(new DbgBreakpointEvent(bp))); } @Override public DebugStatus exception(DebugExceptionRecord64 exception, boolean firstChance) { Msg.info(this, "***Exception: " + exception + ", first=" + firstChance); - return manager.processEvent(new DbgExceptionEvent(exception)); + return checkInterrupt(manager.processEvent(new DbgExceptionEvent(exception))); } @Override public DebugStatus createThread(DebugThreadInfo threadInfo) { Msg.info(this, "***Thread created: " + Long.toHexString(threadInfo.handle)); - return manager.processEvent(new DbgThreadCreatedEvent(threadInfo)); + return checkInterrupt(manager.processEvent(new DbgThreadCreatedEvent(threadInfo))); } @Override public DebugStatus exitThread(int exitCode) { Msg.info(this, "***Thread exited: " + exitCode); - return manager.processEvent(new DbgThreadExitedEvent(exitCode)); + return checkInterrupt(manager.processEvent(new DbgThreadExitedEvent(exitCode))); } @Override @@ -63,20 +70,20 @@ public class DbgDebugEventCallbacksAdapter extends DebugEventCallbacksAdapter { Msg.info(this, "***Process created: " + Long.toHexString(processInfo.handle)); Msg.info(this, " **Thread created: " + Long.toHexString(processInfo.initialThreadInfo.handle)); - return manager.processEvent(new DbgProcessCreatedEvent(processInfo)); + return checkInterrupt(manager.processEvent(new DbgProcessCreatedEvent(processInfo))); } @Override public DebugStatus exitProcess(int exitCode) { Msg.info(this, "***Process exited: " + exitCode); Msg.info(this, " **Thread exited"); - return manager.processEvent(new DbgProcessExitedEvent(exitCode)); + return checkInterrupt(manager.processEvent(new DbgProcessExitedEvent(exitCode))); } @Override public DebugStatus loadModule(DebugModuleInfo moduleInfo) { Msg.info(this, "***Module Loaded: " + moduleInfo); - return manager.processEvent(new DbgModuleLoadedEvent(moduleInfo)); + return checkInterrupt(manager.processEvent(new DbgModuleLoadedEvent(moduleInfo))); } @Override @@ -85,7 +92,7 @@ public class DbgDebugEventCallbacksAdapter extends DebugEventCallbacksAdapter { "***Module Unloaded: " + imageBaseName + ", " + Long.toHexString(baseOffset)); DebugModuleInfo info = new DebugModuleInfo(0L, baseOffset, 0, basename(imageBaseName), imageBaseName, 0, 0); - return manager.processEvent(new DbgModuleUnloadedEvent(info)); + return checkInterrupt(manager.processEvent(new DbgModuleUnloadedEvent(info))); } private String basename(String path) { @@ -103,27 +110,27 @@ public class DbgDebugEventCallbacksAdapter extends DebugEventCallbacksAdapter { DebugStatus status = DebugStatus.fromArgument(argument); Msg.info(this, "***ExecutionStatus: " + status); if (status.equals(DebugStatus.NO_DEBUGGEE)) { - event.setState(DbgState.EXIT); + event.setState(DbgState.SESSION_EXIT); } - return manager.processEvent(event); + return checkInterrupt(manager.processEvent(event)); } if (flags.contains(ChangeEngineState.BREAKPOINTS)) { Msg.info(this, "***BreakpointChanged: " + flags + ", " + argument + " on " + Thread.currentThread()); - return manager.processEvent(event); + return checkInterrupt(manager.processEvent(event)); } if (flags.contains(ChangeEngineState.CURRENT_THREAD)) { Msg.info(this, "***CurrentThread: " + argument); if (argument < 0) { - return manager.processEvent(event); + return checkInterrupt(manager.processEvent(event)); } } if (flags.contains(ChangeEngineState.SYSTEMS)) { Msg.info(this, "***Systems: " + argument); event.setState(DbgState.RUNNING); - return manager.processEvent(event); + return checkInterrupt(manager.processEvent(event)); } - return DebugStatus.NO_CHANGE; + return checkInterrupt(DebugStatus.NO_CHANGE); } //@Override diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgManagerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgManagerImpl.java index a72a0574eb..1bd9bcd68f 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgManagerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgManagerImpl.java @@ -15,7 +15,7 @@ */ package agent.dbgeng.manager.impl; -import static ghidra.async.AsyncUtils.sequence; +import static ghidra.async.AsyncUtils.*; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -34,6 +34,7 @@ import agent.dbgeng.dbgeng.DebugControl.DebugInterrupt; import agent.dbgeng.gadp.impl.AbstractClientThreadExecutor; import agent.dbgeng.gadp.impl.DbgEngClientThreadExecutor; import agent.dbgeng.impl.dbgeng.DbgEngUtil; +import agent.dbgeng.jna.dbgeng.WinNTExtra; import agent.dbgeng.manager.*; import agent.dbgeng.manager.DbgCause.Causes; import agent.dbgeng.manager.breakpoint.DbgBreakpointInfo; @@ -42,7 +43,6 @@ import agent.dbgeng.manager.cmd.*; import agent.dbgeng.manager.evt.*; import agent.dbgeng.model.iface1.DbgModelTargetFocusScope; import ghidra.async.*; -import ghidra.async.seq.AsyncSequenceHandlerForRunner; import ghidra.comm.util.BitmaskSet; import ghidra.dbg.target.TargetObject; import ghidra.dbg.util.HandlerMap; @@ -237,7 +237,7 @@ public class DbgManagerImpl implements DbgManager { public DbgSessionImpl getSessionComputeIfAbsent(DebugSessionId id) { synchronized (sessions) { - if (!sessions.containsKey(id)) { + if (!sessions.containsKey(id) && id.id >= 0) { DbgSessionImpl session = new DbgSessionImpl(this, id); session.add(); } @@ -425,19 +425,17 @@ public class DbgManagerImpl implements DbgManager { //} if (engThread.isCurrentThread()) { - sequence(TypeSpec.VOID).then((seq) -> { - addCommand(cmd, pcmd, seq); - seq.exit(); - }).finish().exceptionally((exc) -> { + try { + addCommand(cmd, pcmd); + } + catch (Throwable exc) { pcmd.completeExceptionally(exc); - return null; - }); + } } else { - sequence(TypeSpec.VOID).then(engThread, (seq) -> { - addCommand(cmd, pcmd, seq); - seq.exit(); - }).finish().exceptionally((exc) -> { + CompletableFuture.runAsync(() -> { + addCommand(cmd, pcmd); + }, engThread).exceptionally((exc) -> { pcmd.completeExceptionally(exc); return null; }); @@ -445,8 +443,7 @@ public class DbgManagerImpl implements DbgManager { return pcmd; } - private void addCommand(DbgCommand cmd, DbgPendingCommand pcmd, - AsyncSequenceHandlerForRunner seq) { + private void addCommand(DbgCommand cmd, DbgPendingCommand pcmd) { synchronized (this) { if (!cmd.validInState(state.get())) { throw new DbgCommandError("Command " + cmd + " is not valid while " + state.get()); @@ -519,7 +516,7 @@ public class DbgManagerImpl implements DbgManager { synchronized (this) { boolean waitState = isWaiting(); waiting = false; - DebugStatus ret = handlerMap.handle(evt, null); + DebugStatus ret = evt.isStolen() ? null : handlerMap.handle(evt, null); if (ret == null) { ret = DebugStatus.NO_CHANGE; } @@ -602,11 +599,18 @@ public class DbgManagerImpl implements DbgManager { DebugThreadId etid = so.getEventThread(); DebugProcessId epid = so.getEventProcess(); DebugSessionId esid = so.getCurrentSystemId(); - so.setCurrentProcessId(epid); - so.setCurrentThreadId(etid); DebugControl control = dbgeng.getControl(); - int execType = control.getExecutingProcessorType(); + int execType = WinNTExtra.Machine.IMAGE_FILE_MACHINE_AMD64.val; + try { + so.setCurrentProcessId(epid); + so.setCurrentThreadId(etid); + execType = control.getExecutingProcessorType(); + } + catch (Exception e) { + // Ignore for now + } + lastEventInformation = control.getLastEventInformation(); lastEventInformation.setSession(esid); lastEventInformation.setExecutingProcessorType(execType); @@ -687,6 +691,7 @@ public class DbgManagerImpl implements DbgManager { DbgProcessImpl process = getCurrentProcess(); int tid = so.getCurrentThreadSystemId(); DbgThreadImpl thread = getThreadComputeIfAbsent(eventId, process, tid); + getEventListeners().fire.threadCreated(thread, DbgCause.Causes.UNCLAIMED); getEventListeners().fire.threadSelected(thread, null, evt.getCause()); String key = Integer.toHexString(eventId.id); @@ -759,18 +764,17 @@ public class DbgManagerImpl implements DbgManager { so.setCurrentProcessId(id); int pid = so.getCurrentProcessSystemId(); DbgProcessImpl proc = getProcessComputeIfAbsent(id, pid); + getEventListeners().fire.processAdded(proc, DbgCause.Causes.UNCLAIMED); getEventListeners().fire.processSelected(proc, evt.getCause()); handle = info.initialThreadInfo.handle; DebugThreadId idt = so.getThreadIdByHandle(handle); int tid = so.getCurrentThreadSystemId(); - DbgThreadImpl thread = new DbgThreadImpl(this, proc, idt, tid); - thread.add(); - getEventListeners().fire.threadCreated(thread, evt.getCause()); + DbgThreadImpl thread = getThreadComputeIfAbsent(idt, proc, tid); getEventListeners().fire.threadSelected(thread, null, evt.getCause()); - proc.moduleLoaded(info.moduleInfo); - getEventListeners().fire.moduleLoaded(proc, info.moduleInfo, evt.getCause()); + //proc.moduleLoaded(info.moduleInfo); + //getEventListeners().fire.moduleLoaded(proc, info.moduleInfo, evt.getCause()); String key = Integer.toHexString(id.id); if (statusByNameMap.containsKey(key)) { @@ -788,20 +792,21 @@ public class DbgManagerImpl implements DbgManager { */ protected DebugStatus processProcessExited(DbgProcessExitedEvent evt, Void v) { DebugThreadId eventId = updateState(); - - DbgProcessImpl process = getCurrentProcess(); - process.remove(evt.getCause()); - getEventListeners().fire.processRemoved(process.getId(), evt.getCause()); - DbgThreadImpl thread = getCurrentThread(); - if (thread != null) { - thread.remove(); - } + DbgProcessImpl process = getCurrentProcess(); + process.setExitCode(Long.valueOf(evt.getInfo())); getEventListeners().fire.threadExited(eventId, process, evt.getCause()); + getEventListeners().fire.processExited(process, evt.getCause()); + for (DebugBreakpoint bpt : getBreakpoints()) { breaksById.remove(bpt.getId()); } + if (thread != null) { + thread.remove(); + } + process.remove(evt.getCause()); + getEventListeners().fire.processRemoved(process.getId(), evt.getCause()); String key = Integer.toHexString(process.getId().id); if (statusByNameMap.containsKey(key)) { @@ -844,7 +849,7 @@ public class DbgManagerImpl implements DbgManager { process.moduleLoaded(info); getEventListeners().fire.moduleLoaded(process, info, evt.getCause()); - String key = info.moduleName; + String key = info.getModuleName(); if (statusByNameMap.containsKey(key)) { return statusByNameMap.get(key); } @@ -865,7 +870,7 @@ public class DbgManagerImpl implements DbgManager { process.moduleUnloaded(info); getEventListeners().fire.moduleUnloaded(process, info, evt.getCause()); - String key = info.moduleName; + String key = info.getModuleName(); if (statusByNameMap.containsKey(key)) { return statusByNameMap.get(key); } @@ -908,8 +913,11 @@ public class DbgManagerImpl implements DbgManager { dbgState = DbgState.RUNNING; processEvent(new DbgRunningEvent(eventThread.getId())); } + if (!threads.containsValue(eventThread)) { + dbgState = DbgState.EXIT; + } // Don't fire - if (dbgState != null) { + if (dbgState != null && dbgState != DbgState.EXIT) { processEvent(new DbgThreadSelectedEvent(dbgState, eventThread, evt.getFrame(eventThread))); } @@ -1023,12 +1031,14 @@ public class DbgManagerImpl implements DbgManager { DbgBreakpointInfo knownBreakpoint = getKnownBreakpoint(bptId); if (knownBreakpoint == null) { breakpointInfo = new DbgBreakpointInfo(bpt, getCurrentProcess()); - if (!breakpointInfo.getLocation().equals("0")) { + if (breakpointInfo.getOffset() != null) { doBreakpointCreated(breakpointInfo, evt.getCause()); } return; } breakpointInfo = knownBreakpoint; + breakpointInfo.setBreakpoint(bpt); + } doBreakpointModified(breakpointInfo, evt.getCause()); } @@ -1110,6 +1120,13 @@ public class DbgManagerImpl implements DbgManager { doBreakpointModifiedSameLocations(newInfo, oldInfo, cause); } + private long orZero(Long l) { + if (l == null) { + return 0; + } + return l; + } + private void changeBreakpoints() { Set retained = new HashSet<>(); DebugSystemObjects so = getSystemObjects(); @@ -1134,7 +1151,7 @@ public class DbgManagerImpl implements DbgManager { } int id = bpt.getId(); retained.add(id); - long newOffset = bpt.getOffset(); + long newOffset = orZero(bpt.getOffset()); BreakpointTag tag = breaksById.get(id); if (tag == null) { for (DebugThreadId tid : tids) { @@ -1400,8 +1417,7 @@ public class DbgManagerImpl implements DbgManager { return execute(new DbgSessionSelectCommand(this, session)); } - public CompletableFuture requestFocus(DbgModelTargetFocusScope scope, - TargetObject obj) { + public CompletableFuture requestFocus(DbgModelTargetFocusScope scope, TargetObject obj) { return execute(new DbgRequestFocusCommand(this, scope, obj)); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgMinimalSymbol.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgMinimalSymbol.java index c53d407d3f..6e8b641fea 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgMinimalSymbol.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgMinimalSymbol.java @@ -17,25 +17,30 @@ package agent.dbgeng.manager.impl; public class DbgMinimalSymbol { protected final long index; - protected final String type; + protected final int typeId; protected final String name; protected final long address; + protected final long size; + private final int tag; + private final long moduleBase; - public DbgMinimalSymbol(long index, String type, String name, long address) { + public DbgMinimalSymbol(long index, int typeId, String name, long address, long size, int tag, + long moduleBase) { this.index = index; - this.type = type; + this.typeId = typeId; this.name = name; this.address = address; + this.size = size; + this.tag = tag; + this.moduleBase = moduleBase; } public long getIndex() { return index; } - public String getType() { - // TODO: Interpret these types - // Observed: t, T, D, S - return type; + public int getTypeId() { + return typeId; } public String getName() { @@ -45,4 +50,16 @@ public class DbgMinimalSymbol { public long getAddress() { return address; } + + public long getSize() { + return size; + } + + public int getTag() { + return tag; + } + + public long getModuleBase() { + return moduleBase; + } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgModuleImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgModuleImpl.java index 071ad3f589..87e5b6c37e 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgModuleImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgModuleImpl.java @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import agent.dbgeng.dbgeng.DebugModuleInfo; import agent.dbgeng.manager.DbgCause.Causes; import agent.dbgeng.manager.DbgModule; +import agent.dbgeng.manager.cmd.DbgListSymbolsCommand; import ghidra.async.AsyncLazyValue; public class DbgModuleImpl implements DbgModule { @@ -44,7 +45,7 @@ public class DbgModuleImpl implements DbgModule { this.manager = manager; this.process = process; this.info = info; - this.name = info.moduleName; + this.name = info.getModuleName(); } @Override @@ -70,12 +71,12 @@ public class DbgModuleImpl implements DbgModule { @Override public String getImageName() { - return info == null ? getName() : info.imageName; + return info == null ? getName() : info.getImageName(); } @Override public String getModuleName() { - return info == null ? getName() : info.moduleName; + return info == null ? getName() : info.getModuleName(); } @Override @@ -94,9 +95,7 @@ public class DbgModuleImpl implements DbgModule { } protected CompletableFuture> doGetMinimalSymbols() { - // TODO: Apparently, this is using internal GDB-debugging commands.... - // TODO: Also make methods for "full" symbols (DWARF?) - return null; + return manager.execute(new DbgListSymbolsCommand(manager, process, this)); } @Override diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgProcessImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgProcessImpl.java index 0b0cce6de8..e8713d27aa 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgProcessImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgProcessImpl.java @@ -15,7 +15,7 @@ */ package agent.dbgeng.manager.impl; -import static ghidra.async.AsyncUtils.sequence; +import static ghidra.async.AsyncUtils.*; import java.nio.ByteBuffer; import java.util.*; @@ -109,7 +109,7 @@ public class DbgProcessImpl implements DbgProcess { */ public void add() { manager.processes.put(id, this); - manager.getEventListeners().fire.processAdded(this, DbgCause.Causes.UNCLAIMED); + //manager.getEventListeners().fire.processAdded(this, DbgCause.Causes.UNCLAIMED); //manager.addProcess(this, cause); } @@ -352,7 +352,7 @@ public class DbgProcessImpl implements DbgProcess { } protected void moduleLoaded(DebugModuleInfo info) { - if (!modules.containsKey(info.moduleName)) { + if (!modules.containsKey(info.getModuleName())) { DbgModuleImpl module = new DbgModuleImpl(manager, this, info); modules.put(info.toString(), module); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgThreadImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgThreadImpl.java index d2f6ce112b..45d5fbbe02 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgThreadImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/manager/impl/DbgThreadImpl.java @@ -15,8 +15,6 @@ */ package agent.dbgeng.manager.impl; -import static ghidra.async.AsyncUtils.*; - import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.*; @@ -34,7 +32,8 @@ import agent.dbgeng.manager.DbgManager.ExecSuffix; import agent.dbgeng.manager.breakpoint.DbgBreakpointInfo; import agent.dbgeng.manager.breakpoint.DbgBreakpointType; import agent.dbgeng.manager.cmd.*; -import ghidra.async.*; +import ghidra.async.AsyncLazyValue; +import ghidra.async.AsyncReference; import ghidra.util.Msg; public class DbgThreadImpl implements DbgThread { @@ -91,7 +90,7 @@ public class DbgThreadImpl implements DbgThread { */ public void add() { manager.threads.put(id, this); - manager.getEventListeners().fire.threadCreated(this, DbgCause.Causes.UNCLAIMED); + //manager.getEventListeners().fire.threadCreated(this, DbgCause.Causes.UNCLAIMED); process.addThread(this); state.addChangeListener((oldState, newState, pair) -> { this.manager.getEventListeners().fire.threadStateChanged(this, newState, pair.cause, @@ -147,18 +146,18 @@ public class DbgThreadImpl implements DbgThread { } private CompletableFuture doListRegisters() { - return sequence(TypeSpec.cls(DbgRegisterSet.class)).then((seq) -> { - manager.execute(new DbgListRegisterDescriptionsCommand(manager)).handle(seq::next); - }, TypeSpec.cls(DebugRegisterDescription.class).list()).then((descs, seq) -> { + CompletableFuture> listCmd = + manager.execute(new DbgListRegisterDescriptionsCommand(manager)); + return listCmd.thenApply(descs -> { if (descs == null) { - return; + return new DbgRegisterSet(Set.of()); } List regs = new ArrayList<>(); for (DebugRegisterDescription desc : descs) { regs.add(new DbgRegister(desc)); } - seq.exit(new DbgRegisterSet(regs)); - }).finish(); + return new DbgRegisterSet(regs); + }); } @Override @@ -208,47 +207,37 @@ public class DbgThreadImpl implements DbgThread { @Override public CompletableFuture cont() { - return sequence(TypeSpec.VOID).then((seq) -> { - select().handle(seq::next); - }).then((seq) -> { - manager.execute(new DbgContinueCommand(manager)).handle(seq::exit); - }).finish(); + return select().thenCompose(__ -> { + return manager.execute(new DbgContinueCommand(manager)); + }); } @Override public CompletableFuture step(ExecSuffix suffix) { - return sequence(TypeSpec.VOID).then((seq) -> { - select().handle(seq::next); - }).then((seq) -> { - manager.execute(new DbgStepCommand(manager, id, suffix)).handle(seq::exit); - }).finish(); + return select().thenCompose(__ -> { + return manager.execute(new DbgStepCommand(manager, id, suffix)); + }); } @Override public CompletableFuture step(Map args) { - return sequence(TypeSpec.VOID).then((seq) -> { - select().handle(seq::next); - }).then((seq) -> { - manager.execute(new DbgStepCommand(manager, id, args)).handle(seq::exit); - }).finish(); + return select().thenCompose(__ -> { + return manager.execute(new DbgStepCommand(manager, id, args)); + }); } @Override public CompletableFuture kill() { - return sequence(TypeSpec.VOID).then((seq) -> { - select().handle(seq::next); - }).then((seq) -> { - manager.execute(new DbgKillCommand(manager)).handle(seq::exit); - }).finish(); + return select().thenCompose(__ -> { + return manager.execute(new DbgKillCommand(manager)); + }); } @Override public CompletableFuture detach() { - return sequence(TypeSpec.VOID).then((seq) -> { - select().handle(seq::next); - }).then((seq) -> { - manager.execute(new DbgDetachCommand(manager, process)).handle(seq::exit); - }).finish(); + return select().thenCompose(__ -> { + return manager.execute(new DbgDetachCommand(manager, process)); + }); } public DebugEventInformation getInfo() { diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/AbstractDbgModel.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/AbstractDbgModel.java index f8c26fe87a..de1a51b3a3 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/AbstractDbgModel.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/AbstractDbgModel.java @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import agent.dbgeng.manager.DbgManager; import agent.dbgeng.model.iface2.DbgModelTargetSession; import ghidra.dbg.agent.AbstractDebuggerObjectModel; +import ghidra.dbg.target.TargetObject; import ghidra.program.model.address.AddressFactory; public abstract class AbstractDbgModel extends AbstractDebuggerObjectModel { @@ -37,4 +38,8 @@ public abstract class AbstractDbgModel extends AbstractDebuggerObjectModel { public abstract DbgModelTargetSession getSession(); + public abstract void addModelObject(Object object, TargetObject targetObject); + + public abstract TargetObject getModelObject(Object object); + } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetAttachable.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetAttachable.java index 1b7f9bcc3d..b83e0921db 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetAttachable.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetAttachable.java @@ -21,6 +21,7 @@ import ghidra.dbg.target.TargetAttachable; /** * An interface which indicates this object is capable of launching targets. * + *

* The targets this launcher creates ought to appear in its successors. * * @param type for this diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetAttacher.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetAttacher.java index 41d5fdc45a..bcc40d1823 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetAttacher.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetAttacher.java @@ -21,8 +21,6 @@ import agent.dbgeng.manager.DbgProcess; import agent.dbgeng.manager.impl.DbgProcessImpl; import agent.dbgeng.model.iface2.DbgModelTargetAvailable; import agent.dbgeng.model.iface2.DbgModelTargetObject; -import ghidra.async.AsyncUtils; -import ghidra.async.TypeSpec; import ghidra.dbg.target.TargetAttachable; import ghidra.dbg.target.TargetAttacher; import ghidra.util.Msg; @@ -30,6 +28,7 @@ import ghidra.util.Msg; /** * An interface which indicates this object is capable of launching targets. * + *

* The targets this launcher creates ought to appear in its successors. * * @param type for this @@ -42,22 +41,20 @@ public interface DbgModelTargetAttacher extends DbgModelTargetObject, TargetAtta getModel().assertMine(DbgModelTargetAvailable.class, attachable); // TODO: This and the below new DbgProcessImpl seem to do the same thing // Both should be expressed the same way - return getManager().addProcess().thenAccept(process -> { - process.attach(available.getPid()); - }).exceptionally((exc) -> { + return getModel().gateFuture(getManager().addProcess().thenCompose(process -> { + return process.attach(available.getPid()); + }).exceptionally(exc -> { Msg.error(this, "attach failed"); return null; - }); + })).thenApply(__ -> null); } @Override public default CompletableFuture attach(long pid) { - return AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - DbgProcess process = new DbgProcessImpl(getManager()); - process.attach(pid).handle(seq::nextIgnore); - }).finish().exceptionally((exc) -> { + DbgProcess process = new DbgProcessImpl(getManager()); + return getModel().gateFuture(process.attach(pid).exceptionally(exc -> { Msg.error(this, "attach failed"); return null; - }); + })).thenApply(__ -> null); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetExecutionStateful.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetExecutionStateful.java index 3c17ae449e..d8c784db02 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetExecutionStateful.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetExecutionStateful.java @@ -46,7 +46,6 @@ public interface DbgModelTargetExecutionStateful changeAttributes(List.of(), Map.of( // STATE_ATTRIBUTE_NAME, state // ), reason); - getListeners().fire(TargetExecutionStateListener.class).executionStateChanged(this, state); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetFocusScope.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetFocusScope.java index fa71a9ca87..96344003f7 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetFocusScope.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetFocusScope.java @@ -43,16 +43,16 @@ public interface DbgModelTargetFocusScope extends DbgModelTargetObject, TargetFo // (but, of course, may then cause change in state) @Override public default CompletableFuture requestFocus(TargetObject obj) { - return getManager().requestFocus(this, obj); + return getModel().gateFuture(getManager().requestFocus(this, obj)); } public default CompletableFuture doRequestFocus(TargetObject obj) { if (getManager().isWaiting()) { - return CompletableFuture.completedFuture(null); + return AsyncUtils.NIL; } getModel().assertMine(TargetObject.class, obj); if (obj.equals(getFocus())) { - return CompletableFuture.completedFuture(null); + return AsyncUtils.NIL; } if (!PathUtils.isAncestor(this.getPath(), obj.getPath())) { throw new DebuggerIllegalArgumentException("Can only focus a successor of the scope"); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetInterpreter.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetInterpreter.java index 23c0ffe3a0..4553a1f355 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetInterpreter.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetInterpreter.java @@ -31,12 +31,12 @@ public interface DbgModelTargetInterpreter extends DbgModelTargetObject, TargetI @Override public default CompletableFuture execute(String cmd) { - return getManager().console(cmd); + return getModel().gateFuture(getManager().console(cmd)); } @Override public default CompletableFuture executeCapture(String cmd) { - return getManager().consoleCapture(cmd); + return getModel().gateFuture(getManager().consoleCapture(cmd)); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetResumable.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetResumable.java index 38b4598a15..79cab9fbda 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetResumable.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetResumable.java @@ -19,6 +19,7 @@ import java.util.concurrent.CompletableFuture; import agent.dbgeng.manager.DbgProcess; import agent.dbgeng.model.iface2.DbgModelTargetObject; +import ghidra.async.AsyncUtils; import ghidra.dbg.target.TargetResumable; /** @@ -33,6 +34,9 @@ public interface DbgModelTargetResumable extends DbgModelTargetObject, TargetRes @Override public default CompletableFuture resume() { DbgProcess process = getManager().getCurrentProcess(); + if (process == null) { + return AsyncUtils.NIL; + } return process.cont(); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetSteppable.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetSteppable.java index c5d4c8a072..e33758393a 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetSteppable.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface1/DbgModelTargetSteppable.java @@ -66,20 +66,21 @@ public interface DbgModelTargetSteppable extends DbgModelTargetObject, TargetSte default: if (this instanceof DbgModelTargetThread) { DbgModelTargetThread targetThread = (DbgModelTargetThread) this; - return targetThread.getThread().step(convertToDbg(kind)); + return getModel().gateFuture(targetThread.getThread().step(convertToDbg(kind))); } if (this instanceof DbgModelTargetProcess) { DbgModelTargetProcess targetProcess = (DbgModelTargetProcess) this; - return targetProcess.getProcess().step(convertToDbg(kind)); + return getModel() + .gateFuture(targetProcess.getProcess().step(convertToDbg(kind))); } - return thread.step(convertToDbg(kind)); + return getModel().gateFuture(thread.step(convertToDbg(kind))); } } @Override default CompletableFuture step(Map args) { DbgThread thread = getManager().getCurrentThread(); - return thread.step(args); + return getModel().gateFuture(thread.step(args)); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetAvailable.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetAvailable.java index a162c52fa7..250dd0a4ed 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetAvailable.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetAvailable.java @@ -18,6 +18,8 @@ package agent.dbgeng.model.iface2; import ghidra.dbg.target.TargetAttachable; public interface DbgModelTargetAvailable extends DbgModelTargetObject, TargetAttachable { + String PID_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "pid"; + // TODO: DESCRIPTION, TYPE, USER? public long getPid(); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointContainer.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointContainer.java index 808ddb5a52..c6c2bf0a13 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointContainer.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointContainer.java @@ -22,8 +22,9 @@ import java.util.function.Function; import agent.dbgeng.manager.DbgEventsListenerAdapter; import agent.dbgeng.manager.breakpoint.DbgBreakpointType; import ghidra.async.AsyncFence; -import ghidra.dbg.target.TargetBreakpointContainer; +import ghidra.dbg.target.TargetBreakpointLocationContainer; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.dbg.target.schema.*; import ghidra.program.model.address.AddressRange; @@ -34,8 +35,10 @@ import ghidra.program.model.address.AddressRange; attributes = { @TargetAttributeType(type = Void.class) }, canonicalContainer = true) -public interface DbgModelTargetBreakpointContainer - extends DbgModelTargetObject, TargetBreakpointContainer, DbgEventsListenerAdapter { +public interface DbgModelTargetBreakpointContainer extends DbgModelTargetObject, // + TargetBreakpointSpecContainer, // + TargetBreakpointLocationContainer, // + DbgEventsListenerAdapter { /* @Override @@ -65,13 +68,13 @@ public interface DbgModelTargetBreakpointContainer else if (kinds.contains(TargetBreakpointKind.WRITE)) { fence.include(placer.apply(DbgBreakpointType.HW_WATCHPOINT)); } - if (kinds.contains(TargetBreakpointKind.EXECUTE)) { + if (kinds.contains(TargetBreakpointKind.HW_EXECUTE)) { fence.include(placer.apply(DbgBreakpointType.HW_BREAKPOINT)); } - if (kinds.contains(TargetBreakpointKind.SOFTWARE)) { + if (kinds.contains(TargetBreakpointKind.SW_EXECUTE)) { fence.include(placer.apply(DbgBreakpointType.BREAKPOINT)); } - return fence.ready(); + return getModel().gateFuture(fence.ready()); } @Override diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointLocation.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointLocation.java index c8f7940e78..cfeeff35bc 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointLocation.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointLocation.java @@ -15,7 +15,6 @@ */ package agent.dbgeng.model.iface2; -import ghidra.dbg.attributes.TargetObjectList; import ghidra.dbg.target.TargetBreakpointLocation; import ghidra.program.model.address.Address; @@ -25,7 +24,4 @@ public interface DbgModelTargetBreakpointLocation @Override public Address getAddress(); - @Override - public TargetObjectList getAffects(); - } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointSpec.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointSpec.java index 80b9b69358..ee240abfa7 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointSpec.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetBreakpointSpec.java @@ -21,9 +21,8 @@ import java.util.concurrent.CompletableFuture; import agent.dbgeng.manager.breakpoint.DbgBreakpointInfo; import agent.dbgeng.model.iface1.DbgModelTargetBptHelper; -import ghidra.dbg.attributes.TargetObjectList; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.program.model.address.*; public interface DbgModelTargetBreakpointSpec extends // @@ -42,24 +41,24 @@ public interface DbgModelTargetBreakpointSpec extends // @Override public default CompletableFuture delete() { - return getManager().deleteBreakpoints(getNumber()); + return getModel().gateFuture(getManager().deleteBreakpoints(getNumber())); } @Override public default CompletableFuture disable() { setEnabled(false, "Disabled"); - return getManager().disableBreakpoints(getNumber()); + return getModel().gateFuture(getManager().disableBreakpoints(getNumber())); } @Override public default CompletableFuture enable() { setEnabled(true, "Enabled"); - return getManager().enableBreakpoints(getNumber()); + return getModel().gateFuture(getManager().enableBreakpoints(getNumber())); } @Override public default String getExpression() { - return getBreakpointInfo().getLocation(); + return getBreakpointInfo().getExpression(); } public default long getNumber() { @@ -70,9 +69,9 @@ public interface DbgModelTargetBreakpointSpec extends // public default TargetBreakpointKindSet getKinds() { switch (getBreakpointInfo().getType()) { case BREAKPOINT: - return TargetBreakpointKindSet.of(TargetBreakpointKind.SOFTWARE); + return TargetBreakpointKindSet.of(TargetBreakpointKind.SW_EXECUTE); case HW_BREAKPOINT: - return TargetBreakpointKindSet.of(TargetBreakpointKind.EXECUTE); + return TargetBreakpointKindSet.of(TargetBreakpointKind.HW_EXECUTE); case HW_WATCHPOINT: return TargetBreakpointKindSet.of(TargetBreakpointKind.WRITE); case READ_WATCHPOINT: @@ -106,7 +105,6 @@ public interface DbgModelTargetBreakpointSpec extends // catch (AddressFormatException e) { e.printStackTrace(); } - map.put(AFFECTS_ATTRIBUTE_NAME, doGetAffects()); map.put(SPEC_ATTRIBUTE_NAME, this); map.put(EXPRESSION_ATTRIBUTE_NAME, addstr); map.put(KINDS_ATTRIBUTE_NAME, getKinds()); @@ -124,14 +122,16 @@ public interface DbgModelTargetBreakpointSpec extends // }); } - public default Address doGetAddress() { - DbgBreakpointInfo info = getBreakpointInfo(); - return getModel().getAddress("ram", info.addrAsLong()); + private long orZero(Long l) { + if (l == null) { + return 0; + } + return l; } - public default TargetObjectList doGetAffects() { - DbgModelTargetProcess process = getParentProcess(); - return TargetObjectList.of(process); + public default Address doGetAddress() { + DbgBreakpointInfo info = getBreakpointInfo(); + return getModel().getAddress("ram", orZero(info.getOffset())); } public default void updateInfo(DbgBreakpointInfo oldInfo, DbgBreakpointInfo newInfo, @@ -157,7 +157,6 @@ public interface DbgModelTargetBreakpointSpec extends // setBreakpointEnabled(enabled); changeAttributes(List.of(), Map.of(ENABLED_ATTRIBUTE_NAME, enabled // ), reason); - getListeners().fire(TargetBreakpointSpecListener.class).breakpointToggled(this, enabled); } @Override @@ -176,7 +175,10 @@ public interface DbgModelTargetBreakpointSpec extends // } public default void breakpointHit() { - getActions().fire.breakpointHit(this, getParentProcess(), null, this); + DbgModelTargetThread targetThread = + getParentProcess().getThreads().getTargetThread(getManager().getEventThread()); + getActions().fire.breakpointHit((DbgModelTargetBreakpointSpec) getProxy(), targetThread, + null, this); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetDebugContainer.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetDebugContainer.java index 63261dd851..84df97afe2 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetDebugContainer.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetDebugContainer.java @@ -15,6 +15,8 @@ */ package agent.dbgeng.model.iface2; -public interface DbgModelTargetDebugContainer extends DbgModelTargetObject { +import ghidra.dbg.target.TargetAggregate; + +public interface DbgModelTargetDebugContainer extends DbgModelTargetObject, TargetAggregate { } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetModuleContainer.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetModuleContainer.java index 9d4a3cc4f3..5a1cc039e2 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetModuleContainer.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetModuleContainer.java @@ -27,7 +27,7 @@ public interface DbgModelTargetModuleContainer @Override public CompletableFuture addSyntheticModule(String name); - public CompletableFuture getTargetModule(String name); + public DbgModelTargetModule getTargetModule(String name); public void libraryLoaded(String name); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetModuleSectionContainer.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetModuleSectionContainer.java index f53c337df7..4c673175b6 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetModuleSectionContainer.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetModuleSectionContainer.java @@ -15,6 +15,9 @@ */ package agent.dbgeng.model.iface2; -public interface DbgModelTargetModuleSectionContainer extends DbgModelTargetObject { +import ghidra.dbg.target.TargetSectionContainer; + +public interface DbgModelTargetModuleSectionContainer + extends DbgModelTargetObject, TargetSectionContainer { } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetObject.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetObject.java index c18e4f623a..20bbbddfbf 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetObject.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetObject.java @@ -22,6 +22,8 @@ import java.util.concurrent.CompletableFuture; import agent.dbgeng.dbgeng.DebugClient.DebugStatus; import agent.dbgeng.manager.impl.DbgManagerImpl; import agent.dbgeng.model.AbstractDbgModel; +import ghidra.async.AsyncUtils; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.agent.InvalidatableTargetObjectIf; import ghidra.dbg.agent.SpiTargetObject; import ghidra.dbg.target.TargetObject; @@ -54,17 +56,17 @@ public interface DbgModelTargetObject extends SpiTargetObject, InvalidatableTarg return impl; } - @Override - public CompletableFuture> fetchElements(); - - @Override - public CompletableFuture> fetchAttributes(); - public Delta changeAttributes(List remove, Map add, String reason); public CompletableFuture> requestNativeAttributes(); - public ListenerSet getListeners(); + public default CompletableFuture requestAugmentedAttributes() { + return AsyncUtils.NIL; + } + + public CompletableFuture> requestNativeElements(); + + public ListenerSet getListeners(); public DbgModelTargetSession getParentSession(); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetProcess.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetProcess.java index 4c052740d5..2c5cdaebae 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetProcess.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetProcess.java @@ -19,8 +19,7 @@ import java.util.concurrent.CompletableFuture; import agent.dbgeng.dbgeng.DebugProcessId; import agent.dbgeng.dbgeng.DebugSystemObjects; -import agent.dbgeng.manager.DbgEventsListenerAdapter; -import agent.dbgeng.manager.DbgProcess; +import agent.dbgeng.manager.*; import agent.dbgeng.manager.impl.DbgManagerImpl; import agent.dbgeng.model.iface1.*; import ghidra.dbg.target.TargetAggregate; @@ -46,12 +45,12 @@ public interface DbgModelTargetProcess extends // public void processStarted(Long pid); - public void processExited(Long exitCode); - public DbgModelTargetThreadContainer getThreads(); public DbgModelTargetModuleContainer getModules(); + public void threadStateChangedSpecific(DbgThread thread, DbgState state); + public default DbgProcess getProcess() { DbgManagerImpl manager = getManager(); DebugSystemObjects so = manager.getSystemObjects(); @@ -78,4 +77,5 @@ public interface DbgModelTargetProcess extends // } return manager.selectProcess(process); } + } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegister.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegister.java index a1f665b22c..98a46e1c74 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegister.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegister.java @@ -15,8 +15,11 @@ */ package agent.dbgeng.model.iface2; +import java.math.BigInteger; + import agent.dbgeng.manager.impl.DbgRegister; import ghidra.dbg.target.TargetRegister; +import ghidra.dbg.util.ConversionUtils; public interface DbgModelTargetRegister extends DbgModelTargetObject, TargetRegister { @@ -25,4 +28,10 @@ public interface DbgModelTargetRegister extends DbgModelTargetObject, TargetRegi public DbgRegister getRegister(); + public default byte[] getBytes() { + String val = (String) getCachedAttributes().get(VALUE_ATTRIBUTE_NAME); + BigInteger value = new BigInteger(val, 16); + return ConversionUtils.bigIntegerToBytes(16, value); + } + } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegisterBank.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegisterBank.java index d15e5e4d4a..e010e26ad4 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegisterBank.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegisterBank.java @@ -17,13 +17,15 @@ package agent.dbgeng.model.iface2; import java.math.BigInteger; import java.util.*; +import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; -import agent.dbgeng.manager.DbgThread; +import agent.dbgeng.manager.*; import agent.dbgeng.manager.impl.*; import ghidra.async.AsyncUtils; import ghidra.async.TypeSpec; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.error.DebuggerRegisterAccessException; import ghidra.dbg.target.TargetRegisterBank; import ghidra.dbg.util.ConversionUtils; @@ -34,6 +36,11 @@ public interface DbgModelTargetRegisterBank extends DbgModelTargetObject, Target public DbgModelTargetRegister getTargetRegister(DbgRegister register); + public default void threadStateChangedSpecific(DbgState state, DbgReason reason) { + readRegistersNamed(getCachedElements().keySet()); + } + + // NB: Does anyone call this anymore? @Override public default CompletableFuture> readRegistersNamed( Collection names) { @@ -84,9 +91,9 @@ public interface DbgModelTargetRegisterBank extends DbgModelTargetObject, Target reg.setModified(value.toString(16).equals(oldval)); } } - ListenerSet listeners = getListeners(); + ListenerSet listeners = getListeners(); if (listeners != null) { - listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, result); + listeners.fire.registersUpdated(getProxy(), result); } return result; }); @@ -96,7 +103,7 @@ public interface DbgModelTargetRegisterBank extends DbgModelTargetObject, Target public default CompletableFuture writeRegistersNamed(Map values) { DbgThread thread = getParentThread().getThread(); return AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - fetchElements().handle(seq::nextIgnore); + requestNativeElements().handle(seq::nextIgnore); }).then(seq -> { thread.listRegisters().handle(seq::next); }, TypeSpec.cls(DbgRegisterSet.class)).then((regset, seq) -> { @@ -115,8 +122,26 @@ public interface DbgModelTargetRegisterBank extends DbgModelTargetObject, Target getParentThread().getThread().writeRegisters(toWrite).handle(seq::next); // TODO: Should probably filter only effective and normalized writes in the callback }).then(seq -> { - getListeners().fire(TargetRegisterBankListener.class).registersUpdated(this, values); + getListeners().fire.registersUpdated(getProxy(), values); seq.exit(); }).finish(); } + + @Override + public default Map getCachedRegisters() { + return getValues(); + } + + public default Map getValues() { + Map result = new HashMap<>(); + for (Entry entry : this.getCachedAttributes().entrySet()) { + if (entry.getValue() instanceof DbgModelTargetRegister) { + DbgModelTargetRegister reg = (DbgModelTargetRegister) entry.getValue(); + byte[] bytes = reg.getBytes(); + result.put(entry.getKey(), bytes); + } + } + return result; + } + } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegisterContainerAndBank.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegisterContainerAndBank.java index d26d0a370e..e86c24636e 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegisterContainerAndBank.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetRegisterContainerAndBank.java @@ -19,8 +19,10 @@ import agent.dbgeng.manager.impl.DbgRegister; import ghidra.dbg.target.TargetRegisterBank; import ghidra.dbg.target.TargetRegisterContainer; -public interface DbgModelTargetRegisterContainerAndBank - extends DbgModelTargetObject, TargetRegisterContainer, TargetRegisterBank { +public interface DbgModelTargetRegisterContainerAndBank extends // + DbgModelTargetObject, // + TargetRegisterContainer, // + TargetRegisterBank { public DbgModelTargetRegister getTargetRegister(DbgRegister register); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetSession.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetSession.java index 83c958d331..8db04022d9 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetSession.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetSession.java @@ -26,7 +26,6 @@ import agent.dbgeng.model.iface1.*; import ghidra.dbg.target.TargetAggregate; import ghidra.dbg.target.TargetConsole; import ghidra.dbg.target.TargetConsole.Channel; -import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener; import ghidra.dbg.util.PathUtils; public interface DbgModelTargetSession extends // @@ -38,7 +37,6 @@ public interface DbgModelTargetSession extends // DbgModelTargetResumable, // DbgEventsListenerAdapter, // DbgModelSelectableObject, // - TargetFocusScopeListener, // TargetAggregate { DbgModelTargetProcessContainer getProcesses(); @@ -69,7 +67,7 @@ public interface DbgModelTargetSession extends // if (output.contains("loaded *kernel* extension dll for usermode")) { return; } - getListeners().fire(TargetInterpreterListener.class).consoleOutput(this, chan, output); + getListeners().fire.consoleOutput(getProxy(), chan, output); } @Override diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetStackFrame.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetStackFrame.java index fab0c3731e..9ab0313123 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetStackFrame.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetStackFrame.java @@ -61,27 +61,32 @@ public interface DbgModelTargetStackFrame extends // if (attrs == null) { return CompletableFuture.completedFuture(null); } - TargetObject attributes = (TargetObject) attrs.get("Attributes"); + DbgModelTargetObject attributes = (DbgModelTargetObject) attrs.get("Attributes"); if (attributes == null) { return CompletableFuture.completedFuture(null); } - return attributes.fetchAttributes(true); - }).thenCompose(subattrs -> { - if (subattrs == null) { - return CompletableFuture.completedFuture(null); - } - TargetObject frameNumber = (TargetObject) subattrs.get("FrameNumber"); - return frameNumber.fetchAttribute(VALUE_ATTRIBUTE_NAME).thenCompose(noval -> { - String nostr = noval.toString(); - TargetObject instructionOffset = (TargetObject) subattrs.get("InstructionOffset"); - return instructionOffset.fetchAttribute(VALUE_ATTRIBUTE_NAME).thenAccept(pcval -> { - String oldval = (String) getCachedAttribute(DISPLAY_ATTRIBUTE_NAME); - String pcstr = pcval.toString(); - long pc = Long.parseUnsignedLong(pcstr, 16); - map.put(PC_ATTRIBUTE_NAME, space.getAddress(pc)); - String display = String.format("#%s 0x%s", nostr, pcstr); - map.put(DISPLAY_ATTRIBUTE_NAME, display); - setModified(map, !display.equals(oldval)); + return attributes.requestAugmentedAttributes().thenCompose(ax -> { + Map subattrs = attributes.getCachedAttributes(); + if (subattrs == null) { + return CompletableFuture.completedFuture(null); + } + DbgModelTargetObject frameNumber = + (DbgModelTargetObject) subattrs.get("FrameNumber"); + return frameNumber.requestAugmentedAttributes().thenCompose(bx -> { + Object noval = frameNumber.getCachedAttribute(VALUE_ATTRIBUTE_NAME); + String nostr = noval.toString(); + DbgModelTargetObject instructionOffset = + (DbgModelTargetObject) subattrs.get("InstructionOffset"); + return instructionOffset.requestAugmentedAttributes().thenAccept(cx -> { + String oldval = (String) getCachedAttribute(DISPLAY_ATTRIBUTE_NAME); + Object pcval = instructionOffset.getCachedAttribute(VALUE_ATTRIBUTE_NAME); + String pcstr = pcval.toString(); + long pc = Long.parseUnsignedLong(pcstr, 16); + map.put(PC_ATTRIBUTE_NAME, space.getAddress(pc)); + String display = String.format("#%s 0x%s", nostr, pcstr); + map.put(DISPLAY_ATTRIBUTE_NAME, display); + setModified(map, !display.equals(oldval)); + }); }); }); }); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetTTD.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetTTD.java index 26a00aa696..d2adf512b1 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetTTD.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetTTD.java @@ -18,8 +18,6 @@ package agent.dbgeng.model.iface2; import java.util.Map; import java.util.concurrent.CompletableFuture; -import ghidra.dbg.target.TargetObject; - public interface DbgModelTargetTTD extends DbgModelTargetObject { @Override @@ -28,25 +26,28 @@ public interface DbgModelTargetTTD extends DbgModelTargetObject { if (attrs == null) { return CompletableFuture.completedFuture(null); } - TargetObject attributes = (TargetObject) attrs.get("Position"); + DbgModelTargetObject attributes = (DbgModelTargetObject) attrs.get("Position"); if (attributes == null) { return CompletableFuture.completedFuture(null); } - return attributes.fetchAttributes(true); - }).thenCompose(subattrs -> { - if (subattrs == null) { - return CompletableFuture.completedFuture(null); - } - TargetObject seq = (TargetObject) subattrs.get("Sequence"); - return seq.fetchAttribute(VALUE_ATTRIBUTE_NAME).thenCompose(sqval -> { - String sqstr = sqval.toString(); - TargetObject steps = (TargetObject) subattrs.get("Steps"); - return steps.fetchAttribute(VALUE_ATTRIBUTE_NAME).thenAccept(stval -> { - String oldval = (String) getCachedAttribute(DISPLAY_ATTRIBUTE_NAME); - String ststr = stval.toString(); - String display = String.format("TTD %s:%s", sqstr, ststr); - map.put(DISPLAY_ATTRIBUTE_NAME, display); - setModified(map, !display.equals(oldval)); + return attributes.requestAugmentedAttributes().thenCompose(ax -> { + Map subattrs = attributes.getCachedAttributes(); + if (subattrs == null) { + return CompletableFuture.completedFuture(null); + } + DbgModelTargetObject seq = (DbgModelTargetObject) subattrs.get("Sequence"); + return seq.requestAugmentedAttributes().thenCompose(bx -> { + Object sqval = seq.getCachedAttribute(VALUE_ATTRIBUTE_NAME); + String sqstr = sqval.toString(); + DbgModelTargetObject steps = (DbgModelTargetObject) subattrs.get("Steps"); + return steps.requestAugmentedAttributes().thenAccept(cx -> { + Object stval = steps.getCachedAttribute(VALUE_ATTRIBUTE_NAME); + String oldval = (String) getCachedAttribute(DISPLAY_ATTRIBUTE_NAME); + String ststr = stval.toString(); + String display = String.format("TTD %s:%s", sqstr, ststr); + map.put(DISPLAY_ATTRIBUTE_NAME, display); + setModified(map, !display.equals(oldval)); + }); }); }); }); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetThread.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetThread.java index 0d24c15cda..4def70c441 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetThread.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/iface2/DbgModelTargetThread.java @@ -55,7 +55,7 @@ public interface DbgModelTargetThread extends // } } - public void threadStateChanged(DbgState state, DbgReason reason); + public void threadStateChangedSpecific(DbgState state, DbgReason reason); @Override public default CompletableFuture select() { diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImpl.java index bf5d16f02e..14b80d8bb4 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImpl.java @@ -16,21 +16,30 @@ package agent.dbgeng.model.impl; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.RejectedExecutionException; + +import org.apache.commons.lang3.exception.ExceptionUtils; import agent.dbgeng.dbgeng.DebugSessionId; import agent.dbgeng.manager.DbgManager; import agent.dbgeng.manager.impl.DbgManagerImpl; import agent.dbgeng.manager.impl.DbgSessionImpl; import agent.dbgeng.model.AbstractDbgModel; -import agent.dbgeng.model.iface2.DbgModelTargetSession; -import agent.dbgeng.model.iface2.DbgModelTargetSessionContainer; +import agent.dbgeng.model.iface2.*; +import ghidra.async.AsyncUtils; +import ghidra.dbg.DebuggerModelClosedReason; +import ghidra.dbg.DebuggerObjectModelWithMemory; +import ghidra.dbg.error.DebuggerModelTerminatingException; +import ghidra.dbg.target.TargetMemory; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.AnnotatedSchemaContext; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.program.model.address.*; -public class DbgModelImpl extends AbstractDbgModel { +public class DbgModelImpl extends AbstractDbgModel implements DebuggerObjectModelWithMemory { // TODO: Need some minimal memory modeling per architecture on the model/agent side. // The model must convert to and from Ghidra's address space names protected static final String SPACE_NAME = "ram"; @@ -51,6 +60,8 @@ public class DbgModelImpl extends AbstractDbgModel { protected final CompletableFuture completedRoot; + protected Map objectMap = new HashMap<>(); + public DbgModelImpl() { this.dbg = DbgManager.newInstance(); //System.out.println(XmlSchemaContext.serialize(SCHEMA_CTX)); @@ -79,7 +90,7 @@ public class DbgModelImpl extends AbstractDbgModel { @Override public CompletableFuture startDbgEng(String[] args) { - return dbg.start(args); + return dbg.start(args).thenApplyAsync(__ -> null, clientExecutor); } @Override @@ -89,6 +100,8 @@ public class DbgModelImpl extends AbstractDbgModel { @Override public void terminate() throws IOException { + listeners.fire.modelClosed(DebuggerModelClosedReason.NORMAL); + root.invalidateSubtree(root, "Dbgeng is terminating"); dbg.terminate(); } @@ -113,6 +126,10 @@ public class DbgModelImpl extends AbstractDbgModel { terminate(); return super.close(); } + catch (RejectedExecutionException e) { + reportError(this, "Model is already closing", e); + return AsyncUtils.NIL; + } catch (Throwable t) { return CompletableFuture.failedFuture(t); } @@ -122,4 +139,39 @@ public class DbgModelImpl extends AbstractDbgModel { public DbgModelTargetSession getSession() { return session; } + + @Override + public TargetMemory getMemory(TargetObject target, Address address, int length) { + if (target instanceof DbgModelTargetProcess) { + DbgModelTargetProcess process = (DbgModelTargetProcess) target; + return new DbgModelTargetMemoryContainerImpl(process); + } + return null; + } + + @Override + public void addModelObject(Object object, TargetObject targetObject) { + objectMap.put(object, targetObject); + } + + @Override + public TargetObject getModelObject(Object object) { + return objectMap.get(object); + } + + public void deleteModelObject(Object object) { + objectMap.remove(object); + } + + @Override + public CompletableFuture gateFuture(CompletableFuture future) { + return super.gateFuture(future).exceptionally(ex -> { + for (Throwable cause = ex; cause != null; cause = cause.getCause()) { + if (cause instanceof RejectedExecutionException) { + throw new DebuggerModelTerminatingException("dbgeng is terminating", ex); + } + } + return ExceptionUtils.rethrow(ex); + }); + } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImplUtils.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImplUtils.java index d324153e38..5d44fbb051 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImplUtils.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelImplUtils.java @@ -20,16 +20,12 @@ import java.util.concurrent.CompletableFuture; import agent.dbgeng.manager.DbgProcess; import agent.dbgeng.model.AbstractDbgModel; -import ghidra.async.AsyncUtils; -import ghidra.async.TypeSpec; public enum DbgModelImplUtils { ; public static CompletableFuture launch(AbstractDbgModel impl, DbgProcess process, List args) { - return AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - process.fileExecAndSymbols(args.get(0)); - }).finish(); + return process.fileExecAndSymbols(args.get(0)); } public static V noDupMerge(V first, V second) { diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetAvailableContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetAvailableContainerImpl.java index 19a1916ee6..b3c02d8b62 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetAvailableContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetAvailableContainerImpl.java @@ -25,13 +25,19 @@ import org.apache.commons.lang3.tuple.Pair; import agent.dbgeng.model.iface2.*; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.util.datastruct.WeakValueHashMap; -@TargetObjectSchemaInfo(name = "AvailableContainer", elements = { // - @TargetElementType(type = DbgModelTargetAvailableImpl.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "AvailableContainer", + elements = { + @TargetElementType(type = DbgModelTargetAvailableImpl.class) + }, + elementResync = ResyncMode.ALWAYS, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class DbgModelTargetAvailableContainerImpl extends DbgModelTargetObjectImpl implements DbgModelTargetAvailableContainer { @@ -40,9 +46,6 @@ public class DbgModelTargetAvailableContainerImpl extends DbgModelTargetObjectIm public DbgModelTargetAvailableContainerImpl(DbgModelTargetRoot root) { super(root.getModel(), root, "Available", "AvailableContainer"); - changeAttributes(List.of(), List.of(), Map.of( // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.SOLICITED // - ), "Initialized"); } @Override diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetAvailableImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetAvailableImpl.java index ce4793266f..c9e2989ae1 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetAvailableImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetAvailableImpl.java @@ -23,17 +23,17 @@ import agent.dbgeng.model.iface2.DbgModelTargetAvailableContainer; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; -@TargetObjectSchemaInfo(name = "Available", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo( + name = "Available", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(type = Void.class) + }) public class DbgModelTargetAvailableImpl extends DbgModelTargetObjectImpl implements DbgModelTargetAvailable { - protected static final String PID_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "pid"; - // TODO: DESCRIPTION, TYPE, USER? - protected static String indexAttachable(int pid) { return Integer.toHexString(pid); } @@ -51,8 +51,7 @@ public class DbgModelTargetAvailableImpl extends DbgModelTargetObjectImpl this.changeAttributes(List.of(), List.of(), Map.of(// PID_ATTRIBUTE_NAME, (long) pid, // - DISPLAY_ATTRIBUTE_NAME, keyAttachable(pid) + " : " + name.trim(), - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, keyAttachable(pid) + " : " + name.trim() // ), "Initialized"); } @@ -62,8 +61,7 @@ public class DbgModelTargetAvailableImpl extends DbgModelTargetObjectImpl this.changeAttributes(List.of(), List.of(), Map.of(// PID_ATTRIBUTE_NAME, (long) pid, // - DISPLAY_ATTRIBUTE_NAME, keyAttachable(pid), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, keyAttachable(pid) // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetBreakpointContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetBreakpointContainerImpl.java index 03e48b0c89..49bd954f12 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetBreakpointContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetBreakpointContainerImpl.java @@ -27,7 +27,6 @@ import agent.dbgeng.model.iface2.*; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; -import ghidra.util.datastruct.WeakValueHashMap; @TargetObjectSchemaInfo(name = "BreakpointContainer", elements = { // @TargetElementType(type = DbgModelTargetBreakpointSpecImpl.class) // @@ -40,8 +39,6 @@ public class DbgModelTargetBreakpointContainerImpl extends DbgModelTargetObjectI protected static final TargetBreakpointKindSet SUPPORTED_KINDS = TargetBreakpointKindSet.of(TargetBreakpointKind.values()); - private final Map specsByNumber = new WeakValueHashMap<>(); - public DbgModelTargetBreakpointContainerImpl(DbgModelTargetDebugContainer debug) { super(debug.getModel(), debug, "Breakpoints", "BreakpointContainer"); @@ -66,9 +63,8 @@ public class DbgModelTargetBreakpointContainerImpl extends DbgModelTargetObjectI @Override public void breakpointDeleted(DbgBreakpointInfo info, DbgCause cause) { - synchronized (this) { - getSpecsByNumber().remove(info.getNumber()); - } + DbgModelImpl impl = (DbgModelImpl) model; + impl.deleteModelObject(info.getDebugBreakpoint()); changeElements(List.of( // DbgModelTargetBreakpointSpecImpl.indexBreakpoint(info) // ), List.of(), Map.of(), "Deleted"); @@ -76,17 +72,20 @@ public class DbgModelTargetBreakpointContainerImpl extends DbgModelTargetObjectI @Override public void breakpointHit(DbgBreakpointInfo info, DbgCause cause) { + DbgModelTargetThread targetThread = + getParentProcess().getThreads().getTargetThread(getManager().getEventThread()); DbgModelTargetBreakpointSpec spec = getTargetBreakpointSpec(info); - listeners.fire(TargetBreakpointListener.class) - .breakpointHit(this, getParentProcess(), null, spec, spec); + listeners.fire.breakpointHit(getProxy(), targetThread, null, spec, spec); spec.breakpointHit(); } public DbgModelTargetBreakpointSpec getTargetBreakpointSpec(DbgBreakpointInfo info) { - synchronized (this) { - return getSpecsByNumber().computeIfAbsent(info.getNumber(), - i -> new DbgModelTargetBreakpointSpecImpl(this, info)); + DbgModelImpl impl = (DbgModelImpl) model; + TargetObject modelObject = impl.getModelObject(info.getDebugBreakpoint()); + if (modelObject != null) { + return (DbgModelTargetBreakpointSpec) modelObject; } + return new DbgModelTargetBreakpointSpecImpl(this, info); } @Override @@ -103,8 +102,4 @@ public class DbgModelTargetBreakpointContainerImpl extends DbgModelTargetObjectI setElements(specs, Map.of(), "Refreshed"); }); } - - public Map getSpecsByNumber() { - return specsByNumber; - } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetBreakpointSpecImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetBreakpointSpecImpl.java index dcfa6524d2..346e1fbb37 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetBreakpointSpecImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetBreakpointSpecImpl.java @@ -21,7 +21,8 @@ import java.util.concurrent.CompletableFuture; import agent.dbgeng.manager.breakpoint.DbgBreakpointInfo; import agent.dbgeng.model.iface2.DbgModelTargetBreakpointContainer; import agent.dbgeng.model.iface2.DbgModelTargetBreakpointSpec; -import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointLocation; +import ghidra.dbg.target.TargetBreakpointSpec; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; import ghidra.dbg.util.PathUtils; @@ -56,20 +57,18 @@ public class DbgModelTargetBreakpointSpecImpl extends DbgModelTargetObjectImpl public void changeAttributeSet(String reason) { this.changeAttributes(List.of(), List.of(), Map.of( // - DISPLAY_ATTRIBUTE_NAME, "[" + info.getNumber() + "] " + info.getLocation(), // + DISPLAY_ATTRIBUTE_NAME, "[" + info.getNumber() + "] " + info.getExpression(), // ADDRESS_ATTRIBUTE_NAME, doGetAddress(), // LENGTH_ATTRIBUTE_NAME, info.getSize(), // - AFFECTS_ATTRIBUTE_NAME, doGetAffects(), // SPEC_ATTRIBUTE_NAME, this, // - EXPRESSION_ATTRIBUTE_NAME, info.getLocation(), // + EXPRESSION_ATTRIBUTE_NAME, info.getExpression(), // KINDS_ATTRIBUTE_NAME, getKinds() // ), reason); this.changeAttributes(List.of(), List.of(), Map.of( // BPT_TYPE_ATTRIBUTE_NAME, info.getType().name(), // BPT_DISP_ATTRIBUTE_NAME, info.getDisp().name(), // BPT_PENDING_ATTRIBUTE_NAME, info.getPending(), // - BPT_TIMES_ATTRIBUTE_NAME, info.getTimes(), // - TargetObject.UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + BPT_TIMES_ATTRIBUTE_NAME, info.getTimes() // ), reason); } @@ -84,11 +83,13 @@ public class DbgModelTargetBreakpointSpecImpl extends DbgModelTargetObjectImpl public DbgModelTargetBreakpointSpecImpl(DbgModelTargetBreakpointContainer breakpoints, DbgBreakpointInfo info) { super(breakpoints.getModel(), breakpoints, keyBreakpoint(info), "BreakpointSpec"); - this.setBreakpointInfo(info); + this.getModel().addModelObject(info.getDebugBreakpoint(), this); + //this.setBreakpointInfo(info); updateInfo(null, info, "Created"); } + @Override public void updateInfo(DbgBreakpointInfo oldInfo, DbgBreakpointInfo newInfo, String reason) { synchronized (this) { assert oldInfo == getBreakpointInfo(); @@ -116,18 +117,19 @@ public class DbgModelTargetBreakpointSpecImpl extends DbgModelTargetObjectImpl /** * Update the enabled field * - * This does not actually toggle the breakpoint. It just updates the field and calls the proper - * listeners. To actually toggle the breakpoint, use {@link #toggle(boolean)} instead, which if - * effective, should eventually cause this method to be called. + * This does not actually toggle the breakpoint. It just updates the field + * and calls the proper listeners. To actually toggle the breakpoint, use + * {@link #toggle(boolean)} instead, which if effective, should eventually + * cause this method to be called. * * @param enabled true if enabled, false if disabled * @param reason a description of the cause (not really used, yet) */ + @Override public void setEnabled(boolean enabled, String reason) { setBreakpointEnabled(enabled); changeAttributes(List.of(), List.of(), Map.of(ENABLED_ATTRIBUTE_NAME, enabled // ), reason); - getListeners().fire(TargetBreakpointSpecListener.class).breakpointToggled(this, enabled); } @Override @@ -140,6 +142,7 @@ public class DbgModelTargetBreakpointSpecImpl extends DbgModelTargetObjectImpl this.enabled = enabled; } + @Override public ListenerSet getActions() { return actions; } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetConnectorContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetConnectorContainerImpl.java index 28f154ae66..994184e4dc 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetConnectorContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetConnectorContainerImpl.java @@ -23,13 +23,32 @@ import agent.dbgeng.model.iface2.DbgModelTargetRoot; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; -@TargetObjectSchemaInfo(name = "ConnectorContainer", attributes = { // - @TargetAttributeType(name = "Launch process", type = DbgModelTargetProcessLaunchConnectorImpl.class, required = true, fixed = true), // - @TargetAttributeType(name = "Attach to process", type = DbgModelTargetProcessAttachConnectorImpl.class, required = true, fixed = true), // - @TargetAttributeType(name = "Load trace/dump", type = DbgModelTargetTraceOrDumpConnectorImpl.class, required = true, fixed = true), // - @TargetAttributeType(name = "Attach to kernel", type = DbgModelTargetKernelConnectorImpl.class, required = true, fixed = true), // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "ConnectorContainer", + attributes = { + @TargetAttributeType( + name = "Launch process", + type = DbgModelTargetProcessLaunchConnectorImpl.class, + required = true, + fixed = true), + @TargetAttributeType( + name = "Attach to process", + type = DbgModelTargetProcessAttachConnectorImpl.class, + required = true, + fixed = true), + @TargetAttributeType( + name = "Load trace/dump", + type = DbgModelTargetTraceOrDumpConnectorImpl.class, + required = true, + fixed = true), + @TargetAttributeType( + name = "Attach to kernel", + type = DbgModelTargetKernelConnectorImpl.class, + required = true, + fixed = true), + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class DbgModelTargetConnectorContainerImpl extends DbgModelTargetObjectImpl { protected final DbgModelTargetRoot root; diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetDebugContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetDebugContainerImpl.java index eb078c95cc..ed0c590045 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetDebugContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetDebugContainerImpl.java @@ -23,10 +23,17 @@ import agent.dbgeng.model.iface2.DbgModelTargetProcess; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; -@TargetObjectSchemaInfo(name = "DebugContainer", attributes = { // - @TargetAttributeType(name = "Breakpoints", type = DbgModelTargetBreakpointContainerImpl.class, required = true, fixed = true), // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "DebugContainer", + attributes = { + @TargetAttributeType( + name = "Breakpoints", + type = DbgModelTargetBreakpointContainerImpl.class, + required = true, + fixed = true), + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class DbgModelTargetDebugContainerImpl extends DbgModelTargetObjectImpl implements DbgModelTargetDebugContainer { diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetKernelConnectorImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetKernelConnectorImpl.java index b21422256d..8b3288f2e3 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetKernelConnectorImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetKernelConnectorImpl.java @@ -28,11 +28,14 @@ import ghidra.dbg.target.TargetMethod.ParameterDescription; import ghidra.dbg.target.TargetMethod.TargetParameterMap; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "KernelConnector", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo( + name = "KernelConnector", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(type = Void.class) + }) public class DbgModelTargetKernelConnectorImpl extends DbgModelTargetObjectImpl implements DbgModelTargetConnector { @@ -47,8 +50,7 @@ public class DbgModelTargetKernelConnectorImpl extends DbgModelTargetObjectImpl changeAttributes(List.of(), List.of(), Map.of( // DISPLAY_ATTRIBUTE_NAME, getDisplay(), // TargetMethod.PARAMETERS_ATTRIBUTE_NAME, - paramDescs = TargetParameterMap.copyOf(computeParameters()), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + paramDescs = TargetParameterMap.copyOf(computeParameters()) // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetMemoryContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetMemoryContainerImpl.java index 04995966b7..1b96e5c0cb 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetMemoryContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetMemoryContainerImpl.java @@ -27,6 +27,7 @@ import agent.dbgeng.manager.DbgModuleMemory; import agent.dbgeng.manager.cmd.*; import agent.dbgeng.manager.impl.DbgManagerImpl; import agent.dbgeng.model.iface2.*; +import ghidra.async.AsyncUtils; import ghidra.dbg.error.DebuggerMemoryAccessException; import ghidra.dbg.error.DebuggerModelAccessException; import ghidra.dbg.target.TargetObject; @@ -34,15 +35,9 @@ import ghidra.dbg.target.schema.*; import ghidra.program.model.address.Address; import ghidra.util.datastruct.WeakValueHashMap; -@TargetObjectSchemaInfo( - name = "Memory", - elements = { - @TargetElementType(type = DbgModelTargetMemoryRegionImpl.class) - }, - attributes = { - @TargetAttributeType(type = Void.class) - }, - canonicalContainer = true) +@TargetObjectSchemaInfo(name = "Memory", elements = { + @TargetElementType(type = DbgModelTargetMemoryRegionImpl.class) }, attributes = { + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class DbgModelTargetMemoryContainerImpl extends DbgModelTargetObjectImpl implements DbgModelTargetMemoryContainer { @@ -58,7 +53,10 @@ public class DbgModelTargetMemoryContainerImpl extends DbgModelTargetObjectImpl @Override public CompletableFuture requestElements(boolean refresh) { - //return CompletableFuture.completedFuture(null); + DbgModelTargetProcess targetProcess = getParentProcess(); + if (!targetProcess.getProcess().equals(getManager().getCurrentProcess())) { + return AsyncUtils.NIL; + } return listMemory().thenAccept(byName -> { List sections; synchronized (this) { @@ -111,7 +109,7 @@ public class DbgModelTargetMemoryContainerImpl extends DbgModelTargetObjectImpl if (range == null) { throw new DebuggerMemoryAccessException("Cannot read at " + address); } - listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, buf.array()); + listeners.fire.memoryUpdated(getProxy(), address, buf.array()); return Arrays.copyOf(buf.array(), (int) (range.upperEndpoint() - range.lowerEndpoint())); } @@ -128,11 +126,15 @@ public class DbgModelTargetMemoryContainerImpl extends DbgModelTargetObjectImpl } private void writeAssist(Address address, byte[] data) { - listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, data); + listeners.fire.memoryUpdated(getProxy(), address, data); } @Override public CompletableFuture readMemory(Address address, int length) { + return model.gateFuture(doReadMemory(address, length)); + } + + protected CompletableFuture doReadMemory(Address address, int length) { DbgManagerImpl manager = getManager(); if (manager.isWaiting()) { throw new DebuggerModelAccessException( @@ -197,6 +199,10 @@ public class DbgModelTargetMemoryContainerImpl extends DbgModelTargetObjectImpl @Override public CompletableFuture writeMemory(Address address, byte[] data) { + return model.gateFuture(doWriteMemory(address, data)); + } + + protected CompletableFuture doWriteMemory(Address address, byte[] data) { DbgManagerImpl manager = getManager(); if (manager.isWaiting()) { throw new DebuggerModelAccessException( @@ -252,16 +258,6 @@ public class DbgModelTargetMemoryContainerImpl extends DbgModelTargetObjectImpl return CompletableFuture.completedFuture(null); } - public void invalidateMemoryCaches() { - listeners.fire.invalidateCacheRequested(this); - } - - @Override - public void onRunning() { - invalidateMemoryCaches(); - setAccessible(false); - } - @Override protected void update() { requestElements(true); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetMemoryRegionImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetMemoryRegionImpl.java index 0629696c15..2a8ffe5865 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetMemoryRegionImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetMemoryRegionImpl.java @@ -26,22 +26,18 @@ import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.*; -@TargetObjectSchemaInfo(name = "MemoryRegion", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType( // - name = TargetMemoryRegion.MEMORY_ATTRIBUTE_NAME, // - type = DbgModelTargetMemoryContainerImpl.class), // - @TargetAttributeType(name = "BaseAddress", type = Address.class), // - @TargetAttributeType(name = "EndAddress", type = Address.class), // - @TargetAttributeType(name = "RegionSize", type = String.class), // - @TargetAttributeType(name = "AllocationBase", type = Address.class), // - @TargetAttributeType(name = "AllocationProtect", type = String.class), // - @TargetAttributeType(name = "Protect", type = String.class), // - @TargetAttributeType(name = "State", type = String.class), // - @TargetAttributeType(name = "Type", type = String.class), // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo(name = "MemoryRegion", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(name = TargetMemoryRegion.MEMORY_ATTRIBUTE_NAME, type = DbgModelTargetMemoryContainerImpl.class), + @TargetAttributeType(name = "BaseAddress", type = Address.class), + @TargetAttributeType(name = "EndAddress", type = Address.class), + @TargetAttributeType(name = "RegionSize", type = String.class), + @TargetAttributeType(name = "AllocationBase", type = Address.class), + @TargetAttributeType(name = "AllocationProtect", type = String.class), + @TargetAttributeType(name = "Protect", type = String.class), + @TargetAttributeType(name = "State", type = String.class), + @TargetAttributeType(name = "Type", type = String.class), + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetMemoryRegionImpl extends DbgModelTargetObjectImpl implements DbgModelTargetMemoryRegion { @@ -64,6 +60,7 @@ public class DbgModelTargetMemoryRegionImpl extends DbgModelTargetObjectImpl public DbgModelTargetMemoryRegionImpl(DbgModelTargetMemoryContainer memory, DbgModuleMemory region) { super(memory.getModel(), memory, keySection(region), "Region"); + this.getModel().addModelObject(region, this); this.section = region; this.range = doGetRange(section); @@ -92,8 +89,7 @@ public class DbgModelTargetMemoryRegionImpl extends DbgModelTargetObjectImpl RANGE_ATTRIBUTE_NAME, doGetRange(section), // READABLE_ATTRIBUTE_NAME, isReadable(), // WRITABLE_ATTRIBUTE_NAME, isWritable(), // - EXECUTABLE_ATTRIBUTE_NAME, isExecutable(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + EXECUTABLE_ATTRIBUTE_NAME, isExecutable() // ), "Initialized"); AddressSpace space = getModel().getAddressSpace("ram"); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleContainerImpl.java index e5db2bbac7..d5833593de 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleContainerImpl.java @@ -22,42 +22,36 @@ import agent.dbgeng.manager.DbgModule; import agent.dbgeng.manager.DbgProcess; import agent.dbgeng.model.iface2.DbgModelTargetModule; import agent.dbgeng.model.iface2.DbgModelTargetModuleContainer; -import ghidra.async.AsyncFence; -import ghidra.async.AsyncLazyMap; -import ghidra.dbg.target.TargetModule; -import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.*; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.lifecycle.Internal; -import ghidra.util.Msg; -@TargetObjectSchemaInfo( - name = "ModuleContainer", - elements = { // - @TargetElementType(type = DbgModelTargetModuleImpl.class) // - }, - attributes = { // - @TargetAttributeType(type = Void.class) // - }, - canonicalContainer = true) +@TargetObjectSchemaInfo(name = "ModuleContainer", elements = { // + @TargetElementType(type = DbgModelTargetModuleImpl.class) // +}, // + elementResync = ResyncMode.ONCE, // + attributes = { // + @TargetAttributeType(type = Void.class) // + }, canonicalContainer = true) public class DbgModelTargetModuleContainerImpl extends DbgModelTargetObjectImpl implements DbgModelTargetModuleContainer { // NOTE: -file-list-shared-libraries omits the main module and system-supplied DSO. + protected final DbgModelTargetProcessImpl targetProcess; protected final DbgProcess process; - // TODO: Is it possible to load the same object twice? - protected final AsyncLazyMap modulesByName = - new AsyncLazyMap(new HashMap<>(), this::doGetTargetModule); - public DbgModelTargetModuleContainerImpl(DbgModelTargetProcessImpl process) { super(process.getModel(), process, "Modules", "ModuleContainer"); + this.targetProcess = process; this.process = process.process; + requestElements(false); } @Override @Internal public void libraryLoaded(String name) { - CompletableFuture module; + DbgModelTargetModule module; synchronized (this) { /** * It's not a good idea to remove "stale" entries. If the entry's already present, it's @@ -65,32 +59,26 @@ public class DbgModelTargetModuleContainerImpl extends DbgModelTargetObjectImpl * sections loaded. Removing it will cause it to load all module sections again! */ //modulesByName.remove(name); - module = doGetTargetModule(name); + module = getTargetModule(name); } - module.thenAccept(mod -> { - changeElements(List.of(), List.of(mod), Map.of(), "Loaded"); - getListeners().fire(TargetEventScopeListener.class) - .event(this, null, TargetEventType.MODULE_LOADED, "Library " + name + " loaded", - List.of(mod)); - }).exceptionally(e -> { - Msg.error(this, "Problem getting module for library load: " + name, e); - return null; - }); + TargetThread eventThread = + (TargetThread) getModel().getModelObject(getManager().getEventThread()); + changeElements(List.of(), List.of(module), Map.of(), "Loaded"); + getListeners().fire.event(getProxy(), eventThread, TargetEventType.MODULE_LOADED, + "Library " + name + " loaded", List.of(module)); } @Override @Internal public void libraryUnloaded(String name) { - if (!modulesByName.containsKey(name)) { - return; - } - modulesByName.get(name).thenAccept(mod -> { - getListeners().fire(TargetEventScopeListener.class) - .event(this, null, TargetEventType.MODULE_UNLOADED, - "Library " + name + " unloaded", List.of(mod)); - }); - synchronized (this) { - modulesByName.remove(name); + DbgModelTargetModule targetModule = getTargetModule(name); + if (targetModule != null) { + TargetThread eventThread = + (TargetThread) getModel().getModelObject(getManager().getEventThread()); + getListeners().fire.event(getProxy(), eventThread, TargetEventType.MODULE_UNLOADED, + "Library " + name + " unloaded", List.of(targetModule)); + DbgModelImpl impl = (DbgModelImpl) model; + impl.deleteModelObject(targetModule.getDbgModule()); } changeElements(List.of(name), List.of(), Map.of(), "Unloaded"); } @@ -108,34 +96,28 @@ public class DbgModelTargetModuleContainerImpl extends DbgModelTargetObjectImpl @Override public CompletableFuture requestElements(boolean refresh) { List result = new ArrayList<>(); - return process.listModules().thenCompose(byName -> { - AsyncFence fence = new AsyncFence(); + return process.listModules().thenAccept(byName -> { synchronized (this) { - modulesByName.retainKeys(byName.keySet()); for (Map.Entry ent : byName.entrySet()) { - fence.include(getTargetModule(ent.getKey()).thenAccept(module -> { - result.add(module); - })); + result.add(getTargetModule(ent.getKey())); } } - return fence.ready(); - }).thenAccept(__ -> { changeElements(List.of(), result, Map.of(), "Refreshed"); }); } - protected CompletableFuture doGetTargetModule(String name) { + public DbgModelTargetModule getTargetModule(String name) { // Only get here from libraryLoaded or getElements. The known list should be fresh. DbgModule module = process.getKnownModules().get(name); if (module == null) { - return CompletableFuture.completedFuture(null); + return null; } - return CompletableFuture.completedFuture(new DbgModelTargetModuleImpl(this, module)); - //TODO: return module.listSections().thenApply(__ -> new DbgModelTargetModule(this, module)); + DbgModelImpl impl = (DbgModelImpl) model; + TargetObject modelObject = impl.getModelObject(module); + if (modelObject != null) { + return (DbgModelTargetModule) modelObject; + } + return new DbgModelTargetModuleImpl(this, module); } - @Override - public CompletableFuture getTargetModule(String name) { - return modulesByName.get(name); - } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleImpl.java index 59ae3a746d..1e5781375f 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleImpl.java @@ -24,16 +24,14 @@ import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.*; -@TargetObjectSchemaInfo(name = "Module", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(name = "Symbols", type = DbgModelTargetSymbolContainerImpl.class, required = true, fixed = true), // - @TargetAttributeType(name = "BaseAddress", type = Address.class), // - @TargetAttributeType(name = "ImageName", type = String.class), // - @TargetAttributeType(name = "TimeStamp", type = Integer.class), // - @TargetAttributeType(name = "Len", type = String.class), // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo(name = "Module", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(name = "Symbols", type = DbgModelTargetSymbolContainerImpl.class, required = true, fixed = true), + @TargetAttributeType(name = "BaseAddress", type = Address.class), + @TargetAttributeType(name = "ImageName", type = String.class), + @TargetAttributeType(name = "TimeStamp", type = Integer.class), + @TargetAttributeType(name = "Len", type = String.class), + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetModuleImpl extends DbgModelTargetObjectImpl implements DbgModelTargetModule { protected static String indexModule(DbgModule module) { @@ -52,6 +50,7 @@ public class DbgModelTargetModuleImpl extends DbgModelTargetObjectImpl public DbgModelTargetModuleImpl(DbgModelTargetModuleContainerImpl modules, DbgModule module) { super(modules.getModel(), modules, keyModule(module), "Module"); + this.getModel().addModelObject(module, this); this.process = modules.process; this.module = module; diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleSectionContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleSectionContainerImpl.java index 3d4d67ac8d..796e8ea99c 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleSectionContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleSectionContainerImpl.java @@ -15,28 +15,22 @@ */ package agent.dbgeng.model.impl; -import java.util.Map; import java.util.concurrent.CompletableFuture; import agent.dbgeng.manager.DbgModule; import agent.dbgeng.manager.DbgModuleSection; import agent.dbgeng.model.iface2.*; +import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; -import ghidra.util.datastruct.WeakValueHashMap; -@TargetObjectSchemaInfo(name = "SectionContainer", elements = { // - @TargetElementType(type = DbgModelTargetModuleSectionImpl.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo(name = "SectionContainer", elements = { + @TargetElementType(type = DbgModelTargetModuleSectionImpl.class) }, attributes = { + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class DbgModelTargetModuleSectionContainerImpl extends DbgModelTargetObjectImpl implements DbgModelTargetModuleSectionContainer { protected final DbgModule module; - protected final Map sectionsByStart = - new WeakValueHashMap<>(); - public DbgModelTargetModuleSectionContainerImpl(DbgModelTargetModule module) { super(module.getModel(), module, "Sections", "ModuleSections"); this.module = module.getDbgModule(); @@ -61,8 +55,12 @@ public class DbgModelTargetModuleSectionContainerImpl extends DbgModelTargetObje } protected synchronized DbgModelTargetModuleSection getModuleSection(DbgModuleSection section) { - return sectionsByStart.computeIfAbsent(section.getStart(), - s -> new DbgModelTargetModuleSectionImpl(this, section)); + DbgModelImpl impl = (DbgModelImpl) model; + TargetObject modelObject = impl.getModelObject(section); + if (modelObject != null) { + return (DbgModelTargetModuleSection) modelObject; + } + return new DbgModelTargetModuleSectionImpl(this, section); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleSectionImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleSectionImpl.java index 6f1eb35fd1..220da5a051 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleSectionImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetModuleSectionImpl.java @@ -23,14 +23,9 @@ import agent.dbgeng.model.iface2.DbgModelTargetModuleSection; import ghidra.dbg.target.schema.*; import ghidra.program.model.address.*; -@TargetObjectSchemaInfo( - name = "Section", - elements = { // - @TargetElementType(type = Void.class) // - }, - attributes = { // - @TargetAttributeType(type = Void.class) // - }) +@TargetObjectSchemaInfo(name = "Section", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetModuleSectionImpl extends DbgModelTargetObjectImpl implements DbgModelTargetModuleSection { protected static final String OBJFILE_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "objfile"; @@ -40,6 +35,7 @@ public class DbgModelTargetModuleSectionImpl extends DbgModelTargetObjectImpl public DbgModelTargetModuleSectionImpl(DbgModelTargetModuleSectionContainerImpl sections, DbgModuleSection section) { super(sections.getModel(), sections, section.getName(), "Section"); + this.getModel().addModelObject(section, this); AddressSpace space = getModel().getAddressSpace("ram"); Address min = space.getAddress(section.getStart()); @@ -50,8 +46,7 @@ public class DbgModelTargetModuleSectionImpl extends DbgModelTargetObjectImpl changeAttributes(List.of(), List.of(), Map.of( // MODULE_ATTRIBUTE_NAME, sections.getParent(), // RANGE_ATTRIBUTE_NAME, range, // - DISPLAY_ATTRIBUTE_NAME, section.getName(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, section.getName() // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetObjectImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetObjectImpl.java index 0b2b292976..adbcf161a8 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetObjectImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetObjectImpl.java @@ -26,7 +26,6 @@ import agent.dbgeng.model.iface1.DbgModelTargetExecutionStateful; import agent.dbgeng.model.iface2.*; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.schema.TargetObjectSchema; @@ -71,10 +70,6 @@ public class DbgModelTargetObjectImpl extends DefaultTargetObject> requestNativeElements() { + throw new AssertionError(); // shouldn't ever be here + } + @Override public DbgModelTargetSession getParentSession() { DbgModelTargetObject test = (DbgModelTargetObject) parent; @@ -178,7 +182,6 @@ public class DbgModelTargetObjectImpl extends DefaultTargetObject map, boolean b) { if (modified) { map.put(MODIFIED_ATTRIBUTE_NAME, modified); - listeners.fire.displayChanged(this, getDisplay()); } } @@ -188,7 +191,6 @@ public class DbgModelTargetObjectImpl extends DefaultTargetObject type) { + List pathToClass = model.getRootSchema().searchForSuitable(type, path); + return model.getModelObject(pathToClass); + } + } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessAttachConnectorImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessAttachConnectorImpl.java index a8ff4b81a2..fd6b4189bd 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessAttachConnectorImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessAttachConnectorImpl.java @@ -29,11 +29,14 @@ import ghidra.dbg.target.TargetMethod.ParameterDescription; import ghidra.dbg.target.TargetMethod.TargetParameterMap; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "ProcessAttachConnector", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo( + name = "ProcessAttachConnector", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(type = Void.class) + }) public class DbgModelTargetProcessAttachConnectorImpl extends DbgModelTargetObjectImpl implements DbgModelTargetConnector { @@ -48,8 +51,7 @@ public class DbgModelTargetProcessAttachConnectorImpl extends DbgModelTargetObje changeAttributes(List.of(), List.of(), Map.of( // DISPLAY_ATTRIBUTE_NAME, getDisplay(), // TargetMethod.PARAMETERS_ATTRIBUTE_NAME, - paramDescs = TargetParameterMap.copyOf(computeParameters()), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + paramDescs = TargetParameterMap.copyOf(computeParameters()) // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessContainerImpl.java index d2106b42ff..ec47124bf7 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessContainerImpl.java @@ -25,23 +25,13 @@ import agent.dbgeng.manager.*; import agent.dbgeng.model.iface2.*; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; -import ghidra.util.datastruct.WeakValueHashMap; -@TargetObjectSchemaInfo( - name = "ProcessContainer", - elements = { // - @TargetElementType(type = DbgModelTargetProcessImpl.class) // - }, - attributes = { // - @TargetAttributeType(type = Void.class) // - }, - canonicalContainer = true) +@TargetObjectSchemaInfo(name = "ProcessContainer", elements = { + @TargetElementType(type = DbgModelTargetProcessImpl.class) }, attributes = { + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class DbgModelTargetProcessContainerImpl extends DbgModelTargetObjectImpl implements DbgModelTargetProcessContainer { - protected final Map processesById = - new WeakValueHashMap<>(); - public DbgModelTargetProcessContainerImpl(DbgModelTargetSession session) { super(session.getModel(), session, "Processes", "ProcessContainer"); @@ -55,9 +45,9 @@ public class DbgModelTargetProcessContainerImpl extends DbgModelTargetObjectImpl DbgModelTargetProcess process = getTargetProcess(proc); changeElements(List.of(), List.of(process), Map.of(), "Added"); process.processStarted(proc.getPid()); - getListeners().fire(TargetEventScopeListener.class) - .event(this, null, TargetEventType.PROCESS_CREATED, "Process " + proc.getId() + - " started " + "notepad.exe" + " pid=" + proc.getPid(), List.of(process)); + getListeners().fire.event(getProxy(), null, TargetEventType.PROCESS_CREATED, + "Process " + proc.getId() + " started " + process.getName() + "pid=" + proc.getPid(), + List.of(process)); } @Override @@ -66,21 +56,8 @@ public class DbgModelTargetProcessContainerImpl extends DbgModelTargetObjectImpl process.processStarted(proc.getPid()); } - @Override - public void processExited(DbgProcess proc, DbgCause cause) { - DbgModelTargetProcess process = getTargetProcess(proc); - process.processExited(proc.getExitCode()); - getListeners().fire(TargetEventScopeListener.class) - .event(this, null, TargetEventType.PROCESS_EXITED, - "Process " + proc.getId() + " exited code=" + proc.getExitCode(), - List.of(process)); - } - @Override public void processRemoved(DebugProcessId processId, DbgCause cause) { - synchronized (this) { - processesById.remove(processId); - } changeElements(List.of( // DbgModelTargetProcessImpl.indexProcess(processId) // ), List.of(), Map.of(), "Removed"); @@ -96,21 +73,24 @@ public class DbgModelTargetProcessContainerImpl extends DbgModelTargetObjectImpl public void threadStateChanged(DbgThread thread, DbgState state, DbgCause cause, DbgReason reason) { DbgModelTargetProcess process = getTargetProcess(thread.getProcess()); - process.threadStateChanged(thread, state, cause, reason); + process.threadStateChangedSpecific(thread, state); } @Override public void threadExited(DebugThreadId threadId, DbgProcess proc, DbgCause cause) { - DbgModelTargetProcess targetProcess = processesById.get(proc.getId()); - if (targetProcess != null) { - targetProcess.getThreads().threadExited(threadId); + DbgModelTargetProcess process = getTargetProcess(proc); + if (process != null) { + process.getThreads().threadExited(threadId); } } @Override public void moduleLoaded(DbgProcess proc, DebugModuleInfo info, DbgCause cause) { DbgModelTargetProcess process = getTargetProcess(proc); - process.getModules().libraryLoaded(info.toString()); + DbgModelTargetModuleContainer modules = process.getModules(); + if (modules != null) { + modules.libraryLoaded(info.toString()); + } } @Override @@ -135,14 +115,22 @@ public class DbgModelTargetProcessContainerImpl extends DbgModelTargetObjectImpl @Override public synchronized DbgModelTargetProcess getTargetProcess(DebugProcessId id) { - return processesById.computeIfAbsent(id, - i -> new DbgModelTargetProcessImpl(this, getManager().getKnownProcesses().get(id))); + DbgModelImpl impl = (DbgModelImpl) model; + TargetObject modelObject = impl.getModelObject(id); + if (modelObject != null) { + return (DbgModelTargetProcess) modelObject; + } + return new DbgModelTargetProcessImpl(this, getManager().getKnownProcesses().get(id)); } @Override public synchronized DbgModelTargetProcess getTargetProcess(DbgProcess process) { - return processesById.computeIfAbsent(process.getId(), - i -> new DbgModelTargetProcessImpl(this, process)); + DbgModelImpl impl = (DbgModelImpl) model; + TargetObject modelObject = impl.getModelObject(process); + if (modelObject != null) { + return (DbgModelTargetProcess) modelObject; + } + return new DbgModelTargetProcessImpl(this, process); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessImpl.java index 241d328432..165313838e 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessImpl.java @@ -18,48 +18,25 @@ package agent.dbgeng.model.impl; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import agent.dbgeng.dbgeng.DebugProcessId; import agent.dbgeng.manager.*; import agent.dbgeng.manager.impl.DbgManagerImpl; import agent.dbgeng.model.iface1.DbgModelTargetFocusScope; import agent.dbgeng.model.iface2.*; -import ghidra.async.AsyncUtils; -import ghidra.async.TypeSpec; -import ghidra.dbg.DebugModelConventions; import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEventScope.TargetEventType; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; -@TargetObjectSchemaInfo( - name = "Process", - elements = { - @TargetElementType(type = Void.class) - }, - attributes = { - @TargetAttributeType( - name = "Debug", - type = DbgModelTargetDebugContainerImpl.class, - required = true, - fixed = true), - @TargetAttributeType( - name = "Memory", - type = DbgModelTargetMemoryContainerImpl.class, - required = true, - fixed = true), - @TargetAttributeType( - name = "Modules", - type = DbgModelTargetModuleContainerImpl.class, - required = true, - fixed = true), - @TargetAttributeType( - name = "Threads", - type = DbgModelTargetThreadContainerImpl.class, - required = true, - fixed = true), - @TargetAttributeType(type = Void.class) - }) +@TargetObjectSchemaInfo(name = "Process", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(name = "Debug", type = DbgModelTargetDebugContainerImpl.class, required = true, fixed = true), + @TargetAttributeType(name = "Memory", type = DbgModelTargetMemoryContainerImpl.class, required = true, fixed = true), + @TargetAttributeType(name = "Modules", type = DbgModelTargetModuleContainerImpl.class, required = true, fixed = true), + @TargetAttributeType(name = "Threads", type = DbgModelTargetThreadContainerImpl.class, required = true, fixed = true), + @TargetAttributeType(name = DbgModelTargetProcessImpl.EXIT_CODE_ATTRIBUTE_NAME, type = Long.class), + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetProcessImpl extends DbgModelTargetObjectImpl implements DbgModelTargetProcess { @@ -92,6 +69,8 @@ public class DbgModelTargetProcessImpl extends DbgModelTargetObjectImpl public DbgModelTargetProcessImpl(DbgModelTargetProcessContainer processes, DbgProcess process) { super(processes.getModel(), processes, keyProcess(process), "Process"); + this.getModel().addModelObject(process, this); + this.getModel().addModelObject(process.getId(), this); this.process = process; this.debug = new DbgModelTargetDebugContainerImpl(this); @@ -107,7 +86,7 @@ public class DbgModelTargetProcessImpl extends DbgModelTargetObjectImpl //sections, // threads // ), Map.of( // - ACCESSIBLE_ATTRIBUTE_NAME, false, // + ACCESSIBLE_ATTRIBUTE_NAME, accessible = false, // DISPLAY_ATTRIBUTE_NAME, getDisplay(), // TargetMethod.PARAMETERS_ATTRIBUTE_NAME, PARAMETERS, // SUPPORTED_ATTACH_KINDS_ATTRIBUTE_NAME, SUPPORTED_KINDS, // @@ -129,36 +108,28 @@ public class DbgModelTargetProcessImpl extends DbgModelTargetObjectImpl @Override public void processSelected(DbgProcess eventProcess, DbgCause cause) { if (eventProcess.equals(process)) { - AtomicReference scope = new AtomicReference<>(); - AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - DebugModelConventions.findSuitable(DbgModelTargetFocusScope.class, this) - .handle(seq::next); - }, scope).then(seq -> { - scope.get().setFocus(this); - }).finish(); + ((DbgModelTargetFocusScope) searchForSuitable(TargetFocusScope.class)).setFocus(this); } } - @Override - public void threadStateChanged(DbgThread thread, DbgState state, DbgCause cause, - DbgReason reason) { + public void threadStateChangedSpecific(DbgThread thread, DbgState state) { TargetExecutionState targetState = convertState(state); setExecutionState(targetState, "ThreadStateChanged"); } @Override public CompletableFuture launch(List args) { - return DbgModelImplUtils.launch(getModel(), process, args); + return model.gateFuture(DbgModelImplUtils.launch(getModel(), process, args)); } @Override public CompletableFuture resume() { - return process.cont(); + return model.gateFuture(process.cont()); } @Override public CompletableFuture kill() { - return process.kill(); + return model.gateFuture(process.kill()); } @Override @@ -166,22 +137,22 @@ public class DbgModelTargetProcessImpl extends DbgModelTargetObjectImpl getModel().assertMine(TargetObject.class, attachable); // NOTE: Get the object and type check it myself. // The typed ref could have been unsafely cast - return process.reattach(attachable).thenApply(set -> null); + return model.gateFuture(process.reattach(attachable)).thenApply(set -> null); } @Override public CompletableFuture attach(long pid) { - return process.attach(pid).thenApply(set -> null); + return model.gateFuture(process.attach(pid)).thenApply(set -> null); } @Override public CompletableFuture detach() { - return process.detach(); + return model.gateFuture(process.detach()); } @Override public CompletableFuture delete() { - return process.remove(); + return model.gateFuture(process.remove()); } @Override @@ -192,13 +163,13 @@ public class DbgModelTargetProcessImpl extends DbgModelTargetObjectImpl case ADVANCE: // Why no exec-advance in dbgeng? throw new UnsupportedOperationException(kind.name()); default: - return process.step(convertToDbg(kind)); + return model.gateFuture(process.step(convertToDbg(kind))); } } @Override public CompletableFuture step(Map args) { - return process.step(args); + return model.gateFuture(process.step(args)); } @Override @@ -213,20 +184,16 @@ public class DbgModelTargetProcessImpl extends DbgModelTargetObjectImpl } @Override - public void processExited(Long exitCode) { - if (exitCode != null) { + public void processExited(DbgProcess proc, DbgCause cause) { + if (proc.equals(this.process)) { changeAttributes(List.of(), List.of(), Map.of( // - EXIT_CODE_ATTRIBUTE_NAME, exitCode // + STATE_ATTRIBUTE_NAME, TargetExecutionState.TERMINATED, // + EXIT_CODE_ATTRIBUTE_NAME, proc.getExitCode() // ), "Exited"); + getListeners().fire.event(getProxy(), null, TargetEventType.PROCESS_EXITED, + "Process " + proc.getId() + " exited code=" + proc.getExitCode(), + List.of(getProxy())); } - setExecutionState(TargetExecutionState.TERMINATED, "Exited"); - } - - @Override - public void onExit() { - super.onExit(); - DbgModelTargetProcessContainer processes = (DbgModelTargetProcessContainer) getParent(); - processes.processRemoved(process.getId(), DbgCause.Causes.UNCLAIMED); } @Override diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessLaunchConnectorImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessLaunchConnectorImpl.java index 2911cacfcd..93bac17286 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessLaunchConnectorImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetProcessLaunchConnectorImpl.java @@ -27,11 +27,14 @@ import ghidra.dbg.target.TargetMethod.ParameterDescription; import ghidra.dbg.target.TargetMethod.TargetParameterMap; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "ProcessLaunchConnector", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo( + name = "ProcessLaunchConnector", + elements = { // + @TargetElementType(type = Void.class) // + }, + attributes = { // + @TargetAttributeType(type = Void.class) // + }) public class DbgModelTargetProcessLaunchConnectorImpl extends DbgModelTargetObjectImpl implements DbgModelTargetConnector { @@ -46,8 +49,7 @@ public class DbgModelTargetProcessLaunchConnectorImpl extends DbgModelTargetObje changeAttributes(List.of(), List.of(), Map.of( // DISPLAY_ATTRIBUTE_NAME, getDisplay(), // TargetMethod.PARAMETERS_ATTRIBUTE_NAME, - paramDescs = TargetParameterMap.copyOf(computeParameters()), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + paramDescs = TargetParameterMap.copyOf(computeParameters()) // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRegisterContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRegisterContainerImpl.java index ae595a6bf0..c257de0766 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRegisterContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRegisterContainerImpl.java @@ -20,42 +20,36 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; -import agent.dbgeng.manager.DbgThread; +import agent.dbgeng.manager.*; import agent.dbgeng.manager.impl.DbgRegister; -import agent.dbgeng.manager.impl.DbgRegisterSet; import agent.dbgeng.model.iface2.*; import ghidra.async.AsyncUtils; -import ghidra.async.TypeSpec; import ghidra.dbg.error.DebuggerRegisterAccessException; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetRegisterBank; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.dbg.util.ConversionUtils; -@TargetObjectSchemaInfo( - name = "RegisterContainer", - elements = { - @TargetElementType(type = DbgModelTargetRegisterImpl.class) - }, - attributes = { - @TargetAttributeType( - name = TargetRegisterBank.DESCRIPTIONS_ATTRIBUTE_NAME, - type = DbgModelTargetRegisterContainerImpl.class), - @TargetAttributeType(type = Void.class) - }, - canonicalContainer = true) +@TargetObjectSchemaInfo(name = "RegisterContainer", elements = { + @TargetElementType(type = DbgModelTargetRegisterImpl.class) }, elementResync = ResyncMode.ONCE, // + attributes = { + @TargetAttributeType(name = TargetRegisterBank.DESCRIPTIONS_ATTRIBUTE_NAME, type = DbgModelTargetRegisterContainerImpl.class), + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class DbgModelTargetRegisterContainerImpl extends DbgModelTargetObjectImpl implements DbgModelTargetRegisterContainerAndBank { protected final DbgThread thread; - protected final Map registersByNumber = new HashMap<>(); protected final Map registersByName = new HashMap<>(); + private Map values = new HashMap<>(); + public DbgModelTargetRegisterContainerImpl(DbgModelTargetThread thread) { super(thread.getModel(), thread, "Registers", "RegisterContainer"); this.thread = thread.getThread(); + requestElements(false); changeAttributes(List.of(), List.of(), Map.of( // TargetRegisterBank.DESCRIPTIONS_ATTRIBUTE_NAME, this // ), "Initialized"); @@ -64,9 +58,13 @@ public class DbgModelTargetRegisterContainerImpl extends DbgModelTargetObjectImp @Override public CompletableFuture requestElements(boolean refresh) { return thread.listRegisters().thenAccept(regs -> { - if (regs.size() != registersByNumber.size()) { - registersByNumber.clear(); + if (regs.size() != registersByName.size()) { + DbgModelImpl impl = (DbgModelImpl) model; + for (DbgRegister reg : regs) { + impl.deleteModelObject(reg); + } registersByName.clear(); + } List registers; synchronized (this) { @@ -79,10 +77,20 @@ public class DbgModelTargetRegisterContainerImpl extends DbgModelTargetObjectImp }); } + public void threadStateChangedSpecific(DbgState state, DbgReason reason) { + if (state.equals(DbgState.STOPPED)) { + readRegistersNamed(getCachedElements().keySet()); + } + } + @Override public synchronized DbgModelTargetRegister getTargetRegister(DbgRegister register) { - DbgModelTargetRegister reg = registersByNumber.computeIfAbsent(register.getNumber(), - n -> new DbgModelTargetRegisterImpl(this, register)); + DbgModelImpl impl = (DbgModelImpl) model; + TargetObject modelObject = impl.getModelObject(register); + if (modelObject != null) { + return (DbgModelTargetRegister) modelObject; + } + DbgModelTargetRegister reg = new DbgModelTargetRegisterImpl(this, register); registersByName.put(register.getName(), reg); return reg; } @@ -90,14 +98,12 @@ public class DbgModelTargetRegisterContainerImpl extends DbgModelTargetObjectImp @Override public CompletableFuture> readRegistersNamed( Collection names) { - return AsyncUtils.sequence(TypeSpec.map(String.class, byte[].class)).then(seq -> { - thread.listRegisters().handle(seq::next); - }, TypeSpec.cls(DbgRegisterSet.class)).then((regs, seq) -> { - if (regs.size() != registersByNumber.size() || getCachedElements().isEmpty()) { - requestElements(true).handle(seq::next); + return model.gateFuture(thread.listRegisters().thenCompose(regs -> { + if (regs.size() != registersByName.size() || getCachedElements().isEmpty()) { + return requestElements(false); } - seq.next(null, null); - }).then(seq -> { + return AsyncUtils.NIL; + }).thenCompose(__ -> { Set toRead = new LinkedHashSet<>(); for (String regname : names) { DbgModelTargetRegister reg = registersByName.get(regname); @@ -109,11 +115,11 @@ public class DbgModelTargetRegisterContainerImpl extends DbgModelTargetObjectImp //throw new DebuggerRegisterAccessException("No such register: " + regname); } } - thread.readRegisters(toRead).handle(seq::next); - }, TypeSpec.map(DbgRegister.class, BigInteger.class)).then((vals, seq) -> { + return thread.readRegisters(toRead); + }).thenApply(vals -> { Map result = new LinkedHashMap<>(); for (DbgRegister dbgReg : vals.keySet()) { - DbgModelTargetRegister reg = registersByNumber.get(dbgReg.getNumber()); + DbgModelTargetRegister reg = getTargetRegister(dbgReg); String oldval = (String) reg.getCachedAttributes().get(VALUE_ATTRIBUTE_NAME); BigInteger value = vals.get(dbgReg); byte[] bytes = ConversionUtils.bigIntegerToBytes(dbgReg.getSize(), value); @@ -129,18 +135,17 @@ public class DbgModelTargetRegisterContainerImpl extends DbgModelTargetObjectImp reg.setModified(!value.toString(16).equals(oldval)); } } - listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, result); - seq.exit(result); - }).finish(); + this.values = result; + listeners.fire.registersUpdated(getProxy(), result); + return result; + })); } @Override public CompletableFuture writeRegistersNamed(Map values) { - return AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - thread.listRegisters().handle(seq::next); - }, TypeSpec.cls(DbgRegisterSet.class)).then((regs, seq) -> { - fetchElements().handle(seq::nextIgnore); - }).then(seq -> { + return model.gateFuture(thread.listRegisters().thenCompose(regs -> { + return requestElements(false); + }).thenCompose(__ -> { Map regs = getCachedElements(); Map toWrite = new LinkedHashMap<>(); for (Map.Entry ent : values.entrySet()) { @@ -152,32 +157,15 @@ public class DbgModelTargetRegisterContainerImpl extends DbgModelTargetObjectImp BigInteger val = new BigInteger(1, ent.getValue()); toWrite.put(reg.getRegister(), val); } - thread.writeRegisters(toWrite).handle(seq::next); + return thread.writeRegisters(toWrite); // TODO: Should probably filter only effective and normalized writes in the callback - }).then(seq -> { - listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, values); - seq.exit(); - }).finish(); + }).thenAccept(__ -> { + listeners.fire.registersUpdated(getProxy(), values); + })); } - /* - public void invalidateRegisterCaches() { - listeners.fire.invalidateCacheRequested(this); - } - */ - - @Override - public void onRunning() { - // NB: We don't want to do this apparently - //invalidateRegisterCaches(); - setAccessible(false); + public Map getCachedRegisters() { + return values; } - @Override - public void onStopped() { - setAccessible(true); - if (thread.equals(getManager().getEventThread())) { - readRegistersNamed(getCachedElements().keySet()); - } - } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRegisterImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRegisterImpl.java index f86609e92d..f131e301ce 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRegisterImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRegisterImpl.java @@ -25,14 +25,10 @@ import ghidra.dbg.target.TargetRegister; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; -@TargetObjectSchemaInfo(name = "RegisterDescriptor", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType( // - name = TargetRegister.CONTAINER_ATTRIBUTE_NAME, // - type = DbgModelTargetRegisterContainerImpl.class), // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo(name = "RegisterDescriptor", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(name = TargetRegister.CONTAINER_ATTRIBUTE_NAME, type = DbgModelTargetRegisterContainerImpl.class), + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetRegisterImpl extends DbgModelTargetObjectImpl implements DbgModelTargetRegister { @@ -56,6 +52,7 @@ public class DbgModelTargetRegisterImpl extends DbgModelTargetObjectImpl public DbgModelTargetRegisterImpl(DbgModelTargetRegisterContainerAndBank registers, DbgRegister register) { super(registers.getModel(), registers, keyRegister(register), "Register"); + this.getModel().addModelObject(register, this); this.registers = registers; this.register = register; diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRootImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRootImpl.java index e7d619f6d7..0c3e5dc828 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRootImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetRootImpl.java @@ -23,36 +23,17 @@ import agent.dbgeng.manager.impl.DbgProcessImpl; import agent.dbgeng.model.iface1.DbgModelSelectableObject; import agent.dbgeng.model.iface2.DbgModelTargetConnector; import agent.dbgeng.model.iface2.DbgModelTargetRoot; -import ghidra.async.AsyncUtils; -import ghidra.async.TypeSpec; import ghidra.dbg.error.DebuggerUserException; import ghidra.dbg.target.*; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; -@TargetObjectSchemaInfo( - name = "Debugger", - elements = { - @TargetElementType(type = Void.class) - }, - attributes = { - @TargetAttributeType( - name = "Available", - type = DbgModelTargetAvailableContainerImpl.class, - required = true, - fixed = true), - @TargetAttributeType( - name = "Connectors", - type = DbgModelTargetConnectorContainerImpl.class, - required = true, - fixed = true), - @TargetAttributeType( - name = "Sessions", - type = DbgModelTargetSessionContainerImpl.class, - required = true, - fixed = true), - @TargetAttributeType(type = Void.class) - }) +@TargetObjectSchemaInfo(name = "Debugger", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(name = "Available", type = DbgModelTargetAvailableContainerImpl.class, required = true, fixed = true), + @TargetAttributeType(name = "Connectors", type = DbgModelTargetConnectorContainerImpl.class, required = true, fixed = true), + @TargetAttributeType(name = "Sessions", type = DbgModelTargetSessionContainerImpl.class, required = true, fixed = true), + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot implements DbgModelTargetRoot { @@ -77,7 +58,7 @@ public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot connectors, // sessions // ), Map.of( // - ACCESSIBLE_ATTRIBUTE_NAME, true, // + ACCESSIBLE_ATTRIBUTE_NAME, accessible, // DISPLAY_ATTRIBUTE_NAME, "Debugger", // FOCUS_ATTRIBUTE_NAME, this, // SUPPORTED_ATTACH_KINDS_ATTRIBUTE_NAME, DbgModelTargetProcessImpl.SUPPORTED_KINDS, // @@ -117,7 +98,6 @@ public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot changeAttributes(List.of(), List.of(), Map.of( // TargetFocusScope.FOCUS_ATTRIBUTE_NAME, focus // ), "Focus changed"); - listeners.fire(TargetFocusScopeListener.class).focusChanged(this, sel); } return doFire; } @@ -125,20 +105,15 @@ public class DbgModelTargetRootImpl extends DbgModelDefaultTargetModelRoot @Override public CompletableFuture launch(Map args) { DbgModelTargetConnector targetConnector = connectors.getDefaultConnector(); - return AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - targetConnector.launch(args).handle(seq::nextIgnore); - //getManager().launch(args).handle(seq::nextIgnore); - }).finish().exceptionally((exc) -> { + return model.gateFuture(targetConnector.launch(args)).exceptionally(exc -> { throw new DebuggerUserException("Launch failed for " + args); }); } @Override public CompletableFuture attach(long pid) { - return AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - DbgProcess process = new DbgProcessImpl(getManager()); - process.attach(pid).handle(seq::nextIgnore); - }).finish(); + DbgProcess process = new DbgProcessImpl(getManager()); + return model.gateFuture(process.attach(pid)).thenApply(__ -> null); } @Override diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionAttributesImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionAttributesImpl.java index 100c0a8160..f77852ffd4 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionAttributesImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionAttributesImpl.java @@ -25,15 +25,15 @@ import ghidra.dbg.target.schema.*; @TargetObjectSchemaInfo( name = "SessionAttributes", - elements = { // - @TargetElementType(type = Void.class) // + elements = { + @TargetElementType(type = Void.class) }, - attributes = { // + attributes = { @TargetAttributeType( name = "Machine", type = DbgModelTargetSessionAttributesMachineImpl.class, - fixed = true), // - @TargetAttributeType(type = Void.class) // + fixed = true), + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetSessionAttributesImpl extends DbgModelTargetObjectImpl implements DbgModelTargetSessionAttributes { @@ -51,7 +51,7 @@ public class DbgModelTargetSessionAttributesImpl extends DbgModelTargetObjectImp ARCH_ATTRIBUTE_NAME, "x86_64", // DEBUGGER_ATTRIBUTE_NAME, "dbgeng", // OS_ATTRIBUTE_NAME, "Windows", // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + ENDIAN_ATTRIBUTE_NAME, "little" // ), "Initialized"); getManager().addEventsListener(this); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionAttributesMachineImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionAttributesMachineImpl.java index 29e8bd1466..2b89657ca2 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionAttributesMachineImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionAttributesMachineImpl.java @@ -30,15 +30,18 @@ import ghidra.async.AsyncUtils; import ghidra.async.TypeSpec; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "SessionAttributesMachine", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(name = "Arch", type = String.class), // - @TargetAttributeType(name = "Debugger", type = String.class), // - @TargetAttributeType(name = "OS", type = String.class), // - @TargetAttributeType(name = "Mode", type = String.class), // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo( + name = "SessionAttributesMachine", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(name = "Arch", type = String.class), + @TargetAttributeType(name = "Debugger", type = String.class), + @TargetAttributeType(name = "OS", type = String.class), + @TargetAttributeType(name = "Mode", type = String.class), + @TargetAttributeType(type = Void.class) + }) public class DbgModelTargetSessionAttributesMachineImpl extends DbgModelTargetObjectImpl implements DbgModelTargetSessionAttributesMachine { diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionContainerImpl.java index 0b50a149c4..ae2f3ed5b1 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionContainerImpl.java @@ -23,20 +23,15 @@ import agent.dbgeng.dbgeng.DebugSessionId; import agent.dbgeng.manager.DbgCause; import agent.dbgeng.manager.DbgSession; import agent.dbgeng.model.iface2.*; +import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; -import ghidra.util.datastruct.WeakValueHashMap; -@TargetObjectSchemaInfo(name = "SessionContainer", elements = { // - @TargetElementType(type = DbgModelTargetSessionImpl.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo(name = "SessionContainer", elements = { + @TargetElementType(type = DbgModelTargetSessionImpl.class) }, attributes = { + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class DbgModelTargetSessionContainerImpl extends DbgModelTargetObjectImpl implements DbgModelTargetSessionContainer { - protected final Map sessionsById = - new WeakValueHashMap<>(); - public DbgModelTargetSessionContainerImpl(DbgModelTargetRoot root) { super(root.getModel(), root, "Sessions", "SessionContainer"); @@ -51,9 +46,9 @@ public class DbgModelTargetSessionContainerImpl extends DbgModelTargetObjectImpl @Override public void sessionRemoved(DebugSessionId sessionId, DbgCause cause) { - synchronized (this) { - sessionsById.remove(sessionId); - } + //synchronized (this) { + // sessionsById.remove(sessionId); + //} changeElements(List.of( // DbgModelTargetSessionImpl.indexSession(sessionId) // ), List.of(), Map.of(), "Removed"); @@ -61,8 +56,12 @@ public class DbgModelTargetSessionContainerImpl extends DbgModelTargetObjectImpl @Override public synchronized DbgModelTargetSession getTargetSession(DbgSession session) { - DebugSessionId id = session.getId(); - return sessionsById.computeIfAbsent(id, i -> new DbgModelTargetSessionImpl(this, session)); + DbgModelImpl impl = (DbgModelImpl) model; + TargetObject modelObject = impl.getModelObject(session); + if (modelObject != null) { + return (DbgModelTargetSession) modelObject; + } + return new DbgModelTargetSessionImpl(this, session); } @Override diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionImpl.java index c38f966d8e..7832cf8f45 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSessionImpl.java @@ -30,8 +30,7 @@ import ghidra.dbg.util.PathUtils; @TargetObjectSchemaInfo( name = "Session", elements = { - @TargetElementType(type = Void.class) - }, + @TargetElementType(type = Void.class) }, attributes = { @TargetAttributeType( name = "Attributes", @@ -42,8 +41,7 @@ import ghidra.dbg.util.PathUtils; type = DbgModelTargetProcessContainerImpl.class, required = true, fixed = true), - @TargetAttributeType(type = Void.class) - }) + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetSessionImpl extends DbgModelTargetObjectImpl implements DbgModelTargetSession { @@ -71,6 +69,7 @@ public class DbgModelTargetSessionImpl extends DbgModelTargetObjectImpl public DbgModelTargetSessionImpl(DbgModelTargetSessionContainerImpl sessions, DbgSession session) { super(sessions.getModel(), sessions, keySession(session), "Session"); + this.getModel().addModelObject(session, this); this.attributes = new DbgModelTargetSessionAttributesImpl(this); this.processes = new DbgModelTargetProcessContainerImpl(this); @@ -79,10 +78,9 @@ public class DbgModelTargetSessionImpl extends DbgModelTargetObjectImpl attributes, // processes // ), Map.of( // - ACCESSIBLE_ATTRIBUTE_NAME, true, // + ACCESSIBLE_ATTRIBUTE_NAME, accessible, // PROMPT_ATTRIBUTE_NAME, DBG_PROMPT, // - STATE_ATTRIBUTE_NAME, TargetExecutionState.ALIVE, // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + STATE_ATTRIBUTE_NAME, TargetExecutionState.ALIVE // ), "Initialized"); getManager().addEventsListener(this); diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetStackFrameImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetStackFrameImpl.java index f379647510..553e179f3d 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetStackFrameImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetStackFrameImpl.java @@ -19,61 +19,31 @@ import java.math.BigInteger; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import agent.dbgeng.manager.*; import agent.dbgeng.manager.impl.DbgManagerImpl; import agent.dbgeng.model.iface1.DbgModelTargetFocusScope; import agent.dbgeng.model.iface2.*; -import ghidra.async.AsyncUtils; -import ghidra.async.TypeSpec; -import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.target.TargetFocusScope; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.Address; -@TargetObjectSchemaInfo( - name = "StackFrame", - elements = { - @TargetElementType(type = Void.class) - }, - attributes = { - @TargetAttributeType( - name = DbgModelTargetStackFrame.FUNC_ATTRIBUTE_NAME, - type = String.class), - @TargetAttributeType( - name = DbgModelTargetStackFrame.FUNC_TABLE_ENTRY_ATTRIBUTE_NAME, - type = String.class), - @TargetAttributeType( - name = DbgModelTargetStackFrame.INST_OFFSET_ATTRIBUTE_NAME, - type = String.class), - @TargetAttributeType( - name = DbgModelTargetStackFrame.FRAME_OFFSET_ATTRIBUTE_NAME, - type = String.class), - @TargetAttributeType( - name = DbgModelTargetStackFrame.RETURN_OFFSET_ATTRIBUTE_NAME, - type = String.class), - @TargetAttributeType( - name = DbgModelTargetStackFrame.STACK_OFFSET_ATTRIBUTE_NAME, - type = String.class), - @TargetAttributeType( - name = DbgModelTargetStackFrame.VIRTUAL_ATTRIBUTE_NAME, - type = Boolean.class), - @TargetAttributeType( - name = DbgModelTargetStackFrame.PARAM0_ATTRIBUTE_NAME, - type = String.class), - @TargetAttributeType( - name = DbgModelTargetStackFrame.PARAM1_ATTRIBUTE_NAME, - type = String.class), - @TargetAttributeType( - name = DbgModelTargetStackFrame.PARAM2_ATTRIBUTE_NAME, - type = String.class), - @TargetAttributeType( - name = DbgModelTargetStackFrame.PARAM3_ATTRIBUTE_NAME, - type = String.class), - @TargetAttributeType(type = Void.class) - }) +@TargetObjectSchemaInfo(name = "StackFrame", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(name = DbgModelTargetStackFrame.FUNC_ATTRIBUTE_NAME, type = String.class), + @TargetAttributeType(name = DbgModelTargetStackFrame.FUNC_TABLE_ENTRY_ATTRIBUTE_NAME, type = String.class), + @TargetAttributeType(name = DbgModelTargetStackFrame.INST_OFFSET_ATTRIBUTE_NAME, type = String.class), + @TargetAttributeType(name = DbgModelTargetStackFrame.FRAME_OFFSET_ATTRIBUTE_NAME, type = String.class), + @TargetAttributeType(name = DbgModelTargetStackFrame.RETURN_OFFSET_ATTRIBUTE_NAME, type = String.class), + @TargetAttributeType(name = DbgModelTargetStackFrame.STACK_OFFSET_ATTRIBUTE_NAME, type = String.class), + @TargetAttributeType(name = DbgModelTargetStackFrame.VIRTUAL_ATTRIBUTE_NAME, type = Boolean.class), + @TargetAttributeType(name = DbgModelTargetStackFrame.PARAM0_ATTRIBUTE_NAME, type = String.class), + @TargetAttributeType(name = DbgModelTargetStackFrame.PARAM1_ATTRIBUTE_NAME, type = String.class), + @TargetAttributeType(name = DbgModelTargetStackFrame.PARAM2_ATTRIBUTE_NAME, type = String.class), + @TargetAttributeType(name = DbgModelTargetStackFrame.PARAM3_ATTRIBUTE_NAME, type = String.class), + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetStackFrameImpl extends DbgModelTargetObjectImpl implements DbgModelTargetStackFrame { @@ -102,6 +72,7 @@ public class DbgModelTargetStackFrameImpl extends DbgModelTargetObjectImpl public DbgModelTargetStackFrameImpl(DbgModelTargetStack stack, DbgModelTargetThread thread, DbgStackFrame frame) { super(stack.getModel(), stack, keyFrame(frame), "StackFrame"); + this.getModel().addModelObject(frame, this); this.thread = thread; this.pc = getModel().getAddressSpace("ram").getAddress(-1); @@ -125,13 +96,7 @@ public class DbgModelTargetStackFrameImpl extends DbgModelTargetObjectImpl @Override public void threadSelected(DbgThread eventThread, DbgStackFrame eventFrame, DbgCause cause) { if (eventFrame != null && eventFrame.equals(frame)) { - AtomicReference scope = new AtomicReference<>(); - AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - DebugModelConventions.findSuitable(DbgModelTargetFocusScope.class, this) - .handle(seq::next); - }, scope).then(seq -> { - scope.get().setFocus(this); - }).finish(); + ((DbgModelTargetFocusScope) searchForSuitable(TargetFocusScope.class)).setFocus(this); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSymbolContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSymbolContainerImpl.java index 10c8a0867c..f8208ab44f 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSymbolContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSymbolContainerImpl.java @@ -24,20 +24,21 @@ import agent.dbgeng.manager.impl.DbgMinimalSymbol; import agent.dbgeng.model.iface2.DbgModelTargetSymbolContainer; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; -import ghidra.util.datastruct.WeakValueHashMap; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; -@TargetObjectSchemaInfo(name = "SymbolContainer", elements = { // - @TargetElementType(type = DbgModelTargetSymbolImpl.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "SymbolContainer", + elements = { + @TargetElementType(type = DbgModelTargetSymbolImpl.class) }, + elementResync = ResyncMode.ONCE, + attributes = { + @TargetAttributeType(type = Void.class) }, + canonicalContainer = true) public class DbgModelTargetSymbolContainerImpl extends DbgModelTargetObjectImpl implements DbgModelTargetSymbolContainer { protected final DbgModelTargetModuleImpl module; - protected final Map symbolsByName = new WeakValueHashMap<>(); - public DbgModelTargetSymbolContainerImpl(DbgModelTargetModuleImpl module) { super(module.getModel(), module, "Symbols", "SymbolContainer"); this.module = module; @@ -59,7 +60,11 @@ public class DbgModelTargetSymbolContainerImpl extends DbgModelTargetObjectImpl @Override public synchronized DbgModelTargetSymbolImpl getTargetSymbol(DbgMinimalSymbol symbol) { - return symbolsByName.computeIfAbsent(symbol.getName(), - n -> new DbgModelTargetSymbolImpl(this, symbol)); + DbgModelImpl impl = (DbgModelImpl) model; + TargetObject modelObject = impl.getModelObject(symbol); + if (modelObject != null) { + return (DbgModelTargetSymbolImpl) modelObject; + } + return new DbgModelTargetSymbolImpl(this, symbol); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSymbolImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSymbolImpl.java index 95caab5c06..6d00283449 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSymbolImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetSymbolImpl.java @@ -20,19 +20,22 @@ import java.util.Map; import agent.dbgeng.manager.impl.DbgMinimalSymbol; import agent.dbgeng.model.iface2.DbgModelTargetSymbol; +import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetSymbol; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.Address; -@TargetObjectSchemaInfo(name = "Symbol", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType( // - name = TargetSymbol.NAMESPACE_ATTRIBUTE_NAME, // - type = DbgModelTargetSymbolContainerImpl.class), // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo(name = "Symbol", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(name = TargetSymbol.NAMESPACE_ATTRIBUTE_NAME, type = DbgModelTargetSymbolContainerImpl.class), + @TargetAttributeType(name = TargetObject.VALUE_ATTRIBUTE_NAME, type = Address.class), + @TargetAttributeType(name = TargetSymbol.SIZE_ATTRIBUTE_NAME, type = long.class), + @TargetAttributeType(name = "Name", type = String.class), + @TargetAttributeType(name = "Size", type = long.class), + @TargetAttributeType(name = "TypeId", type = int.class), + @TargetAttributeType(name = "Tag", type = int.class), + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetSymbolImpl extends DbgModelTargetObjectImpl implements DbgModelTargetSymbol { protected static String indexSymbol(DbgMinimalSymbol symbol) { @@ -45,21 +48,25 @@ public class DbgModelTargetSymbolImpl extends DbgModelTargetObjectImpl protected final boolean constant; protected final Address value; - protected final int size; + protected final long size; public DbgModelTargetSymbolImpl(DbgModelTargetSymbolContainerImpl symbols, DbgMinimalSymbol symbol) { super(symbols.getModel(), symbols, keySymbol(symbol), "Symbol"); + this.getModel().addModelObject(symbol, this); this.constant = false; this.value = symbols.getModel().getAddressSpace("ram").getAddress(symbol.getAddress()); - this.size = 0; + this.size = symbol.getSize(); changeAttributes(List.of(), List.of(), Map.of( // // TODO: DATA_TYPE NAMESPACE_ATTRIBUTE_NAME, symbols, // VALUE_ATTRIBUTE_NAME, value, // SIZE_ATTRIBUTE_NAME, size, // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + "Name", symbol.getName(), // + "Size", size, // + "TypeId", symbol.getTypeId(), // + "Tag", symbol.getTag() // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadContainerImpl.java index a0999233d6..e12b9d627f 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadContainerImpl.java @@ -26,26 +26,21 @@ import agent.dbgeng.manager.reason.*; import agent.dbgeng.model.iface2.*; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; -import ghidra.util.datastruct.WeakValueHashMap; -@TargetObjectSchemaInfo(name = "ThreadContainer", elements = { // - @TargetElementType(type = DbgModelTargetThreadImpl.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo(name = "ThreadContainer", elements = { + @TargetElementType(type = DbgModelTargetThreadImpl.class) }, attributes = { + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class DbgModelTargetThreadContainerImpl extends DbgModelTargetObjectImpl implements DbgModelTargetThreadContainer { protected final DbgProcess process; - protected final Map threadsById = - new WeakValueHashMap<>(); - public DbgModelTargetThreadContainerImpl(DbgModelTargetProcessImpl process) { super(process.getModel(), process, "Threads", "ThreadContainer"); this.process = process.process; getManager().addEventsListener(this); + requestElements(false); } @Override @@ -53,34 +48,32 @@ public class DbgModelTargetThreadContainerImpl extends DbgModelTargetObjectImpl changeElements(List.of(), List.of(getTargetThread(thread)), Map.of(), "Created"); DbgModelTargetThread targetThread = getTargetThread(thread); changeElements(List.of(), List.of(targetThread), Map.of(), "Created"); - targetThread.threadStateChanged(DbgState.STARTING, DbgReason.getReason(null)); - getListeners().fire(TargetEventScopeListener.class) - .event(this, targetThread, TargetEventType.THREAD_CREATED, - "Thread " + thread.getId() + " started", List.of(targetThread)); + targetThread.threadStateChangedSpecific(DbgState.STARTING, DbgReason.getReason(null)); + getListeners().fire.event(getProxy(), targetThread, TargetEventType.THREAD_CREATED, + "Thread " + thread.getId() + " started", List.of(targetThread)); } @Override public void threadStateChanged(DbgThread thread, DbgState state, DbgCause cause, DbgReason reason) { DbgModelTargetThread targetThread = getTargetThread(thread); - targetThread.threadStateChanged(state, reason); TargetEventType eventType = getEventType(state, cause, reason); - getListeners().fire(TargetEventScopeListener.class) - .event(this, targetThread, eventType, "Thread " + thread.getId() + " state changed", - List.of(targetThread)); + getListeners().fire.event(getProxy(), targetThread, eventType, + "Thread " + thread.getId() + " state changed", List.of(targetThread)); + targetThread.threadStateChangedSpecific(state, reason); } @Override public void threadExited(DebugThreadId threadId) { - DbgModelTargetThread targetThread = threadsById.get(threadId); + DbgModelImpl impl = (DbgModelImpl) model; + DbgModelTargetThread targetThread = (DbgModelTargetThread) impl.getModelObject(threadId); if (targetThread != null) { - getListeners().fire(TargetEventScopeListener.class) - .event(this, targetThread, TargetEventType.THREAD_EXITED, - "Thread " + threadId + " exited", List.of(targetThread)); - } - synchronized (this) { - threadsById.remove(threadId); + getListeners().fire.event(getProxy(), targetThread, TargetEventType.THREAD_EXITED, + "Thread " + threadId + " exited", List.of(targetThread)); } + //synchronized (this) { + // threadsById.remove(threadId); + //} changeElements(List.of( // DbgModelTargetThreadImpl.indexThread(threadId) // ), List.of(), Map.of(), "Exited"); @@ -125,8 +118,12 @@ public class DbgModelTargetThreadContainerImpl extends DbgModelTargetObjectImpl @Override public synchronized DbgModelTargetThread getTargetThread(DbgThread thread) { - return threadsById.computeIfAbsent(thread.getId(), - i -> new DbgModelTargetThreadImpl(this, (DbgModelTargetProcess) parent, thread)); + DbgModelImpl impl = (DbgModelImpl) model; + TargetObject modelObject = impl.getModelObject(thread); + if (modelObject != null) { + return (DbgModelTargetThread) modelObject; + } + return new DbgModelTargetThreadImpl(this, (DbgModelTargetProcess) parent, thread); } } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadImpl.java index 53d781063e..e0bc532af6 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetThreadImpl.java @@ -18,7 +18,6 @@ package agent.dbgeng.model.impl; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import agent.dbgeng.dbgeng.DebugThreadId; import agent.dbgeng.manager.*; @@ -26,32 +25,17 @@ import agent.dbgeng.manager.cmd.DbgThreadSelectCommand; import agent.dbgeng.manager.impl.DbgManagerImpl; import agent.dbgeng.model.iface1.DbgModelTargetFocusScope; import agent.dbgeng.model.iface2.*; -import ghidra.async.AsyncUtils; -import ghidra.async.TypeSpec; -import ghidra.dbg.DebugModelConventions; import ghidra.dbg.target.TargetEnvironment; +import ghidra.dbg.target.TargetFocusScope; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; -@TargetObjectSchemaInfo( - name = "Thread", - elements = { - @TargetElementType(type = Void.class) - }, - attributes = { - @TargetAttributeType( - name = "Registers", - type = DbgModelTargetRegisterContainerImpl.class, - required = true, - fixed = true), - @TargetAttributeType( - name = "Stack", - type = DbgModelTargetStackImpl.class, - required = true, - fixed = true), +@TargetObjectSchemaInfo(name = "Thread", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(name = "Registers", type = DbgModelTargetRegisterContainerImpl.class, required = true, fixed = true), + @TargetAttributeType(name = "Stack", type = DbgModelTargetStackImpl.class, required = true, fixed = true), @TargetAttributeType(name = TargetEnvironment.ARCH_ATTRIBUTE_NAME, type = String.class), - @TargetAttributeType(type = Void.class) - }) + @TargetAttributeType(type = Void.class) }) public class DbgModelTargetThreadImpl extends DbgModelTargetObjectImpl implements DbgModelTargetThread { @@ -87,6 +71,8 @@ public class DbgModelTargetThreadImpl extends DbgModelTargetObjectImpl public DbgModelTargetThreadImpl(DbgModelTargetThreadContainer threads, DbgModelTargetProcess process, DbgThread thread) { super(threads.getModel(), threads, keyThread(thread), "Thread"); + this.getModel().addModelObject(thread, this); + this.getModel().addModelObject(thread.getId(), this); this.process = process; this.thread = thread; @@ -97,7 +83,7 @@ public class DbgModelTargetThreadImpl extends DbgModelTargetObjectImpl registers, // stack // ), Map.of( // - ACCESSIBLE_ATTRIBUTE_NAME, false, // + ACCESSIBLE_ATTRIBUTE_NAME, accessible = false, // DISPLAY_ATTRIBUTE_NAME, getDisplay(), // SUPPORTED_STEP_KINDS_ATTRIBUTE_NAME, SUPPORTED_KINDS // ), "Initialized"); @@ -118,18 +104,12 @@ public class DbgModelTargetThreadImpl extends DbgModelTargetObjectImpl @Override public void threadSelected(DbgThread eventThread, DbgStackFrame frame, DbgCause cause) { if (eventThread.equals(thread)) { - AtomicReference scope = new AtomicReference<>(); - AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - DebugModelConventions.findSuitable(DbgModelTargetFocusScope.class, this) - .handle(seq::next); - }, scope).then(seq -> { - scope.get().setFocus(this); - }).finish(); + ((DbgModelTargetFocusScope) searchForSuitable(TargetFocusScope.class)).setFocus(this); } } @Override - public void threadStateChanged(DbgState state, DbgReason reason) { + public void threadStateChangedSpecific(DbgState state, DbgReason reason) { TargetExecutionState targetState = convertState(state); String executionType = thread.getExecutingProcessorType().description; changeAttributes(List.of(), List.of(), Map.of( // @@ -137,6 +117,7 @@ public class DbgModelTargetThreadImpl extends DbgModelTargetObjectImpl TargetEnvironment.ARCH_ATTRIBUTE_NAME, executionType // ), reason.desc()); setExecutionState(targetState, reason.desc()); + registers.threadStateChangedSpecific(state, reason); } @Override @@ -147,13 +128,13 @@ public class DbgModelTargetThreadImpl extends DbgModelTargetObjectImpl case ADVANCE: // Why no exec-advance in GDB/MI? return thread.console("advance"); default: - return thread.step(convertToDbg(kind)); + return model.gateFuture(thread.step(convertToDbg(kind))); } } @Override public CompletableFuture step(Map args) { - return thread.step(args); + return model.gateFuture(thread.step(args)); } @Override diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetTraceOrDumpConnectorImpl.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetTraceOrDumpConnectorImpl.java index e5c049086e..fa75c06858 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetTraceOrDumpConnectorImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/java/agent/dbgeng/model/impl/DbgModelTargetTraceOrDumpConnectorImpl.java @@ -27,11 +27,14 @@ import ghidra.dbg.target.TargetMethod.ParameterDescription; import ghidra.dbg.target.TargetMethod.TargetParameterMap; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "TraceOrDumpConnector", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo( + name = "TraceOrDumpConnector", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(type = Void.class) + }) public class DbgModelTargetTraceOrDumpConnectorImpl extends DbgModelTargetObjectImpl implements DbgModelTargetConnector { @@ -46,8 +49,7 @@ public class DbgModelTargetTraceOrDumpConnectorImpl extends DbgModelTargetObject changeAttributes(List.of(), List.of(), Map.of( // DISPLAY_ATTRIBUTE_NAME, getDisplay(), // TargetMethod.PARAMETERS_ATTRIBUTE_NAME, - paramDescs = TargetParameterMap.copyOf(computeParameters()), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + paramDescs = TargetParameterMap.copyOf(computeParameters()) // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractDbgengModelHost.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractDbgengModelHost.java new file mode 100644 index 0000000000..2cb8358d63 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractDbgengModelHost.java @@ -0,0 +1,27 @@ +/* ### + * 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 agent.dbgeng.model; + +import java.util.Map; + +import ghidra.dbg.test.AbstractModelHost; + +public abstract class AbstractDbgengModelHost extends AbstractModelHost { + @Override + public Map getFactoryOptions() { + return Map.ofEntries(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengBreakpointsTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengBreakpointsTest.java new file mode 100644 index 0000000000..107fe39b2b --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengBreakpointsTest.java @@ -0,0 +1,88 @@ +/* ### + * 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 agent.dbgeng.model; + +import static org.junit.Assert.*; + +import java.util.List; + +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; +import ghidra.dbg.test.*; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.*; + +public abstract class AbstractModelForDbgengBreakpointsTest + extends AbstractDebuggerModelBreakpointsTest implements ProvidesTargetViaLaunchSpecimen { + + @Override + public AbstractDebuggerModelTest getTest() { + return this; + } + + @Override + protected List seedPath() { + return List.of(); + } + + @Override + public DebuggerTestSpecimen getLaunchSpecimen() { + return WindowsSpecimen.PRINT; + } + + @Override + public List getExpectedBreakpointContainerPath(List targetPath) { + return PathUtils.extend(targetPath, PathUtils.parse("Debug.Breakpoints")); + } + + @Override + public TargetBreakpointKindSet getExpectedSupportedKinds() { + return TargetBreakpointKindSet.of( // + TargetBreakpointKind.SW_EXECUTE, // + TargetBreakpointKind.HW_EXECUTE, // + TargetBreakpointKind.READ, // + TargetBreakpointKind.WRITE); // + } + + @Override + public AddressRange getSuitableRangeForBreakpoint(TargetObject target, + TargetBreakpointKind kind) throws Throwable { + TargetStackFrame frame = retry(() -> { + TargetStackFrame f = findAnyStackFrame(target.getPath()); + assertNotNull(f); + return f; + }, List.of(AssertionError.class)); + waitOn(frame.fetchAttributes()); + Address pc = frame.getProgramCounter(); + switch (kind) { + case SW_EXECUTE: + case HW_EXECUTE: + return new AddressRangeImpl(pc, pc); + case READ: + case WRITE: + return new AddressRangeImpl(pc, 4); + default: + throw new AssertionError(); + } + } + + @Override + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, PathUtils.parse("Sessions[0]")); + } + +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengFactoryTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengFactoryTest.java new file mode 100644 index 0000000000..4a0039a4e5 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengFactoryTest.java @@ -0,0 +1,28 @@ +/* ### + * 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 agent.dbgeng.model; + +import java.util.Map; + +import ghidra.dbg.test.AbstractDebuggerModelFactoryTest; + +public abstract class AbstractModelForDbgengFactoryTest extends AbstractDebuggerModelFactoryTest { + @Override + protected Map getFailingFactoryOptions() { + // TODO: No options to test for IN-VM variant + return Map.ofEntries(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengFrameFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengFrameFocusTest.java new file mode 100644 index 0000000000..5b213a3163 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengFrameFocusTest.java @@ -0,0 +1,54 @@ +/* ### + * 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 agent.dbgeng.model; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.*; + +import ghidra.dbg.target.*; +import ghidra.dbg.test.AbstractDebuggerModelFocusTest; + +public abstract class AbstractModelForDbgengFrameFocusTest + extends AbstractDebuggerModelFocusTest { + + protected DebuggerTestSpecimen getSpecimen() { + return WindowsSpecimen.STACK; + } + + @Override + protected Set getFocusableThings() throws Throwable { + DebuggerTestSpecimen specimen = getSpecimen(); + TargetLauncher launcher = findLauncher(); // root launcher should generate new inferiors + waitOn(launcher.launch(specimen.getLauncherArgs())); + + TargetProcess process = retry(() -> { + TargetProcess p = m.findAny(TargetProcess.class, seedPath()); + assertNotNull(p); + return p; + }, List.of(AssertionError.class)); + + trapAt("expStack!break_here", process); + + return retry(() -> { + Map, TargetStackFrame> frames = + m.findAll(TargetStackFrame.class, seedPath()); + assertTrue(frames.size() >= 3); + return Set.copyOf(frames.values()); + }, List.of(AssertionError.class)); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengInterpreterTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengInterpreterTest.java new file mode 100644 index 0000000000..3eaa4b7a9b --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengInterpreterTest.java @@ -0,0 +1,60 @@ +/* ### + * 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 agent.dbgeng.model; + +import java.util.List; + +import ghidra.dbg.test.AbstractDebuggerModelInterpreterTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForDbgengInterpreterTest + extends AbstractDebuggerModelInterpreterTest { + + @Override + protected List seedPath() { + return PathUtils.parse("Sessions[0]"); + } + + @Override + public List getExpectedInterpreterPath() { + return PathUtils.parse("Sessions[0]"); + } + + @Override + protected String getEchoCommand(String msg) { + return ".echo " + msg; + } + + @Override + protected String getQuitCommand() { + return "q"; + } + + @Override + protected String getAttachCommand() { + return ".attach " + Long.toHexString(dummy.pid); + } + + @Override + public DebuggerTestSpecimen getAttachSpecimen() { + return WindowsSpecimen.NOTEPAD; + } + + @Override + public DebuggerTestSpecimen getLaunchSpecimen() { + return WindowsSpecimen.PRINT; + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengProcessFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengProcessFocusTest.java new file mode 100644 index 0000000000..8f3dabc1da --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengProcessFocusTest.java @@ -0,0 +1,52 @@ +/* ### + * 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 agent.dbgeng.model; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import ghidra.dbg.target.*; +import ghidra.dbg.test.AbstractDebuggerModelFocusTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForDbgengProcessFocusTest + extends AbstractDebuggerModelFocusTest { + + protected int getCount() { + return 3; + } + + protected DebuggerTestSpecimen getSpecimen() { + return WindowsSpecimen.PRINT; + } + + @Override + protected Set getFocusableThings() throws Throwable { + DebuggerTestSpecimen specimen = getSpecimen(); + TargetLauncher launcher = findLauncher(); + int count = getCount(); + for (int i = 0; i < count; i++) { + waitOn(launcher.launch(specimen.getLauncherArgs())); + } + return retry(() -> { + Map, TargetProcess> found = + m.findAll(TargetProcess.class, PathUtils.parse("Sessions[0]")); + assertEquals(count, found.size()); + return Set.copyOf(found.values()); + }, List.of(AssertionError.class)); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengRootAttacherTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengRootAttacherTest.java new file mode 100644 index 0000000000..a53d325351 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengRootAttacherTest.java @@ -0,0 +1,75 @@ +/* ### + * 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 agent.dbgeng.model; + +import static org.junit.Assert.*; + +import java.util.List; + +import agent.dbgeng.model.invm.InVmDbgengModelHost; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetMethod.TargetParameterMap; +import ghidra.dbg.test.AbstractDebuggerModelAttacherTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForDbgengRootAttacherTest + extends AbstractDebuggerModelAttacherTest { + + public class InVmModelForDbgengRootAttacherTest extends AbstractModelForDbgengRootAttacherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } + } + + @Override + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, PathUtils.parse("Sessions[0]")); + } + + @Override + public List getExpectedAttachableContainerPath() { + return List.of("Available"); + } + + @Override + public List getExpectedProcessesContainerPath() { + return PathUtils.parse("Sessions[0].Processes"); + } + + @Override + public List getExpectedAttacherPath() { + return PathUtils.parse(""); + } + + @Override + public DebuggerTestSpecimen getAttachSpecimen() { + return WindowsSpecimen.NOTEPAD; + } + + @Override + public TargetParameterMap getExpectedAttachParameters() { + return null; // TODO + } + + @Override + public void assertEnvironment(TargetEnvironment environment) { + assertEquals("x86_64", environment.getArchitecture()); + assertEquals("Windows", environment.getOperatingSystem()); + assertEquals("little", environment.getEndian()); + assertTrue(environment.getDebugger().toLowerCase().contains("dbgeng")); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengRootLauncherTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengRootLauncherTest.java new file mode 100644 index 0000000000..7ab270ed91 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengRootLauncherTest.java @@ -0,0 +1,67 @@ +/* ### + * 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 agent.dbgeng.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Map; + +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetMethod.ParameterDescription; +import ghidra.dbg.target.TargetMethod.TargetParameterMap; +import ghidra.dbg.test.AbstractDebuggerModelLauncherTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForDbgengRootLauncherTest + extends AbstractDebuggerModelLauncherTest { + + @Override + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, PathUtils.parse("Sessions[0]")); + } + + @Override + public List getExpectedProcessesContainerPath() { + return PathUtils.parse("Sessions[0].Processes"); + } + + @Override + public List getExpectedLauncherPath() { + return PathUtils.parse(""); + } + + @Override + public DebuggerTestSpecimen getLaunchSpecimen() { + return WindowsSpecimen.PRINT; + } + + @Override + public TargetParameterMap getExpectedLauncherParameters() { + return TargetParameterMap.copyOf(Map.ofEntries( + Map.entry("args", ParameterDescription.create(String.class, "args", true, "", + "Command Line", "space-separated command-line arguments")))); + } + + @Override + public void assertEnvironment(TargetEnvironment environment) { + assertEquals("x86_64", environment.getArchitecture()); + assertEquals("Windows", environment.getOperatingSystem()); + assertEquals("little", environment.getEndian()); + assertTrue(environment.getDebugger().toLowerCase().contains("dbgeng")); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioCloneExitTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioCloneExitTest.java new file mode 100644 index 0000000000..3543a3cf75 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioCloneExitTest.java @@ -0,0 +1,40 @@ +/* ### + * 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 agent.dbgeng.model; + +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.test.AbstractDebuggerModelScenarioCloneExitTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForDbgengScenarioCloneExitTest + extends AbstractDebuggerModelScenarioCloneExitTest { + + @Override + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, PathUtils.parse("Sessions[0]")); + } + + @Override + protected DebuggerTestSpecimen getSpecimen() { + return WindowsSpecimen.CREATE_THREAD_EXIT; + } + + @Override + protected String getBreakpointExpression() { + return "expCreateThreadExit!work"; + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioForkExitTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioForkExitTest.java new file mode 100644 index 0000000000..41d8134739 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioForkExitTest.java @@ -0,0 +1,55 @@ +/* ### + * 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 agent.dbgeng.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import ghidra.dbg.target.*; +import ghidra.dbg.test.AbstractDebuggerModelScenarioForkExitTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForDbgengScenarioForkExitTest + extends AbstractDebuggerModelScenarioForkExitTest { + + @Override + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, PathUtils.parse("Sessions[0]")); + } + + @Override + protected DebuggerTestSpecimen getSpecimen() { + return WindowsSpecimen.CREATE_PROCESS; + } + + @Override + protected String getParentBreakpointExpression() { + return "expCreateProcess!func"; + } + + @Override + protected String getChildBreakpointExpression() { + return "expCreateProcess!func"; + } + + @Override + public void assertEnvironment(TargetEnvironment environment) { + assertEquals("x86_64", environment.getArchitecture()); + assertEquals("Windows", environment.getOperatingSystem()); + assertEquals("little", environment.getEndian()); + assertTrue(environment.getDebugger().toLowerCase().contains("dbgeng")); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioMemoryTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioMemoryTest.java new file mode 100644 index 0000000000..f7e7ef169d --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioMemoryTest.java @@ -0,0 +1,77 @@ +/* ### + * 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 agent.dbgeng.model; + +import static org.junit.Assert.assertEquals; + +import java.util.List; +import java.util.Objects; + +import agent.dbgeng.model.impl.DbgModelTargetProcessImpl; +import ghidra.dbg.target.*; +import ghidra.dbg.test.AbstractDebuggerModelScenarioMemoryTest; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.Address; + +public abstract class AbstractModelForDbgengScenarioMemoryTest + extends AbstractDebuggerModelScenarioMemoryTest { + + @Override + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, PathUtils.parse("Sessions[0]")); + } + + @Override + protected WindowsSpecimen getSpecimen() { + return WindowsSpecimen.PRINT; + } + + protected String getSymbolName() { + return "overwrite"; + } + + @Override + protected Address getAddressToWrite(TargetProcess process) throws Throwable { + // It seems this is the only test case that exercises module symbols. + List modulePath = PathUtils.extend(process.getPath(), + PathUtils.parse("Modules[" + getSpecimen().getBinModuleKey() + "]")); + TargetObject container = + Objects.requireNonNull(m.findContainer(TargetSymbol.class, modulePath)); + TargetSymbol symbol = + waitOn(container.fetchElements()).get(getSymbolName()).as(TargetSymbol.class); + return symbol.getValue(); + } + + @Override + protected byte[] getBytesToWrite() { + return "Speak".getBytes(); + } + + @Override + protected byte[] getExpectedBytes() { + return "Speak, World!".getBytes(); + } + + @Override + protected void verifyExpectedEffect(TargetProcess process) throws Throwable { + // TODO: Should (optional) exitCode be standardized on all models? + retryVoid(() -> { + long status = process.getTypedAttributeNowByName( + DbgModelTargetProcessImpl.EXIT_CODE_ATTRIBUTE_NAME, Long.class, 0L); + assertEquals('S', status); + }, List.of(AssertionError.class)); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioStackTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioStackTest.java new file mode 100644 index 0000000000..8043110215 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioStackTest.java @@ -0,0 +1,71 @@ +/* ### + * 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 agent.dbgeng.model; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.Map.Entry; + +import ghidra.dbg.target.*; +import ghidra.dbg.test.AbstractDebuggerModelScenarioStackTest; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.Address; + +public abstract class AbstractModelForDbgengScenarioStackTest + extends AbstractDebuggerModelScenarioStackTest { + protected static List expectedSymbols = + List.of("break_here", "funcC", "funcB", "funcA"); + protected NavigableMap symbolsByAddress = new TreeMap<>(); + + @Override + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, PathUtils.parse("Sessions[0]")); + } + + @Override + protected WindowsSpecimen getSpecimen() { + return WindowsSpecimen.STACK; + } + + @Override + protected String getBreakpointExpression() { + return "expStack!break_here"; + } + + @Override + protected void postLaunch(TargetProcess process) throws Throwable { + TargetModuleContainer modules = m.find(TargetModuleContainer.class, process.getPath()); + // NB. NEVER is recommended resync mode for modules container + // It's not guaranteed to come before process is alive, though + TargetModule binMod = (TargetModule) waitOn(m.getAddedWaiter() + .wait(PathUtils.index(modules.getPath(), getSpecimen().getBinModuleKey()))); + + // NB. this heuristic assumes all function bodies are contiguous in memory + TargetSymbolNamespace symbols = m.find(TargetSymbolNamespace.class, binMod.getPath()); + // NB. ONCE is recommended resync mode for module symbols + for (Entry entry : waitOn(symbols.fetchElements()) + .entrySet()) { + symbolsByAddress.put(entry.getValue().as(TargetSymbol.class).getValue(), + entry.getKey()); + } + } + + @Override + protected void validateFramePC(int index, Address pc) { + assertEquals(expectedSymbols.get(index), symbolsByAddress.floorEntry(pc).getValue()); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioX64RegistersTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioX64RegistersTest.java new file mode 100644 index 0000000000..3da0568e59 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengScenarioX64RegistersTest.java @@ -0,0 +1,58 @@ +/* ### + * 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 agent.dbgeng.model; + +import static org.junit.Assert.assertEquals; + +import java.util.Map; + +import agent.dbgeng.model.impl.DbgModelTargetProcessImpl; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.test.AbstractDebuggerModelScenarioRegistersTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForDbgengScenarioX64RegistersTest + extends AbstractDebuggerModelScenarioRegistersTest { + + @Override + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, PathUtils.parse("Sessions[0]")); + } + + @Override + protected DebuggerTestSpecimen getSpecimen() { + return WindowsSpecimen.REGISTERS; + } + + @Override + protected String getBreakpointExpression() { + return "expRegisters!break_here"; + } + + @Override + protected Map getRegisterWrites() { + // RCX is first parameter `val` of break_here(int val) + return Map.of("rcx", arr("0000000000000041")); + } + + @Override + protected void verifyExpectedEffect(TargetProcess process) throws Throwable { + long status = process.getTypedAttributeNowByName( + DbgModelTargetProcessImpl.EXIT_CODE_ATTRIBUTE_NAME, Long.class, 0L); + assertEquals(0x41, status); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengSessionFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengSessionFocusTest.java new file mode 100644 index 0000000000..fc7c695e53 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengSessionFocusTest.java @@ -0,0 +1,33 @@ +/* ### + * 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 agent.dbgeng.model; + +import static ghidra.lifecycle.Unfinished.TODO; + +import java.util.Set; + +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.test.AbstractDebuggerModelFocusTest; + +public abstract class AbstractModelForDbgengSessionFocusTest + extends AbstractDebuggerModelFocusTest { + + @Override + protected Set getFocusableThings() throws Throwable { + TODO("Don't know how to make multiple sessions"); + return null; + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengSteppableTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengSteppableTest.java new file mode 100644 index 0000000000..8d64f780a7 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengSteppableTest.java @@ -0,0 +1,47 @@ +/* ### + * 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 agent.dbgeng.model; + +import java.util.List; + +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.test.*; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForDbgengSteppableTest extends AbstractDebuggerModelSteppableTest + implements ProvidesTargetViaLaunchSpecimen { + + @Override + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, PathUtils.parse("Sessions[0]")); + } + + @Override + public AbstractDebuggerModelTest getTest() { + return this; + } + + @Override + public List getExpectedSteppablePath(List threadPath) { + return threadPath; + } + + @Override + public DebuggerTestSpecimen getLaunchSpecimen() { + return WindowsSpecimen.PRINT; + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengThreadFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengThreadFocusTest.java new file mode 100644 index 0000000000..f4289640da --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengThreadFocusTest.java @@ -0,0 +1,52 @@ +/* ### + * 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 agent.dbgeng.model; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import ghidra.dbg.target.*; +import ghidra.dbg.test.AbstractDebuggerModelFocusTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForDbgengThreadFocusTest + extends AbstractDebuggerModelFocusTest { + + protected int getCount() { + return 3; + } + + protected DebuggerTestSpecimen getSpecimen() { + return WindowsSpecimen.PRINT; + } + + @Override + protected Set getFocusableThings() throws Throwable { + DebuggerTestSpecimen specimen = getSpecimen(); + TargetLauncher launcher = findLauncher(); + int count = getCount(); + for (int i = 0; i < count; i++) { + waitOn(launcher.launch(specimen.getLauncherArgs())); + } + return retry(() -> { + Map, TargetThread> found = + m.findAll(TargetThread.class, PathUtils.parse("Sessions[0]")); + assertEquals(count, found.size()); + return Set.copyOf(found.values()); + }, List.of(AssertionError.class)); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengX64RegistersTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengX64RegistersTest.java new file mode 100644 index 0000000000..88106957ec --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/AbstractModelForDbgengX64RegistersTest.java @@ -0,0 +1,59 @@ +/* ### + * 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 agent.dbgeng.model; + +import java.util.List; +import java.util.Map; + +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.test.*; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForDbgengX64RegistersTest + extends AbstractDebuggerModelRegistersTest + implements ProvidesTargetViaLaunchSpecimen { + public final Map REG_VALS = Map.ofEntries( + Map.entry("rax", arr("0123456789abcdef")), + Map.entry("ymm0", arr( + "0123456789abcdef" + "fedcba9876543210" // TODO: Why 16 bytes instead of 32? + /*+ "0011223344556677" + "8899aabbccddeeff"*/))); + + @Override + public AbstractDebuggerModelTest getTest() { + return this; + } + + @Override + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, PathUtils.parse("Sessions[0]")); + } + + @Override + public List getExpectedRegisterBankPath(List threadPath) { + return PathUtils.extend(threadPath, PathUtils.parse("Registers")); + } + + @Override + public Map getRegisterWrites() { + return REG_VALS; + } + + @Override + public DebuggerTestSpecimen getLaunchSpecimen() { + return WindowsSpecimen.PRINT; + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/WindowsSpecimen.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/WindowsSpecimen.java new file mode 100644 index 0000000000..789860e850 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/WindowsSpecimen.java @@ -0,0 +1,125 @@ +/* ### + * 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 agent.dbgeng.model; + +import java.io.File; +import java.util.*; + +import agent.dbgeng.model.iface2.DbgModelTargetAvailable; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; +import ghidra.dbg.test.AbstractDebuggerModelTest; +import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen; +import ghidra.dbg.testutil.DebuggerModelTestUtils; +import ghidra.dbg.testutil.DummyProc; + +public enum WindowsSpecimen implements DebuggerTestSpecimen, DebuggerModelTestUtils { + PRINT { + @Override + String getCommandLine() { + return DummyProc.which("expPrint.exe"); + } + }, + NOTEPAD { + @Override + String getCommandLine() { + return "C:\\Windows\\notepad.exe"; + } + }, + CREATE_PROCESS { + @Override + String getCommandLine() { + return DummyProc.which("expCreateProcess.exe"); + } + }, + CREATE_THREAD_EXIT { + @Override + String getCommandLine() { + return DummyProc.which("expCreateThreadExit.exe"); + } + }, + REGISTERS { + @Override + String getCommandLine() { + return DummyProc.which("expRegisters.exe"); + } + }, + STACK { + @Override + String getCommandLine() { + return DummyProc.which("expStack.exe"); + } + }; + + abstract String getCommandLine(); + + @Override + public DummyProc runDummy() throws Throwable { + // This is not great, but.... + return DummyProc.run(getCommandLine().split("\\s+")); + } + + @Override + public Map getLauncherArgs() { + return Map.ofEntries(Map.entry(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, getCommandLine())); + } + + @Override + public List getLaunchScript() { + // NB: this will not appear on the process list until cont + return List.of(".create " + getCommandLine() + "; g"); + } + + protected static String getShortName(String fullPath) { + if (fullPath == null) { + return null; + } + return new File(fullPath).getName(); + } + + public String getBinModuleKey() { + String moduleName = getBinModuleName(); + if (moduleName.endsWith(".exe")) { + return moduleName.substring(0, moduleName.length() - ".exe".length()); + } + return moduleName; + } + + public String getBinModuleName() { + return getShortName(getCommandLine().split("\\s+")[0]); + + } + + @Override + public boolean isRunningIn(TargetProcess process, AbstractDebuggerModelTest test) + throws Throwable { + // NB. ShellUtils.parseArgs removes the \s. Not good. + String expected = getBinModuleName(); + Collection modules = + test.m.findAll(TargetModule.class, process.getPath()).values(); + return modules.stream() + .anyMatch(m -> expected.equalsIgnoreCase(getShortName(m.getModuleName()))); + } + + @Override + public boolean isAttachable(DummyProc dummy, TargetAttachable attachable, + AbstractDebuggerModelTest test) throws Throwable { + waitOn(attachable.fetchAttributes()); + long pid = attachable.getTypedAttributeNowByName(DbgModelTargetAvailable.PID_ATTRIBUTE_NAME, + Long.class, -1L); + return pid == dummy.pid; + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpDbgengModelHost.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpDbgengModelHost.java new file mode 100644 index 0000000000..3d81ccb9b7 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpDbgengModelHost.java @@ -0,0 +1,27 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.gadp.DbgEngLocalDebuggerModelFactory; +import agent.dbgeng.model.AbstractDbgengModelHost; +import ghidra.dbg.DebuggerModelFactory; + +public class GadpDbgengModelHost extends AbstractDbgengModelHost { + @Override + public DebuggerModelFactory getModelFactory() { + return new DbgEngLocalDebuggerModelFactory(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengBreakpointsTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengBreakpointsTest.java new file mode 100644 index 0000000000..e0dfde4fb9 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengBreakpointsTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengBreakpointsTest; + +public class GadpModelForDbgengBreakpointsTest extends AbstractModelForDbgengBreakpointsTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengFactoryTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengFactoryTest.java new file mode 100644 index 0000000000..2af5e65956 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengFactoryTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengFactoryTest; + +public class GadpModelForDbgengFactoryTest extends AbstractModelForDbgengFactoryTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengFrameFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengFrameFocusTest.java new file mode 100644 index 0000000000..dba2a13779 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengFrameFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengFrameFocusTest; + +public class GadpModelForDbgengFrameFocusTest extends AbstractModelForDbgengFrameFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengInterpreterTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengInterpreterTest.java new file mode 100644 index 0000000000..b65b1f697f --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengInterpreterTest.java @@ -0,0 +1,47 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import org.junit.Ignore; +import org.junit.Test; + +import agent.dbgeng.model.AbstractModelForDbgengInterpreterTest; +import ghidra.dbg.error.DebuggerModelTerminatingException; + +public class GadpModelForDbgengInterpreterTest extends AbstractModelForDbgengInterpreterTest { + + // NB: testLaunchViaInterpreterShowInProcessContainer fails + + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } + + @Override + @Ignore + @Test + public void testAttachViaInterpreterShowsInProcessContainer() throws Throwable { + super.testAttachViaInterpreterShowsInProcessContainer(); + } + + @Override + @Ignore + @Test(expected = DebuggerModelTerminatingException.class) + public void testExecuteQuit() throws Throwable { + // Hangs after DebuggerModelTerminatingException + } + +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengProcessFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengProcessFocusTest.java new file mode 100644 index 0000000000..0d454aa305 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengProcessFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengProcessFocusTest; + +public class GadpModelForDbgengProcessFocusTest extends AbstractModelForDbgengProcessFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengRootAttacherTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengRootAttacherTest.java new file mode 100644 index 0000000000..8a2781e86b --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengRootAttacherTest.java @@ -0,0 +1,28 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengRootAttacherTest; + +public class GadpModelForDbgengRootAttacherTest extends AbstractModelForDbgengRootAttacherTest { + + // NB: testListAttachable fails with OTE - [] not invalidated + + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengRootLauncherTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengRootLauncherTest.java new file mode 100644 index 0000000000..ec49f6fa84 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengRootLauncherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengRootLauncherTest; + +public class GadpModelForDbgengRootLauncherTest extends AbstractModelForDbgengRootLauncherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioCloneExitTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioCloneExitTest.java new file mode 100644 index 0000000000..43fbd71659 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioCloneExitTest.java @@ -0,0 +1,26 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioCloneExitTest; + +public class GadpModelForDbgengScenarioCloneExitTest + extends AbstractModelForDbgengScenarioCloneExitTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioForkExitTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioForkExitTest.java new file mode 100644 index 0000000000..5bb2afef9f --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioForkExitTest.java @@ -0,0 +1,35 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import org.junit.Ignore; +import org.junit.Test; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioForkExitTest; + +public class GadpModelForDbgengScenarioForkExitTest + extends AbstractModelForDbgengScenarioForkExitTest { + + @Ignore("Specimen is currently defunct") + @Test + public void testScenario() throws Throwable { + } + + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioMemoryTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioMemoryTest.java new file mode 100644 index 0000000000..682de3dae1 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioMemoryTest.java @@ -0,0 +1,28 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioMemoryTest; + +public class GadpModelForDbgengScenarioMemoryTest extends AbstractModelForDbgengScenarioMemoryTest { + + // NB: Fails on validateCompletionThread + + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioStackTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioStackTest.java new file mode 100644 index 0000000000..4e123f6552 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioStackTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioStackTest; + +public class GadpModelForDbgengScenarioStackTest extends AbstractModelForDbgengScenarioStackTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioX64RegistersTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioX64RegistersTest.java new file mode 100644 index 0000000000..11142fea8b --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengScenarioX64RegistersTest.java @@ -0,0 +1,26 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioX64RegistersTest; + +public class GadpModelForDbgengScenarioX64RegistersTest + extends AbstractModelForDbgengScenarioX64RegistersTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengSessionFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengSessionFocusTest.java new file mode 100644 index 0000000000..f8d6c21103 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengSessionFocusTest.java @@ -0,0 +1,28 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import org.junit.Ignore; + +import agent.dbgeng.model.AbstractModelForDbgengSessionFocusTest; + +@Ignore("Don't know how to make multiple sessions") +public class GadpModelForDbgengSessionFocusTest extends AbstractModelForDbgengSessionFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengSteppableTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengSteppableTest.java new file mode 100644 index 0000000000..083935e74f --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengSteppableTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengSteppableTest; + +public class GadpModelForDbgengSteppableTest extends AbstractModelForDbgengSteppableTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengThreadFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengThreadFocusTest.java new file mode 100644 index 0000000000..522d2eb923 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengThreadFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengThreadFocusTest; + +public class GadpModelForDbgengThreadFocusTest extends AbstractModelForDbgengThreadFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengX64RegistersTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengX64RegistersTest.java new file mode 100644 index 0000000000..3cf2638c7e --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/gadp/GadpModelForDbgengX64RegistersTest.java @@ -0,0 +1,28 @@ +/* ### + * 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 agent.dbgeng.model.gadp; + +import agent.dbgeng.model.AbstractModelForDbgengX64RegistersTest; + +public class GadpModelForDbgengX64RegistersTest extends AbstractModelForDbgengX64RegistersTest { + + // NB: Fails testWriteRegisters + + @Override + public ModelHost modelHost() throws Throwable { + return new GadpDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmDbgengModelHost.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmDbgengModelHost.java new file mode 100644 index 0000000000..326df22d13 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmDbgengModelHost.java @@ -0,0 +1,27 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.DbgEngInJvmDebuggerModelFactory; +import agent.dbgeng.model.AbstractDbgengModelHost; +import ghidra.dbg.DebuggerModelFactory; + +public class InVmDbgengModelHost extends AbstractDbgengModelHost { + @Override + public DebuggerModelFactory getModelFactory() { + return new DbgEngInJvmDebuggerModelFactory(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengBreakpointsTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengBreakpointsTest.java new file mode 100644 index 0000000000..396ae99e8c --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengBreakpointsTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengBreakpointsTest; + +public class InVmModelForDbgengBreakpointsTest extends AbstractModelForDbgengBreakpointsTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengFactoryTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengFactoryTest.java new file mode 100644 index 0000000000..0810619534 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengFactoryTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengFactoryTest; + +public class InVmModelForDbgengFactoryTest extends AbstractModelForDbgengFactoryTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengFrameFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengFrameFocusTest.java new file mode 100644 index 0000000000..bd27a3bb27 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengFrameFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengFrameFocusTest; + +public class InVmModelForDbgengFrameFocusTest extends AbstractModelForDbgengFrameFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengInterpreterTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengInterpreterTest.java new file mode 100644 index 0000000000..fce492faf8 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengInterpreterTest.java @@ -0,0 +1,35 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import org.junit.Ignore; +import org.junit.Test; + +import agent.dbgeng.model.AbstractModelForDbgengInterpreterTest; + +public class InVmModelForDbgengInterpreterTest extends AbstractModelForDbgengInterpreterTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } + + @Override + @Ignore + @Test + public void testAttachViaInterpreterShowsInProcessContainer() throws Throwable { + super.testAttachViaInterpreterShowsInProcessContainer(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengProcessFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengProcessFocusTest.java new file mode 100644 index 0000000000..b9f4a54592 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengProcessFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengProcessFocusTest; + +public class InVmModelForDbgengProcessFocusTest extends AbstractModelForDbgengProcessFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengRootAttacherTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengRootAttacherTest.java new file mode 100644 index 0000000000..6b21401aca --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengRootAttacherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengRootAttacherTest; + +public class InVmModelForDbgengRootAttacherTest extends AbstractModelForDbgengRootAttacherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengRootLauncherTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengRootLauncherTest.java new file mode 100644 index 0000000000..85dfb66129 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengRootLauncherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengRootLauncherTest; + +public class InVmModelForDbgengRootLauncherTest extends AbstractModelForDbgengRootLauncherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioCloneExitTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioCloneExitTest.java new file mode 100644 index 0000000000..72fb2aef73 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioCloneExitTest.java @@ -0,0 +1,26 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioCloneExitTest; + +public class InVmModelForDbgengScenarioCloneExitTest + extends AbstractModelForDbgengScenarioCloneExitTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioForkExitTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioForkExitTest.java new file mode 100644 index 0000000000..988cd2c13e --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioForkExitTest.java @@ -0,0 +1,35 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import org.junit.Ignore; +import org.junit.Test; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioForkExitTest; + +public class InVmModelForDbgengScenarioForkExitTest + extends AbstractModelForDbgengScenarioForkExitTest { + + @Ignore("Specimen is currently defunct") + @Test + public void testScenario() throws Throwable { + } + + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioMemoryTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioMemoryTest.java new file mode 100644 index 0000000000..98b238b089 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioMemoryTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioMemoryTest; + +public class InVmModelForDbgengScenarioMemoryTest extends AbstractModelForDbgengScenarioMemoryTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioStackTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioStackTest.java new file mode 100644 index 0000000000..6dad9ca48f --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioStackTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioStackTest; + +public class InVmModelForDbgengScenarioStackTest extends AbstractModelForDbgengScenarioStackTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioX64RegistersTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioX64RegistersTest.java new file mode 100644 index 0000000000..c175e300fd --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengScenarioX64RegistersTest.java @@ -0,0 +1,26 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioX64RegistersTest; + +public class InVmModelForDbgengScenarioX64RegistersTest + extends AbstractModelForDbgengScenarioX64RegistersTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengSessionFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengSessionFocusTest.java new file mode 100644 index 0000000000..507890a9b5 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengSessionFocusTest.java @@ -0,0 +1,28 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import org.junit.Ignore; + +import agent.dbgeng.model.AbstractModelForDbgengSessionFocusTest; + +@Ignore("Don't know how to make multiple sessions") +public class InVmModelForDbgengSessionFocusTest extends AbstractModelForDbgengSessionFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengSteppableTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengSteppableTest.java new file mode 100644 index 0000000000..6d24173a11 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengSteppableTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengSteppableTest; + +public class InVmModelForDbgengSteppableTest extends AbstractModelForDbgengSteppableTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengThreadFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengThreadFocusTest.java new file mode 100644 index 0000000000..773bed6acf --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengThreadFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengThreadFocusTest; + +public class InVmModelForDbgengThreadFocusTest extends AbstractModelForDbgengThreadFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengX64RegistersTest.java b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengX64RegistersTest.java new file mode 100644 index 0000000000..8168ad53fe --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/test/java/agent/dbgeng/model/invm/InVmModelForDbgengX64RegistersTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgeng.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengX64RegistersTest; + +public class InVmModelForDbgengX64RegistersTest extends AbstractModelForDbgengX64RegistersTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgengModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2Impl.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2Impl.java index 5b05d33db7..f8ad36c661 100644 --- a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2Impl.java +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2Impl.java @@ -16,7 +16,7 @@ package agent.dbgmodel.model.impl; import java.io.IOException; -import java.util.List; +import java.util.*; import java.util.concurrent.CompletableFuture; import org.jdom.JDOMException; @@ -26,6 +26,7 @@ import agent.dbgeng.model.AbstractDbgModel; import agent.dbgeng.model.iface2.DbgModelTargetObject; import agent.dbgeng.model.iface2.DbgModelTargetSession; import agent.dbgmodel.manager.DbgManager2Impl; +import ghidra.dbg.DebuggerModelClosedReason; import ghidra.dbg.agent.AbstractTargetObject; import ghidra.dbg.agent.AbstractTargetObject.ProxyFactory; import ghidra.dbg.agent.SpiTargetObject; @@ -33,6 +34,7 @@ import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.XmlSchemaContext; import ghidra.program.model.address.*; +import ghidra.util.Msg; import utilities.util.ProxyUtilities; public class DbgModel2Impl extends AbstractDbgModel @@ -63,10 +65,9 @@ public class DbgModel2Impl extends AbstractDbgModel new DefaultAddressFactory(new AddressSpace[] { space }); protected final DbgManager2Impl dbg; - protected final DbgModel2TargetRootImpl root; protected DbgModelTargetSession session; - protected final CompletableFuture completedRoot; + protected Map objectMap = new HashMap<>(); public DbgModel2Impl() { this.dbg = new DbgManager2Impl(); @@ -100,7 +101,7 @@ public class DbgModel2Impl extends AbstractDbgModel @Override public CompletableFuture startDbgEng(String[] args) { - return dbg.start(args); + return dbg.start(args).thenApplyAsync(__ -> null, clientExecutor); } @Override @@ -110,6 +111,8 @@ public class DbgModel2Impl extends AbstractDbgModel @Override public void terminate() throws IOException { + listeners.fire.modelClosed(DebuggerModelClosedReason.NORMAL); + root.invalidateSubtree(root, "Dbgmodel is terminating"); dbg.terminate(); } @@ -143,4 +146,19 @@ public class DbgModel2Impl extends AbstractDbgModel public DbgModelTargetSession getSession() { return session; } + + @Override + public void addModelObject(Object object, TargetObject modelObject) { + if (modelObject == null) { + Msg.error(this, "Attempt to add null for key: " + object); + return; + } + objectMap.put(object, modelObject); + } + + @Override + public TargetObject getModelObject(Object object) { + return objectMap.get(object); + } + } diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetAvailableContainerImpl.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetAvailableContainerImpl.java index cf284e189b..a28c0059d0 100644 --- a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetAvailableContainerImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetAvailableContainerImpl.java @@ -33,9 +33,6 @@ public class DbgModel2TargetAvailableContainerImpl extends DbgModel2TargetObject public DbgModel2TargetAvailableContainerImpl(DbgModelTargetObject obj) { super(obj.getModel(), obj, "Available", "AvailableContainer"); - changeAttributes(List.of(), List.of(), Map.of( // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.SOLICITED // - ), "Initialized"); } @Override diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java index a6542676c9..462bb892bd 100644 --- a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetObjectImpl.java @@ -30,13 +30,14 @@ import agent.dbgmodel.dbgmodel.main.ModelObject; import agent.dbgmodel.jna.dbgmodel.DbgModelNative.ModelObjectKind; import agent.dbgmodel.jna.dbgmodel.DbgModelNative.TypeKind; import agent.dbgmodel.manager.DbgManager2Impl; +import ghidra.async.AsyncUtils; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.schema.TargetObjectSchema; -import ghidra.dbg.util.CollectionUtils.Delta; import ghidra.dbg.util.PathUtils; import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator; import ghidra.util.Msg; @@ -82,8 +83,7 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject DbgModel2TargetObjectImpl(ProxyFactory proxyFactory, I proxyInfo, - AbstractDbgModel model, TargetObject parent, String name, - String typeHint) { + AbstractDbgModel model, TargetObject parent, String name, String typeHint) { super(proxyFactory, proxyInfo, model, parent, name, typeHint); } @@ -105,11 +105,15 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject requestAugmentedAttributes() { + return requestAttributes(false); + } + @Override public CompletableFuture requestElements(boolean refresh) { - synchronized (elements) { - List nlist = new ArrayList<>(); - return requestNativeElements().thenCompose(list -> { + List nlist = new ArrayList<>(); + return requestNativeElements().thenCompose(list -> { + synchronized (elements) { for (TargetObject element : elements.values()) { if (!list.contains(element)) { if (element instanceof DbgStateListener) { @@ -121,11 +125,12 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject { - setElements(nlist, Map.of(), "Refreshed"); - }); - } + return AsyncUtils.NIL; + //return processModelObjectElements(nlist); + } + }).thenAccept(__ -> { + changeElements(List.of(), nlist, Map.of(), "Refreshed"); + }); } @Override @@ -150,7 +155,7 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject { - setAttributes(List.of(), nmap, "Refreshed"); + changeAttributes(List.of(), nmap, "Refreshed"); }); } @@ -169,7 +174,7 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject getCachedElements().get(trimKey)); + return requestElements(false).thenApply(__ -> getCachedElements().get(trimKey)); } } synchronized (attributes) { @@ -313,7 +317,7 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject getCachedAttribute(key)); + return requestAttributes(false).thenApply(__ -> getCachedAttribute(key)); } } @@ -345,7 +349,7 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject attrs, boolean modified) { if (modified) { attrs.put(MODIFIED_ATTRIBUTE_NAME, modified); - listeners.fire.displayChanged(this, getDisplay()); } } @@ -393,7 +396,6 @@ public class DbgModel2TargetObjectImpl extends DefaultTargetObject setAttributes(Map attributes, String reason) { - Delta delta; - synchronized (this.attributes) { - delta = Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL); - } - TargetObjectSchema schemax = getSchema(); - if (schemax != null) { - schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema()); - } - doInvalidateAttributes(delta.removed, reason); - if (!delta.isEmpty()) { - listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added); - return delta; - } - return delta; + /* + protected static Set> dependencySet = Set.of(// + TargetProcess.class, // + TargetThread.class, // + TargetStack.class, // + TargetStackFrame.class, // + TargetRegisterBank.class, // + TargetRegisterContainer.class, // + TargetRegister.class, // + TargetMemory.class, // + TargetMemoryRegion.class, // + TargetModule.class, // + TargetModuleContainer.class, // + TargetSection.class, // + TargetBreakpointContainer.class, // + TargetBreakpointSpec.class, // + TargetBreakpointLocation.class, // + TargetEventScope.class, // + TargetFocusScope.class, // + TargetExecutionStateful.class // + ); + + private CompletableFuture findDependencies(TargetObjectListener l) { + System.err.println("findDependencies " + this); + Map resultAttrs = new HashMap<>(); + Map resultElems = new HashMap<>(); + AsyncFence fence = new AsyncFence(); + fence.include(fetchAttributes(false).thenCompose(attrs -> { + AsyncFence af = new AsyncFence(); + for (String key : attrs.keySet()) { //requiredObjKeys) { + Object object = attrs.get(key); + if (!(object instanceof TargetObjectRef)) { + continue; + } + TargetObjectRef ref = (TargetObjectRef) object; + if (PathUtils.isLink(getPath(), key, ref.getPath())) { + continue; + } + af.include(ref.fetch().thenAccept(obj -> { + if (isDependency(obj)) { + synchronized (this) { + resultAttrs.put(key, obj); + obj.addListener(l); + } + } + })); + } + return af.ready(); + })); + fence.include(fetchElements(false).thenCompose(elems -> { + AsyncFence ef = new AsyncFence(); + for (Entry entry : elems.entrySet()) { + ef.include(entry.getValue().fetch().thenAccept(obj -> { + synchronized (this) { + resultElems.put(entry.getKey(), obj); + obj.addListener(l); + } + })); + } + return ef.ready(); + })); + return fence.ready().thenAccept(__ -> { + listeners.fire.attributesChanged(this, List.of(), resultAttrs); + listeners.fire.elementsChanged(this, List.of(), resultElems); + }); } - - @Override - public Delta changeAttributes(List remove, Map add, String reason) { - Delta delta; - synchronized (attributes) { - delta = Delta.apply(this.attributes, remove, add, Delta.EQUAL); + + public boolean isDependency(TargetObject object) { + String name = object.getName(); + if (name != null) { + if (name.equals("Debug")) + return true; + if (name.equals("Stack")) + return true; } - TargetObjectSchema schemax = getSchema(); - if (schemax != null) { - schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema()); + + Set> interfaces = object.getSchema().getInterfaces(); + for (Class ifc : interfaces) { + if (dependencySet.contains(ifc)) { + return true; + } } - doInvalidateAttributes(delta.removed, reason); - if (!delta.isEmpty()) { - listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added); - } - return delta; + return false; } + + @Override + public void addListener(TargetObjectListener l) { + listeners.add(l); + if (isDependency(this)) { + findDependencies(l); + } + } + */ } diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetRootImpl.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetRootImpl.java index 1fd94fcdaf..fc06dc474c 100644 --- a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetRootImpl.java +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DbgModel2TargetRootImpl.java @@ -31,7 +31,6 @@ import agent.dbgmodel.manager.DbgManager2Impl; import ghidra.async.AsyncUtils; import ghidra.async.TypeSpec; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointListener; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.PathUtils; @@ -93,6 +92,7 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot // DEBUGGER_ATTRIBUTE_NAME, "dbgeng", // // OS_ATTRIBUTE_NAME, "Windows", // ), "Initialized"); + impl.getManager().addEventsListener(this); } @@ -115,7 +115,6 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot intrinsics.put(TargetFocusScope.FOCUS_ATTRIBUTE_NAME, focus); DbgModelTargetSession session = focus.getParentSession(); session.select(); - listeners.fire(TargetFocusScopeListener.class).focusChanged(this, sel); } return doFire; } @@ -167,31 +166,30 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot @Override public void processAdded(DbgProcess proc, DbgCause cause) { - stateChanged(proc, DbgState.STARTING, "ProcessAdded"); getObject(proc).thenAccept(obj -> { - DbgModelTargetProcess process = (DbgModelTargetProcess) obj; - if (process == null) { + DbgModelTargetProcess targetProcess = (DbgModelTargetProcess) obj; + if (targetProcess == null) { + System.err.println("processAdded - null"); return; } - getListeners().fire(TargetEventScopeListener.class) - .event( - this, null, TargetEventType.PROCESS_CREATED, "Process " + proc.getId() + - " started " + "notepad.exe" + " pid=" + proc.getPid(), - List.of(process)); + System.err.println("SERVER:processAdded: " + proc); + getListeners().fire.event(getProxy(), null, TargetEventType.PROCESS_CREATED, + "Process " + proc.getId() + " started " + "notepad.exe" + " pid=" + proc.getPid(), + List.of(targetProcess)); }); } @Override public void threadCreated(DbgThread thread, DbgCause cause) { - stateChanged(thread, DbgState.STARTING, "ThreadCreated"); getObject(thread).thenAccept(obj -> { DbgModelTargetThread targetThread = (DbgModelTargetThread) obj; if (targetThread == null) { + System.err.println("threadCreated - null"); return; } - getListeners().fire(TargetEventScopeListener.class) - .event(this, targetThread, TargetEventType.THREAD_CREATED, - "Thread " + thread.getId() + " started", List.of(targetThread)); + System.err.println("SERVER:threadCreated: " + targetThread); + getListeners().fire.event(getProxy(), targetThread, TargetEventType.THREAD_CREATED, + "Thread " + thread.getId() + " started", List.of(targetThread)); }); } @@ -202,9 +200,11 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot if (mod == null) { return; } - getListeners().fire(TargetEventScopeListener.class) - .event(this, null, TargetEventType.MODULE_LOADED, - "Library " + info.moduleName + " loaded", List.of(mod)); + getObject(getManager().getEventThread()).thenAccept(t -> { + TargetThread eventThread = (TargetThread) t; + getListeners().fire.event(getProxy(), eventThread, TargetEventType.MODULE_LOADED, + "Library " + info.getModuleName() + " loaded", List.of(mod)); + }); }); } @@ -215,13 +215,19 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot if (mod == null) { return; } - getListeners().fire(TargetEventScopeListener.class) - .event(this, null, TargetEventType.MODULE_UNLOADED, - "Library " + info.moduleName + " unloaded", List.of(mod)); + getObject(getManager().getEventThread()).thenAccept(t -> { + TargetThread eventThread = (TargetThread) t; + getListeners().fire.event(getProxy(), eventThread, TargetEventType.MODULE_UNLOADED, + "Library " + info.getModuleName() + " unloaded", List.of(mod)); + }); }); } private CompletableFuture getObject(Object object) { + DbgModelTargetObject modelObject = (DbgModelTargetObject) getModel().getModelObject(object); + if (modelObject != null) { + return CompletableFuture.completedFuture(modelObject); + } List objPath = findObject(object); if (objPath == null) { return CompletableFuture.completedFuture(null); @@ -232,10 +238,12 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot getModel().fetchModelObject(objPath).handle(seq::next); }, TypeSpec.cls(TargetObject.class)).then((pobj, seq) -> { DbgModelTargetObject pimpl = (DbgModelTargetObject) pobj; + getModel().addModelObject(object, pimpl); seq.exit(pimpl); }).finish(); } + //TODO: fix this private CompletableFuture getObjectRevisited(Object object, List ext, Object info) { List objPath = findObject(object); @@ -279,30 +287,23 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot @Override public void processRemoved(DebugProcessId processId, DbgCause cause) { - getObject(processId).thenAccept(obj -> { - DbgModelTargetProcess process = obj.as(DbgModelTargetProcess.class); - if (process == null) { - return; - } - DbgProcess proc = process.getProcess(); - getListeners().fire(TargetEventScopeListener.class) - .event(this, null, TargetEventType.PROCESS_EXITED, - "Process " + proc.getId() + " exited code=" + proc.getExitCode(), - List.of(process)); - }); + DbgModelTargetProcess process = (DbgModelTargetProcess) getObject(processId); + if (process == null) { + return; + } + DbgProcess proc = process.getProcess(); + getListeners().fire.event(getProxy(), null, TargetEventType.PROCESS_EXITED, + "Process " + proc.getId() + " exited code=" + proc.getExitCode(), List.of(process)); } @Override public void threadExited(DebugThreadId threadId, DbgProcess process, DbgCause cause) { - getObject(threadId).thenAccept(obj -> { - DbgModelTargetThread targetThread = obj.as(DbgModelTargetThread.class); - if (targetThread == null) { - return; - } - getListeners().fire(TargetEventScopeListener.class) - .event(this, targetThread, TargetEventType.THREAD_EXITED, - "Thread " + threadId + " exited", List.of(targetThread)); - }); + DbgModelTargetThread targetThread = (DbgModelTargetThread) getObject(threadId); + if (targetThread == null) { + return; + } + getListeners().fire.event(getProxy(), targetThread, TargetEventType.THREAD_EXITED, + "Thread " + threadId + " exited", List.of(targetThread)); } @Override @@ -323,9 +324,8 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot intrinsics.put(TargetEventScope.EVENT_THREAD_ATTRIBUTE_NAME, Long.toHexString(thread.getTid())); TargetEventType eventType = getEventType(state, cause, reason); - getListeners().fire(TargetEventScopeListener.class) - .event(this, targetThread, eventType, - "Thread " + thread.getId() + " state changed", List.of(targetThread)); + getListeners().fire.event(getProxy(), targetThread, eventType, + "Thread " + thread.getId() + " state changed", List.of(targetThread)); }); } @@ -336,8 +336,7 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot getModel().fetchModelValue(objPath).handle(seq::next); }, TypeSpec.cls(Object.class)).then((obj, seq) -> { if (obj instanceof DbgModelTargetExecutionStateful) { - DbgModelTargetExecutionStateful stateful = - (DbgModelTargetExecutionStateful) obj; + DbgModelTargetExecutionStateful stateful = (DbgModelTargetExecutionStateful) obj; TargetExecutionState execState = stateful.convertState(state); stateful.setExecutionState(execState, reason); } @@ -374,13 +373,13 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot DbgModelTargetBreakpointSpec bpt = (DbgModelTargetBreakpointSpec) obj; if (bpt == null) { Msg.error(this, "Stopped for breakpoint unknown to the agent: " + - info.getNumber() + " (pc=" + info.getLocation() + ")"); + info.getNumber() + " (pc=" + info.getExpression() + ")"); return; } - listeners.fire(TargetBreakpointListener.class) - .breakpointHit((TargetBreakpointContainer) bpt.getParent(), - getParentProcess(), null, bpt, bpt); + DbgModelTargetThread targetThread = getParentProcess().getThreads() + .getTargetThread(getManager().getEventThread()); + listeners.fire.breakpointHit(bpt.getParent(), targetThread, null, bpt, bpt); bpt.breakpointHit(); }); } @@ -470,6 +469,9 @@ public class DbgModel2TargetRootImpl extends DbgModel2DefaultTargetModelRoot return TargetEventType.THREAD_EXITED; } return TargetEventType.STOPPED; + case SESSION_EXIT: + getModel().close(); + break; default: break; } diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DelegateDbgModel2TargetObject.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DelegateDbgModel2TargetObject.java index a87d1a8951..42f44270f0 100644 --- a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DelegateDbgModel2TargetObject.java +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/java/agent/dbgmodel/model/impl/DelegateDbgModel2TargetObject.java @@ -183,6 +183,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp public DelegateDbgModel2TargetObject(DbgModel2Impl model, DbgModelTargetObject parent, String key, ModelObject modelObject, List> mixins) { super(model, mixins, model, parent.getProxy(), key, getHintForObject(modelObject)); + //System.err.println(this); this.state = new ProxyState(model, modelObject); this.cleanable = CLEANER.register(this, state); @@ -192,6 +193,7 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp model.getManager().addEventsListener((DbgEventsListener) proxy); } setModelObject(modelObject); + update0(); } public DelegateDbgModel2TargetObject clone(String key, ModelObject modelObject) { @@ -247,6 +249,10 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp onExit(); break; } + case SESSION_EXIT: { + getModel().close(); + return; + } } if (proxy instanceof TargetExecutionStateful) { setExecutionState(exec, "Refreshed"); @@ -261,12 +267,50 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp proxy instanceof DbgModelTargetStackFrame || // proxy instanceof DbgModelTargetStack || // proxy instanceof DbgModelTargetTTD) { - listeners.fire.invalidateCacheRequested(proxy); + //listeners.fire.invalidateCacheRequested(proxy); + return; + } + } + + public void update0() { + if (PathUtils.isLink(parent.getPath(), proxy.getName(), proxy.getPath())) { + return; + } + if (proxy instanceof DbgModelTargetSession || // + proxy instanceof DbgModelTargetProcess || // + proxy instanceof DbgModelTargetThread) { + requestAttributes(false); + return; + } + if (proxy instanceof DbgModelTargetRegisterContainer || // + proxy.getName().equals("Stack")) { + requestAttributes(false); + return; + } + if (proxy instanceof DbgModelTargetRegisterBank) { + requestAttributes(false).thenAccept(__ -> { + DbgModelTargetRegisterBank bank = (DbgModelTargetRegisterBank) proxy; + Map result = bank.getValues(); + System.err.println("SERVER:fire.registersUpdated " + bank); + listeners.fire.registersUpdated(bank, result); + }); + return; + } + + if (proxy instanceof DbgModelTargetProcessContainer || // + proxy instanceof DbgModelTargetThreadContainer || // + proxy instanceof DbgModelTargetModuleContainer || // + proxy instanceof DbgModelTargetBreakpointContainer || // + proxy instanceof DbgModelTargetStack) { + requestElements(false); return; } } private void update() { + if (PathUtils.isLink(parent.getPath(), proxy.getName(), proxy.getPath())) { + return; + } if (proxy instanceof DbgModelTargetProcessContainer || // proxy instanceof DbgModelTargetThreadContainer || // proxy instanceof DbgModelTargetModuleContainer || // @@ -275,15 +319,22 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp proxy instanceof DbgModelTargetRegisterBank || // proxy instanceof DbgModelTargetStack || // proxy instanceof DbgModelTargetTTD) { - requestElements(true); - requestAttributes(true); + requestElements(false); + requestAttributes(false); return; } - if (proxy instanceof DbgModelTargetRegister || proxy instanceof DbgModelTargetStackFrame) { - DbgThread thread = getProxy().getParentThread().getThread(); - if (thread.equals(getManager().getEventThread())) { - requestAttributes(true); - } + if (proxy instanceof DbgModelTargetRegisterBank) { + requestAttributes(false).thenAccept(__ -> { + DbgModelTargetRegisterBank bank = (DbgModelTargetRegisterBank) proxy; + Map result = bank.getValues(); + System.err.println("SERVER:fire.registersUpdated " + bank); + listeners.fire.registersUpdated(bank, result); + }); + return; + } + if (proxy instanceof DbgModelTargetRegister || // + proxy instanceof DbgModelTargetStackFrame) { + requestAttributes(false); return; } } @@ -319,6 +370,8 @@ public class DelegateDbgModel2TargetObject extends DbgModel2TargetObjectImpl imp changeAttributes(List.of(), List.of(), Map.of( // TargetAccessConditioned.ACCESSIBLE_ATTRIBUTE_NAME, accessible // ), "Accessibility changed"); + DbgModelTargetAccessConditioned accessConditioned = + (DbgModelTargetAccessConditioned) proxy; } } diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/resources/agent/dbgmodel/model/impl/dbgmodel_schema.xml b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/resources/agent/dbgmodel/model/impl/dbgmodel_schema.xml index 7dcc3f47fe..3240c8c641 100644 --- a/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/resources/agent/dbgmodel/model/impl/dbgmodel_schema.xml +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/main/resources/agent/dbgmodel/model/impl/dbgmodel_schema.xml @@ -1,6 +1,6 @@ - + @@ -30,7 +30,7 @@ - + - + - + - + - + - + - + - + @@ -150,7 +150,7 @@ - + - + - + - + - + @@ -254,7 +254,7 @@ - + - + @@ -284,7 +284,7 @@ - + - + - + @@ -338,7 +338,7 @@ - + @@ -364,7 +364,7 @@ - + - - + + + - + - + @@ -427,12 +428,13 @@ - + - + + - + - + - + @@ -498,7 +517,7 @@ - + - + - + @@ -549,19 +568,21 @@ - + - + + diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/dbgmodel/DbgModelTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/dbgmodel/DbgModelTest.java index 04a042982b..a843b0fb61 100644 --- a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/dbgmodel/DbgModelTest.java +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/dbgmodel/DbgModelTest.java @@ -453,8 +453,8 @@ public class DbgModelTest extends AbstractGhidraHeadlessIntegrationTest { System.out.println(p.toString()); } catch (COMException e) { - System.out.println( - "Error with PID " + util.getCtlId(p) + ": " + e.getMessage()); + System.out + .println("Error with PID " + util.getCtlId(p) + ": " + e.getMessage()); } } } @@ -726,8 +726,7 @@ public class DbgModelTest extends AbstractGhidraHeadlessIntegrationTest { module.getVersion(); DebugHostModuleImpl1 impl = (DebugHostModuleImpl1) module; System.out.println(" FVer: " + Long.toHexString(impl.getFileVersion())); - System.out.println( - " Pvar: " + Long.toHexString(impl.getProductVersion())); + System.out.println(" Pvar: " + Long.toHexString(impl.getProductVersion())); } } } @@ -805,8 +804,7 @@ public class DbgModelTest extends AbstractGhidraHeadlessIntegrationTest { maker.start(); client.getControl() - .execute( - ".load c:\\Software\\windbg\\amd64\\winext\\JSProvider.dll"); + .execute(".load c:\\Software\\windbg\\amd64\\winext\\JSProvider.dll"); client.getControl().execute(".load c:\\Software\\windbg\\amd64\\ttd\\TTDReplayCPU.dll"); client.getControl().execute(".load c:\\Software\\windbg\\amd64\\ttd\\TTDAnalyze.dll"); client.getControl().execute(".load c:\\Software\\windbg\\amd64\\ttd\\TtdExt.dll"); @@ -864,8 +862,7 @@ public class DbgModelTest extends AbstractGhidraHeadlessIntegrationTest { @Test public void testSymbols() { - try (ProcMaker maker = - new ProcMaker("c:\\Users\\dbmilla\\Desktop\\ConsoleApplication1.exe")) { + try (ProcMaker maker = new ProcMaker("c:\\Users\\user\\Desktop\\ConsoleApplication1.exe")) { maker.start(); DebugSymbols ds = client.getSymbols(); @@ -883,8 +880,7 @@ public class DbgModelTest extends AbstractGhidraHeadlessIntegrationTest { System.out.println(module.getName().toString()); System.out.println(module.getSymbolKind()); - List symbol0 = - client.getSymbols().getSymbolIdsByName(""); + List symbol0 = client.getSymbols().getSymbolIdsByName(""); System.out.println(symbol0.size()); try { @@ -940,8 +936,7 @@ public class DbgModelTest extends AbstractGhidraHeadlessIntegrationTest { maker.start(); DebugModule umod = client.getSymbols() - .getModuleByIndex( - client.getSymbols().getNumberLoadedModules() + 1); + .getModuleByIndex(client.getSymbols().getNumberLoadedModules() + 1); System.out.println(umod.getBase()); } } @@ -998,8 +993,7 @@ public class DbgModelTest extends AbstractGhidraHeadlessIntegrationTest { // Debugger should be able to modify program code. DebugMemoryBasicInformation writable = null; space: for (DebugMemoryBasicInformation info : client.getDataSpaces() - .iterateVirtual( - 0)) { + .iterateVirtual(0)) { for (PageProtection prot : info.protect) { if (prot.isWrite()) { writable = info; diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmDbgmodelModelHost.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmDbgmodelModelHost.java new file mode 100644 index 0000000000..d125a3c3db --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmDbgmodelModelHost.java @@ -0,0 +1,27 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractDbgengModelHost; +import agent.dbgmodel.DbgModelInJvmDebuggerModelFactory; +import ghidra.dbg.DebuggerModelFactory; + +public class InVmDbgmodelModelHost extends AbstractDbgengModelHost { + @Override + public DebuggerModelFactory getModelFactory() { + return new DbgModelInJvmDebuggerModelFactory(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelBreakpointsTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelBreakpointsTest.java new file mode 100644 index 0000000000..4ac595cf0d --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelBreakpointsTest.java @@ -0,0 +1,26 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengBreakpointsTest; + +public class InVmModelForDbgmodelBreakpointsTest extends AbstractModelForDbgengBreakpointsTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } + +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelFactoryTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelFactoryTest.java new file mode 100644 index 0000000000..14c2de181d --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelFactoryTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengFactoryTest; + +public class InVmModelForDbgmodelFactoryTest extends AbstractModelForDbgengFactoryTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelFrameFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelFrameFocusTest.java new file mode 100644 index 0000000000..dc84ab07b3 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelFrameFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengFrameFocusTest; + +public class InVmModelForDbgmodelFrameFocusTest extends AbstractModelForDbgengFrameFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelInterpreterTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelInterpreterTest.java new file mode 100644 index 0000000000..c0a961dc69 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelInterpreterTest.java @@ -0,0 +1,35 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import org.junit.Ignore; +import org.junit.Test; + +import agent.dbgeng.model.AbstractModelForDbgengInterpreterTest; + +public class InVmModelForDbgmodelInterpreterTest extends AbstractModelForDbgengInterpreterTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } + + @Override + @Ignore + @Test + public void testAttachViaInterpreterShowsInProcessContainer() throws Throwable { + super.testAttachViaInterpreterShowsInProcessContainer(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelProcessFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelProcessFocusTest.java new file mode 100644 index 0000000000..b0b82bfceb --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelProcessFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengProcessFocusTest; + +public class InVmModelForDbgmodelProcessFocusTest extends AbstractModelForDbgengProcessFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelRootAttacherTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelRootAttacherTest.java new file mode 100644 index 0000000000..fb449fa215 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelRootAttacherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengRootAttacherTest; + +public class InVmModelForDbgmodelRootAttacherTest extends AbstractModelForDbgengRootAttacherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelRootLauncherTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelRootLauncherTest.java new file mode 100644 index 0000000000..c37eeb9fbd --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelRootLauncherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengRootLauncherTest; + +public class InVmModelForDbgmodelRootLauncherTest extends AbstractModelForDbgengRootLauncherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioCloneExitTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioCloneExitTest.java new file mode 100644 index 0000000000..9e111a3332 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioCloneExitTest.java @@ -0,0 +1,26 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioCloneExitTest; + +public class InVmModelForDbgmodelScenarioCloneExitTest + extends AbstractModelForDbgengScenarioCloneExitTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioForkExitTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioForkExitTest.java new file mode 100644 index 0000000000..9817d941d0 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioForkExitTest.java @@ -0,0 +1,35 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import org.junit.Ignore; +import org.junit.Test; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioForkExitTest; + +public class InVmModelForDbgmodelScenarioForkExitTest + extends AbstractModelForDbgengScenarioForkExitTest { + + @Ignore("Specimen is currently defunct") + @Test + public void testScenario() throws Throwable { + } + + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioMemoryTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioMemoryTest.java new file mode 100644 index 0000000000..279d5e8623 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioMemoryTest.java @@ -0,0 +1,26 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioMemoryTest; + +public class InVmModelForDbgmodelScenarioMemoryTest + extends AbstractModelForDbgengScenarioMemoryTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioStackTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioStackTest.java new file mode 100644 index 0000000000..45cfa34b15 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioStackTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioStackTest; + +public class InVmModelForDbgmodelScenarioStackTest extends AbstractModelForDbgengScenarioStackTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioX64RegistersTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioX64RegistersTest.java new file mode 100644 index 0000000000..45968d7ba3 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelScenarioX64RegistersTest.java @@ -0,0 +1,26 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengScenarioX64RegistersTest; + +public class InVmModelForDbgmodelScenarioX64RegistersTest + extends AbstractModelForDbgengScenarioX64RegistersTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelSessionFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelSessionFocusTest.java new file mode 100644 index 0000000000..982d1f1318 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelSessionFocusTest.java @@ -0,0 +1,28 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import org.junit.Ignore; + +import agent.dbgeng.model.AbstractModelForDbgengSessionFocusTest; + +@Ignore("Don't know how to make multiple sessions") +public class InVmModelForDbgmodelSessionFocusTest extends AbstractModelForDbgengSessionFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelSteppableTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelSteppableTest.java new file mode 100644 index 0000000000..80ead91db2 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelSteppableTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengSteppableTest; + +public class InVmModelForDbgmodelSteppableTest extends AbstractModelForDbgengSteppableTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelThreadFocusTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelThreadFocusTest.java new file mode 100644 index 0000000000..59357cdec6 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelThreadFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengThreadFocusTest; + +public class InVmModelForDbgmodelThreadFocusTest extends AbstractModelForDbgengThreadFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelX64RegistersTest.java b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelX64RegistersTest.java new file mode 100644 index 0000000000..8430c0f6dc --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgmodel/src/test/java/agent/dbgmodel/model/invm/InVmModelForDbgmodelX64RegistersTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.dbgmodel.model.invm; + +import agent.dbgeng.model.AbstractModelForDbgengX64RegistersTest; + +public class InVmModelForDbgmodelX64RegistersTest extends AbstractModelForDbgengX64RegistersTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmDbgmodelModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbInferior.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbInferior.java index 43559017aa..3f75d00fa9 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbInferior.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbInferior.java @@ -26,6 +26,7 @@ import agent.gdb.manager.impl.GdbMemoryMapping; /** * A handle to a GDB inferior * + *

* Each inferior controlled by GDB is numbered and usually corresponds to a target process. Methods * that return a {@link CompletableFuture} send a command to GDB via its GDB/MI interpreter. Each * method issuing a command will first change focus to this inferior. The returned future completes @@ -50,6 +51,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * If exited (implying a previous start), get the process exit code * + *

* This may be slightly system-dependent, as the exit code may specify either the status of a * normal exit, or the cause of an abnormal exit. * @@ -60,6 +62,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Get the executable path * + *

* TODO: I presume path on the target system * * @return the executable @@ -69,6 +72,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Get a thread belonging to this inferior * + *

* GDB (at least recent versions) numbers its threads using a global counter. The thread ID is * this number, not the OS-assigned TID. * @@ -80,6 +84,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Enumerate the threads known to the manager to belong to this inferior * + *

* This does not send any commands to GDB. Rather it simply returns a read-only handle to the * manager's internal map for tracking threads and inferiors. * @@ -90,6 +95,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * List GDB's threads in this inferior (thread group) * + *

* This is equivalent to the CLI command: {@code info threads}. * * @return a future that completes with a map of global thread IDs to thread handles @@ -99,6 +105,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Enumerate the modules known to the manager to belong to this inferior * + *

* This does not send any commands to GDB. Rather it simply returns a read-only handle to the * manager's internal map for tracking modules. * @@ -109,6 +116,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * List GDB's modules in this inferior (process, thread group) * + *

* This is equivalent to the CLI command: {@code maintenance info sections ALLOBJ}. This command * is more thorough than {@code info shared} as it contains the executable module, shared * libraries, system-supplied objects, and enumerates all sections thereof, not just @@ -135,8 +143,10 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Change CLI focus to this inferior * + *

* This is equivalent to the CLI command: {@code inferior [THIS_ID]}. * + *

* GDB's CLI has the concept of focus. That is, commands issued must be applied to some * "current" inferior. This method changes GDB's current inferior so that subsequent commands * will apply to this inferior. Commands issued from this handle are always executed with this @@ -149,6 +159,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Specify a binary image for execution and debug symbols * + *

* This is equivalent to the CLI command: {@code file [FILE]}. * * @param file the path to the binary image @@ -159,11 +170,11 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Begin execution * + *

* This is equivalent to the CLI command: {@code run}. Note this will not stop at - * {@code main}. The caller should first set breakpoints if an immediate stop is desired. If the - * same behavior as {@code start} is desired, consider using {@link #console(String)} or - * {@link #consoleCapture(String)}. + * {@code main}. The caller should first set breakpoints if an immediate stop is desired. * + *

* This command completes as soon as the inferior is running. If a stop is expected at a * breakpoint, then the caller should listen for that event before issuing additional commands. * Alternatively, the caller may interrupt the inferior. The manager has only been tested on GDB @@ -173,9 +184,33 @@ public interface GdbInferior extends GdbMemoryOperations { */ CompletableFuture run(); + /** + * Begin execution, stopping at {@code main} + * + *

+ * This is equivalent to the CLI command: {@code start}. Otherwise, it behaves the same as + * {@link #run()}. + * + * @return a future that completes with a handle to the first thread of the running inferior + */ + CompletableFuture start(); + + /** + * Begin execution, stopping at the first instruction + * + *

+ * This is equivalent to the CLI command: {@code starti}. Otherwise, it behaves the same as + * {@link #run()}. Note that {@code starti} is a relatively new command to GDB. Your version may + * not support it. + * + * @return a future that completes with a handle to the first thread of the running inferior + */ + CompletableFuture starti(); + /** * Attach to a running process * + *

* This is equivalent to the CLI command: {@code attach [PID]}. * * @param pid the OS-assigned process ID of the target process @@ -186,6 +221,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Execute an arbitrary CLI command, printing output to the CLI console * + *

* Note: to ensure a certain thread has focus for a console command, see * {@link GdbThread#console(String)}. * @@ -197,6 +233,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Execute an arbitrary CLI command, capturing its console output * + *

* The output will not be printed to the CLI console. To ensure a certain thread has focus for a * console command, see {@link GdbThread#consoleCapture(String)}. * @@ -208,6 +245,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Continue execution * + *

* This is equivalent to the CLI command: {@code continue}. * * @return a future that completes once the inferior is running @@ -217,6 +255,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Step execution * + *

* This is equivalent to the CLI command: {@code step}. * * @param suffix specifies how far to step, or on what conditions stepping ends. @@ -228,6 +267,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Evaluate an expression * + *

* This evaluates an expression in the same way that the CLI commands {@code print}, * {@code output}, and {@code call} would. * @@ -239,6 +279,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Set the controlling TTY for future executions * + *

* This is equivalent to the CLI command: {@code set inferior-tty [TTY]}. It does not affect the * currently running process, if any. This is useful, e.g., to separate target output from GDB's * output. If, e.g., a program outputs lines which look like GDB/MI records, the manager will @@ -253,6 +294,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Get the value of an internal GDB variable * + *

* This is equivalent to the CLI command: {@code show [VAR_NAME]}. * * @param varName the name of the GDB variable @@ -263,6 +305,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Set the value of an internal GDB variable * + *

* This is equivalent to the CLI command: {@code set [VAR_NAME]=[VAL]}. * * @param varName the name of the GDB variable @@ -283,6 +326,7 @@ public interface GdbInferior extends GdbMemoryOperations { /** * Kill the process * + *

* This is equivalent to the CLI command {@code kill}. * * @return a future that completes when GDB has executed the command diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java index 39fdf49785..608ab5a2f4 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java @@ -364,6 +364,13 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions { */ CompletableFuture addInferior(); + /** + * Find an unused inferior, possibly creating a new one + * + * @return a future which completes with the handle to the found inferior + */ + CompletableFuture availableInferior(); + /** * Remove an inferior * diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbStackFrame.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbStackFrame.java index 12936c8cd1..dbe0e9ae6b 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbStackFrame.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbStackFrame.java @@ -57,4 +57,16 @@ public interface GdbStackFrame extends GdbStackFrameOperations { * @return the thread */ GdbThread getThread(); + + /** + * Fill in missing address and function fields with those from the given frame + * + *

+ * If the given frame is null, or is also missing those fields, they will be filled is with 0 + * and "". + * + * @param frame + * @return + */ + GdbStackFrame fillWith(GdbStackFrame frame); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbThread.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbThread.java index c624d1db3c..95c491cf82 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbThread.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbThread.java @@ -25,6 +25,7 @@ import agent.gdb.manager.impl.GdbThreadInfo; /** * A handle to a thread controlled by GDB * + *

* Each thread is numbered by GDB. Methods that return a {@link CompletableFuture} send a command to * GDB via its GDB/MI interpreter. Where applicable, the {@code --thread} parameter is provided to * GDB to ensure commands are executed on this thread. The returned future completes when GDB has @@ -43,6 +44,7 @@ public interface GdbThread /** * Get the GDB-assigned thread number * + *

* This is not the OS-assigned TID. * * @return the number @@ -73,6 +75,7 @@ public interface GdbThread /** * Set the value of an internal GDB variable * + *

* This is equivalent to the CLI command: {@code set [VAR_NAME]=[VAL]}. * * @param varName the name of the GDB variable @@ -98,6 +101,7 @@ public interface GdbThread /** * Continue execution * + *

* This is equivalent to the CLI command: {@code continue}. Depending on GDB's execution mode, * this may allow other threads to execute, too. * @@ -108,12 +112,12 @@ public interface GdbThread /** * Step the thread * + *

* Note that the command can complete before the thread has finished stepping. The command * completes as soon as the thread is running. A separate stop event is emitted when the step is * completed. * * @param suffix specifies how far to step, or on what conditions stepping ends. - * * @return a future that completes once the thread is running */ CompletableFuture step(ExecSuffix suffix); @@ -121,6 +125,7 @@ public interface GdbThread /** * Detach from the entire process * + *

* This is equivalent to the CLI command {@code detach}. It will detach the entire process, not * just this thread. * @@ -131,6 +136,7 @@ public interface GdbThread /** * Kill the entire process * + *

* This is equivalent to the CLI command {@code kill}. It will kill the entire process, not just * this thread. * diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/evt/GdbThreadSelectedEvent.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/evt/GdbThreadSelectedEvent.java index 9857f3d3e2..13fec119ad 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/evt/GdbThreadSelectedEvent.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/evt/GdbThreadSelectedEvent.java @@ -29,6 +29,7 @@ public class GdbThreadSelectedEvent extends AbstractGdbEventWithFields { /** * Construct a new event by parsing the tail for information * + *

* The selected thread ID must be specified by GDB. * * @param tail the text following the event type in the GDB/MI event record diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbCommand.java index 8c78f11883..3ea83c364b 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbCommand.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbCommand.java @@ -25,6 +25,7 @@ import agent.gdb.manager.impl.cmd.AbstractGdbCommandWithThreadId; /** * The interface for GDB command implementations * + *

* Commands are executed by GDB in serial. In order to distinguish the likely cause of events, the * manager will wait to issue each command until it has seen a prompt. Thus, commands are queued up, * and the manager uses the {@link CompletableFuture} pattern to "return" results after execution @@ -34,14 +35,19 @@ import agent.gdb.manager.impl.cmd.AbstractGdbCommandWithThreadId; * use {@link AbstractGdbCommand} or {@link AbstractGdbCommandWithThreadId} to ensure consistent * processing. * - * To begin executing the command, the manager encodes the command, using {@link #encode()}. Any - * event that occurs during command execution is given to - * {@link #handle(GdbEvent, GdbPendingCommand)}. The implementor then has the option to "claim" or - * "steal" the event. When claimed, any subsequent event processor or listener is provided this - * command as the event's cause. When stolen, no subsequent event processors are called. The - * implementation ought to keep a list of claimed and stolen events. Once GDB has finished executing - * the command, the manager calls {@link #complete(GdbPendingCommand)}, allowing the implementation - * to process its claimed and stolen events and return the result of the command. + *

+ * Before executing the command, the manager calls {@link #preCheck(GdbPendingCommand)}, giving the + * implementation an opportunity to cancel the command or complete it early. If the command is + * completed after the pre-check, the manager will not encode it, and instead proceed to the next + * command. This is useful for eliminating unneeded "focus" commands. To begin executing the + * command, the manager encodes the command, using {@link #encode()}. Any event that occurs during + * command execution is given to {@link #handle(GdbEvent, GdbPendingCommand)}. The implementor then + * has the option to "claim" or "steal" the event. When claimed, any subsequent event processor or + * listener is provided this command as the event's cause. When stolen, no subsequent event + * processors are called. The implementation ought to keep a list of claimed and stolen events. Once + * GDB has finished executing the command, the manager calls {@link #complete(GdbPendingCommand)}, + * allowing the implementation to process its claimed and stolen events and return the result of the + * command. * * @param the type of object "returned" by the command */ @@ -55,6 +61,16 @@ public interface GdbCommand { */ public boolean validInState(GdbState state); + /** + * Perform any pre-execution screening for this command + * + *

+ * Complete {@code} pending with a result to short-circuit the execution of this command. + * + * @param pending the pending command result + */ + void preCheck(GdbPendingCommand pending); + /** * Encode the command in GDB/MI * @@ -63,7 +79,21 @@ public interface GdbCommand { public String encode(); /** - * Handle an event that ocurred during the execution of this command + * If executing this command changes the current thread, return that thread's ID + * + * @return the new current thread ID + */ + public Integer impliesCurrentThreadId(); + + /** + * If executing this command change the current frame, return that frame's ID + * + * @return the new current frame ID + */ + public Integer impliesCurrentFrameId(); + + /** + * Handle an event that occurred during the execution of this command * * @param evt the event * @param pending a copy of the executing command instance @@ -74,6 +104,7 @@ public interface GdbCommand { /** * Called when the manager believes this command is finished executing * + *

* This is presumed when the manager receives the prompt after issuing the encoded command * * @param pending a copy of the now-finished-executing command instance diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbInferiorImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbInferiorImpl.java index 05576e4b09..9247d1fd27 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbInferiorImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbInferiorImpl.java @@ -21,8 +21,6 @@ import java.nio.ByteOrder; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -203,9 +201,19 @@ public class GdbInferiorImpl implements GdbInferior { return threads; } + protected CompletableFuture execute(GdbCommand cmd) { + /** + * Queue select and execute one immediately after the other. If I do thenCompose, it's + * possible for some other command to get inserted between, which means this inferior may no + * longer be current for the actual command execution. NB: The select command will cancel + * itself if this inferior is already current. + */ + return select().thenCombine(manager.execute(cmd), (s, e) -> e); + } + @Override public CompletableFuture> listThreads() { - return manager.execute(new GdbListThreadsCommand(manager, this)); + return execute(new GdbListThreadsCommand(manager, this)); } @Override @@ -215,9 +223,9 @@ public class GdbInferiorImpl implements GdbInferior { @Override public CompletableFuture> listModules() { - // "unlikely" is an unlikely section name. Goal is to exclude section lines. + // "nosections" is an unlikely section name. Goal is to exclude section lines. // TODO: See how this behaves on other GDB versions. - return manager.consoleCapture("maintenance info sections ALLOBJ unlikely") + return consoleCapture("maintenance info sections ALLOBJ nosections") .thenApply(this::parseModuleNames); } @@ -226,7 +234,7 @@ public class GdbInferiorImpl implements GdbInferior { } protected CompletableFuture doLoadSections() { - return manager.consoleCapture("maintenance info sections ALLOBJ") + return consoleCapture("maintenance info sections ALLOBJ") .thenAccept(this::parseAndUpdateAllModuleSections); } @@ -311,7 +319,7 @@ public class GdbInferiorImpl implements GdbInferior { @Override public CompletableFuture> listMappings() { - return manager.consoleCapture("info proc mappings").thenApply(this::parseMappings); + return consoleCapture("info proc mappings").thenApply(this::parseMappings); } protected Map parseMappings(String out) { @@ -345,116 +353,93 @@ public class GdbInferiorImpl implements GdbInferior { @Override public CompletableFuture fileExecAndSymbols(String file) { - return select().thenCompose(__ -> { - return manager.execute(new GdbFileExecAndSymbolsCommand(manager, file)); - }); + return execute(new GdbFileExecAndSymbolsCommand(manager, file)); } @Override public CompletableFuture run() { - return select().thenCompose(__ -> { - return manager.execute(new GdbRunCommand(manager)); - }); + return execute(new GdbRunCommand(manager)); + } + + @Override + public CompletableFuture start() { + return execute(new GdbStartCommand(manager)); + } + + @Override + public CompletableFuture starti() { + return execute(new GdbStartInstructionCommand(manager)); } @Override public CompletableFuture> attach(long toPid) { - return select().thenCompose(__ -> { - return manager.execute(new GdbAttachCommand(manager, toPid)); - }); - } - - protected CompletableFuture preferThread( - Function> viaThread, - Supplier> viaThis) { - Optional first = threads.values().stream().findFirst(); - if (first.isPresent()) { - return viaThread.apply(first.get()); - } - return select().thenCompose(__ -> viaThis.get()); + return execute(new GdbAttachCommand(manager, toPid)); } @Override public CompletableFuture console(String command) { - return preferThread(t -> t.console(command), () -> manager.console(command)); + return execute(new GdbConsoleExecCommand(manager, null, null, command, + GdbConsoleExecCommand.Output.CONSOLE)).thenApply(e -> null); } @Override public CompletableFuture consoleCapture(String command) { - return preferThread(t -> t.consoleCapture(command), () -> manager.consoleCapture(command)); + return execute(new GdbConsoleExecCommand(manager, null, null, command, + GdbConsoleExecCommand.Output.CAPTURE)); } @Override public CompletableFuture cont() { - return select().thenCompose(__ -> { - return manager.execute(new GdbContinueCommand(manager, null)); - }); + return execute(new GdbContinueCommand(manager, null)); } @Override public CompletableFuture step(ExecSuffix suffix) { - return select().thenCompose(__ -> { - return manager.execute(new GdbStepCommand(manager, null, suffix)); - }); + return execute(new GdbStepCommand(manager, null, suffix)); } @Override public CompletableFuture evaluate(String expression) { - return preferThread(t -> t.evaluate(expression), - () -> manager.execute(new GdbEvaluateCommand(manager, null, null, expression))); + return execute(new GdbEvaluateCommand(manager, null, null, expression)); } @Override public CompletableFuture setTty(String tty) { - return select().thenCompose(__ -> { - return manager.execute(new GdbSetInferiorTtyCommand(manager, tty)); - }); + return execute(new GdbSetInferiorTtyCommand(manager, tty)); } @Override public CompletableFuture getVar(String varName) { // TODO: Are these actually per-inferior? // If so, should make them accessible via thread - return select().thenCompose(__ -> { - return manager.execute(new GdbGetVarCommand(manager, varName)); - }); + return execute(new GdbGetVarCommand(manager, varName)); } @Override public CompletableFuture setVar(String varName, String val) { // TODO: Are these actually per-inferior? // If so, should make them accessible via thread - return select().thenCompose(__ -> { - return manager.execute(new GdbSetVarCommand(manager, null, varName, val)); - }); + return execute(new GdbSetVarCommand(manager, null, varName, val)); } @Override public CompletableFuture detach() { - return select().thenCompose(__ -> { - return manager.execute(new GdbDetachCommand(manager, this, null)); - }); + return execute(new GdbDetachCommand(manager, this, null)); } @Override public CompletableFuture kill() { - return select().thenCompose(__ -> { - return manager.execute(new GdbKillCommand(manager, null)); - }); + return execute(new GdbKillCommand(manager, null)); } @Override public CompletableFuture> readMemory(long addr, ByteBuffer buf, int len) { - // I can't imagine this working without a thread.... - return preferThread(t -> t.readMemory(addr, buf, len), - () -> manager.execute(new GdbReadMemoryCommand(manager, null, addr, buf, len))); + return execute(new GdbReadMemoryCommand(manager, null, addr, buf, len)); } @Override public CompletableFuture writeMemory(long addr, ByteBuffer buf, int len) { - // I can't imagine this working without a thread.... - return preferThread(t -> t.writeMemory(addr, buf, len), - () -> manager.execute(new GdbWriteMemoryCommand(manager, null, addr, buf, len))); + return execute(new GdbWriteMemoryCommand(manager, null, addr, buf, len)); } @Override diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java index dbbdfba4c1..f50bd05a4f 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java @@ -63,12 +63,14 @@ import sun.misc.SignalHandler; * event is processed using a {@link HandlerMap}. */ public class GdbManagerImpl implements GdbManager { + private static final String GDB_IS_TERMINATING = "GDB is terminating"; + @Internal public enum Interpreter { CLI, MI2; } - private static final boolean LOG_IO = false; + private static final boolean LOG_IO = true; private static final PrintWriter DBG_LOG; static { if (LOG_IO) { @@ -445,9 +447,9 @@ public class GdbManagerImpl implements GdbManager { } private void checkStartedNotExit() { - GdbState st = state.get(); - if (st == GdbState.NOT_STARTED || st == GdbState.EXIT) { - throw new IllegalStateException("GDB is not yet, or no longer running"); + checkStarted(); + if (state.get() == GdbState.EXIT) { + throw new DebuggerModelTerminatingException(GDB_IS_TERMINATING); } } @@ -553,14 +555,22 @@ public class GdbManagerImpl implements GdbManager { if (gdbCmd != null) { iniThread = new PtyThread(Pty.openpty(), Channel.STDOUT, null); + gdb = iniThread.pty.getSlave().session(fullargs.toArray(new String[] {}), null); + gdbWaiter = new Thread(this::waitGdbExit, "GDB WaitExit"); + gdbWaiter.start(); + iniThread.start(); try { - iniThread.hasWriter.get(10, TimeUnit.SECONDS); + CompletableFuture.anyOf(iniThread.hasWriter, state.waitValue(GdbState.EXIT)) + .get(10, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new IOException("Could not detect GDB's interpreter mode"); } + if (state.get() == GdbState.EXIT) { + throw new IOException("GDB terminated before first prompt"); + } switch (iniThread.interpreter) { case CLI: cliThread = iniThread; @@ -584,9 +594,6 @@ public class GdbManagerImpl implements GdbManager { mi2Thread.setName("GDB Read MI2"); break; } - - gdbWaiter = new Thread(this::waitGdbExit, "GDB WaitExit"); - gdbWaiter.start(); } else { mi2Thread = new PtyThread(Pty.openpty(), Channel.STDOUT, Interpreter.MI2); @@ -617,10 +624,8 @@ public class GdbManagerImpl implements GdbManager { state.set(GdbState.EXIT, Causes.UNCLAIMED); exited.set(true); if (!executor.isShutdown()) { - submit(() -> { - processGdbExited(exitcode); - terminate(); - }); + processGdbExited(exitcode); + terminate(); } } catch (InterruptedException e) { @@ -654,7 +659,7 @@ public class GdbManagerImpl implements GdbManager { gdb.destroyForcibly(); } DebuggerModelTerminatingException reason = - new DebuggerModelTerminatingException("GDB is terminating"); + new DebuggerModelTerminatingException(GDB_IS_TERMINATING); cmdLock.dispose(reason); state.dispose(reason); mi2Prompt.dispose(reason); @@ -667,13 +672,18 @@ public class GdbManagerImpl implements GdbManager { } } + protected CompletableFuture execute(GdbCommand cmd) { + // NB. curCmd::finish is passed to eventThread already + return doExecute(cmd);//.thenApplyAsync(t -> t, eventThread); + } + /** * Schedule a command for execution * * @param cmd the command to execute * @return the pending command, which acts as a future for later completion */ - protected GdbPendingCommand execute(GdbCommand cmd) { + protected GdbPendingCommand doExecute(GdbCommand cmd) { assert cmd != null; checkStartedNotExit(); GdbPendingCommand pcmd = new GdbPendingCommand<>(cmd); @@ -690,6 +700,11 @@ public class GdbManagerImpl implements GdbManager { throw new GdbCommandError( "Command " + cmd + " is not valid while " + state.get()); } + cmd.preCheck(pcmd); + if (pcmd.isDone()) { + cmdLockHold.getAndSet(null).release(); + return; + } curCmd = pcmd; } //Msg.debug(this, "CURCMD = " + curCmd); @@ -728,6 +743,22 @@ public class GdbManagerImpl implements GdbManager { } } + protected void checkImpliedFocusChange() { + Integer tid = curCmd.impliesCurrentThreadId(); + GdbThreadImpl thread = null; + if (tid != null) { + thread = getThread(tid); + } + Integer level = curCmd.impliesCurrentFrameId(); + GdbStackFrameImpl frame = null; + if (level != null) { + frame = new GdbStackFrameImpl(thread, level, null, null); + } + if (thread != null) { + doThreadSelected(thread, frame, curCmd); + } + } + protected synchronized void processEvent(GdbEvent evt) { /** * NOTE: I've forgotten why, but the the state update needs to happen between handle and @@ -736,21 +767,24 @@ public class GdbManagerImpl implements GdbManager { boolean cmdFinished = false; if (curCmd != null) { cmdFinished = curCmd.handle(evt); + if (cmdFinished) { + checkImpliedFocusChange(); + } } GdbState newState = evt.newState(); //Msg.debug(this, evt + " transitions state to " + newState); state.set(newState, evt.getCause()); + // NOTE: Do not check if claimed here. + // Downstream processing should check for cause + handlerMap.handle(evt, null); + if (cmdFinished) { event(curCmd::finish, "curCmd::finish"); curCmd = null; cmdLockHold.getAndSet(null).release(); } - - // NOTE: Do not check if claimed here. - // Downstream processing should check for cause - handlerMap.handle(evt, null); } /** @@ -1126,7 +1160,7 @@ public class GdbManagerImpl implements GdbManager { return; } GdbBreakpointInfo newInfo = oldInfo.withEnabled(false); - oldInfo = oldInfo.withEnabled(true); + //oldInfo = oldInfo.withEnabled(true); doBreakpointModifiedSameLocations(newInfo, oldInfo, cause); } @@ -1137,7 +1171,7 @@ public class GdbManagerImpl implements GdbManager { return; } GdbBreakpointInfo newInfo = oldInfo.withEnabled(true); - oldInfo = oldInfo.withEnabled(false); + //oldInfo = oldInfo.withEnabled(false); doBreakpointModifiedSameLocations(newInfo, oldInfo, cause); } @@ -1458,6 +1492,18 @@ public class GdbManagerImpl implements GdbManager { return execute(new GdbAddInferiorCommand(this)); } + @Override + public CompletableFuture availableInferior() { + return listInferiors().thenCompose(map -> { + for (GdbInferior inf : map.values()) { + if (inf.getPid() == null) { + return CompletableFuture.completedFuture(inf); + } + } + return addInferior(); + }); + } + @Override public CompletableFuture removeInferior(GdbInferior inferior) { return execute(new GdbRemoveInferiorCommand(this, inferior.getId())); @@ -1466,6 +1512,7 @@ public class GdbManagerImpl implements GdbManager { /** * Select the given inferior * + *

* This issues a command to GDB to change its focus. It is not just a manager concept. * * @param inferior the inferior to select diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbPendingCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbPendingCommand.java index 4a28bd830c..dfba0017c5 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbPendingCommand.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbPendingCommand.java @@ -54,6 +54,14 @@ public class GdbPendingCommand extends CompletableFuture implements GdbCau return cmd; } + public Integer impliesCurrentThreadId() { + return cmd.impliesCurrentThreadId(); + } + + public Integer impliesCurrentFrameId() { + return cmd.impliesCurrentFrameId(); + } + /** * Finish the execution of this command */ @@ -71,6 +79,7 @@ public class GdbPendingCommand extends CompletableFuture implements GdbCau /** * Handle an event * + *

* This gives the command implementation the first chance to claim or steal an event * * @param evt the event diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbStackFrameImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbStackFrameImpl.java index b10f6bc9df..55f8a65376 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbStackFrameImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbStackFrameImpl.java @@ -76,6 +76,32 @@ public class GdbStackFrameImpl implements GdbStackFrame { return thread; } + @Override + public GdbStackFrame fillWith(GdbStackFrame frame) { + if (addr != null && func != null) { + return this; + } + BigInteger fAddr = addr; + if (fAddr == null) { + if (frame != null && frame.getAddress() != null) { + fAddr = frame.getAddress(); + } + else { + fAddr = BigInteger.ZERO; + } + } + String fFunc = func; + if (fFunc == null) { + if (frame != null && frame.getFunction() != null) { + fFunc = frame.getFunction(); + } + else { + fFunc = ""; + } + } + return new GdbStackFrameImpl(thread, level, fAddr, fFunc); + } + @Override public CompletableFuture select() { return manager.execute(new GdbThreadSelectCommand(manager, thread.getId(), level)); @@ -95,7 +121,9 @@ public class GdbStackFrameImpl implements GdbStackFrame { @Override public CompletableFuture writeRegisters(Map regVals) { - return manager.execute(new GdbWriteRegistersCommand(manager, thread, level, regVals)); + return thread.getInferior().syncEndianness().thenCompose(__ -> { + return manager.execute(new GdbWriteRegistersCommand(manager, thread, level, regVals)); + }); } @Override diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbThreadImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbThreadImpl.java index c2cdf1912c..6bbe2b59f3 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbThreadImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbThreadImpl.java @@ -55,7 +55,7 @@ public class GdbThreadImpl implements GdbThread { private final GdbInferiorImpl inferior; private final AsyncReference state = - new AsyncReference<>(GdbState.STOPPED); + new AsyncReference<>(GdbState.RUNNING); private final AsyncLazyValue registers = new AsyncLazyValue<>(this::doListRegisters); @@ -128,7 +128,7 @@ public class GdbThreadImpl implements GdbThread { protected CompletableFuture execute(AbstractGdbCommand cmd) { switch (cmd.getInterpreter()) { case CLI: - return select().thenCompose(v -> manager.execute(cmd)); + return select().thenCombine(manager.execute(cmd), (__, v) -> v); case MI2: return manager.execute(cmd); default: diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommand.java index 52657bb67a..13371e48a5 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommand.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommand.java @@ -16,9 +16,12 @@ package agent.gdb.manager.impl.cmd; import agent.gdb.manager.GdbState; -import agent.gdb.manager.impl.GdbCommand; -import agent.gdb.manager.impl.GdbManagerImpl; +import agent.gdb.manager.evt.GdbCommandErrorEvent; +import agent.gdb.manager.evt.GdbConsoleOutputEvent; +import agent.gdb.manager.impl.*; import agent.gdb.manager.impl.GdbManagerImpl.Interpreter; +import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError; +import ghidra.util.Msg; /** * A base class for interacting with specific GDB commands @@ -43,6 +46,10 @@ public abstract class AbstractGdbCommand implements GdbCommand { return true; // With dual interpreters, shouldn't have to worry. } + @Override + public void preCheck(GdbPendingCommand pending) { + } + @Override public String toString() { return ""; @@ -58,4 +65,45 @@ public abstract class AbstractGdbCommand implements GdbCommand { public Interpreter getInterpreter() { return Interpreter.MI2; } + + /** + * Check for an error reported in MI2 syntax via the CLI + * + *

+ * This must be used in the {@link #handle(GdbEvent, GdbPendingCommand)} callback when the + * command is encoded as a MI2 command (using {@code interpreter-exec mi2}) but issued via the + * CLI. Depending on the GDB version and the outcome of the command, the result may be reported + * via the CLI, but in MI2 syntax. As of yet, this has only been observed for {@code ^error} + * results. + * + * @param evt the event to check + * @return the decoded error event, if applicable, or the original unmodified event. + */ + protected GdbEvent checkErrorViaCli(GdbEvent evt) { + if (evt instanceof GdbConsoleOutputEvent) { + GdbConsoleOutputEvent outEvt = (GdbConsoleOutputEvent) evt; + // This is quirky in 8.0.1. + // I don't know to what other version(s) it applies. + String out = outEvt.getOutput(); + if (out.startsWith("^error")) { + try { + return GdbCommandErrorEvent.fromMi2(out.split(",", 2)[1].trim()); + } + catch (GdbParseError e) { + Msg.error(this, "Could not parse error result", e); + } + } + } + return evt; + } + + @Override + public Integer impliesCurrentThreadId() { + return null; + } + + @Override + public Integer impliesCurrentFrameId() { + return null; + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandExpectRunning.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandExpectRunning.java new file mode 100644 index 0000000000..2ee0ecc4d1 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandExpectRunning.java @@ -0,0 +1,59 @@ +/* ### + * 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 agent.gdb.manager.impl.cmd; + +import agent.gdb.manager.GdbThread; +import agent.gdb.manager.evt.*; +import agent.gdb.manager.impl.*; + +public abstract class AbstractGdbCommandExpectRunning extends AbstractGdbCommand { + + protected AbstractGdbCommandExpectRunning(GdbManagerImpl manager) { + super(manager); + } + + @Override + public boolean handle(GdbEvent evt, GdbPendingCommand pending) { + evt = checkErrorViaCli(evt); + if (evt instanceof GdbCommandRunningEvent) { + pending.claim(evt); + return pending.hasAny(GdbRunningEvent.class); + } + else if (evt instanceof AbstractGdbCompletedCommandEvent) { + pending.claim(evt); + return true; // Not the expected Completed event + } + else if (evt instanceof GdbRunningEvent) { + // Event happens no matter which interpreter received the command + pending.claim(evt); + return pending.hasAny(GdbCommandRunningEvent.class); + } + else if (evt instanceof GdbThreadCreatedEvent) { + pending.claim(evt); + } + return false; + } + + @Override + public GdbThread complete(GdbPendingCommand pending) { + pending.checkCompletion(GdbCommandRunningEvent.class); + + // Just take the first thread. Others are considered clones. + GdbThreadCreatedEvent created = pending.findFirstOf(GdbThreadCreatedEvent.class); + int tid = created.getThreadId(); + return manager.getThread(tid); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandWithThreadAndFrameId.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandWithThreadAndFrameId.java index 4269904086..c40dd6c2ff 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandWithThreadAndFrameId.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandWithThreadAndFrameId.java @@ -53,6 +53,11 @@ public abstract class AbstractGdbCommandWithThreadAndFrameId return frameId; } + @Override + public Integer impliesCurrentFrameId() { + return frameId; + } + @Override protected String encode(String threadPart) { return encode(threadPart, makeFramePart()); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandWithThreadId.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandWithThreadId.java index 1b8caa53c1..8575b4a021 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandWithThreadId.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/AbstractGdbCommandWithThreadId.java @@ -54,9 +54,15 @@ public abstract class AbstractGdbCommandWithThreadId extends AbstractGdbComma return threadId; } + @Override + public Integer impliesCurrentThreadId() { + return threadId; + } + /** * Encode the command in GDB/MI, given the pre-constructed thread argument part * + *

* The given thread argument is preceded by a space, but not followed by one. For example, a * command can be properly constructed as: * diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbContinueCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbContinueCommand.java index 82ccfa3782..b0f6690ca8 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbContinueCommand.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbContinueCommand.java @@ -52,6 +52,7 @@ public class GdbContinueCommand extends AbstractGdbCommandWithThreadId { @Override public boolean handle(GdbEvent evt, GdbPendingCommand pending) { + evt = checkErrorViaCli(evt); if (evt instanceof GdbCommandRunningEvent) { pending.claim(evt); return pending.hasAny(GdbRunningEvent.class); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbInferiorSelectCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbInferiorSelectCommand.java index 15df8f6f02..e0f315832f 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbInferiorSelectCommand.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbInferiorSelectCommand.java @@ -26,16 +26,18 @@ public class GdbInferiorSelectCommand extends AbstractGdbCommand { this.id = id; } + @Override + public void preCheck(GdbPendingCommand pending) { + if (manager.currentInferior().getId() == id) { + pending.complete(null); + } + } + @Override public String encode() { /** * There does not appear to be a real -inferior-select command - * - * Also, if the requested inferior is already current, don't do anything. */ - if (manager.currentInferior().getId() == id) { - return "-interpreter-exec console echo"; - } return "-interpreter-exec console \"inferior " + id + "\""; } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbInterruptCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbInterruptCommand.java index 2962639ba4..e9769f5a01 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbInterruptCommand.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbInterruptCommand.java @@ -21,6 +21,7 @@ import agent.gdb.manager.evt.AbstractGdbCompletedCommandEvent; import agent.gdb.manager.evt.GdbStoppedEvent; import agent.gdb.manager.impl.*; import agent.gdb.manager.impl.GdbManagerImpl.Interpreter; +import ghidra.util.Msg; /** * Implementation of {@link GdbManager#interrupt()} when we start GDB @@ -40,12 +41,15 @@ public class GdbInterruptCommand extends AbstractGdbCommand { public String encode() { Interpreter i = getInterpreter(); if (i == manager.getRunningInterpreter()) { + Msg.debug(this, "Using ^C to interrupt"); return "\u0003"; } switch (i) { case CLI: + Msg.debug(this, "Interrupting via CLI"); return "interrupt"; case MI2: + Msg.debug(this, "Interrupting via MI2"); return "-exec-interrupt"; default: throw new AssertionError(); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbRunCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbRunCommand.java index b2eea78bce..b4c2ac6352 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbRunCommand.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbRunCommand.java @@ -16,15 +16,13 @@ package agent.gdb.manager.impl.cmd; import agent.gdb.manager.GdbInferior; -import agent.gdb.manager.GdbThread; -import agent.gdb.manager.evt.*; -import agent.gdb.manager.impl.*; +import agent.gdb.manager.impl.GdbManagerImpl; import agent.gdb.manager.impl.GdbManagerImpl.Interpreter; /** * Implementation of {@link GdbInferior#run()} */ -public class GdbRunCommand extends AbstractGdbCommand { +public class GdbRunCommand extends AbstractGdbCommandExpectRunning { public GdbRunCommand(GdbManagerImpl manager) { super(manager); @@ -51,38 +49,4 @@ public class GdbRunCommand extends AbstractGdbCommand { throw new AssertionError(); } } - - @Override - public boolean handle(GdbEvent evt, GdbPendingCommand pending) { - if (evt instanceof GdbCommandRunningEvent) { - pending.claim(evt); - return pending.hasAny(GdbRunningEvent.class); - } - else if (evt instanceof AbstractGdbCompletedCommandEvent) { - pending.claim(evt); - return true; // Not the expected Completed event - } - else if (evt instanceof GdbRunningEvent) { - pending.claim(evt); - return pending.hasAny(GdbCommandRunningEvent.class); - } - else if (evt instanceof GdbThreadCreatedEvent) { - /** - * TODO: Why do I care? Because I think as this stands, none of these get claimed, since - * the command will be completed by the time these events arrive. - */ - pending.claim(evt); - } - return false; - } - - @Override - public GdbThread complete(GdbPendingCommand pending) { - pending.checkCompletion(GdbCommandRunningEvent.class); - - // Just take the first thread. Others are considered clones. - GdbThreadCreatedEvent created = pending.findFirstOf(GdbThreadCreatedEvent.class); - int tid = created.getThreadId(); - return manager.getThread(tid); - } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStartCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStartCommand.java new file mode 100644 index 0000000000..364f518117 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStartCommand.java @@ -0,0 +1,40 @@ +/* ### + * 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 agent.gdb.manager.impl.cmd; + +import agent.gdb.manager.GdbInferior; +import agent.gdb.manager.impl.GdbManagerImpl; +import agent.gdb.manager.impl.GdbManagerImpl.Interpreter; + +/** + * Implementation of {@link GdbInferior#start()} + */ +public class GdbStartCommand extends AbstractGdbCommandExpectRunning { + + public GdbStartCommand(GdbManagerImpl manager) { + super(manager); + } + + @Override + public Interpreter getInterpreter() { + return Interpreter.MI2; + } + + @Override + public String encode() { + return "-interpreter-exec console start"; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStartInstructionCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStartInstructionCommand.java new file mode 100644 index 0000000000..d166c5a015 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStartInstructionCommand.java @@ -0,0 +1,40 @@ +/* ### + * 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 agent.gdb.manager.impl.cmd; + +import agent.gdb.manager.GdbInferior; +import agent.gdb.manager.impl.GdbManagerImpl; +import agent.gdb.manager.impl.GdbManagerImpl.Interpreter; + +/** + * Implementation of {@link GdbInferior#starti()} + */ +public class GdbStartInstructionCommand extends AbstractGdbCommandExpectRunning { + + public GdbStartInstructionCommand(GdbManagerImpl manager) { + super(manager); + } + + @Override + public Interpreter getInterpreter() { + return Interpreter.MI2; + } + + @Override + public String encode() { + return "-interpreter-exec console starti"; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStateChangeRecord.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStateChangeRecord.java new file mode 100644 index 0000000000..62c877d808 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStateChangeRecord.java @@ -0,0 +1,108 @@ +/* ### + * 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 agent.gdb.manager.impl.cmd; + +import java.util.Collection; + +import agent.gdb.manager.*; +import agent.gdb.manager.reason.GdbReason; + +/** + * A record of a state change in GDB + */ +public class GdbStateChangeRecord { + private GdbInferior inferior; + private GdbThread eventThread; + private Collection affectedThreads; + private GdbState state; + private GdbCause cause; + private GdbReason reason; + + /** + * Construct a new record + * + * @param inferior the inferior affected by this changed + * @param affectedThreads the threads affected by this change (includes only those from this + * inferior) + * @param state the new state of the inferior and threads + * @param eventThread the thread causing the change (may not be from this inferior) + * @param cause the user-driven cause of this change, e.g., a command + * @param reason the target-driven reason for this change, e.g., an event + */ + public GdbStateChangeRecord(GdbInferior inferior, Collection affectedThreads, + GdbState state, GdbThread eventThread, GdbCause cause, GdbReason reason) { + this.inferior = inferior; + this.affectedThreads = affectedThreads; + this.state = state; + this.eventThread = eventThread; + this.cause = cause; + this.reason = reason; + } + + /** + * Get the inferior affected by this change + * + * @return the inferior + */ + public GdbInferior getInferior() { + return inferior; + } + + /** + * Get the thread causing this change + * + * @return the event thread + */ + public GdbThread getEventThread() { + return eventThread; + } + + /** + * Get the threads affected by this change + * + * @return the threads + */ + public Collection getAffectedThreads() { + return affectedThreads; + } + + /** + * Get the new state of the affected items + * + * @return the new state + */ + public GdbState getState() { + return state; + } + + /** + * Get the user-driven cause of the change + * + * @return the cause + */ + public GdbCause getCause() { + return cause; + } + + /** + * Get the target-driven reason for this change + * + * @return the reason + */ + public GdbReason getReason() { + return reason; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStepCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStepCommand.java index 51d17e5257..4aaf63003d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStepCommand.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbStepCommand.java @@ -57,6 +57,7 @@ public class GdbStepCommand extends AbstractGdbCommandWithThreadId { @Override public boolean handle(GdbEvent evt, GdbPendingCommand pending) { + evt = checkErrorViaCli(evt); if (evt instanceof GdbCommandRunningEvent) { pending.claim(evt); return pending.hasAny(GdbRunningEvent.class); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbThreadSelectCommand.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbThreadSelectCommand.java index e75d94a031..a7ecc8bb47 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbThreadSelectCommand.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/cmd/GdbThreadSelectCommand.java @@ -23,6 +23,7 @@ public class GdbThreadSelectCommand extends AbstractGdbCommandWithThreadAndFrame /** * Select the given thread and frame level * + *

* To simply select a thread, you should use frame 0 as the default. * * @param manager the manager to execute the command diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/reason/GdbExitNormallyReason.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/reason/GdbExitNormallyReason.java index 6a98af9a61..6bc84a6e4b 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/reason/GdbExitNormallyReason.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/reason/GdbExitNormallyReason.java @@ -20,13 +20,14 @@ import agent.gdb.manager.parsing.GdbMiParser.GdbMiFieldList; /** * The inferior stopped because it has exited (with status 0) */ -public class GdbExitNormallyReason implements GdbReason { +public class GdbExitNormallyReason extends GdbExitedReason { public GdbExitNormallyReason(GdbMiFieldList info) { + super(0); // Nothing additional to parse } @Override public String desc() { - return "Exited normally"; + return "Exited normally (with code 0)"; } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/reason/GdbExitedReason.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/reason/GdbExitedReason.java index 29b1011b37..b8a71d3037 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/reason/GdbExitedReason.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/reason/GdbExitedReason.java @@ -27,6 +27,10 @@ public class GdbExitedReason implements GdbReason { this.exitCode = Integer.parseInt(info.getString("exit-code")); } + public GdbExitedReason(int exitCode) { + this.exitCode = exitCode; + } + /** * Get the exit code of the inferior * diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java index 4404ae242f..78d830992e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java @@ -25,6 +25,7 @@ import agent.gdb.manager.impl.cmd.GdbCommandError; import ghidra.async.AsyncUtils; import ghidra.dbg.DebuggerModelClosedReason; import ghidra.dbg.agent.AbstractDebuggerObjectModel; +import ghidra.dbg.error.DebuggerModelTerminatingException; import ghidra.dbg.error.DebuggerUserException; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.AnnotatedSchemaContext; @@ -126,7 +127,8 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel { return gdb.runRC(); } catch (IOException e) { - return CompletableFuture.failedFuture(e); + return CompletableFuture.failedFuture( + new DebuggerModelTerminatingException("Error while starting GDB", e)); } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImplUtils.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImplUtils.java index 4ba31bda2a..7be1eaa9fa 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImplUtils.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImplUtils.java @@ -19,29 +19,33 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import agent.gdb.manager.GdbInferior; -import ghidra.async.AsyncUtils; +import agent.gdb.manager.GdbThread; import ghidra.dbg.util.ShellUtils; public enum GdbModelImplUtils { ; - public static CompletableFuture launch(GdbModelImpl impl, GdbInferior inferior, + public static CompletableFuture launch(GdbModelImpl impl, GdbInferior inferior, List args) { - return inferior.fileExecAndSymbols(args.get(0)).thenCompose(__ -> { - return inferior.setVar("args", ShellUtils.generateLine(args.subList(1, args.size()))); - }).thenCompose(__ -> { - if (impl.noStarti) { - return inferior.console("start"); - } - return inferior.console("starti").thenApply(___ -> true).exceptionally(e -> { + // Queue all these up to avoid other commands getting between. + CompletableFuture feas = inferior.fileExecAndSymbols(args.get(0)); + CompletableFuture sargs = + inferior.setVar("args", ShellUtils.generateLine(args.subList(1, args.size()))); + CompletableFuture both = CompletableFuture.allOf(feas, sargs); + if (impl.noStarti) { + return both.thenCombine(inferior.start(), (__, t) -> t); + } + else { + return both.thenCombine(inferior.starti(), (__, t) -> t).exceptionally(ex -> { impl.noStarti = true; - return false; - }).thenCompose(success -> { - if (success) { - return AsyncUtils.NIL; + // TODO: Check that the error is actually Undefined command: "starti" + return null; + }).thenCompose(thread -> { + if (thread == null) { + return inferior.start(); } - return inferior.console("start"); + return CompletableFuture.completedStage(thread); }); - }); + } } public static V noDupMerge(V first, V second) { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetAttachable.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetAttachable.java index 6170100dd0..14b5529eef 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetAttachable.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetAttachable.java @@ -36,7 +36,8 @@ import ghidra.dbg.util.PathUtils; public class GdbModelTargetAttachable extends DefaultTargetObject implements TargetAttachable { - protected static final String PID_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "pid"; + + public static final String PID_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "pid"; // TODO: DESCRIPTION, TYPE, USER? protected static String indexAttachable(GdbProcessThreadGroup process) { @@ -62,8 +63,7 @@ public class GdbModelTargetAttachable this.changeAttributes(List.of(), List.of(), Map.of( // PID_ATTRIBUTE_NAME, pid, // - DISPLAY_ATTRIBUTE_NAME, display, // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, display // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetAvailableContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetAvailableContainer.java index 4a6b16080d..aab42ee9f1 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetAvailableContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetAvailableContainer.java @@ -23,11 +23,13 @@ import java.util.stream.Collectors; import agent.gdb.manager.GdbProcessThreadGroup; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.schema.TargetAttributeType; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; import ghidra.util.datastruct.WeakValueHashMap; @TargetObjectSchemaInfo( name = "AvailableContainer", + elementResync = ResyncMode.ALWAYS, attributes = { @TargetAttributeType(type = Void.class) }, @@ -44,9 +46,6 @@ public class GdbModelTargetAvailableContainer public GdbModelTargetAvailableContainer(GdbModelTargetSession session) { super(session.impl, session, NAME, "AvailableContainer"); this.impl = session.impl; - changeAttributes(List.of(), Map.of( - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.SOLICITED // - ), "Initialized"); } @Override diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointContainer.java index d8eb4f6a59..39a6453837 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointContainer.java @@ -24,10 +24,12 @@ import agent.gdb.manager.GdbCause; import agent.gdb.manager.GdbEventsListenerAdapter; import agent.gdb.manager.breakpoint.GdbBreakpointInfo; import agent.gdb.manager.breakpoint.GdbBreakpointType; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; import ghidra.async.AsyncFence; +import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; -import ghidra.dbg.target.TargetBreakpointContainer; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; import ghidra.program.model.address.AddressRange; @@ -37,12 +39,11 @@ import ghidra.util.datastruct.WeakValueHashMap; @TargetObjectSchemaInfo( name = "BreakpointContainer", attributes = { - @TargetAttributeType(type = Void.class) - }, + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class GdbModelTargetBreakpointContainer extends DefaultTargetObject - implements TargetBreakpointContainer, GdbEventsListenerAdapter { + implements TargetBreakpointSpecContainer, GdbEventsListenerAdapter { public static final String NAME = "Breakpoints"; protected static final TargetBreakpointKindSet SUPPORTED_KINDS = @@ -79,23 +80,24 @@ public class GdbModelTargetBreakpointContainer getTargetBreakpointSpec(oldInfo).updateInfo(oldInfo, newInfo, "Modified"); } - protected void breakpointHit(long bpId, GdbModelTargetStackFrame frame) { + protected GdbModelTargetBreakpointLocation breakpointHit(long bpId, + GdbModelTargetStackFrame frame) { GdbModelTargetBreakpointSpec spec = getTargetBreakpointSpecIfPresent(bpId); if (spec == null) { Msg.error(this, "Stopped for breakpoint unknown to the agent: " + bpId + " (pc=" + frame.getProgramCounter() + ")"); - return; + return null; } - GdbModelTargetBreakpointLocation eb = spec.findEffective(frame); - if (eb == null) { + GdbModelTargetBreakpointLocation loc = spec.findLocation(frame); + if (loc == null) { Msg.warn(this, "Stopped for a breakpoint whose location is unknown to the agent: " + spec + " (pc=" + frame.getProgramCounter() + ")"); - //return; // Not idea, but eb == null should be fine, since the spec holds the actions + //return; // Not ideal, but eb == null should be fine, since the spec holds the actions } - listeners.fire(TargetBreakpointListener.class) - .breakpointHit(this, frame.thread, frame, spec, eb); - spec.breakpointHit(frame, eb); + listeners.fire.breakpointHit(this, frame.thread, frame, spec, loc); + spec.breakpointHit(frame, loc); + return loc; } @Override @@ -120,13 +122,13 @@ public class GdbModelTargetBreakpointContainer else if (kinds.contains(TargetBreakpointKind.WRITE)) { fence.include(placer.apply(GdbBreakpointType.HW_WATCHPOINT)); } - if (kinds.contains(TargetBreakpointKind.EXECUTE)) { + if (kinds.contains(TargetBreakpointKind.HW_EXECUTE)) { fence.include(placer.apply(GdbBreakpointType.HW_BREAKPOINT)); } - if (kinds.contains(TargetBreakpointKind.SOFTWARE)) { + if (kinds.contains(TargetBreakpointKind.SW_EXECUTE)) { fence.include(placer.apply(GdbBreakpointType.BREAKPOINT)); } - return fence.ready().exceptionally(GdbModelImpl::translateEx); + return impl.gateFuture(fence.ready().exceptionally(GdbModelImpl::translateEx)); } @Override @@ -138,7 +140,6 @@ public class GdbModelTargetBreakpointContainer @Override public CompletableFuture placeBreakpoint(AddressRange range, Set kinds) { - // TODO: Consider how to translate address spaces long offset = range.getMinAddress().getOffset(); int len = (int) range.getLength(); return doPlaceBreakpoint(kinds, t -> impl.gdb.insertBreakpoint(offset, len, t)); @@ -164,9 +165,7 @@ public class GdbModelTargetBreakpointContainer .collect(Collectors.toList()); } return CompletableFuture - .allOf(specs.stream() - .map(s -> s.init()) - .toArray(CompletableFuture[]::new)) + .allOf(specs.stream().map(s -> s.init()).toArray(CompletableFuture[]::new)) .thenRun(() -> { setElements(specs, "Refreshed"); }); @@ -179,4 +178,9 @@ public class GdbModelTargetBreakpointContainer } return impl.gdb.listBreakpoints().thenCompose(this::updateUsingBreakpoints); } + + public CompletableFuture stateChanged(GdbStateChangeRecord sco) { + // NB. This container should be updated via GDB's events. + return AsyncUtils.NIL; + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointLocation.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointLocation.java index 0f5aa44321..b7c0207c7d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointLocation.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointLocation.java @@ -18,15 +18,12 @@ package agent.gdb.model.impl; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; import agent.gdb.manager.breakpoint.GdbBreakpointLocation; import agent.gdb.manager.parsing.GdbCValueParser; import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError; import generic.Unique; import ghidra.dbg.agent.DefaultTargetObject; -import ghidra.dbg.attributes.TargetObjectList; -import ghidra.dbg.attributes.TargetObjectList.DefaultTargetObjectList; import ghidra.dbg.target.TargetBreakpointLocation; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; @@ -57,7 +54,6 @@ public class GdbModelTargetBreakpointLocation protected Address address; protected Integer length; - protected final TargetObjectList affects; protected String display; public GdbModelTargetBreakpointLocation(GdbModelTargetBreakpointSpec spec, @@ -66,24 +62,21 @@ public class GdbModelTargetBreakpointLocation this.impl = spec.impl; this.loc = loc; - this.affects = doGetAffects(); if (!spec.info.getType().isWatchpoint()) { this.address = doGetAddress(); this.length = 1; doChangeAttributes("Initialized"); } - assert !this.affects.isEmpty(); } protected void doChangeAttributes(String reason) { this.changeAttributes(List.of(), Map.of( SPEC_ATTRIBUTE_NAME, parent, - AFFECTS_ATTRIBUTE_NAME, affects, ADDRESS_ATTRIBUTE_NAME, address, LENGTH_ATTRIBUTE_NAME, length, - DISPLAY_ATTRIBUTE_NAME, display = computeDisplay(), - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, display = computeDisplay() // ), reason); + placeLocations(); } /** @@ -103,7 +96,8 @@ public class GdbModelTargetBreakpointLocation throw new AssertionError("non-location location"); } String exp = what.substring(GdbBreakpointLocation.WATCHPOINT_LOCATION_PREFIX.length()); - GdbModelTargetInferior inf = Unique.assertOne(affects); + int iid = Unique.assertOne(loc.getInferiorIds()); + GdbModelTargetInferior inf = impl.session.inferiors.getTargetInferior(iid); String addrSizeExp = String.format("{(long long)&(%s), (long long)sizeof(%s)}", exp, exp); return inf.inferior.evaluate(addrSizeExp).thenAccept(result -> { List vals; @@ -136,16 +130,28 @@ public class GdbModelTargetBreakpointLocation return length; } - protected TargetObjectList doGetAffects() { - return loc.getInferiorIds() - .stream() - .map(impl.session.inferiors::getTargetInferior) - .collect(Collectors.toCollection(DefaultTargetObjectList::new)); + protected void placeLocations() { + for (GdbModelTargetInferior inf : impl.session.inferiors.getCachedElements().values()) { + if (loc.getInferiorIds().contains(inf.inferior.getId())) { + inf.addBreakpointLocation(this); + } + else { + inf.removeBreakpointLocation(this); + } + } } @Override - public TargetObjectList getAffects() { - return affects; + protected void doInvalidate(TargetObject branch, String reason) { + removeLocations(); + super.doInvalidate(branch, reason); + } + + protected void removeLocations() { + // TODO: Shouldn't the framework do this for us? The location is invalidated. + for (GdbModelTargetInferior inf : impl.session.inferiors.getCachedElements().values()) { + inf.removeBreakpointLocation(this); + } } @Override diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointLocationContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointLocationContainer.java new file mode 100644 index 0000000000..355df02564 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointLocationContainer.java @@ -0,0 +1,55 @@ +/* ### + * 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 agent.gdb.model.impl; + +import java.util.List; +import java.util.Map; + +import ghidra.dbg.agent.DefaultTargetObject; +import ghidra.dbg.target.TargetBreakpointLocationContainer; +import ghidra.dbg.target.schema.TargetAttributeType; +import ghidra.dbg.target.schema.TargetObjectSchemaInfo; + +/** + * This is a container of links only, as a way to encode "affects" within the model + */ +@TargetObjectSchemaInfo( + name = "BreakpointLocationContainer", + canonicalContainer = true, + attributes = { + @TargetAttributeType(type = Void.class) + }) +public class GdbModelTargetBreakpointLocationContainer + extends DefaultTargetObject + implements TargetBreakpointLocationContainer { + public static final String NAME = "Breakpoints"; + + protected static String indexLoc(GdbModelTargetBreakpointLocation loc) { + return loc.getSpecification().getIndex() + "," + loc.getIndex(); + } + + public GdbModelTargetBreakpointLocationContainer(GdbModelTargetInferior inferior) { + super(inferior.impl, inferior, NAME, "BreakpointLocationContainer"); + } + + public void addBreakpointLocation(GdbModelTargetBreakpointLocation loc) { + changeElements(List.of(), Map.of(indexLoc(loc), loc), "Added"); + } + + public void removeBreakpointLocation(GdbModelTargetBreakpointLocation loc) { + changeElements(List.of(indexLoc(loc)), Map.of(), "Removed"); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointSpec.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointSpec.java index f0d27a5b16..48b780f9f2 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointSpec.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetBreakpointSpec.java @@ -23,12 +23,11 @@ import agent.gdb.manager.breakpoint.GdbBreakpointInfo; import agent.gdb.manager.breakpoint.GdbBreakpointLocation; import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet; import ghidra.dbg.target.TargetBreakpointSpec; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.target.TargetDeletable; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; -import ghidra.dbg.util.CollectionUtils.Delta; import ghidra.dbg.util.PathUtils; import ghidra.util.Msg; import ghidra.util.datastruct.ListenerSet; @@ -92,7 +91,7 @@ public class GdbModelTargetBreakpointSpec extends @Override public CompletableFuture delete() { - return impl.gdb.deleteBreakpoints(number); + return impl.gateFuture(impl.gdb.deleteBreakpoints(number)); } @Override @@ -108,9 +107,9 @@ public class GdbModelTargetBreakpointSpec extends protected TargetBreakpointKindSet computeKinds(GdbBreakpointInfo from) { switch (from.getType()) { case BREAKPOINT: - return TargetBreakpointKindSet.of(TargetBreakpointKind.SOFTWARE); + return TargetBreakpointKindSet.of(TargetBreakpointKind.SW_EXECUTE); case HW_BREAKPOINT: - return TargetBreakpointKindSet.of(TargetBreakpointKind.EXECUTE); + return TargetBreakpointKindSet.of(TargetBreakpointKind.HW_EXECUTE); case HW_WATCHPOINT: return TargetBreakpointKindSet.of(TargetBreakpointKind.WRITE); case READ_WATCHPOINT: @@ -155,12 +154,12 @@ public class GdbModelTargetBreakpointSpec extends @Override public CompletableFuture disable() { - return impl.gdb.disableBreakpoints(number); + return impl.gateFuture(impl.gdb.disableBreakpoints(number)); } @Override public CompletableFuture enable() { - return impl.gdb.enableBreakpoints(number); + return impl.gateFuture(impl.gdb.enableBreakpoints(number)); } protected CompletableFuture updateInfo(GdbBreakpointInfo oldInfo, @@ -174,28 +173,21 @@ public class GdbModelTargetBreakpointSpec extends } protected void updateAttributesFromInfo(String reason) { - Delta delta = changeAttributes(List.of(), Map.of( + changeAttributes(List.of(), Map.of( ENABLED_ATTRIBUTE_NAME, enabled = info.isEnabled(), EXPRESSION_ATTRIBUTE_NAME, expression = info.getOriginalLocation(), KINDS_ATTRIBUTE_NAME, kinds = computeKinds(info), DISPLAY_ATTRIBUTE_NAME, updateDisplay() // ), reason); - // TODO: These attribute-specific conveniences should be done by DTO. - if (delta.added.containsKey(ENABLED_ATTRIBUTE_NAME)) { - listeners.fire(TargetBreakpointSpecListener.class).breakpointToggled(this, enabled); - } - if (delta.added.containsKey(DISPLAY_ATTRIBUTE_NAME)) { - listeners.fire.displayChanged(this, display); - } } - protected synchronized List setInfoAndComputeEffectives( + protected synchronized List setInfoAndComputeLocations( GdbBreakpointInfo oldInfo, GdbBreakpointInfo newInfo) { assert oldInfo == this.info; this.info = newInfo; List effectives = newInfo.getLocations() .stream() - .map(this::getTargetEffectiveBreakpoint) + .map(this::getTargetBreakpointLocation) .collect(Collectors.toList()); breaksBySub.keySet() .retainAll( @@ -205,31 +197,31 @@ public class GdbModelTargetBreakpointSpec extends protected CompletableFuture updateBktpInfo(GdbBreakpointInfo oldInfo, GdbBreakpointInfo newInfo, String reason) { - List effectives = - setInfoAndComputeEffectives(oldInfo, newInfo); + List locs = + setInfoAndComputeLocations(oldInfo, newInfo); updateAttributesFromInfo(reason); - setElements(effectives, reason); + setElements(locs, reason); return AsyncUtils.NIL; } protected CompletableFuture updateWptInfo(GdbBreakpointInfo oldInfo, GdbBreakpointInfo newInfo, String reason) { - List effectives = - setInfoAndComputeEffectives(oldInfo, newInfo); + List locs = + setInfoAndComputeLocations(oldInfo, newInfo); updateAttributesFromInfo(reason); - assert effectives.size() == 1; - return effectives.get(0).initWpt().thenAccept(__ -> { - setElements(effectives, reason); + assert locs.size() == 1; + return locs.get(0).initWpt().thenAccept(__ -> { + setElements(locs, reason); }); } - protected GdbModelTargetBreakpointLocation findEffective(GdbModelTargetStackFrame frame) { + protected GdbModelTargetBreakpointLocation findLocation(GdbModelTargetStackFrame frame) { for (GdbModelTargetBreakpointLocation bp : breaksBySub.values()) { // TODO: Is this necessary? /*if (bp.range.contains(frame.pc)) { continue; }*/ - if (!bp.affects.contains(frame.inferior)) { + if (!bp.loc.getInferiorIds().contains(frame.inferior.inferior.getId())) { continue; } return bp; @@ -242,7 +234,7 @@ public class GdbModelTargetBreakpointSpec extends actions.fire.breakpointHit(this, frame.thread, frame, eb); } - public synchronized GdbModelTargetBreakpointLocation getTargetEffectiveBreakpoint( + public synchronized GdbModelTargetBreakpointLocation getTargetBreakpointLocation( GdbBreakpointLocation loc) { return breaksBySub.computeIfAbsent(loc.getSub(), i -> new GdbModelTargetBreakpointLocation(this, loc)); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetEnvironment.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetEnvironment.java index 40cf93ed39..a46590d8d4 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetEnvironment.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetEnvironment.java @@ -59,8 +59,7 @@ public class GdbModelTargetEnvironment ENDIAN_ATTRIBUTE_NAME, endian, VISIBLE_ARCH_ATTRIBUTE_NAME, arch, VISIBLE_OS_ATTRIBUTE_NAME, os, - VISIBLE_ENDIAN_ATTRIBUTE_NAME, endian, - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED), + VISIBLE_ENDIAN_ATTRIBUTE_NAME, endian), "Initialized"); refreshInternal(); } @@ -83,9 +82,10 @@ public class GdbModelTargetEnvironment * is a known thread -- unlikely for an inferior that is just starting -- we could use the * --thread parameter. */ - return impl.gdb.consoleCapture("show architecture").thenAccept(out -> { + return CompletableFuture.supplyAsync(() -> null).thenCompose(__ -> { + return impl.gdb.consoleCapture("show architecture"); + }).thenAccept(out -> { String[] tokens = out.split("\\s+"); - @SuppressWarnings("hiding") String arch = tokens[tokens.length - 1].trim(); while (arch.endsWith(".") || arch.endsWith(")") || arch.endsWith("\"")) { arch = arch.substring(0, arch.length() - 1); @@ -115,9 +115,10 @@ public class GdbModelTargetEnvironment /** * TODO: Ditto the "current inferior" issue as refreshArchitecture */ - return impl.gdb.consoleCapture("show os").thenAccept(out -> { + return CompletableFuture.supplyAsync(() -> null).thenCompose(__ -> { + return impl.gdb.consoleCapture("show os"); + }).thenAccept(out -> { String[] tokens = out.split("\n")[0].split("\\s+"); - @SuppressWarnings("hiding") String os = tokens[tokens.length - 1].trim(); if (os.endsWith(".")) { os = os.substring(0, os.length() - 1); @@ -143,7 +144,9 @@ public class GdbModelTargetEnvironment protected CompletableFuture refreshEndian() { // TODO: This duplicates GdbInferiorImpl.syncEndianness.... - return impl.gdb.consoleCapture("show endian").thenAccept(out -> { + return CompletableFuture.supplyAsync(() -> null).thenCompose(__ -> { + return impl.gdb.consoleCapture("show endian"); + }).thenAccept(out -> { if (out.toLowerCase().contains("little endian")) { endian = "little"; } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java index 6a6cd114f7..3c6eac59a1 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferior.java @@ -18,18 +18,18 @@ package agent.gdb.model.impl; import java.util.*; import java.util.concurrent.CompletableFuture; -import agent.gdb.manager.GdbInferior; +import agent.gdb.manager.*; import agent.gdb.manager.GdbManager.ExecSuffix; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; +import agent.gdb.manager.reason.*; import ghidra.async.AsyncFence; import ghidra.dbg.agent.DefaultTargetObject; -import ghidra.dbg.error.DebuggerModelNoSuchPathException; -import ghidra.dbg.error.DebuggerModelTypeException; import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEventScope.TargetEventType; import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; import ghidra.lifecycle.Internal; -import ghidra.util.Msg; @TargetObjectSchemaInfo( name = "Inferior", @@ -62,13 +62,21 @@ public class GdbModelTargetInferior protected final GdbModelImpl impl; protected final GdbInferior inferior; + protected String display; + protected TargetExecutionState state; + /** + * When state=INACTIVE/TERMINATED, and we're waiting on a refresh, this keep the "real" state, + * which is the last state actually reported by *running or *stopped. + */ + protected TargetExecutionState realState; protected final GdbModelTargetEnvironment environment; protected final GdbModelTargetProcessMemory memory; protected final GdbModelTargetModuleContainer modules; - protected final GdbModelTargetRegisterContainer registers; + //protected final GdbModelTargetRegisterContainer registers; protected final GdbModelTargetThreadContainer threads; + protected final GdbModelTargetBreakpointLocationContainer breakpoints; protected Long exitCode; @@ -80,20 +88,24 @@ public class GdbModelTargetInferior this.environment = new GdbModelTargetEnvironment(this); this.memory = new GdbModelTargetProcessMemory(this); this.modules = new GdbModelTargetModuleContainer(this); - this.registers = new GdbModelTargetRegisterContainer(this); + //this.registers = new GdbModelTargetRegisterContainer(this); this.threads = new GdbModelTargetThreadContainer(this); + this.breakpoints = new GdbModelTargetBreakpointLocationContainer(this); + + this.realState = TargetExecutionState.INACTIVE; changeAttributes(List.of(), // List.of( // environment, // memory, // modules, // - registers, // - threads), // - Map.of(STATE_ATTRIBUTE_NAME, TargetExecutionState.INACTIVE, // + //registers, // + threads, // + breakpoints), // + Map.of( // + STATE_ATTRIBUTE_NAME, state = realState, // DISPLAY_ATTRIBUTE_NAME, updateDisplay(), // TargetMethod.PARAMETERS_ATTRIBUTE_NAME, TargetCmdLineLauncher.PARAMETERS, // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED, // SUPPORTED_ATTACH_KINDS_ATTRIBUTE_NAME, SUPPORTED_KINDS, // SUPPORTED_STEP_KINDS_ATTRIBUTE_NAME, GdbModelTargetThread.SUPPORTED_KINDS), // "Initialized"); @@ -114,24 +126,35 @@ public class GdbModelTargetInferior return modules; } + /* @TargetAttributeType(name = GdbModelTargetRegisterContainer.NAME, required = true, fixed = true) public GdbModelTargetRegisterContainer getRegisters() { return registers; } + */ @TargetAttributeType(name = GdbModelTargetThreadContainer.NAME, required = true, fixed = true) public GdbModelTargetThreadContainer getThreads() { return threads; } + @TargetAttributeType( + name = GdbModelTargetBreakpointLocationContainer.NAME, + required = true, + fixed = true) + public GdbModelTargetBreakpointLocationContainer getBreakpoints() { + return breakpoints; + } + @Override public CompletableFuture launch(List args) { - return GdbModelImplUtils.launch(impl, inferior, args); + return impl + .gateFuture(GdbModelImplUtils.launch(impl, inferior, args).thenApply(__ -> null)); } @Override public CompletableFuture resume() { - return inferior.cont(); + return impl.gateFuture(inferior.cont()); } protected ExecSuffix convertToGdb(TargetStepKind kind) { @@ -164,72 +187,55 @@ public class GdbModelTargetInferior throw new UnsupportedOperationException(kind.name()); case ADVANCE: // Why no exec-advance in GDB/MI? // TODO: This doesn't work, since advance requires a parameter - return inferior.console("advance"); + return model.gateFuture(inferior.console("advance")); default: - return inferior.step(convertToGdb(kind)); + return model.gateFuture(inferior.step(convertToGdb(kind))); } } @Override public CompletableFuture kill() { - return inferior.kill(); + return model.gateFuture(inferior.kill()); } @Override public CompletableFuture attach(TargetAttachable attachable) { - impl.assertMine(TargetObject.class, attachable); - // NOTE: These can change at any time. Just use the path to derive the target PID - if (!Objects.equals(PathUtils.parent(attachable.getPath()), - impl.session.available.getPath())) { - throw new DebuggerModelTypeException( - "Target of attach must be a child of " + impl.session.available.getPath()); - } - long pid; - try { - pid = Long.parseLong(attachable.getIndex()); - } - catch (IllegalArgumentException e) { - throw new DebuggerModelNoSuchPathException("Badly-formatted PID", e); - } - return attach(pid); + GdbModelTargetAttachable mine = impl.assertMine(GdbModelTargetAttachable.class, attachable); + return attach(mine.pid); } @Override public CompletableFuture attach(long pid) { - return inferior.attach(pid).thenApply(__ -> null); + return model.gateFuture(inferior.attach(pid)).thenApply(__ -> null); } @Override public CompletableFuture detach() { - return inferior.detach(); + return model.gateFuture(inferior.detach()); } @Override public CompletableFuture delete() { - return inferior.remove(); + return model.gateFuture(inferior.remove()); } protected CompletableFuture inferiorStarted(Long pid) { + parent.getListeners().fire.event( + parent, null, TargetEventType.PROCESS_CREATED, "Inferior " + inferior.getId() + + " started " + inferior.getExecutable() + " pid=" + inferior.getPid(), + List.of(this)); AsyncFence fence = new AsyncFence(); fence.include(modules.refreshInternal()); - fence.include(registers.resync()); + //fence.include(registers.refreshInternal()); fence.include(environment.refreshInternal()); + fence.include(impl.gdb.listInferiors()); // HACK to update inferior.getExecutable() + // NB. Hack also updates inferior.getPid(), so ignore pid parameter return fence.ready().thenAccept(__ -> { - if (pid != null) { - changeAttributes(List.of(), Map.of( // - STATE_ATTRIBUTE_NAME, TargetExecutionState.ALIVE, // - PID_ATTRIBUTE_NAME, pid, // - DISPLAY_ATTRIBUTE_NAME, updateDisplay() // - ), "Started"); - } - else { - changeAttributes(List.of(), Map.of( // - STATE_ATTRIBUTE_NAME, TargetExecutionState.ALIVE, // - DISPLAY_ATTRIBUTE_NAME, updateDisplay() // - ), "Started"); - } - listeners.fire(TargetExecutionStateListener.class) - .executionStateChanged(this, TargetExecutionState.ALIVE); + changeAttributes(List.of(), + Map.ofEntries(Map.entry(STATE_ATTRIBUTE_NAME, state = realState), + Map.entry(PID_ATTRIBUTE_NAME, inferior.getPid()), + Map.entry(DISPLAY_ATTRIBUTE_NAME, updateDisplay())), + "Refresh on started"); }); } @@ -237,7 +243,7 @@ public class GdbModelTargetInferior this.exitCode = exitCode; if (exitCode != null) { changeAttributes(List.of(), Map.of( // - STATE_ATTRIBUTE_NAME, TargetExecutionState.TERMINATED, // + STATE_ATTRIBUTE_NAME, state = TargetExecutionState.TERMINATED, // EXIT_CODE_ATTRIBUTE_NAME, exitCode, // DISPLAY_ATTRIBUTE_NAME, updateDisplay() // ), "Exited"); @@ -248,8 +254,76 @@ public class GdbModelTargetInferior DISPLAY_ATTRIBUTE_NAME, updateDisplay() // ), "Exited"); } - listeners.fire(TargetExecutionStateListener.class) - .executionStateChanged(this, TargetExecutionState.TERMINATED); + } + + protected void gatherThreads(List into, + Collection from) { + for (GdbThread t : from) { + GdbModelTargetThread p = threads.getTargetThread(t); + if (p != null) { + into.add(p); + } + } + } + + protected void emitEvent(GdbStateChangeRecord sco, GdbModelTargetThread targetEventThread) { + GdbReason reason = sco.getReason(); + if (reason instanceof GdbBreakpointHitReason) { + GdbBreakpointHitReason bpHit = (GdbBreakpointHitReason) reason; + List params = new ArrayList<>(); + GdbModelTargetBreakpointLocation loc = threads.breakpointHit(bpHit); + if (loc != null) { + // e.g. target could execute INT3, causing "trapped" for unknown bp/loc + params.add(loc); + } + gatherThreads(params, sco.getAffectedThreads()); + impl.session.getListeners().fire.event(impl.session, targetEventThread, + TargetEventType.BREAKPOINT_HIT, + bpHit.desc(), params); + } + else if (reason instanceof GdbEndSteppingRangeReason) { + List params = new ArrayList<>(); + gatherThreads(params, sco.getAffectedThreads()); + impl.session.getListeners().fire.event(impl.session, targetEventThread, + TargetEventType.STEP_COMPLETED, + reason.desc(), params); + } + else if (reason instanceof GdbSignalReceivedReason) { + GdbSignalReceivedReason signal = (GdbSignalReceivedReason) reason; + List params = new ArrayList<>(); + params.add(signal.getSignalName()); + gatherThreads(params, sco.getAffectedThreads()); + impl.session.getListeners().fire.event(impl.session, targetEventThread, + TargetEventType.SIGNAL, + reason.desc(), params); + } + else { + List params = new ArrayList<>(); + gatherThreads(params, sco.getAffectedThreads()); + impl.session.getListeners().fire.event(impl.session, targetEventThread, + TargetEventType.STOPPED, + reason.desc(), params); + } + } + + protected void inferiorRunning(GdbReason reason) { + realState = TargetExecutionState.RUNNING; + if (!state.isAlive()) { + return; + } + changeAttributes(List.of(), Map.of( // + STATE_ATTRIBUTE_NAME, state = realState // + ), reason.desc()); + } + + protected void inferiorStopped(GdbReason reason) { + realState = TargetExecutionState.STOPPED; + if (!state.isAlive()) { + return; + } + changeAttributes(List.of(), Map.of( // + STATE_ATTRIBUTE_NAME, state = realState // + ), reason.desc()); } protected void updateDisplayAttribute() { @@ -271,27 +345,91 @@ public class GdbModelTargetInferior return display; } + @Override + public TargetExecutionState getExecutionState() { + return state; + } + protected void invalidateMemoryAndRegisterCaches() { memory.invalidateMemoryCaches(); threads.invalidateRegisterCaches(); } - protected void updateMemory() { - // This is a little ew. Wish I didn't have to list regions every STOP - memory.update().exceptionally(ex -> { - Msg.error(this, "Could not update process memory mappings", ex); - return null; - }); - } - @Override @Internal public CompletableFuture select() { - return inferior.select(); + return impl.gateFuture(inferior.select()); } @TargetAttributeType(name = EXIT_CODE_ATTRIBUTE_NAME) public Long getExitCode() { return exitCode; } + + /** + * Handle state changes for this inferior + * + *

+ * Desired order of updates: + *

    + *
  1. TargetEvent emitted
  2. + *
  3. Thread states/stacks updated
  4. + *
  5. Memory regions updated (Ew)
  6. + *
+ * + *

+ * Note that the event thread may not belong to this inferior. When it does not, this inferior + * will not emit any event(). Presumably, this same method will be called on the relevant + * inferior, which will report the event. However, this inferior must still update its state. + * Without this screening: + *

    + *
  1. The thread gets replicated into a different inferior
  2. + *
  3. The event() gets replicated on a different inferior
    + * (We only need to report state changes, not event, for non-event inferiors
  4. + *
+ * + * @param sco the record of the change + */ + public CompletableFuture stateChanged(GdbStateChangeRecord sco) { + + GdbModelTargetThread targetEventThread = null; + GdbThread gdbEventThread = sco.getEventThread(); + if (gdbEventThread != null && gdbEventThread.getInferior() == inferior) { + targetEventThread = threads.getTargetThread(gdbEventThread); + } + if (sco.getState() == GdbState.RUNNING) { + inferiorRunning(sco.getReason()); + List params = new ArrayList<>(); + gatherThreads(params, sco.getAffectedThreads()); + if (targetEventThread != null) { + impl.session.getListeners().fire.event(impl.session, targetEventThread, + TargetEventType.RUNNING, "Running", params); + } + } + if (sco.getState() != GdbState.STOPPED) { + return threads.stateChanged(sco); + } + + if (targetEventThread != null) { + emitEvent(sco, targetEventThread); + } + + AsyncFence fence = new AsyncFence(); + // TODO: How does GDB for Windows handle WoW64? + // Can there be architecture changes during execution? Per thread? + //fence.include(environment.refreshArchitecture()); + fence.include(threads.stateChanged(sco)); + inferiorStopped(sco.getReason()); + //registers.stateChanged(sco); + fence.include(memory.stateChanged(sco)); + return fence.ready(); + } + + public void addBreakpointLocation(GdbModelTargetBreakpointLocation loc) { + breakpoints.addBreakpointLocation(loc); + } + + public void removeBreakpointLocation(GdbModelTargetBreakpointLocation loc) { + breakpoints.removeBreakpointLocation(loc); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferiorContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferiorContainer.java index a66ae42915..56a728b267 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferiorContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetInferiorContainer.java @@ -15,18 +15,16 @@ */ package agent.gdb.model.impl; -import java.util.*; +import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import agent.gdb.manager.*; -import agent.gdb.manager.reason.*; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; -import ghidra.dbg.target.TargetEventScope.TargetEventScopeListener; import ghidra.dbg.target.TargetEventScope.TargetEventType; -import ghidra.dbg.target.TargetExecutionStateful; -import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; import ghidra.util.datastruct.WeakValueHashMap; @@ -34,8 +32,7 @@ import ghidra.util.datastruct.WeakValueHashMap; @TargetObjectSchemaInfo( name = "InferiorContainer", attributes = { - @TargetAttributeType(type = Void.class) - }, + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class GdbModelTargetInferiorContainer extends DefaultTargetObject @@ -62,16 +59,7 @@ public class GdbModelTargetInferiorContainer @Override public void inferiorStarted(GdbInferior inf, GdbCause cause) { GdbModelTargetInferior inferior = getTargetInferior(inf); - // TODO: Move PROCESS_CREATED here to restore proper order of event reporting - // Pending some client-side changes to handle architecture selection, though. - inferior.inferiorStarted(inf.getPid()).thenAccept(__ -> { - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event( - parent, null, TargetEventType.PROCESS_CREATED, "Inferior " + inf.getId() + - " started " + inf.getExecutable() + " pid=" + inf.getPid(), - List.of(inferior)); - }).exceptionally(ex -> { + inferior.inferiorStarted(inf.getPid()).exceptionally(ex -> { impl.reportError(this, "Could not notify inferior started", ex); return null; }); @@ -80,11 +68,8 @@ public class GdbModelTargetInferiorContainer @Override public void inferiorExited(GdbInferior inf, GdbCause cause) { GdbModelTargetInferior inferior = getTargetInferior(inf); - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event(parent, null, TargetEventType.PROCESS_EXITED, - "Inferior " + inf.getId() + " exited code=" + inf.getExitCode(), - List.of(inferior)); + parent.getListeners().fire.event(parent, null, TargetEventType.PROCESS_EXITED, + "Inferior " + inf.getId() + " exited code=" + inf.getExitCode(), List.of(inferior)); inferior.inferiorExited(inf.getExitCode()); } @@ -97,105 +82,20 @@ public class GdbModelTargetInferiorContainer "Removed"); } - protected void gatherThreads(List into, - GdbModelTargetInferior inferior, Collection from) { - for (GdbThread t : from) { - GdbModelTargetThread p = inferior.threads.getTargetThread(t); - if (p != null) { - into.add(p); - } - } - } - - @Override - public void inferiorStateChanged(GdbInferior inf, Collection threads, GdbState state, - GdbThread thread, GdbCause cause, GdbReason reason) { - // Desired order of updates: - // 1. TargetEvent emitted - // 2. Thread states/stacks updated - // 3. Memory regions updated (Ew) - GdbModelTargetInferior inferior = getTargetInferior(inf); - GdbModelTargetThread targetThread = - thread == null ? null : inferior.threads.getTargetThread(thread); - if (state == GdbState.RUNNING) { - inferior.changeAttributes(List.of(), Map.of( // - TargetExecutionStateful.STATE_ATTRIBUTE_NAME, TargetExecutionState.RUNNING // - ), reason.desc()); - List params = new ArrayList<>(); - gatherThreads(params, inferior, threads); - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event(parent, targetThread, TargetEventType.RUNNING, "Running", params); - } - if (state != GdbState.STOPPED) { - inferior.threads.threadsStateChanged(threads, state, reason); - return; - } - if (reason instanceof GdbBreakpointHitReason) { - GdbBreakpointHitReason bptHit = (GdbBreakpointHitReason) reason; - List params = new ArrayList<>(); - GdbModelTargetBreakpointSpec spec = - parent.breakpoints.getTargetBreakpointSpecIfPresent(bptHit.getBreakpointId()); - if (spec != null) { - params.add(spec); - } - gatherThreads(params, inferior, threads); - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event(parent, targetThread, TargetEventType.BREAKPOINT_HIT, bptHit.desc(), - params); - } - else if (reason instanceof GdbEndSteppingRangeReason) { - List params = new ArrayList<>(); - gatherThreads(params, inferior, threads); - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event(parent, targetThread, TargetEventType.STEP_COMPLETED, reason.desc(), - params); - } - else if (reason instanceof GdbSignalReceivedReason) { - GdbSignalReceivedReason signal = (GdbSignalReceivedReason) reason; - List params = new ArrayList<>(); - params.add(signal.getSignalName()); - gatherThreads(params, inferior, threads); - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event(parent, targetThread, TargetEventType.SIGNAL, reason.desc(), params); - } - else { - List params = new ArrayList<>(); - gatherThreads(params, inferior, threads); - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event(parent, targetThread, TargetEventType.STOPPED, reason.desc(), params); - } - // This will update stacks of newly-STOPPED threads - inferior.changeAttributes(List.of(), Map.of( // - TargetExecutionStateful.STATE_ATTRIBUTE_NAME, TargetExecutionState.STOPPED // - ), reason.desc()); - inferior.threads.threadsStateChanged(threads, state, reason); - // Ew. I wish I didn't have to, but there doesn't seem to be a "(un)mapped" event - inferior.updateMemory(); - } - @Override public void threadCreated(GdbThread thread, GdbCause cause) { GdbModelTargetInferior inferior = getTargetInferior(thread.getInferior()); GdbModelTargetThread targetThread = inferior.threads.threadCreated(thread); - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event(parent, targetThread, TargetEventType.THREAD_CREATED, - "Thread " + thread.getId() + " started", List.of(targetThread)); + parent.getListeners().fire.event(parent, targetThread, TargetEventType.THREAD_CREATED, + "Thread " + thread.getId() + " started", List.of(targetThread)); } @Override public void threadExited(int threadId, GdbInferior inf, GdbCause cause) { GdbModelTargetInferior inferior = getTargetInferior(inf); GdbModelTargetThread targetThread = inferior.threads.getTargetThreadIfPresent(threadId); - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event(parent, targetThread, TargetEventType.THREAD_EXITED, - "Thread " + threadId + " exited", List.of(targetThread)); + parent.getListeners().fire.event(parent, targetThread, TargetEventType.THREAD_EXITED, + "Thread " + threadId + " exited", List.of(targetThread)); inferior.threads.threadExited(threadId); } @@ -203,20 +103,16 @@ public class GdbModelTargetInferiorContainer public void libraryLoaded(GdbInferior inf, String name, GdbCause cause) { GdbModelTargetInferior inferior = getTargetInferior(inf); GdbModelTargetModule module = inferior.modules.libraryLoaded(name); - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event(parent, null, TargetEventType.MODULE_LOADED, "Library " + name + " loaded", - List.of(module)); + parent.getListeners().fire.event(parent, null, TargetEventType.MODULE_LOADED, + "Library " + name + " loaded", List.of(module)); } @Override public void libraryUnloaded(GdbInferior inf, String name, GdbCause cause) { GdbModelTargetInferior inferior = getTargetInferior(inf); GdbModelTargetModule module = inferior.modules.getTargetModuleIfPresent(name); - parent.getListeners() - .fire(TargetEventScopeListener.class) - .event(parent, null, TargetEventType.MODULE_UNLOADED, - "Library " + name + " unloaded", List.of(module)); + parent.getListeners().fire.event(parent, null, TargetEventType.MODULE_UNLOADED, + "Library " + name + " unloaded", List.of(module)); inferior.modules.libraryUnloaded(name); } @@ -269,4 +165,8 @@ public class GdbModelTargetInferiorContainer inf.invalidateMemoryAndRegisterCaches(); } } + + public CompletableFuture stateChanged(GdbStateChangeRecord sco) { + return getTargetInferior(sco.getInferior()).stateChanged(sco); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetMemoryRegion.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetMemoryRegion.java index 9d9d93a191..f3b13b89c5 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetMemoryRegion.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetMemoryRegion.java @@ -17,8 +17,11 @@ package agent.gdb.model.impl; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import agent.gdb.manager.impl.GdbMemoryMapping; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; +import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.TargetMemoryRegion; import ghidra.dbg.target.TargetObject; @@ -29,11 +32,9 @@ import ghidra.program.model.address.*; @TargetObjectSchemaInfo( name = "MemoryRegion", elements = { - @TargetElementType(type = Void.class) - }, + @TargetElementType(type = Void.class) }, attributes = { - @TargetAttributeType(type = Void.class) - }) + @TargetAttributeType(type = Void.class) }) public class GdbModelTargetMemoryRegion extends DefaultTargetObject implements TargetMemoryRegion { @@ -81,8 +82,7 @@ public class GdbModelTargetMemoryRegion EXECUTABLE_ATTRIBUTE_NAME, isExecutable(), // OBJFILE_ATTRIBUTE_NAME, objfile = mapping.getObjfile(), // OFFSET_ATTRIBUTE_NAME, offset = mapping.getOffset().longValue(), // - DISPLAY_ATTRIBUTE_NAME, display = computeDisplay(mapping), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, display = computeDisplay(mapping) // ), "Initialized"); } @@ -137,4 +137,9 @@ public class GdbModelTargetMemoryRegion public long getOffset() { return offset; } + + public CompletableFuture stateChanged(GdbStateChangeRecord sco) { + // Nothing to do here + return AsyncUtils.NIL; + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModule.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModule.java index 1a05f92d2e..5388ed508c 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModule.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModule.java @@ -27,7 +27,6 @@ import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.*; -import ghidra.util.Msg; @TargetObjectSchemaInfo( name = "Module", @@ -77,7 +76,6 @@ public class GdbModelTargetModule VISIBLE_MODULE_NAME_ATTRIBUTE_NAME, module.getName(), // RANGE_ATTRIBUTE_NAME, range, // MODULE_NAME_ATTRIBUTE_NAME, module.getName(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED, // SHORT_DISPLAY_ATTRIBUTE_NAME, getDisplay(), // DISPLAY_ATTRIBUTE_NAME, getDisplay() // ), "Initialized"); @@ -85,7 +83,7 @@ public class GdbModelTargetModule public CompletableFuture init() { return sections.requestElements(true).exceptionally(ex -> { - Msg.error(this, "Could not initialize module sections and base", ex); + impl.reportError(this, "Could not initialize module sections and base", ex); return null; }); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModuleContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModuleContainer.java index 987ef7d823..35d794b014 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModuleContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetModuleContainer.java @@ -22,18 +22,18 @@ import java.util.stream.Collectors; import agent.gdb.manager.GdbInferior; import agent.gdb.manager.GdbModule; import ghidra.async.AsyncFence; -import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.error.DebuggerUserException; import ghidra.dbg.target.TargetModule; import ghidra.dbg.target.TargetModuleContainer; import ghidra.dbg.target.schema.TargetAttributeType; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; import ghidra.lifecycle.Internal; -import ghidra.util.Msg; @TargetObjectSchemaInfo( name = "ModuleContainer", + elementResync = ResyncMode.ONCE, // TODO: Should this be NEVER? attributes = { @TargetAttributeType(type = Void.class) }, @@ -122,11 +122,8 @@ public class GdbModelTargetModuleContainer } public CompletableFuture refreshInternal() { - if (!isObserved()) { - return AsyncUtils.NIL; - } return doRefresh().exceptionally(ex -> { - Msg.error(this, "Problem refreshing inferior's modules", ex); + impl.reportError(this, "Problem refreshing inferior's modules", ex); return null; }); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetProcessMemory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetProcessMemory.java index 06f0ddc885..002f8bf125 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetProcessMemory.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetProcessMemory.java @@ -28,6 +28,8 @@ import com.google.common.collect.Range; import agent.gdb.manager.GdbInferior; import agent.gdb.manager.impl.GdbMemoryMapping; import agent.gdb.manager.impl.cmd.GdbCommandError; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; +import ghidra.async.AsyncFence; import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.error.DebuggerMemoryAccessException; @@ -41,8 +43,7 @@ import ghidra.util.datastruct.WeakValueHashMap; @TargetObjectSchemaInfo( name = "Memory", attributes = { - @TargetAttributeType(type = Void.class) - }, + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class GdbModelTargetProcessMemory extends DefaultTargetObject @@ -106,7 +107,7 @@ public class GdbModelTargetProcessMemory } byte[] content = Arrays.copyOf(buf.array(), (int) (r.upperEndpoint() - r.lowerEndpoint())); - listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, content); + listeners.fire.memoryUpdated(this, address, content); return content; }).exceptionally(e -> { e = AsyncUtils.unwrapThrowable(e); @@ -114,12 +115,10 @@ public class GdbModelTargetProcessMemory GdbCommandError gce = (GdbCommandError) e; e = new DebuggerMemoryAccessException( "Cannot read at " + address + ": " + gce.getInfo().getString("msg")); - listeners.fire(TargetMemoryListener.class) - .memoryReadError(this, range, (DebuggerMemoryAccessException) e); + listeners.fire.memoryReadError(this, range, (DebuggerMemoryAccessException) e); } if (e instanceof DebuggerMemoryAccessException) { - listeners.fire(TargetMemoryListener.class) - .memoryReadError(this, range, (DebuggerMemoryAccessException) e); + listeners.fire.memoryReadError(this, range, (DebuggerMemoryAccessException) e); } return ExceptionUtils.rethrow(e); }); @@ -133,7 +132,7 @@ public class GdbModelTargetProcessMemory @Override public CompletableFuture writeMemory(Address address, byte[] data) { return inferior.writeMemory(address.getOffset(), ByteBuffer.wrap(data)).thenAccept(__ -> { - listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, data); + listeners.fire.memoryUpdated(this, address, data); }); } @@ -141,16 +140,6 @@ public class GdbModelTargetProcessMemory listeners.fire.invalidateCacheRequested(this); } - protected CompletableFuture update() { - if (!isObserved()) { - return AsyncUtils.NIL; - } - return fetchElements(true).exceptionally(e -> { - Msg.error(this, "Could not update memory regions " + this + " on STOPPED"); - return null; - }); - } - public void memoryChanged(long offset, int len) { Address address = impl.getAddressFactory().getDefaultAddressSpace().getAddress(offset); doReadMemory(address, offset, len).exceptionally(ex -> { @@ -158,4 +147,14 @@ public class GdbModelTargetProcessMemory return null; }); } + + public CompletableFuture stateChanged(GdbStateChangeRecord sco) { + return requestElements(false).thenCompose(__ -> { + AsyncFence fence = new AsyncFence(); + for (GdbModelTargetMemoryRegion modelRegion : regionsByStart.values()) { + fence.include(modelRegion.stateChanged(sco)); + } + return fence.ready(); + }); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetRegister.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetRegister.java index 7e0dbc39b2..3004012c33 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetRegister.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetRegister.java @@ -19,20 +19,16 @@ import java.util.List; import java.util.Map; import agent.gdb.manager.GdbRegister; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetRegister; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; -@TargetObjectSchemaInfo( - name = "RegisterDescriptor", - elements = { - @TargetElementType(type = Void.class) - }, - attributes = { - @TargetAttributeType(type = Void.class) - }) +@TargetObjectSchemaInfo(name = "RegisterDescriptor", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(type = Void.class) }) public class GdbModelTargetRegister extends DefaultTargetObject implements TargetRegister { @@ -64,8 +60,7 @@ public class GdbModelTargetRegister changeAttributes(List.of(), Map.of( // CONTAINER_ATTRIBUTE_NAME, registers, // LENGTH_ATTRIBUTE_NAME, bitLength, // - DISPLAY_ATTRIBUTE_NAME, getName(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, getName() // ), "Initialized"); } @@ -83,4 +78,8 @@ public class GdbModelTargetRegister public GdbModelTargetRegisterContainer getContainer() { return parent; } + + public void stateChanged(GdbStateChangeRecord sco) { + } + } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetRegisterContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetRegisterContainer.java index 23e14c3a7a..00c0590246 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetRegisterContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetRegisterContainer.java @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import agent.gdb.manager.*; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.TargetRegisterContainer; @@ -29,12 +30,8 @@ import ghidra.dbg.target.schema.TargetObjectSchemaInfo; import ghidra.util.Msg; import ghidra.util.datastruct.WeakValueHashMap; -@TargetObjectSchemaInfo( - name = "RegisterContainer", - attributes = { - @TargetAttributeType(type = Void.class) - }, - canonicalContainer = true) +@TargetObjectSchemaInfo(name = "RegisterContainer", attributes = { + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class GdbModelTargetRegisterContainer extends DefaultTargetObject implements TargetRegisterContainer { @@ -88,7 +85,7 @@ public class GdbModelTargetRegisterContainer n -> new GdbModelTargetRegister(this, register)); } - public CompletableFuture refresh() { + public CompletableFuture refreshInternal() { if (!isObserved()) { return AsyncUtils.NIL; } @@ -97,4 +94,13 @@ public class GdbModelTargetRegisterContainer return null; }); } + + public void stateChanged(GdbStateChangeRecord sco) { + requestElements(false).thenAccept(__ -> { + for (GdbModelTargetRegister modelRegister : registersByNumber.values()) { + modelRegister.stateChanged(sco); + } + }); + } + } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSection.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSection.java index 5a6a047380..53652c74b0 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSection.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSection.java @@ -66,8 +66,7 @@ public class GdbModelTargetSection MODULE_ATTRIBUTE_NAME, module, RANGE_ATTRIBUTE_NAME, range, VISIBLE_RANGE_ATTRIBUTE_NAME, range, - DISPLAY_ATTRIBUTE_NAME, section.getName(), - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED), + DISPLAY_ATTRIBUTE_NAME, section.getName()), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSectionContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSectionContainer.java index 62232af62c..1eab6e72df 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSectionContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSectionContainer.java @@ -22,18 +22,16 @@ import java.util.stream.Collectors; import agent.gdb.manager.GdbModuleSection; import ghidra.dbg.agent.DefaultTargetObject; +import ghidra.dbg.target.TargetSectionContainer; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; import ghidra.util.datastruct.WeakValueHashMap; -@TargetObjectSchemaInfo( - name = "SectionContainer", - attributes = { - @TargetAttributeType(type = Void.class) - }, - canonicalContainer = true) +@TargetObjectSchemaInfo(name = "SectionContainer", attributes = { + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class GdbModelTargetSectionContainer - extends DefaultTargetObject { + extends DefaultTargetObject + implements TargetSectionContainer { public static final String NAME = "Sections"; protected final GdbModelImpl impl; @@ -57,9 +55,6 @@ public class GdbModelTargetSectionContainer .collect(Collectors.toList()); } setElements(sections, "Refreshed"); - changeAttributes(List.of(), Map.of( - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // - ), "Refreshed"); parent.sectionsRefreshed(); // recompute base } @@ -71,9 +66,8 @@ public class GdbModelTargetSectionContainer } protected synchronized GdbModelTargetSection getTargetSection(String name) { - return sectionsByName.computeIfAbsent(name, - n -> new GdbModelTargetSection(this, module, - module.module.getKnownSections().get(name))); + return sectionsByName.computeIfAbsent(name, n -> new GdbModelTargetSection(this, module, + module.module.getKnownSections().get(name))); } protected synchronized GdbModelTargetSection getTargetSection(GdbModuleSection section) { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSession.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSession.java index 395f39b895..bf4d97a7d5 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSession.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSession.java @@ -18,8 +18,11 @@ package agent.gdb.model.impl; import java.io.IOException; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; import agent.gdb.manager.*; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; +import agent.gdb.manager.reason.GdbReason; import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetModelRoot; import ghidra.dbg.error.DebuggerIllegalArgumentException; @@ -32,15 +35,13 @@ import ghidra.util.Msg; @TargetObjectSchemaInfo( name = "Session", elements = { - @TargetElementType(type = Void.class) - }, + @TargetElementType(type = Void.class) }, attributes = { - @TargetAttributeType(type = Void.class) - }) -public class GdbModelTargetSession extends DefaultTargetModelRoot implements - TargetAccessConditioned, TargetAttacher, TargetFocusScope, TargetInterpreter, - TargetInterruptible, TargetCmdLineLauncher, TargetEventScope, - GdbConsoleOutputListener, GdbEventsListenerAdapter { + @TargetAttributeType(type = Void.class) }) +public class GdbModelTargetSession extends DefaultTargetModelRoot + implements TargetAccessConditioned, TargetAttacher, TargetFocusScope, TargetInterpreter, + TargetInterruptible, TargetCmdLineLauncher, TargetEventScope, GdbConsoleOutputListener, + GdbEventsListenerAdapter { protected static final String GDB_PROMPT = "(gdb)"; protected final GdbModelImpl impl; @@ -51,6 +52,7 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements protected final GdbModelTargetBreakpointContainer breakpoints; private boolean accessible = true; + protected AtomicInteger focusFreezeCount = new AtomicInteger(); protected GdbModelSelectableObject focus; protected String debugger = "gdb"; // Used by GdbModelTargetEnvironment @@ -71,7 +73,8 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements PROMPT_ATTRIBUTE_NAME, GDB_PROMPT, // DISPLAY_ATTRIBUTE_NAME, display, // TargetMethod.PARAMETERS_ATTRIBUTE_NAME, TargetCmdLineLauncher.PARAMETERS, // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + SUPPORTED_ATTACH_KINDS_ATTRIBUTE_NAME, GdbModelTargetInferior.SUPPORTED_KINDS, // + FOCUS_ATTRIBUTE_NAME, this // Satisfy schema. Will be set to first inferior. ), "Initialized"); impl.gdb.addEventsListener(this); impl.gdb.addConsoleOutputListener(this); @@ -109,7 +112,7 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements Map.of(DISPLAY_ATTRIBUTE_NAME, display = out.split("\n")[0].strip() // ), "Version refreshed"); }).exceptionally(e -> { - Msg.error(this, "Could not get GDB version", e); + model.reportError(this, "Could not get GDB version", e); debugger = "gdb"; return null; }); @@ -133,7 +136,7 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements default: throw new AssertionError(); } - listeners.fire(TargetInterpreterListener.class).consoleOutput(this, dbgChannel, out); + listeners.fire.consoleOutput(this, dbgChannel, out); } @Override @@ -158,16 +161,9 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements } public void setAccessible(boolean accessible) { - synchronized (attributes) { - if (this.accessible == accessible) { - return; - } - this.accessible = accessible; - changeAttributes(List.of(), Map.of( // - ACCESSIBLE_ATTRIBUTE_NAME, accessible // - ), "Accessibility changed"); - } - listeners.fire(TargetAccessibilityListener.class).accessibilityChanged(this, accessible); + changeAttributes(List.of(), Map.of( // + ACCESSIBLE_ATTRIBUTE_NAME, this.accessible = accessible // + ), "Accessibility changed"); } @Override @@ -177,29 +173,27 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements @Override public CompletableFuture launch(List args) { - // TODO: Find first unused inferior? - return impl.gdb.addInferior().thenCompose(inf -> { + return impl.gateFuture(impl.gdb.availableInferior().thenCompose(inf -> { return GdbModelImplUtils.launch(impl, inf, args); - }); + }).thenApply(__ -> null)); } @Override public CompletableFuture attach(TargetAttachable attachable) { - GdbModelTargetAttachable mine = - getModel().assertMine(GdbModelTargetAttachable.class, attachable); + GdbModelTargetAttachable mine = impl.assertMine(GdbModelTargetAttachable.class, attachable); return attach(mine.pid); } @Override public CompletableFuture attach(long pid) { - // TODO: Find first unused inferior? - return impl.gdb.addInferior().thenCompose(inf -> { + return impl.gateFuture(impl.gdb.availableInferior().thenCompose(inf -> { return inf.attach(pid).thenApply(__ -> null); - }); + })); } @Override public CompletableFuture interrupt() { + //return impl.gdb.interrupt(); try { impl.gdb.sendInterruptNow(); } @@ -211,12 +205,13 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements @Override public CompletableFuture execute(String cmd) { - return impl.gdb.console(cmd).exceptionally(GdbModelImpl::translateEx); + return impl.gateFuture(impl.gdb.console(cmd).exceptionally(GdbModelImpl::translateEx)); } @Override public CompletableFuture executeCapture(String cmd) { - return impl.gdb.consoleCapture(cmd).exceptionally(GdbModelImpl::translateEx); + return impl + .gateFuture(impl.gdb.consoleCapture(cmd).exceptionally(GdbModelImpl::translateEx)); } @Override @@ -244,17 +239,19 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements inferiors.invalidateMemoryAndRegisterCaches(); } - protected void setFocus(GdbModelSelectableObject sel) { - boolean doFire; - synchronized (this) { - doFire = !Objects.equals(this.focus, sel); - this.focus = sel; - } - if (doFire) { + protected void freezeFocusUpdates() { + focusFreezeCount.incrementAndGet(); + } + + protected void thawFocusUpdates() { + focusFreezeCount.decrementAndGet(); + } + + protected void setFocus(GdbModelSelectableObject focus) { + if (focusFreezeCount.get() == 0) { changeAttributes(List.of(), Map.of( // - FOCUS_ATTRIBUTE_NAME, focus // + FOCUS_ATTRIBUTE_NAME, this.focus = focus // ), "Focus changed"); - listeners.fire(TargetFocusScopeListener.class).focusChanged(this, sel); } } @@ -262,4 +259,20 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot implements public GdbModelSelectableObject getFocus() { return focus; } + + @Override + public void inferiorStateChanged(GdbInferior inf, Collection threads, GdbState state, + GdbThread thread, GdbCause cause, GdbReason reason) { + GdbStateChangeRecord sco = + new GdbStateChangeRecord(inf, threads, state, thread, cause, reason); + GdbInferior toFocus = thread == null ? impl.gdb.currentInferior() : thread.getInferior(); + freezeFocusUpdates(); + CompletableFuture infUpdates = CompletableFuture.allOf( + breakpoints.stateChanged(sco), + inferiors.stateChanged(sco)); + infUpdates.whenComplete((v, t) -> { + thawFocusUpdates(); + toFocus.select(); + }); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStack.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStack.java index d85c6a1904..49024f21d3 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStack.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStack.java @@ -22,23 +22,21 @@ import java.util.stream.Collectors; import agent.gdb.manager.GdbStackFrame; import agent.gdb.manager.GdbThread; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.TargetStack; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; -import ghidra.util.Msg; import ghidra.util.datastruct.WeakValueHashMap; @TargetObjectSchemaInfo( name = "Stack", attributes = { - @TargetAttributeType(type = Void.class) - }, + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) -public class GdbModelTargetStack - extends DefaultTargetObject - implements TargetStack { +public class GdbModelTargetStack extends + DefaultTargetObject implements TargetStack { public static final String NAME = "Stack"; protected final GdbModelImpl impl; @@ -61,8 +59,8 @@ public class GdbModelTargetStack synchronized (this) { frames = f.stream().map(this::getTargetFrame).collect(Collectors.toList()); } - // TODO: This might be a case where "move" is useful setElements(frames, "Refreshed"); + //Msg.debug(this, "Completed stack frames update"); }); } @@ -89,12 +87,15 @@ public class GdbModelTargetStack * GDB doesn't produce stack change events, but they should only ever happen by running a * target. Thus, every time we're STOPPED, this method should be called. */ - protected CompletableFuture update() { - if (!isObserved()) { + public CompletableFuture stateChanged(GdbStateChangeRecord sco) { + return requestElements(true).thenCompose(__ -> { + GdbModelTargetStackFrame innermost = framesByLevel.get(0); + if (innermost != null) { + return innermost.stateChanged(sco); + } return AsyncUtils.NIL; - } - return fetchElements(true).exceptionally(e -> { - Msg.error(this, "Could not update stack " + this + " on STOPPED"); + }).exceptionally(e -> { + impl.reportError(this, "Could not update stack " + this + " on STOPPED", e); return null; }); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrame.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrame.java index 123270551e..596cf23025 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrame.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrame.java @@ -15,21 +15,19 @@ */ package agent.gdb.model.impl; -import java.math.BigInteger; -import java.util.*; +import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; -import agent.gdb.manager.GdbRegister; import agent.gdb.manager.GdbStackFrame; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; import ghidra.dbg.agent.DefaultTargetObject; -import ghidra.dbg.error.DebuggerRegisterAccessException; -import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetStackFrame; import ghidra.dbg.target.schema.*; -import ghidra.dbg.util.ConversionUtils; import ghidra.dbg.util.PathUtils; import ghidra.lifecycle.Internal; import ghidra.program.model.address.Address; -import ghidra.util.Msg; @TargetObjectSchemaInfo( name = "StackFrame", @@ -40,7 +38,7 @@ import ghidra.util.Msg; @TargetAttributeType(type = Void.class) }) public class GdbModelTargetStackFrame extends DefaultTargetObject - implements TargetStackFrame, TargetRegisterBank, GdbModelSelectableObject { + implements TargetStackFrame, GdbModelSelectableObject { public static final String FUNC_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "function"; public static final String FROM_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "from"; // TODO @@ -78,14 +76,8 @@ public class GdbModelTargetStackFrame extends DefaultTargetObject> readRegistersNamed( - Collection names) { - return inferior.registers.fetchElements().thenCompose(regs -> { - Set toRead = new LinkedHashSet<>(); - for (String regname : names) { - GdbModelTargetRegister reg = regs.get(regname); - if (reg == null) { - throw new DebuggerRegisterAccessException("No such register: " + regname); - } - toRead.add(reg.register); - } - return frame.readRegisters(toRead); - }).thenApply(vals -> { - Map result = new LinkedHashMap<>(); - Map values = new LinkedHashMap<>(); - for (Map.Entry ent : vals.entrySet()) { - GdbRegister reg = ent.getKey(); - String regName = reg.getName(); - BigInteger val = ent.getValue(); - if (val == null) { - Msg.warn(this, "Register " + regName + " value came back null."); - continue; - } - byte[] bytes = ConversionUtils.bigIntegerToBytes(reg.getSize(), val); - values.put(reg, val); - result.put(regName, bytes); - } - registers.setValues(values); - changeAttributes(List.of(), List.of( // - registers // - ), Map.of(), "Refreshed"); - listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, result); - return result; - }); - } - - @Override - public CompletableFuture writeRegistersNamed(Map values) { - return inferior.registers.fetchElements().thenCompose(regs -> { - Map toWrite = new LinkedHashMap<>(); - for (Map.Entry ent : values.entrySet()) { - String regname = ent.getKey(); - GdbModelTargetRegister reg = regs.get(regname); - if (reg == null) { - throw new DebuggerRegisterAccessException("No such register: " + regname); - } - BigInteger val = new BigInteger(1, ent.getValue()); - toWrite.put(reg.register, val); - } - return frame.writeRegisters(toWrite); - }).thenAccept(__ -> { - listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, values); - }); - } - protected void setFrame(GdbStackFrame frame) { + frame = frame.fillWith(this.frame); + if (this.frame == frame) { + return; + } + this.frame = frame; this.pc = impl.space.getAddress(frame.getAddress().longValue()); this.func = frame.getFunction(); // TODO: module? "from" - this.frame = frame; changeAttributes(List.of(), List.of( // registers // @@ -180,11 +115,20 @@ public class GdbModelTargetStackFrame extends DefaultTargetObject select() { - return frame.select(); + return impl.gateFuture(frame.select()); } @TargetAttributeType(name = FUNC_ATTRIBUTE_NAME) public String getFunction() { return func; } + + @Override + public Address getProgramCounter() { + return pc; + } + + public CompletableFuture stateChanged(GdbStateChangeRecord sco) { + return registers.stateChanged(sco); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrameRegister.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrameRegister.java index 8678b0bdc7..bceda23751 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrameRegister.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrameRegister.java @@ -22,18 +22,16 @@ import java.util.Map; import agent.gdb.manager.GdbRegister; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetRegister; import ghidra.dbg.target.schema.*; -import ghidra.dbg.util.CollectionUtils.Delta; import ghidra.dbg.util.PathUtils; -@TargetObjectSchemaInfo( - name = "RegisterValue", - elements = { - @TargetElementType(type = Void.class) }, - attributes = { +@TargetObjectSchemaInfo(name = "RegisterValue", elements = { + @TargetElementType(type = Void.class) }, attributes = { @TargetAttributeType(type = Void.class) }) public class GdbModelTargetStackFrameRegister - extends DefaultTargetObject { + extends DefaultTargetObject + implements TargetRegister { protected static String indexRegister(GdbRegister register) { String name = register.getName(); @@ -50,15 +48,20 @@ public class GdbModelTargetStackFrameRegister protected final GdbModelImpl impl; protected final GdbRegister register; + protected final int bitLength; + public GdbModelTargetStackFrameRegister(GdbModelTargetStackFrameRegisterContainer registers, GdbRegister register) { super(registers.impl, registers, keyRegister(register), "Register"); this.impl = registers.impl; this.register = register; + this.bitLength = register.getSize() * 8; + changeAttributes(List.of(), Map.of( // + CONTAINER_ATTRIBUTE_NAME, registers, // + LENGTH_ATTRIBUTE_NAME, bitLength, // DISPLAY_ATTRIBUTE_NAME, getName(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED, // MODIFIED_ATTRIBUTE_NAME, false // ), "Initialized"); } @@ -68,19 +71,18 @@ public class GdbModelTargetStackFrameRegister return getCachedAttribute(DISPLAY_ATTRIBUTE_NAME).toString(); } - public void updateValue(BigInteger bigNewVal) { - String value = bigNewVal.toString(16); + public void stateChanged(byte[] bytes) { + BigInteger bigval = new BigInteger(1, bytes); + String value = bigval.toString(16); Object oldval = getCachedAttributes().get(VALUE_ATTRIBUTE_NAME); - boolean modified = (bigNewVal.longValue() != 0 && value.equals(oldval)); + boolean modified = (bigval.longValue() != 0 && value.equals(oldval)); String newval = getName() + " : " + value; - Delta delta = changeAttributes(List.of(), Map.of( // + changeAttributes(List.of(), Map.of( // VALUE_ATTRIBUTE_NAME, value, // DISPLAY_ATTRIBUTE_NAME, newval, // MODIFIED_ATTRIBUTE_NAME, modified // ), "Value Updated"); - if (delta.added.containsKey(DISPLAY_ATTRIBUTE_NAME)) { - listeners.fire.displayChanged(this, newval); - } } + } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrameRegisterContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrameRegisterContainer.java index 98bf123f42..3d1b74a49e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrameRegisterContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetStackFrameRegisterContainer.java @@ -17,36 +17,120 @@ package agent.gdb.model.impl; import java.math.BigInteger; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import agent.gdb.manager.GdbRegister; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; +import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; +import ghidra.dbg.error.DebuggerRegisterAccessException; +import ghidra.dbg.target.TargetRegisterBank; +import ghidra.dbg.target.TargetRegisterContainer; import ghidra.dbg.target.schema.TargetAttributeType; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; +import ghidra.dbg.util.ConversionUtils; +import ghidra.util.Msg; import ghidra.util.datastruct.WeakValueHashMap; -// NB. The canonical container, but of no recognized interface @TargetObjectSchemaInfo( name = "RegisterValueContainer", + elementResync = ResyncMode.ONCE, attributes = { - @TargetAttributeType(type = Void.class) - }, + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class GdbModelTargetStackFrameRegisterContainer - extends DefaultTargetObject { + extends DefaultTargetObject + implements TargetRegisterContainer, TargetRegisterBank { public static final String NAME = "Registers"; protected final GdbModelImpl impl; protected final GdbModelTargetStackFrame frame; protected final GdbModelTargetThread thread; + private Map regValues = new HashMap<>(); + protected final Map registersByNumber = new WeakValueHashMap<>(); + protected final Set allRegisters = new LinkedHashSet<>(); public GdbModelTargetStackFrameRegisterContainer(GdbModelTargetStackFrame frame) { super(frame.impl, frame, NAME, "StackFrameRegisterContainer"); this.impl = frame.impl; this.frame = frame; this.thread = frame.thread; + + changeAttributes(List.of(), List.of(), Map.of(DESCRIPTIONS_ATTRIBUTE_NAME, this), + "Initialized"); + } + + @Override + public GdbModelTargetStackFrameRegisterContainer getDescriptions() { + return this; + } + + protected CompletableFuture> ensureRegisterDescriptions() { + if (elements.isEmpty()) { + return populateRegisterDescriptions().thenApply(__ -> elements); + } + return CompletableFuture.completedFuture(elements); + } + + /** + * Get the descriptors without populating the values + * + *

+ * This need only be called once, but values must be updated every STOPPED/read + */ + protected CompletableFuture populateRegisterDescriptions() { + return thread.thread.listRegisters().thenAccept(regs -> { + if (regs.size() != registersByNumber.size()) { + allRegisters.clear(); + registersByNumber.clear(); + } + allRegisters.addAll(regs); + List registers; + synchronized (this) { + registers = regs.stream().map(this::getTargetRegister).collect(Collectors.toList()); + } + setElements(registers, Map.of(), "Refreshed"); + }); + } + + protected CompletableFuture> updateRegisterValues(Set toRead) { + return frame.frame.readRegisters(toRead).thenApply(vals -> { + Map result = new LinkedHashMap<>(); + for (Map.Entry ent : vals.entrySet()) { + GdbRegister reg = ent.getKey(); + String regName = reg.getName(); + BigInteger val = ent.getValue(); + if (val == null) { + Msg.warn(this, "Register " + regName + " value came back null."); + continue; + } + byte[] bytes = ConversionUtils.bigIntegerToBytes(reg.getSize(), val); + result.put(regName, bytes); + elements.get(regName).stateChanged(bytes); + } + this.regValues = result; + listeners.fire.registersUpdated(this, result); + return result; + }); + } + + /** + * Does both descriptions and then populates values + */ + @Override + public CompletableFuture requestElements(boolean refresh) { + // NB. GDB manager caches these per thread + return ensureRegisterDescriptions().thenCompose(regs -> { + if (!regs.isEmpty()) { + return updateRegisterValues(allRegisters); + } + return AsyncUtils.nil(); + }).thenApply(__ -> null); } protected synchronized GdbModelTargetStackFrameRegister getTargetRegister( @@ -55,15 +139,50 @@ public class GdbModelTargetStackFrameRegisterContainer n -> new GdbModelTargetStackFrameRegister(this, register)); } - public void setValues(Map values) { - List registers = new ArrayList<>(); - for (GdbRegister gdbreg : values.keySet()) { - GdbModelTargetStackFrameRegister reg = getTargetRegister(gdbreg); - registers.add(reg); - } - for (GdbModelTargetStackFrameRegister reg : registers) { - reg.updateValue(values.get(reg.register)); - } - changeElements(List.of(), registers, "Refreshed"); + @Override + public CompletableFuture> readRegistersNamed( + Collection names) { + return model.gateFuture(ensureRegisterDescriptions().thenCompose(regs -> { + Set toRead = new LinkedHashSet<>(); + for (String regname : names) { + GdbModelTargetStackFrameRegister reg = regs.get(regname); + if (reg == null) { + throw new DebuggerRegisterAccessException("No such register: " + regname); + } + toRead.add(reg.register); + } + return updateRegisterValues(toRead); + })); + } + + @Override + public Map getCachedRegisters() { + return regValues; + } + + @Override + public CompletableFuture writeRegistersNamed(Map values) { + Map toWrite = new LinkedHashMap<>(); + return model.gateFuture(ensureRegisterDescriptions().thenCompose(regs -> { + for (Map.Entry ent : values.entrySet()) { + String regname = ent.getKey(); + GdbModelTargetStackFrameRegister reg = regs.get(regname); + if (reg == null) { + throw new DebuggerRegisterAccessException("No such register: " + regname); + } + BigInteger val = new BigInteger(1, ent.getValue()); + toWrite.put(reg.register, val); + } + return frame.frame.writeRegisters(toWrite); + }).thenCompose(__ -> { + return updateRegisterValues(toWrite.keySet()); + })).thenApply(__ -> null); + } + + public CompletableFuture stateChanged(GdbStateChangeRecord sco) { + return requestElements(false).exceptionally(ex -> { + impl.reportError(this, "Trouble updating registers on state change", ex); + return null; + }); } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSymbol.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSymbol.java index 0947d9a5f7..879e8d01a4 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSymbol.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSymbol.java @@ -60,8 +60,7 @@ public class GdbModelTargetSymbol // TODO: DATA_TYPE VALUE_ATTRIBUTE_NAME, value, SIZE_ATTRIBUTE_NAME, size, - DISPLAY_ATTRIBUTE_NAME, symbol.getName(), - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED), + DISPLAY_ATTRIBUTE_NAME, symbol.getName()), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSymbolContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSymbolContainer.java index 900f1a000e..8de3610ce4 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSymbolContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetSymbolContainer.java @@ -24,11 +24,13 @@ import agent.gdb.manager.impl.GdbMinimalSymbol; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.TargetSymbolNamespace; import ghidra.dbg.target.schema.TargetAttributeType; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; import ghidra.util.datastruct.WeakValueHashMap; @TargetObjectSchemaInfo( name = "SymbolContainer", + elementResync = ResyncMode.ONCE, attributes = { @TargetAttributeType(type = Void.class) }, @@ -60,9 +62,6 @@ public class GdbModelTargetSymbolContainer .collect(Collectors.toList()); } setElements(symbols, "Refreshed"); - changeAttributes(List.of(), Map.of( - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // - ), "Refreshed"); }); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java index 3c91449467..294c1adb6d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThread.java @@ -23,8 +23,9 @@ import agent.gdb.manager.*; import agent.gdb.manager.GdbManager.ExecSuffix; import agent.gdb.manager.impl.GdbFrameInfo; import agent.gdb.manager.impl.GdbThreadInfo; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; import agent.gdb.manager.reason.GdbBreakpointHitReason; -import agent.gdb.manager.reason.GdbReason; +import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.*; import ghidra.dbg.target.schema.*; @@ -85,9 +86,7 @@ public class GdbModelTargetThread STATE_ATTRIBUTE_NAME, convertState(thread.getState()), // SUPPORTED_STEP_KINDS_ATTRIBUTE_NAME, SUPPORTED_KINDS, // SHORT_DISPLAY_ATTRIBUTE_NAME, shortDisplay = computeShortDisplay(), // - DISPLAY_ATTRIBUTE_NAME, display = computeDisplay(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED, // - stack.getName(), stack // + DISPLAY_ATTRIBUTE_NAME, display = computeDisplay() // ), "Initialized"); updateInfo().exceptionally(ex -> { @@ -108,7 +107,6 @@ public class GdbModelTargetThread SHORT_DISPLAY_ATTRIBUTE_NAME, shortDisplay = computeShortDisplay(), // DISPLAY_ATTRIBUTE_NAME, display = computeDisplay() // ), "Initialized"); - listeners.fire.displayChanged(this, getDisplay()); }); } @@ -178,25 +176,6 @@ public class GdbModelTargetThread } } - protected void threadStateChanged(GdbState state, GdbReason reason) { - if (state == GdbState.STOPPED) { - updateStack(); // NB: Callee handles errors - } - TargetExecutionState targetState = convertState(state); - changeAttributes(List.of(), Map.of( // - STATE_ATTRIBUTE_NAME, targetState // - ), reason.desc()); - listeners.fire(TargetExecutionStateListener.class).executionStateChanged(this, targetState); - - if (reason instanceof GdbBreakpointHitReason) { - GdbBreakpointHitReason bpHit = (GdbBreakpointHitReason) reason; - GdbStackFrame frame = bpHit.getFrame(thread); - GdbModelTargetStackFrame f = stack.getTargetFrame(frame); - long bpId = bpHit.getBreakpointId(); - impl.session.breakpoints.breakpointHit(bpId, f); - } - } - protected ExecSuffix convertToGdb(TargetStepKind kind) { switch (kind) { case FINISH: @@ -227,9 +206,9 @@ public class GdbModelTargetThread throw new UnsupportedOperationException(kind.name()); case ADVANCE: // Why no exec-advance in GDB/MI? // TODO: This doesn't work, since advance requires a parameter - return thread.console("advance"); + return model.gateFuture(thread.console("advance")); default: - return thread.step(convertToGdb(kind)); + return model.gateFuture(thread.step(convertToGdb(kind))); } } @@ -237,17 +216,32 @@ public class GdbModelTargetThread stack.invalidateRegisterCaches(); } - protected CompletableFuture updateStack() { - Msg.debug(this, "Updating stack for " + this); - return stack.update().thenCompose(__ -> updateInfo()).exceptionally(ex -> { - model.reportError(this, "Could not update stack for thread " + this, ex); - return null; - }); - } - @Override @Internal public CompletableFuture select() { - return thread.select(); + return impl.gateFuture(thread.select()); + } + + public GdbModelTargetBreakpointLocation breakpointHit(GdbBreakpointHitReason reason) { + GdbStackFrame frame = reason.getFrame(thread); + GdbModelTargetStackFrame targetFrame = stack.getTargetFrame(frame); + long bpId = reason.getBreakpointId(); + return impl.session.breakpoints.breakpointHit(bpId, targetFrame); + } + + public CompletableFuture stateChanged(GdbStateChangeRecord sco) { + GdbState state = sco.getState(); + CompletableFuture result = AsyncUtils.NIL; + if (state == GdbState.STOPPED) { + Msg.debug(this, "Updating stack for " + this); + result = CompletableFuture.allOf( + updateInfo(), + stack.stateChanged(sco)); + } + TargetExecutionState targetState = convertState(state); + changeAttributes(List.of(), Map.of( // + STATE_ATTRIBUTE_NAME, targetState // + ), sco.getReason().desc()); + return result; } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThreadContainer.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThreadContainer.java index 06e7045c2d..28ab27c48c 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThreadContainer.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelTargetThreadContainer.java @@ -15,23 +15,27 @@ */ package agent.gdb.model.impl; -import java.util.*; +import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; -import agent.gdb.manager.*; -import agent.gdb.manager.reason.GdbReason; +import agent.gdb.manager.GdbInferior; +import agent.gdb.manager.GdbThread; +import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; +import agent.gdb.manager.reason.GdbBreakpointHitReason; +import ghidra.async.AsyncFence; import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; +import ghidra.util.Msg; import ghidra.util.datastruct.WeakValueHashMap; @TargetObjectSchemaInfo( name = "ThreadContainer", attributes = { - @TargetAttributeType(type = Void.class) - }, + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class GdbModelTargetThreadContainer extends DefaultTargetObject { @@ -55,17 +59,6 @@ public class GdbModelTargetThreadContainer return targetThread; } - public void threadStateChanged(GdbThread thread, GdbState state, GdbReason reason) { - getTargetThread(thread).threadStateChanged(state, reason); - } - - public void threadsStateChanged(Collection threads, GdbState state, - GdbReason reason) { - for (GdbThread thread : threads) { - threadStateChanged(thread, state, reason); - } - } - public void threadExited(int threadId) { synchronized (this) { threadsById.remove(threadId); @@ -95,6 +88,7 @@ public class GdbModelTargetThreadContainer } public synchronized GdbModelTargetThread getTargetThread(GdbThread thread) { + assert thread.getInferior() == inferior; return threadsById.computeIfAbsent(thread.getId(), i -> new GdbModelTargetThread(this, parent, thread)); } @@ -108,4 +102,22 @@ public class GdbModelTargetThreadContainer thread.invalidateRegisterCaches(); } } + + public CompletableFuture stateChanged(GdbStateChangeRecord sco) { + return requestElements(false).thenCompose(__ -> { + AsyncFence fence = new AsyncFence(); + for (GdbModelTargetThread modelThread : threadsById.values()) { + fence.include(modelThread.stateChanged(sco)); + } + return fence.ready(); + }).exceptionally(__ -> { + Msg.error(this, "Could not update threads " + this + " on STOPPED"); + return null; + }); + } + + public GdbModelTargetBreakpointLocation breakpointHit(GdbBreakpointHitReason reason) { + GdbThread thread = impl.gdb.getThread(reason.getThreadId()); + return getTargetThread(thread).breakpointHit(reason); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/ffi/linux/PtyTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/ffi/linux/PtyTest.java index 0fbbfd5253..100ad8c12e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/ffi/linux/PtyTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/ffi/linux/PtyTest.java @@ -131,6 +131,7 @@ public class PtyTest { Map env = new HashMap<>(); env.put("PS1", "BASH:"); env.put("PROMPT_COMMAND", ""); + env.put("TERM", ""); try (Pty pty = Pty.openpty()) { PtyMaster master = pty.getMaster(); PrintWriter writer = new PrintWriter(master.getOutputStream()); @@ -162,6 +163,8 @@ public class PtyTest { public void testSessionBashInterruptCat() throws IOException, InterruptedException { Map env = new HashMap<>(); env.put("PS1", "BASH:"); + env.put("PROMPT_COMMAND", ""); + env.put("TERM", ""); try (Pty pty = Pty.openpty()) { PtyMaster master = pty.getMaster(); PrintWriter writer = new PrintWriter(master.getOutputStream()); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractGdbModelHost.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractGdbModelHost.java new file mode 100644 index 0000000000..8aaf0a17d1 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractGdbModelHost.java @@ -0,0 +1,29 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package agent.gdb.model; + +import java.util.Map; + +import agent.gdb.manager.GdbManager; +import ghidra.dbg.test.AbstractModelHost; + +public abstract class AbstractGdbModelHost extends AbstractModelHost { + @Override + public Map getFactoryOptions() { + return Map.ofEntries( + Map.entry("GDB launch command", GdbManager.DEFAULT_GDB_CMD)); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbAmd64RegistersTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbAmd64RegistersTest.java new file mode 100644 index 0000000000..703dd4af96 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbAmd64RegistersTest.java @@ -0,0 +1,59 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.List; +import java.util.Map; + +import ghidra.dbg.test.*; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForGdbAmd64RegistersTest + extends AbstractDebuggerModelRegistersTest + implements ProvidesTargetViaLaunchSpecimen { + public final Map REG_VALS = Map.ofEntries( + Map.entry("rax", arr("0123456789abcdef")), + Map.entry("ymm0", arr( + "0123456789abcdef" + "fedcba9876543210" + "0011223344556677" + "8899aabbccddeeff"))); + // TODO: A more complete set of test registers + // TODO: How to reliably test other target platforms? qemu with gdbserver? + + @Override + public AbstractDebuggerModelTest getTest() { + return this; + } + + @Override + public List getExpectedRegisterBankPath(List threadPath) { + return PathUtils.extend(threadPath, PathUtils.parse("Stack[0].Registers")); + } + + @Override + public Map getRegisterWrites() { + return REG_VALS; + } + + /** + * {@inheritDoc} + * + *

+ * Note, this assumes the test is run where 'echo' is an image compiled for amd64 + */ + @Override + public DebuggerTestSpecimen getLaunchSpecimen() { + return GdbLinuxSpecimen.ECHO_HW; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbAttacherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbAttacherTest.java new file mode 100644 index 0000000000..900c05157d --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbAttacherTest.java @@ -0,0 +1,57 @@ +/* ### + * 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 agent.gdb.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import ghidra.dbg.target.TargetEnvironment; +import ghidra.dbg.target.TargetMethod.TargetParameterMap; +import ghidra.dbg.test.AbstractDebuggerModelAttacherTest; + +public abstract class AbstractModelForGdbAttacherTest extends AbstractDebuggerModelAttacherTest { + + @Override + public List getExpectedAttachableContainerPath() { + return List.of("Available"); + } + + @Override + public List getExpectedProcessesContainerPath() { + return List.of("Inferiors"); + } + + @Override + public DebuggerTestSpecimen getAttachSpecimen() { + return GdbLinuxSpecimen.DD; + } + + @Override + public TargetParameterMap getExpectedAttachParameters() { + return null; // TODO + } + + @Override + public void assertEnvironment(TargetEnvironment environment) { + // TODO: This test won't always be on amd64 Linux, no? + assertEquals("i386:x86-64", environment.getArchitecture()); + assertEquals("GNU/Linux", environment.getOperatingSystem()); + assertEquals("little", environment.getEndian()); + assertTrue(environment.getDebugger().toLowerCase().contains("gdb")); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbBreakpointsTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbBreakpointsTest.java new file mode 100644 index 0000000000..50568a8a75 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbBreakpointsTest.java @@ -0,0 +1,78 @@ +/* ### + * 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 agent.gdb.model; + +import static org.junit.Assert.assertNotNull; + +import java.util.List; + +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetStackFrame; +import ghidra.dbg.test.*; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.*; + +public abstract class AbstractModelForGdbBreakpointsTest + extends AbstractDebuggerModelBreakpointsTest implements ProvidesTargetViaLaunchSpecimen { + + @Override + public AbstractDebuggerModelTest getTest() { + return this; + } + + @Override + public DebuggerTestSpecimen getLaunchSpecimen() { + return GdbLinuxSpecimen.ECHO_HW; + } + + @Override + public List getExpectedBreakpointContainerPath(List targetPath) { + return PathUtils.parse("Breakpoints"); + } + + @Override + public TargetBreakpointKindSet getExpectedSupportedKinds() { + return TargetBreakpointKindSet.of( + TargetBreakpointKind.SW_EXECUTE, + TargetBreakpointKind.HW_EXECUTE, + TargetBreakpointKind.READ, + TargetBreakpointKind.WRITE); + } + + @Override + public AddressRange getSuitableRangeForBreakpoint(TargetObject target, + TargetBreakpointKind kind) throws Throwable { + TargetStackFrame frame = retry(() -> { + TargetStackFrame f = findAnyStackFrame(target.getPath()); + assertNotNull(f); + return f; + }, List.of(AssertionError.class)); + waitOn(frame.fetchAttributes()); + Address pc = frame.getProgramCounter().add(16); // Avoid "main" (temporary bp) + switch (kind) { + case SW_EXECUTE: + case HW_EXECUTE: + return new AddressRangeImpl(pc, pc); + case READ: + case WRITE: + return new AddressRangeImpl(pc, 4); + default: + throw new AssertionError(); + } + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbFactoryTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbFactoryTest.java new file mode 100644 index 0000000000..3ab88e37b6 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbFactoryTest.java @@ -0,0 +1,28 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.Map; + +import ghidra.dbg.test.AbstractDebuggerModelFactoryTest; + +public abstract class AbstractModelForGdbFactoryTest extends AbstractDebuggerModelFactoryTest { + @Override + protected Map getFailingFactoryOptions() { + return Map.ofEntries( + Map.entry("GDB launch command", "/THIS/SHOULD/NEVER/EXIST")); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbFrameFocusTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbFrameFocusTest.java new file mode 100644 index 0000000000..3db18d6090 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbFrameFocusTest.java @@ -0,0 +1,61 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.test.AbstractDebuggerModelFocusTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForGdbFrameFocusTest extends AbstractDebuggerModelFocusTest { + + DebuggerTestSpecimen getSpecimen() { + return GdbLinuxSpecimen.STACK; + } + + @Override + protected Set getFocusableThings() throws Throwable { + CompletableFuture frame0 = + m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1].Threads[1].Stack[0]")); + CompletableFuture frame1 = + m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1].Threads[1].Stack[1]")); + CompletableFuture frame2 = + m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1].Threads[1].Stack[2]")); + + DebuggerTestSpecimen specimen = getSpecimen(); + TargetLauncher launcher = findLauncher(); // root launcher should generate new inferiors + waitOn(launcher.launch(specimen.getLauncherArgs())); + TargetBreakpointSpecContainer breakpoints = findBreakpointSpecContainer(List.of()); + waitOn(breakpoints.placeBreakpoint("break_here", Set.of(TargetBreakpointKind.SW_EXECUTE))); + TargetResumable inf = + (TargetResumable) waitOn(m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1]"))); + waitOn(inf.resume()); + + return Set.of( + (TargetObject) waitOn(frame0), + (TargetObject) waitOn(frame1), + (TargetObject) waitOn(frame2)); + } + + @Override + protected List getExpectedDefaultFocus() { + return PathUtils.parse("Inferiors[1].Threads[1].Stack[0]"); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorAttacherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorAttacherTest.java new file mode 100644 index 0000000000..8799975ebc --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorAttacherTest.java @@ -0,0 +1,36 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.List; + +import ghidra.dbg.target.TargetAttacher; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForGdbInferiorAttacherTest + extends AbstractModelForGdbAttacherTest { + protected static final List INF1_PATH = PathUtils.parse("Inferiors[1]"); + + @Override + public List getExpectedAttacherPath() { + return INF1_PATH; + } + + @Override + public TargetAttacher findAttacher() throws Throwable { + return m.find(TargetAttacher.class, INF1_PATH); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorFocusTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorFocusTest.java new file mode 100644 index 0000000000..6fdb510edf --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorFocusTest.java @@ -0,0 +1,50 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import ghidra.dbg.target.TargetInterpreter; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.test.AbstractDebuggerModelFocusTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForGdbInferiorFocusTest extends AbstractDebuggerModelFocusTest { + + @Override + protected Set getFocusableThings() throws Throwable { + CompletableFuture inf1 = m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1]")); + CompletableFuture inf2 = m.getAddedWaiter().wait(PathUtils.parse("Inferiors[2]")); + CompletableFuture inf3 = m.getAddedWaiter().wait(PathUtils.parse("Inferiors[3]")); + + TargetInterpreter interpreter = findInterpreter(); + // The default +2 + waitOn(interpreter.execute("add-inferior")); + waitOn(interpreter.execute("add-inferior")); + + return Set.of( + (TargetObject) waitOn(inf1), + (TargetObject) waitOn(inf2), + (TargetObject) waitOn(inf3)); + } + + @Override + protected List getExpectedDefaultFocus() { + return PathUtils.parse("Inferiors[1]"); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorLauncherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorLauncherTest.java new file mode 100644 index 0000000000..6574b36370 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInferiorLauncherTest.java @@ -0,0 +1,36 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.List; + +import ghidra.dbg.target.TargetLauncher; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForGdbInferiorLauncherTest + extends AbstractModelForGdbLauncherTest { + protected static final List INF1_PATH = PathUtils.parse("Inferiors[1]"); + + @Override + public List getExpectedLauncherPath() { + return INF1_PATH; + } + + @Override + public TargetLauncher findLauncher() throws Throwable { + return m.find(TargetLauncher.class, INF1_PATH); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInterpreterTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInterpreterTest.java new file mode 100644 index 0000000000..addbd57d91 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbInterpreterTest.java @@ -0,0 +1,53 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.List; + +import ghidra.dbg.test.AbstractDebuggerModelInterpreterTest; + +public abstract class AbstractModelForGdbInterpreterTest + extends AbstractDebuggerModelInterpreterTest { + @Override + public List getExpectedInterpreterPath() { + return List.of(); + } + + @Override + protected String getEchoCommand(String msg) { + return "echo " + msg; + } + + @Override + protected String getQuitCommand() { + return "quit"; + } + + @Override + protected String getAttachCommand() { + return "attach " + dummy.pid; + } + + @Override + public DebuggerTestSpecimen getAttachSpecimen() { + return GdbLinuxSpecimen.DD; + } + + @Override + public DebuggerTestSpecimen getLaunchSpecimen() { + return GdbLinuxSpecimen.ECHO_HW; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbLauncherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbLauncherTest.java new file mode 100644 index 0000000000..d0fa3705b0 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbLauncherTest.java @@ -0,0 +1,56 @@ +/* ### + * 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 agent.gdb.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Map; + +import ghidra.dbg.target.TargetEnvironment; +import ghidra.dbg.target.TargetMethod.ParameterDescription; +import ghidra.dbg.target.TargetMethod.TargetParameterMap; +import ghidra.dbg.test.AbstractDebuggerModelLauncherTest; + +public abstract class AbstractModelForGdbLauncherTest extends AbstractDebuggerModelLauncherTest { + + @Override + public List getExpectedProcessesContainerPath() { + return List.of("Inferiors"); + } + + @Override + public DebuggerTestSpecimen getLaunchSpecimen() { + return GdbLinuxSpecimen.ECHO_HW; + } + + @Override + public TargetParameterMap getExpectedLauncherParameters() { + return TargetParameterMap.copyOf(Map.ofEntries( + Map.entry("args", ParameterDescription.create(String.class, "args", true, "", + "Command Line", "space-separated command-line arguments")))); + } + + @Override + public void assertEnvironment(TargetEnvironment environment) { + // TODO: This test won't always be on amd64 Linux, no? + assertEquals("i386:x86-64", environment.getArchitecture()); + assertEquals("GNU/Linux", environment.getOperatingSystem()); + assertEquals("little", environment.getEndian()); + assertTrue(environment.getDebugger().toLowerCase().contains("gdb")); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioAmd64RegistersTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioAmd64RegistersTest.java new file mode 100644 index 0000000000..37deffe6e5 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioAmd64RegistersTest.java @@ -0,0 +1,55 @@ +/* ### + * 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 agent.gdb.model; + +import static org.junit.Assert.assertEquals; + +import java.util.Map; + +import agent.gdb.model.impl.GdbModelTargetInferior; +import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.test.AbstractDebuggerModelScenarioRegistersTest; + +public abstract class AbstractModelForGdbScenarioAmd64RegistersTest + extends AbstractDebuggerModelScenarioRegistersTest { + + @Override + protected GdbLinuxSpecimen getSpecimen() { + return GdbLinuxSpecimen.REGISTERS; + } + + protected String getBinModuleName() { + return getSpecimen().getCommandLine(); + } + + @Override + protected String getBreakpointExpression() { + return "*break_here"; // Don't decode prologue, GDB! + } + + @Override + protected Map getRegisterWrites() { + // RDI is first parameter `val` of break_here(int val) + return Map.of("rdi", arr("0000000000000041")); + } + + @Override + protected void verifyExpectedEffect(TargetProcess process) throws Throwable { + long status = process.getTypedAttributeNowByName( + GdbModelTargetInferior.EXIT_CODE_ATTRIBUTE_NAME, Long.class, 0L); + assertEquals(0x41, status); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioCloneExitTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioCloneExitTest.java new file mode 100644 index 0000000000..f79968de3f --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioCloneExitTest.java @@ -0,0 +1,32 @@ +/* ### + * 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 agent.gdb.model; + +import ghidra.dbg.test.AbstractDebuggerModelScenarioCloneExitTest; + +public abstract class AbstractModelForGdbScenarioCloneExitTest + extends AbstractDebuggerModelScenarioCloneExitTest { + + @Override + protected DebuggerTestSpecimen getSpecimen() { + return GdbLinuxSpecimen.CLONE_EXIT; + } + + @Override + protected String getBreakpointExpression() { + return "work"; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioForkExitTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioForkExitTest.java new file mode 100644 index 0000000000..d842ecea83 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioForkExitTest.java @@ -0,0 +1,52 @@ +/* ### + * 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 agent.gdb.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import ghidra.dbg.target.*; +import ghidra.dbg.test.AbstractDebuggerModelScenarioForkExitTest; + +public abstract class AbstractModelForGdbScenarioForkExitTest + extends AbstractDebuggerModelScenarioForkExitTest { + + @Override + protected DebuggerTestSpecimen getSpecimen() { + return GdbLinuxSpecimen.FORK_EXIT; + } + + @Override + protected void preLaunch(TargetLauncher launcher) throws Throwable { + TargetInterpreter interpreter = findInterpreter(); + waitAcc(interpreter); + waitOn(interpreter.execute("set detach-on-fork off")); + } + + @Override + protected String getParentBreakpointExpression() { + return "func"; + } + + @Override + public void assertEnvironment(TargetEnvironment environment) { + // TODO: This test won't always be on amd64 Linux, no? + assertEquals("i386:x86-64", environment.getArchitecture()); + assertEquals("GNU/Linux", environment.getOperatingSystem()); + assertEquals("little", environment.getEndian()); + assertTrue(environment.getDebugger().toLowerCase().contains("gdb")); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioMemoryTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioMemoryTest.java new file mode 100644 index 0000000000..057d6d87ff --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioMemoryTest.java @@ -0,0 +1,73 @@ +/* ### + * 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 agent.gdb.model; + +import static org.junit.Assert.assertEquals; + +import java.util.List; +import java.util.Objects; + +import agent.gdb.model.impl.GdbModelTargetInferior; +import ghidra.dbg.target.*; +import ghidra.dbg.test.AbstractDebuggerModelScenarioMemoryTest; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.Address; + +public abstract class AbstractModelForGdbScenarioMemoryTest + extends AbstractDebuggerModelScenarioMemoryTest { + + @Override + protected GdbLinuxSpecimen getSpecimen() { + return GdbLinuxSpecimen.PRINT; + } + + protected String getBinModuleName() { + return getSpecimen().getCommandLine(); + } + + protected String getSymbolName() { + return "overwrite"; + } + + @Override + protected Address getAddressToWrite(TargetProcess process) throws Throwable { + List modulePath = PathUtils.extend(process.getPath(), + PathUtils.parse("Modules[" + getBinModuleName() + "]")); + TargetObject container = + Objects.requireNonNull(m.findContainer(TargetSymbol.class, modulePath)); + TargetSymbol symbol = + waitOn(container.fetchElements()).get(getSymbolName()).as(TargetSymbol.class); + return symbol.getValue(); + } + + @Override + protected byte[] getBytesToWrite() { + return "Speak".getBytes(); + } + + @Override + protected byte[] getExpectedBytes() { + return "Speak, World!".getBytes(); + } + + @Override + protected void verifyExpectedEffect(TargetProcess process) throws Throwable { + // TODO: Should (optional) exitCode be standardized on all models? + long status = process.getTypedAttributeNowByName( + GdbModelTargetInferior.EXIT_CODE_ATTRIBUTE_NAME, Long.class, 0L); + assertEquals('S', status); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioStackTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioStackTest.java new file mode 100644 index 0000000000..5de97578c1 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbScenarioStackTest.java @@ -0,0 +1,66 @@ +/* ### + * 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 agent.gdb.model; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.Map.Entry; + +import ghidra.dbg.target.*; +import ghidra.dbg.test.AbstractDebuggerModelScenarioStackTest; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.Address; + +public abstract class AbstractModelForGdbScenarioStackTest + extends AbstractDebuggerModelScenarioStackTest { + protected static List expectedSymbols = + List.of("break_here", "funcC", "funcB", "funcA"); + protected NavigableMap symbolsByAddress = new TreeMap<>(); + + @Override + protected GdbLinuxSpecimen getSpecimen() { + return GdbLinuxSpecimen.STACK; + } + + @Override + protected String getBreakpointExpression() { + return "break_here"; + } + + @Override + protected void postLaunch(TargetProcess process) throws Throwable { + TargetModuleContainer modules = m.find(TargetModuleContainer.class, process.getPath()); + // NB. NEVER is recommended resync mode for modules container + // It's not guaranteed to come before process is alive, though + TargetModule binMod = (TargetModule) waitOn(m.getAddedWaiter() + .wait(PathUtils.index(modules.getPath(), getSpecimen().getCommandLine()))); + + // NB. this heuristic assumes all function bodies are contiguous in memory + TargetSymbolNamespace symbols = m.find(TargetSymbolNamespace.class, binMod.getPath()); + // NB. ONCE is recommended resync mode for module symbols + for (Entry entry : waitOn(symbols.fetchElements()) + .entrySet()) { + symbolsByAddress.put(entry.getValue().as(TargetSymbol.class).getValue(), + entry.getKey()); + } + } + + @Override + protected void validateFramePC(int index, Address pc) { + assertEquals(expectedSymbols.get(index), symbolsByAddress.floorEntry(pc).getValue()); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbSessionAttacherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbSessionAttacherTest.java new file mode 100644 index 0000000000..ed8d82b91e --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbSessionAttacherTest.java @@ -0,0 +1,26 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.List; + +public abstract class AbstractModelForGdbSessionAttacherTest + extends AbstractModelForGdbAttacherTest { + @Override + public List getExpectedAttacherPath() { + return List.of(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbSessionLauncherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbSessionLauncherTest.java new file mode 100644 index 0000000000..729df3e316 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbSessionLauncherTest.java @@ -0,0 +1,27 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.List; + +public abstract class AbstractModelForGdbSessionLauncherTest + extends AbstractModelForGdbLauncherTest { + + @Override + public List getExpectedLauncherPath() { + return List.of(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbSteppableTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbSteppableTest.java new file mode 100644 index 0000000000..d0d4f62b61 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbSteppableTest.java @@ -0,0 +1,39 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.List; + +import ghidra.dbg.test.*; + +public abstract class AbstractModelForGdbSteppableTest extends AbstractDebuggerModelSteppableTest + implements ProvidesTargetViaLaunchSpecimen { + + @Override + public AbstractDebuggerModelTest getTest() { + return this; + } + + @Override + public List getExpectedSteppablePath(List threadPath) { + return threadPath; + } + + @Override + public DebuggerTestSpecimen getLaunchSpecimen() { + return GdbLinuxSpecimen.ECHO_HW; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbTest.java deleted file mode 100644 index 7c15c90d5e..0000000000 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbTest.java +++ /dev/null @@ -1,1027 +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 agent.gdb.model; - -import static ghidra.dbg.testutil.DummyProc.run; -import static ghidra.dbg.testutil.DummyProc.which; -import static ghidra.lifecycle.Unfinished.TODO; -import static org.junit.Assert.*; - -import java.util.*; -import java.util.Map.Entry; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; - -import org.junit.Ignore; -import org.junit.Test; - -import agent.gdb.manager.GdbManager; -import agent.gdb.model.EventSequenceListener.EventRecord; -import agent.gdb.model.impl.GdbModelTargetInferior; -import agent.gdb.model.impl.GdbModelTargetStackFrame; -import ghidra.async.AsyncReference; -import ghidra.async.AsyncUtils; -import ghidra.dbg.DebugModelConventions.AllRequiredAccess; -import ghidra.dbg.DebuggerObjectModel; -import ghidra.dbg.attributes.TargetObjectList; -import ghidra.dbg.error.DebuggerModelNoSuchPathException; -import ghidra.dbg.error.DebuggerModelTypeException; -import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet; -import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; -import ghidra.dbg.target.TargetConsole.Channel; -import ghidra.dbg.target.TargetEventScope.TargetEventType; -import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener; -import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; -import ghidra.dbg.target.TargetMethod.ParameterDescription; -import ghidra.dbg.target.TargetObject.TargetObjectListener; -import ghidra.dbg.target.TargetSteppable.TargetStepKind; -import ghidra.dbg.target.schema.TargetObjectSchema; -import ghidra.dbg.target.schema.XmlSchemaContext; -import ghidra.dbg.testutil.DummyProc; -import ghidra.dbg.util.*; -import ghidra.program.model.address.Address; -import ghidra.test.AbstractGhidraHeadlessIntegrationTest; -import ghidra.util.*; - -public abstract class AbstractModelForGdbTest - extends AbstractGhidraHeadlessIntegrationTest implements DebuggerModelTestUtils { - protected static final Map AMD64_TEST_REG_VALUES = Map.of( // - "rax", NumericUtilities.convertStringToBytes("0123456789abcdef"), // - "ymm0", NumericUtilities.convertStringToBytes("" + // - "0123456789abcdef" + - "fedcba9876543210" + - "0011223344556677" + - "8899aabbccddeeff")); - protected static final long TIMEOUT_MILLISECONDS = - SystemUtilities.isInTestingBatchMode() ? 5000 : Long.MAX_VALUE; - - protected static Map hexlify(Map map) { - return map.entrySet() - .stream() - .collect(Collectors.toMap(Entry::getKey, - e -> NumericUtilities.convertBytesToString(e.getValue()))); - } - - public interface ModelHost extends AutoCloseable { - DebuggerObjectModel getModel(); - - CompletableFuture init(); - } - - protected abstract ModelHost modelHost(String gdbCmd) throws Exception; - - protected ModelHost modelHost() throws Exception { - return modelHost(GdbManager.DEFAULT_GDB_CMD); - } - - protected static class CatchOffThread implements AutoCloseable { - protected Throwable caught; - - void catching(Runnable runnable) { - try { - runnable.run(); - } - catch (Throwable e) { - caught = e; - } - } - - @Override - public void close() throws Exception { - if (caught != null) { - throw new AssertionError("Off-thread exception", caught); - } - } - } - - protected void init(ModelHost m) throws Throwable { - waitOn(m.init()); - } - - protected static boolean isTerminationError(Throwable ex) { - ex = AsyncUtils.unwrapThrowable(ex); - // TODO: Marshall this exception better via GADP - if (ex instanceof RuntimeException || ex instanceof IllegalStateException) { - if (ex.getMessage().contains("GDB is terminating") || - ex.getMessage().contains("Unknown: Unknown server-side error")) { - return true; - } - } - if (ex instanceof InterruptedException) { - return true; // TODO: This is way too broad - } - return false; - } - - protected static T ignoreTermination(Throwable t) { - Throwable ex = AsyncUtils.unwrapThrowable(t); - // TODO: Should it be an error if there's no exception here? - // As long as state is correct and root is invalid after? - if (isTerminationError(ex)) { - return null; // pass - } - throw new AssertionError(t); - } - - @Test - public void testInitFinish() throws Throwable { - try (ModelHost m = modelHost()) { - init(m); - } - } - - @Test - @Ignore("abstract test case is failing on off-thread exceptions") - public void testBadGdbCmd() throws Throwable { - try (ModelHost m = modelHost("/usr/bin/this_shouldnt_exit")) { - init(m); - // TODO: assert model state is TERMINATE or something - } - catch (Exception ex) { - ignoreTermination(ex); - } - } - - @Test - public void testNonExistentPathGivesNull() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - TargetObject obj = waitOn(model.fetchModelObject("Doesn't exist")); - assertNull(obj); - } - } - - @Test - public void testSessionLaunch() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferiors (before launch)..."); - Map inferiors = - waitOn(model.fetchObjectElements(List.of("Inferiors"))); - Msg.debug(this, "Inferiors before: " + inferiors); - assertEquals(1, inferiors.size()); - Msg.debug(this, "Finding TargetLauncher..."); - TargetLauncher launcher = suitable(TargetLauncher.class, root); - Msg.debug(this, "Launching..."); - waitOn(launcher.launch( - Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, "/bin/echo Hello, World!"))); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferiors (after launch)..."); - inferiors = waitOn(model.fetchObjectElements(List.of("Inferiors"))); - Msg.debug(this, "Inferiors after: " + inferiors); - assertEquals(2, inferiors.size()); - } - } - - @Test - public void testInferiorLaunchParameters() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess rootAccess = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(rootAccess); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject(List.of("Inferiors", "[1]"))); - Msg.debug(this, "Tracking inferior access..."); - AllRequiredAccess infAccess = access(inferior); - Msg.debug(this, "Waiting for inferior access..."); - waitAcc(infAccess); - Msg.debug(this, "Reflecting parameters"); - TargetLauncher launcher = inferior.as(TargetLauncher.class); - for (ParameterDescription param : launcher.getParameters().values()) { - Msg.info(this, " Parameter: " + param); - } - waitOn(launcher.launch(Map.of("args", "/bin/echo Hello, World!"))); - - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(rootAccess); - Msg.debug(this, "Getting Inferiors (after launch)..."); - Map inferiors = - waitOn(model.fetchObjectElements(List.of("Inferiors"))); - Msg.debug(this, "Inferiors after: " + inferiors); - assertEquals(1, inferiors.size()); - } - } - - @Test - public void testInferiorLaunch() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess rootAccess = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(rootAccess); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject(List.of("Inferiors", "[1]"))); - Msg.debug(this, "Tracking inferior access..."); - AllRequiredAccess infAccess = access(inferior); - Msg.debug(this, "Waiting for inferior access..."); - waitAcc(infAccess); - Msg.debug(this, "Launching..."); - launch(inferior, - Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, "/bin/echo Hello, World!")); - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(rootAccess); - Msg.debug(this, "Getting Inferiors (after launch)..."); - Map inferiors = - waitOn(model.fetchObjectElements(List.of("Inferiors"))); - Msg.debug(this, "Inferiors after: " + inferiors); - assertEquals(1, inferiors.size()); - } - } - - @Test - public void testListProcesses() throws Throwable { - try (DummyProc dd = run("dd"); ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Map available = - waitOn(model.fetchObjectElements(List.of("Available"))); - assertTrue(available.containsKey(Long.toString(dd.pid))); - } - } - - @Test - public void testSessionAttachKill() throws Throwable { - try (DummyProc dd = run("dd"); ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferiors (before attach)..."); - Map inferiors = - waitOn(model.fetchObjectElements(List.of("Inferiors"))); - Msg.debug(this, "Inferiors before: " + inferiors); - assertEquals(1, inferiors.size()); - Msg.debug(this, "Finding TargetAttacher..."); - TargetAttacher attacher = suitable(TargetAttacher.class, root); - Msg.debug(this, " Got TargetAttacher: " + attacher); - TargetAttachable attachable = - waitOn(model.fetchModelObject("Available", "[" + dd.pid + "]")) - .as(TargetAttachable.class); - Msg.debug(this, " Got Attachable: " + attachable); - Msg.debug(this, "Attaching..."); - waitOn(attacher.attach(attachable)); - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(access); - Msg.debug(this, "Getting Inferiors (after attach)..."); - inferiors = waitOn(model.fetchObjectElements(List.of("Inferiors"))); - Msg.debug(this, "Inferiors after: " + inferiors); - assertEquals(2, inferiors.size()); - Msg.debug(this, "Killing..."); - TargetKillable killable = inferiors.get("2").as(TargetKillable.class); - waitOn(killable.kill()); - } - } - - @Test - public void testInferiorAttachKill() throws Throwable { - try (DummyProc dd = run("dd"); ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject(List.of("Inferiors", "[1]"))); - TargetAttacher attacher = inferior.as(TargetAttacher.class); - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(access); - Msg.debug(this, "Attaching..."); - waitOn(attacher.attach(dd.pid)); - Msg.debug(this, "Waiting for session access (again, again)..."); - waitAcc(access); - Msg.debug(this, "Getting Inferiors (after attach)..."); - Map inferiors = - waitOn(model.fetchObjectElements(List.of("Inferiors"))); - Msg.debug(this, "Inferiors after: " + inferiors); - assertEquals(1, inferiors.size()); - Msg.debug(this, "Killing..."); - TargetObject attached = inferiors.get("1"); - TargetKillable killable = attached.as(TargetKillable.class); - waitOn(killable.kill()); - } - } - - @Test - public void testLaunchContExit() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject("Inferiors", "[1]")); - Msg.debug(this, "Launching..."); - launch(inferior, Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, "echo Hello, World!")); - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(access); - Msg.debug(this, "Resuming..."); - resume(inferior); - Msg.debug(this, "Waiting for session access (after resume)..."); - waitAcc(access); - } - } - - @Test(expected = DebuggerModelNoSuchPathException.class) - public void testAttachNoObjectErr() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject("Inferiors", "[1]")); - Msg.debug(this, "Attaching to bogus path..."); - TargetAttacher attacher = inferior.as(TargetAttacher.class); - TODO(); - } - } - - @Test(expected = DebuggerModelTypeException.class) - public void testAttachNonAttachableErr() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject("Inferiors", "[1]")); - Msg.debug(this, "Attaching to bogus path..."); - TargetAttacher attacher = inferior.as(TargetAttacher.class); - // NOTE: Technically, the "as" call is causing it here. - waitOn(attacher.attach(model.getModelObject("Available").as(TargetAttachable.class))); - fail("Exception expected"); - } - } - - @Test - @Ignore("for developer workstation") - public void stressTestExecute() throws Throwable { - for (int i = 0; i < 100; i++) { - testExecute(); - } - } - - @Test - public void testExecute() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - - AsyncReference lastOut = new AsyncReference<>(); - AllTargetObjectListenerAdapter l = new AllTargetObjectListenerAdapter() { - @Override - public void consoleOutput(TargetObject interpreter, Channel channel, - byte[] out) { - Msg.debug(this, "Got " + channel + " output: " + out); - lastOut.set(new String(out), null); - } - }; - - init(m); - Msg.debug(this, "Getting root object..."); - TargetObject root = root(model); - root.addListener(l); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Running command..."); - cli(root, "echo test"); - Msg.debug(this, "Waiting for expected output..."); - waitOn(lastOut.waitValue("test")); - } - } - - @Test - @Ignore("Abstract test case is failing on off-thread exceptions") - public void testExecuteQuit() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Quitting..."); - cli(root, "quit"); - // TODO: Assert model state is TERMINATED, or something - // (pending merging of DebuggerClient stuff into DebuggerObjectModel - // For now, best option is to assert root is invalid - assertFalse(root.isValid()); - } - catch (Throwable ex) { - ignoreTermination(ex); - } - } - - @Test - public void testExecuteCapture() throws Throwable { - try (ModelHost m = modelHost(); CatchOffThread offThread = new CatchOffThread()) { - DebuggerObjectModel model = m.getModel(); - - AllTargetObjectListenerAdapter l = new AllTargetObjectListenerAdapter() { - @Override - public void consoleOutput(TargetObject interpreter, Channel channel, - String out) { - Msg.debug(this, "Got " + channel + " output: " + out); - if (!out.contains("test")) { - return; - } - offThread.catching(() -> fail("Unexpected output:" + out)); - } - }; - - init(m); - Msg.debug(this, "Getting root object..."); - TargetObject root = root(model); - root.addListener(l); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Running command with capture..."); - String out = captureCli(root, "echo test"); - Msg.debug(this, "Captured: " + out); - assertEquals("test", out); - } - } - - @Test - public void testGetBreakKinds() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object..."); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Finding breakpoint container..."); - TargetBreakpointContainer breaks = suitable(TargetBreakpointContainer.class, root); - Msg.debug(this, "Got: " + breaks); - TargetBreakpointKindSet kinds = breaks.getSupportedBreakpointKinds(); - Msg.debug(this, "Supports: " + kinds); - assertEquals(4, kinds.size()); - } - } - - @Test - public void testPlaceBreakpoint() throws Throwable { - String specimen = which("expFork"); - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object..."); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Setting file to " + specimen + "..."); - cli(root, "file " + specimen); - Msg.debug(this, "Finding breakpoint container..."); - TargetBreakpointContainer breaks = suitable(TargetBreakpointContainer.class, root); - Msg.debug(this, "Placing breakpoint..."); - waitOn(breaks.placeBreakpoint("func", Set.of(TargetBreakpointKind.SOFTWARE))); - Msg.debug(this, "Getting breakpoint specs..."); - Map specs = waitOn(breaks.fetchElements()); - Msg.debug(this, "Got specs: " + specs); - assertEquals(1, specs.size()); - TargetBreakpointSpec spec = specs.get("1").as(TargetBreakpointSpec.class); - Collection ls = waitOn(spec.getLocations()); - Msg.debug(this, "Got locations: " + ls); - assertEquals(1, ls.size()); - TargetBreakpointLocation loc = ls.iterator().next(); - Address addr = loc.getAddress(); - Msg.debug(this, "Got address: " + addr); - TargetObjectList list = loc.getAffects(); - Msg.debug(this, "Got affects: " + list); - assertEquals(1, list.size()); - } - } - - @Test - public void testPlaceWatchpoint() throws Throwable { - String specimen = which("expTypes"); - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object..."); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Setting file to " + specimen + "..."); - cli(root, "file " + specimen); - Msg.debug(this, "Finding breakpoint container..."); - TargetBreakpointContainer breaks = suitable(TargetBreakpointContainer.class, root); - Msg.debug(this, "Placing breakpoint..."); - waitOn(breaks.placeBreakpoint("int_var", - Set.of(TargetBreakpointKind.READ, TargetBreakpointKind.WRITE))); - Msg.debug(this, "Getting breakpoint specs..."); - Map specs = waitOn(breaks.fetchElements()); - Msg.debug(this, "Got specs: " + specs); - assertEquals(1, specs.size()); - TargetBreakpointSpec spec = specs.get("1").as(TargetBreakpointSpec.class); - Collection ls = waitOn(spec.getLocations()); - Msg.debug(this, "Got locations: " + ls); - assertEquals(1, ls.size()); - TargetBreakpointLocation loc = ls.iterator().next(); - Address addr = loc.getAddress(); - Msg.debug(this, "Got address: " + addr); - assertNotNull(addr); - TargetObjectList list = loc.getAffects(); - Msg.debug(this, "Got affects: " + list); - assertEquals(1, list.size()); - } - } - - @Test - public void testExpFork() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - Set

locAddresses = new HashSet<>(); - Set locAffecteds = new HashSet<>(); - - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject("Inferiors", "[1]")); - Msg.debug(this, "Launching..."); - launch(inferior, Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, which("expFork"))); - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(access); - Msg.debug(this, "Setting to stay attached to forks"); - cli(root, "set detach-on-fork off"); - TargetBreakpointContainer breaks = - suitable(TargetBreakpointContainer.class, inferior); - Msg.debug(this, "Setting break on func"); - waitOn(breaks.placeBreakpoint("func", Set.of(TargetBreakpointKind.SOFTWARE))); - Msg.debug(this, "Resuming execution (first time)"); - resume(inferior); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Map inferiors = - waitOn(model.fetchObjectElements("Inferiors")); - Msg.debug(this, "After first break, inferiors are: " + inferiors); - assertEquals(2, inferiors.size()); - // NOTE: Breakpoint 1 was the temporary one on 'main' - Map ls = - waitOn(model.fetchObjectElements("Breakpoints", "[2]")); - Msg.debug(this, "Locations: " + ls); - assertEquals(2, ls.size()); - for (TargetObject obj : ls.values()) { - TargetBreakpointLocation loc = obj.as(TargetBreakpointLocation.class); - locAddresses.add(loc.getAddress()); - locAffecteds.addAll(loc.getAffects()); - } - Msg.debug(this, "Addresses: " + locAddresses + ", affected: " + locAffecteds); - assertEquals(1, locAddresses.size()); - assertEquals(Set.of(List.of("Inferiors", "[1]"), List.of("Inferiors", "[2]")), - locAffecteds.stream().map(TargetObject::getPath).collect(Collectors.toSet())); - } - } - - @Test - @Ignore("Known issue") - public void testExpForkWithListeners() throws Throwable { - ElementTrackingListener infListener = - new ElementTrackingListener<>(TargetObject.class); - ElementTrackingListener bkListener = - new ElementTrackingListener<>(TargetBreakpointSpec.class); - ElementTrackingListener blListener = - new ElementTrackingListener<>(TargetBreakpointLocation.class); - EventSequenceListener evtListener = new EventSequenceListener(); - - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - Set
ebAddresses = new HashSet<>(); - Set ebAffecteds = new HashSet<>(); - - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session events and access..."); - root.addListener(evtListener); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject infCont = waitOn(model.fetchModelObject("Inferiors")); - Msg.debug(this, "Installing listener for inferiors"); - infCont.addListener(infListener); - Msg.debug(this, "Getting inferiors"); - Map inferiors = waitOn(infCont.fetchElements()); - infListener.putAll(inferiors); - Msg.debug(this, "Launching..."); - launch(infListener.elements.get("1"), - Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, which("expFork"))); - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(access); - Msg.debug(this, "Setting to stay attached to forks"); - cli(root, "set detach-on-fork off"); - TargetBreakpointContainer bkCont = - suitable(TargetBreakpointContainer.class, infCont); - Msg.debug(this, "Installing listener for breakpoints"); - bkCont.addListener(bkListener); - Msg.debug(this, "Getting breakpoints"); - Map bkElems = waitOn(bkCont.fetchElements()); - bkListener.putAll(bkElems); - Msg.debug(this, "Setting break on func"); - waitOn(bkCont.placeBreakpoint("func", Set.of(TargetBreakpointKind.SOFTWARE))); - Msg.debug(this, "Breakpoint elements: " + bkListener.elements); - TargetBreakpointSpec bk2 = - waitOn(bkListener.refElement("2").waitUntil(t -> t != null)); - Msg.debug(this, "Installing listener on Breakpoint 2"); - bk2.addListener(blListener); - Msg.debug(this, "Getting locations for 2"); - Map bk2ls = waitOn(bk2.fetchElements()); - blListener.putAll(bk2ls); - Msg.debug(this, "Resuming execution (first time)"); - resume(infListener.elements.get("1")); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "After first break, inferiors are: " + infListener.elements); - waitOn(infListener.size.waitValue(2)); - assertEquals(2, infListener.elements.size()); - waitOn(blListener.size.waitValue(2)); - Msg.debug(this, "Locations: " + blListener.elements); - assertEquals(2, blListener.elements.size()); - for (TargetObject obj : blListener.elements.values()) { - TargetBreakpointLocation eb = obj.as(TargetBreakpointLocation.class); - ebAddresses.add(eb.getAddress()); - ebAffecteds.addAll(eb.getAffects()); - } - Msg.debug(this, "Addresses: " + ebAddresses + ", affected: " + ebAffecteds); - assertEquals(1, ebAddresses.size()); - assertEquals(Set.of(List.of("Inferiors", "[1]"), List.of("Inferiors", "[2]")), - ebAffecteds.stream().map(TargetObject::getPath).collect(Collectors.toSet())); - - // Getting more precise than this could become fragile, as library paths vary - TargetEventType lastType = null; - List typesNoRepeat = new ArrayList<>(); - for (EventRecord rec : evtListener.events) { - if (rec.type == TargetEventType.RUNNING) { - continue; - } - if (rec.type == lastType) { - continue; - } - lastType = rec.type; - typesNoRepeat.add(lastType); - } - assertEquals(List.of( - TargetEventType.PROCESS_CREATED, - TargetEventType.THREAD_CREATED, - TargetEventType.MODULE_LOADED, - TargetEventType.BREAKPOINT_HIT, - TargetEventType.PROCESS_CREATED, - TargetEventType.THREAD_CREATED, - TargetEventType.MODULE_LOADED // - ), typesNoRepeat); - } - } - - @Test - public void testExpClone() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject("Inferiors", "[1]")); - Msg.debug(this, "Launching..."); - launch(inferior, - Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, which("expCloneExit"))); - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(access); - TargetBreakpointContainer breaks = - suitable(TargetBreakpointContainer.class, inferior); - Msg.debug(this, "Setting break on work"); - waitOn(breaks.placeBreakpoint("work", Set.of(TargetBreakpointKind.SOFTWARE))); - Msg.debug(this, "Resuming execution (first time)"); - resume(inferior); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Map threads = - waitOn(model.fetchObjectElements("Inferiors", "[1]", "Threads")); - Msg.debug(this, "After first break, threads are: " + threads); - assertEquals(2, threads.size()); - } - } - - @Test - public void testExpWrite() throws Throwable { - String expPrint = which("expPrint"); - final String toWrite = "Speak"; - final String expected = "Speak, World!"; - - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject("Inferiors", "[1]")); - Msg.debug(this, "Launching..."); - launch(inferior, Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, expPrint)); - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(access); - Msg.debug(this, "Getting symbol to overwrite"); - TargetSymbol overwrite = waitOn(inferior.fetchSuccessor( - "Modules", "[" + expPrint + "]", "Symbols", "[overwrite]")).as(TargetSymbol.class); - Msg.debug(this, "Symbol 'overwrite' is at addr: " + overwrite.getValue()); - Msg.debug(this, "Getting Memory"); - TargetMemory memory = (TargetMemory) waitOn(inferior.fetchSuccessor("Memory")); - Msg.debug(this, "Writing"); - waitOn(memory.writeMemory(overwrite.getValue(), toWrite.getBytes())); - Msg.debug(this, "Getting thread (for stepping)"); - TargetObject thread = waitOn(inferior.fetchSuccessor("Threads", "[1]")); - Msg.debug(this, "Got: " + thread); - Msg.debug(this, "Stepping to clear caches..."); - step(thread, TargetStepKind.INTO); - Msg.debug(this, "Waiting for access..."); - waitAcc(access); - Msg.debug(this, "Reading back..."); - byte[] data = - waitOn(memory.readMemory(overwrite.getValue(), expected.getBytes().length)); - Msg.debug(this, "Read: " + new String(data)); - assertArrayEquals(expected.getBytes(), data); - resume(inferior); - Msg.debug(this, "Waiting for access (i.e., exit)..."); - waitAcc(access); - Msg.debug(this, "Getting exit code..."); - - long status = inferior.getTypedAttributeNowByName( - GdbModelTargetInferior.EXIT_CODE_ATTRIBUTE_NAME, Long.class, 0L); - assertEquals(toWrite.getBytes()[0], status); - } - } - - @Test - public void testStack() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject("Inferiors", "[1]")); - Msg.debug(this, "Launching..."); - launch(inferior, Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, "echo Hello, World!")); - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(access); - Msg.debug(this, "Finding breakpoint container..."); - TargetBreakpointContainer breaks = suitable(TargetBreakpointContainer.class, root); - Msg.debug(this, "Placing breakpoint..."); - waitOn(breaks.placeBreakpoint("write", Set.of(TargetBreakpointKind.SOFTWARE))); - Msg.debug(this, "Resuming..."); - resume(inferior); - Msg.debug(this, "Waiting for session access (after resume)..."); - waitAcc(access); - Map frames = - waitOn(inferior.fetchSubElements("Threads", "[1]", "Stack")); - Msg.debug(this, "Got stack:"); - for (Map.Entry ent : frames.entrySet()) { - TargetStackFrame frame = ent.getValue().as(TargetStackFrame.class); - Msg.debug(this, ent.getKey() + ": " + frame.getProgramCounter()); - } - assertEquals("write", frames.get("0") - .getTypedAttributeNowByName(GdbModelTargetStackFrame.FUNC_ATTRIBUTE_NAME, - String.class, null)); - assertEquals("main", frames.get("" + (frames.size() - 1)) - .getTypedAttributeNowByName(GdbModelTargetStackFrame.FUNC_ATTRIBUTE_NAME, - String.class, null)); - } - } - - @Test - public void testRegisters() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - Set descs = new LinkedHashSet<>(); - - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject("Inferiors", "[1]")); - launch(inferior, Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, "echo Hello, World!")); - Msg.debug(this, "Waiting for session access (again)..."); - waitAcc(access); - TargetRegisterBank bank = - waitOn(inferior.fetchSuccessor("Threads", "[1]", "Stack", "[0]")) - .as(TargetRegisterBank.class); - Msg.debug(this, "Got bank: " + bank); - TargetRegisterContainer cont = bank.getDescriptions(); - Msg.debug(this, "Register descriptions: " + cont); - descs.addAll(waitOn(cont.getRegisters())); - Msg.debug(this, "Elements: "); - for (TargetRegister reg : descs) { - Msg.debug(this, " " + reg.getIndex() + ": " + reg.getBitLength()); - } - Map data = waitOn(bank.readRegisters(descs)); - Msg.debug(this, "Values: "); - for (Map.Entry ent : data.entrySet()) { - Msg.debug(this, " " + ent.getKey() + " = " + - NumericUtilities.convertBytesToString(ent.getValue())); - } - // TODO: Implement Environment, and port these tests - Msg.debug(this, "Writing two registers, general and vector"); - waitOn(bank.writeRegistersNamed(AMD64_TEST_REG_VALUES)); - Msg.debug(this, "Flushing cache"); - waitOn(bank.invalidateCaches()); - Msg.debug(this, "Re-reading values"); - // NOTE: Can't reliably step here since rax may be referenced by the instruction - data = waitOn(bank.readRegistersNamed(AMD64_TEST_REG_VALUES.keySet())); - assertEquals(hexlify(AMD64_TEST_REG_VALUES), hexlify(data)); - } - } - - @Test - public void testFocusInferiors() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - - AsyncReference focus = new AsyncReference<>(); - AsyncReference inferiorCount = new AsyncReference<>(); - - TargetFocusScopeListener focusListener = new TargetFocusScopeListener() { - @Override - public void focusChanged(TargetFocusScope object, TargetObject focused) { - focus.set(focused, null); - } - }; - - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - root.addListener(focusListener); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting inferiors container"); - TargetObject infCont = waitOn(model.fetchModelObject("Inferiors")); - - TargetObjectListener infListener = new TargetObjectListener() { - @Override - public void elementsChanged(TargetObject parent, Collection removed, - Map added) { - inferiorCount.set(infCont.getCachedElements().size(), null); - } - }; - - infCont.addListener(infListener); - Msg.debug(this, "Creating another inferior"); - cli(root, "add-inferior"); - waitOn(inferiorCount.waitValue(2)); - assertSame(model.getModelObject("Inferiors", "[1]"), getFocus(root)); - focus(root, model.getModelObject("Inferiors", "[2]")); - assertSame(model.getModelObject("Inferiors", "[2]"), getFocus(root)); - } - } - - @Test - public void testThreadFocusOnLaunch() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - - Deque focusSeq = new LinkedList<>(); - AsyncReference focusSeqSize = new AsyncReference<>(); - - TargetFocusScopeListener focusListener = new TargetFocusScopeListener() { - @Override - public void focusChanged(TargetFocusScope object, TargetObject focused) { - Msg.debug(this, "Focused: " + focused); - if (focused instanceof TargetProcess) { - return; - } - synchronized (focusSeq) { - focusSeq.add(focused); - focusSeqSize.set(focusSeq.size(), null); - } - } - }; - - init(m); - Msg.debug(this, "Getting root object"); - TargetObject root = root(model); - root.addListener(focusListener); - Msg.debug(this, "Tracking session access..."); - AllRequiredAccess access = access(root); - Msg.debug(this, "Waiting for session access..."); - waitAcc(access); - Msg.debug(this, "Getting Inferior 1..."); - TargetObject inferior = waitOn(model.fetchModelObject(List.of("Inferiors", "[1]"))); - Msg.debug(this, "Launching"); - launch(inferior, - Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, "/bin/echo Hello, World!")); - waitOn(focusSeqSize.waitValue(1)); - assertEquals(model.getModelObject(PathUtils.parse("Inferiors[1].Threads[1].Stack[0]")), - focusSeq.peekLast()); - } - } - - @Test - public void testSerializeSchema() throws Throwable { - try (ModelHost m = modelHost()) { - DebuggerObjectModel model = m.getModel(); - init(m); - - TargetObjectSchema rootSchema = model.getRootSchema(); - String serialized = XmlSchemaContext.serialize(rootSchema.getContext()); - System.out.println(serialized); - - assertEquals("Session", rootSchema.getName().toString()); - } - } -} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbThreadFocusTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbThreadFocusTest.java new file mode 100644 index 0000000000..e8bd46906e --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/AbstractModelForGdbThreadFocusTest.java @@ -0,0 +1,54 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import ghidra.dbg.target.TargetLauncher; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.test.AbstractDebuggerModelFocusTest; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractModelForGdbThreadFocusTest extends AbstractDebuggerModelFocusTest { + + protected DebuggerTestSpecimen getSpecimen() { + return GdbLinuxSpecimen.ECHO_HW; + } + + @Override + protected Set getFocusableThings() throws Throwable { + CompletableFuture inf1 = + m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1].Threads[1]")); + CompletableFuture inf2 = + m.getAddedWaiter().wait(PathUtils.parse("Inferiors[2].Threads[2]")); + + DebuggerTestSpecimen specimen = getSpecimen(); + TargetLauncher launcher = findLauncher(); // root launcher should generate new inferiors + waitOn(launcher.launch(specimen.getLauncherArgs())); + waitOn(launcher.launch(specimen.getLauncherArgs())); + + return Set.of( + (TargetObject) waitOn(inf1), + (TargetObject) waitOn(inf2)); + } + + @Override + protected List getExpectedDefaultFocus() { + return PathUtils.parse("Inferiors[2].Threads[2]"); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GadpForGdbTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GadpForGdbTest.java deleted file mode 100644 index 4ed46328e2..0000000000 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GadpForGdbTest.java +++ /dev/null @@ -1,126 +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 agent.gdb.model; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.nio.channels.AsynchronousSocketChannel; -import java.util.concurrent.CompletableFuture; - -import org.junit.Ignore; -import org.junit.Test; - -import agent.gdb.gadp.GdbGadpServer; -import agent.gdb.manager.GdbManager; -import ghidra.async.AsyncUtils; -import ghidra.async.TypeSpec; -import ghidra.dbg.DebuggerObjectModel; -import ghidra.dbg.gadp.client.GadpClient; -import ghidra.dbg.gadp.client.GadpClientTestHelper; -import ghidra.dbg.gadp.protocol.Gadp; -import ghidra.util.Msg; - -@Ignore("Need compatible GDB version for CI") -public class GadpForGdbTest extends AbstractModelForGdbTest { - - class GdbGadpModelHost implements ModelHost { - final GdbGadpServer server; - final SocketAddress addr; - final AsynchronousSocketChannel socket; - final GadpClient client; - - GdbGadpModelHost(String gdbCmd) throws Exception { - server = GdbGadpServer.newInstance(new InetSocketAddress("localhost", 0)); - server.startGDB(gdbCmd, new String[] {}) - /*.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)*/; - addr = server.getLocalAddress(); - - server.setExitOnClosed(false); - - socket = AsynchronousSocketChannel.open(); - client = new GadpClient("Test", socket); - } - - @Override - public CompletableFuture init() { - Msg.debug(this, "Connecting..."); - return AsyncUtils.completable(TypeSpec.VOID, socket::connect, addr).thenCompose(__ -> { - Msg.debug(this, "Negotiating..."); - return client.connect(); - }); - } - - @Override - public DebuggerObjectModel getModel() { - return client; - } - - @Override - public void close() throws Exception { - // Not too eww - Msg.debug(this, "Disconnecting..."); - try { - waitOn(client.close()); - } - catch (Exception e) { - throw e; - } - catch (Throwable e) { - throw new AssertionError(e); - } - server.terminate(); - } - } - - @Override - protected GdbGadpModelHost modelHost() throws Exception { - return modelHost(GdbManager.DEFAULT_GDB_CMD); - } - - @Override - protected GdbGadpModelHost modelHost(String gdbCmd) throws Exception { - return new GdbGadpModelHost(gdbCmd); - } - - @Test - public void testBadRequest() throws Throwable { - try (GdbGadpModelHost m = modelHost()) { - init(m); - Msg.debug(this, "Sending bogus message..."); - waitOn( - GadpClientTestHelper.sendChecked(m.client, Gadp.ErrorRequest.newBuilder(), null)); - fail("Exception expected"); - } - catch (AssertionError e) { - assertEquals( - "Client implementation sent an invalid request: " + - "EC_BAD_REQUEST: Unrecognized request: ERROR_REQUEST", - e.getMessage()); - } - } - - @Test - public void testPing() throws Throwable { - try (GdbGadpModelHost m = modelHost()) { - waitOn(m.init()); - Msg.debug(this, "Pinging..."); - waitOn(m.client.ping("Hello, Ghidra Async Debugging!")); - } - } -} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GdbLinuxSpecimen.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GdbLinuxSpecimen.java new file mode 100644 index 0000000000..3965ce71fe --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/GdbLinuxSpecimen.java @@ -0,0 +1,115 @@ +/* ### + * 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 agent.gdb.model; + +import java.util.*; + +import agent.gdb.model.impl.GdbModelTargetAttachable; +import ghidra.dbg.target.TargetAttachable; +import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; +import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.test.AbstractDebuggerModelTest; +import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen; +import ghidra.dbg.testutil.DebuggerModelTestUtils; +import ghidra.dbg.testutil.DummyProc; +import ghidra.dbg.util.ShellUtils; + +public enum GdbLinuxSpecimen implements DebuggerTestSpecimen, DebuggerModelTestUtils { + ECHO_HW { + @Override + String getCommandLine() { + return "/bin/echo Hello, World!"; + } + }, + DD { + @Override + String getCommandLine() { + return "dd"; + } + }, + FORK_EXIT { + @Override + String getCommandLine() { + return DummyProc.which("expFork"); + } + }, + + CLONE_EXIT { + @Override + String getCommandLine() { + return DummyProc.which("expCloneExit"); + } + }, + PRINT { + @Override + String getCommandLine() { + return DummyProc.which("expPrint"); + } + }, + REGISTERS { + @Override + String getCommandLine() { + return DummyProc.which("expRegisters"); + } + }, + STACK { + @Override + String getCommandLine() { + return DummyProc.which("expStack"); + } + }; + + abstract String getCommandLine(); + + @Override + public DummyProc runDummy() throws Throwable { + return DummyProc.run(ShellUtils.parseArgs(getCommandLine()).toArray(String[]::new)); + } + + @Override + public Map getLauncherArgs() { + return Map.ofEntries( + Map.entry(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, getCommandLine())); + } + + @Override + public List getLaunchScript() { + List script = new ArrayList<>(); + List parsed = ShellUtils.parseArgs(getCommandLine()); + if (parsed.size() > 1) { + script.add("set args " + ShellUtils.generateLine(parsed.subList(1, parsed.size()))); + } + script.add("file " + ShellUtils.generateLine(parsed.subList(0, 1))); + script.add("start"); + return script; + } + + @Override + public boolean isRunningIn(TargetProcess process, AbstractDebuggerModelTest test) + throws Throwable { + waitOn(process.fetchAttributes()); + return process.getDisplay().contains(ShellUtils.parseArgs(getCommandLine()).get(0)); + } + + @Override + public boolean isAttachable(DummyProc dummy, TargetAttachable attachable, + AbstractDebuggerModelTest test) throws Throwable { + waitOn(attachable.fetchAttributes()); + long pid = attachable.getTypedAttributeNowByName( + GdbModelTargetAttachable.PID_ATTRIBUTE_NAME, Long.class, -1L); + return pid == dummy.pid; + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ModelForGdbTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ModelForGdbTest.java deleted file mode 100644 index f780b98f08..0000000000 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ModelForGdbTest.java +++ /dev/null @@ -1,59 +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 agent.gdb.model; - -import java.util.concurrent.CompletableFuture; - -import org.junit.Ignore; - -import agent.gdb.model.impl.GdbModelImpl; -import ghidra.dbg.DebuggerObjectModel; -import ghidra.util.Msg; - -@Ignore("Need compatible GDB version for CI") -public class ModelForGdbTest extends AbstractModelForGdbTest { - - static class GdbGadpModelHost implements ModelHost { - final GdbModelImpl model; - final String gdbCmd; - - GdbGadpModelHost(String gdbCmd) { - model = new GdbModelImpl(); - this.gdbCmd = gdbCmd; - } - - @Override - public CompletableFuture init() { - Msg.debug(this, "Starting GDB..."); - return model.startGDB(gdbCmd, new String[] {}); - } - - @Override - public DebuggerObjectModel getModel() { - return model; - } - - @Override - public void close() throws Exception { - model.terminate(); - } - } - - @Override - protected ModelHost modelHost(String gdbCmd) throws Exception { - return new GdbGadpModelHost(gdbCmd); - } -} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpGdbModelHost.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpGdbModelHost.java new file mode 100644 index 0000000000..c6e0733a5a --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpGdbModelHost.java @@ -0,0 +1,27 @@ +/* ### + * 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 agent.gdb.model.gadp; + +import agent.gdb.gadp.GdbLocalDebuggerModelFactory; +import agent.gdb.model.AbstractGdbModelHost; +import ghidra.dbg.DebuggerModelFactory; + +class GadpGdbModelHost extends AbstractGdbModelHost { + @Override + public DebuggerModelFactory getModelFactory() { + return new GdbLocalDebuggerModelFactory(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbBreakpointsTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbBreakpointsTest.java new file mode 100644 index 0000000000..712978f45a --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbBreakpointsTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.gadp; + +import agent.gdb.model.AbstractModelForGdbBreakpointsTest; + +public class GadpModelForGdbBreakpointsTest extends AbstractModelForGdbBreakpointsTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbFactoryTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbFactoryTest.java new file mode 100644 index 0000000000..4ff603250a --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbFactoryTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.gadp; + +import agent.gdb.model.AbstractModelForGdbFactoryTest; + +public class GadpModelForGdbFactoryTest extends AbstractModelForGdbFactoryTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbInferiorAttacherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbInferiorAttacherTest.java new file mode 100644 index 0000000000..c22340156a --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbInferiorAttacherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.gadp; + +import agent.gdb.model.AbstractModelForGdbInferiorAttacherTest; + +public class GadpModelForGdbInferiorAttacherTest extends AbstractModelForGdbInferiorAttacherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbInferiorLauncherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbInferiorLauncherTest.java new file mode 100644 index 0000000000..1fe604ae15 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbInferiorLauncherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.gadp; + +import agent.gdb.model.AbstractModelForGdbInferiorLauncherTest; + +public class GadpModelForGdbInferiorLauncherTest extends AbstractModelForGdbInferiorLauncherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbInterpreterTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbInterpreterTest.java new file mode 100644 index 0000000000..9f676bb5b6 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbInterpreterTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.gadp; + +import agent.gdb.model.AbstractModelForGdbInterpreterTest; + +public class GadpModelForGdbInterpreterTest extends AbstractModelForGdbInterpreterTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbScenarioForkExitTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbScenarioForkExitTest.java new file mode 100644 index 0000000000..2433cb033e --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbScenarioForkExitTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.gadp; + +import agent.gdb.model.AbstractModelForGdbScenarioForkExitTest; + +public class GadpModelForGdbScenarioForkExitTest extends AbstractModelForGdbScenarioForkExitTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbSessionLauncherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbSessionLauncherTest.java new file mode 100644 index 0000000000..d874ca154a --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbSessionLauncherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.gadp; + +import agent.gdb.model.AbstractModelForGdbSessionLauncherTest; + +public class GadpModelForGdbSessionLauncherTest extends AbstractModelForGdbSessionLauncherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbSessiopnAttacherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbSessiopnAttacherTest.java new file mode 100644 index 0000000000..6055c13fb8 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/gadp/GadpModelForGdbSessiopnAttacherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.gadp; + +import agent.gdb.model.AbstractModelForGdbSessionAttacherTest; + +public class GadpModelForGdbSessiopnAttacherTest extends AbstractModelForGdbSessionAttacherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new GadpGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmGdbModelHost.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmGdbModelHost.java new file mode 100644 index 0000000000..91cc97423e --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmGdbModelHost.java @@ -0,0 +1,27 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.GdbInJvmDebuggerModelFactory; +import agent.gdb.model.AbstractGdbModelHost; +import ghidra.dbg.DebuggerModelFactory; + +class InVmGdbModelHost extends AbstractGdbModelHost { + @Override + public DebuggerModelFactory getModelFactory() { + return new GdbInJvmDebuggerModelFactory(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbAmd64RegistersTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbAmd64RegistersTest.java new file mode 100644 index 0000000000..cef52950a5 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbAmd64RegistersTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbAmd64RegistersTest; + +public class InVmModelForGdbAmd64RegistersTest extends AbstractModelForGdbAmd64RegistersTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbBreakpointsTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbBreakpointsTest.java new file mode 100644 index 0000000000..5ab98d721a --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbBreakpointsTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbBreakpointsTest; + +public class InVmModelForGdbBreakpointsTest extends AbstractModelForGdbBreakpointsTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbFactoryTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbFactoryTest.java new file mode 100644 index 0000000000..2cbd32368c --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbFactoryTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbFactoryTest; + +public class InVmModelForGdbFactoryTest extends AbstractModelForGdbFactoryTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbFrameFocusTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbFrameFocusTest.java new file mode 100644 index 0000000000..b373c796eb --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbFrameFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbFrameFocusTest; + +public class InVmModelForGdbFrameFocusTest extends AbstractModelForGdbFrameFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInferiorAttacherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInferiorAttacherTest.java new file mode 100644 index 0000000000..d891803cfb --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInferiorAttacherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbInferiorAttacherTest; + +public class InVmModelForGdbInferiorAttacherTest extends AbstractModelForGdbInferiorAttacherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInferiorFocusTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInferiorFocusTest.java new file mode 100644 index 0000000000..4216f648d2 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInferiorFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbInferiorFocusTest; + +public class InVmModelForGdbInferiorFocusTest extends AbstractModelForGdbInferiorFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInferiorLauncherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInferiorLauncherTest.java new file mode 100644 index 0000000000..2c81b2bc9e --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInferiorLauncherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbInferiorLauncherTest; + +public class InVmModelForGdbInferiorLauncherTest extends AbstractModelForGdbInferiorLauncherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInterpreterTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInterpreterTest.java new file mode 100644 index 0000000000..128a2f3090 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbInterpreterTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbInterpreterTest; + +public class InVmModelForGdbInterpreterTest extends AbstractModelForGdbInterpreterTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioAmd64RegistersTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioAmd64RegistersTest.java new file mode 100644 index 0000000000..be430c9d9e --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioAmd64RegistersTest.java @@ -0,0 +1,27 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbScenarioAmd64RegistersTest; +import ghidra.dbg.testutil.TestDebuggerModelProvider.ModelHost; + +public class InVmModelForGdbScenarioAmd64RegistersTest + extends AbstractModelForGdbScenarioAmd64RegistersTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioCloneExitTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioCloneExitTest.java new file mode 100644 index 0000000000..1371757b9f --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioCloneExitTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbScenarioCloneExitTest; + +public class InVmModelForGdbScenarioCloneExitTest extends AbstractModelForGdbScenarioCloneExitTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioForkExitTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioForkExitTest.java new file mode 100644 index 0000000000..7d13c3c696 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioForkExitTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbScenarioForkExitTest; + +public class InVmModelForGdbScenarioForkExitTest extends AbstractModelForGdbScenarioForkExitTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioMemoryTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioMemoryTest.java new file mode 100644 index 0000000000..7abdecf256 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioMemoryTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbScenarioMemoryTest; + +public class InVmModelForGdbScenarioMemoryTest extends AbstractModelForGdbScenarioMemoryTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioStackTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioStackTest.java new file mode 100644 index 0000000000..55680b6a5e --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbScenarioStackTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbScenarioStackTest; + +public class InVmModelForGdbScenarioStackTest extends AbstractModelForGdbScenarioStackTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbSessionLauncherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbSessionLauncherTest.java new file mode 100644 index 0000000000..a5b1228783 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbSessionLauncherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbSessionLauncherTest; + +public class InVmModelForGdbSessionLauncherTest extends AbstractModelForGdbSessionLauncherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbSessiopnAttacherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbSessiopnAttacherTest.java new file mode 100644 index 0000000000..caae154df9 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbSessiopnAttacherTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbSessionAttacherTest; + +public class InVmModelForGdbSessiopnAttacherTest extends AbstractModelForGdbSessionAttacherTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbSteppableTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbSteppableTest.java new file mode 100644 index 0000000000..11af6d4228 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbSteppableTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbSteppableTest; + +public class InVmModelForGdbSteppableTest extends AbstractModelForGdbSteppableTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbThreadFocusTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbThreadFocusTest.java new file mode 100644 index 0000000000..c528044c32 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/invm/InVmModelForGdbThreadFocusTest.java @@ -0,0 +1,25 @@ +/* ### + * 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 agent.gdb.model.invm; + +import agent.gdb.model.AbstractModelForGdbThreadFocusTest; + +public class InVmModelForGdbThreadFocusTest extends AbstractModelForGdbThreadFocusTest { + @Override + public ModelHost modelHost() throws Throwable { + return new InVmGdbModelHost(); + } +} diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/GadpRegistry.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/GadpRegistry.java index 2fa30b6540..f928a38fa7 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/GadpRegistry.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/GadpRegistry.java @@ -41,9 +41,9 @@ public enum GadpRegistry { public static final Map, Class> MIXIN_REGISTRY = new HashMap<>(); - public static void registerInterface(Class iface, + public static void registerInterface(Class iface, Class mixin) { - String name = DebuggerObjectModel.requireIfaceName(iface); + DebuggerObjectModel.requireIfaceName(iface); MIXIN_REGISTRY.put(iface, mixin); } @@ -52,8 +52,10 @@ public enum GadpRegistry { registerInterface(TargetAggregate.class, GadpClientTargetAggregate.class); registerInterface(TargetAttachable.class, GadpClientTargetAttachable.class); registerInterface(TargetAttacher.class, GadpClientTargetAttacher.class); - registerInterface(TargetBreakpointContainer.class, - GadpClientTargetBreakpointContainer.class); + registerInterface(TargetBreakpointLocationContainer.class, + GadpClientTargetBreakpointLocationContainer.class); + registerInterface(TargetBreakpointSpecContainer.class, + GadpClientTargetBreakpointSpecContainer.class); registerInterface(TargetBreakpointSpec.class, GadpClientTargetBreakpointSpec.class); registerInterface(TargetDataTypeMember.class, GadpClientTargetDataTypeMember.class); registerInterface(TargetDataTypeNamespace.class, GadpClientTargetDataTypeNamespace.class); @@ -80,12 +82,14 @@ public enum GadpRegistry { registerInterface(TargetRegisterContainer.class, GadpClientTargetRegisterContainer.class); registerInterface(TargetResumable.class, GadpClientTargetResumable.class); registerInterface(TargetSection.class, GadpClientTargetSection.class); + registerInterface(TargetSectionContainer.class, GadpClientTargetSectionContainer.class); registerInterface(TargetStack.class, GadpClientTargetStack.class); registerInterface(TargetStackFrame.class, GadpClientTargetStackFrame.class); registerInterface(TargetSteppable.class, GadpClientTargetSteppable.class); registerInterface(TargetSymbol.class, GadpClientTargetSymbol.class); registerInterface(TargetSymbolNamespace.class, GadpClientTargetSymbolNamespace.class); registerInterface(TargetThread.class, GadpClientTargetThread.class); + registerInterface(TargetTogglable.class, GadpClientTargetTogglable.class); } public static List> getMixins( diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/DelegateGadpClientTargetObject.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/DelegateGadpClientTargetObject.java index 68302306c6..9782a933ad 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/DelegateGadpClientTargetObject.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/DelegateGadpClientTargetObject.java @@ -24,17 +24,13 @@ import java.util.concurrent.CompletableFuture; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.gadp.GadpRegistry; -import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback; import ghidra.dbg.gadp.client.annot.GadpEventHandler; import ghidra.dbg.gadp.protocol.Gadp; import ghidra.dbg.gadp.protocol.Gadp.EventNotification.EvtCase; import ghidra.dbg.memory.CachedMemory; -import ghidra.dbg.target.TargetAccessConditioned; -import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointAction; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.TargetObjectSchema; -import ghidra.dbg.util.CollectionUtils.Delta; import ghidra.program.model.address.AddressSpace; import ghidra.util.Msg; import ghidra.util.datastruct.ListenerSet; @@ -129,28 +125,9 @@ public class DelegateGadpClientTargetObject } } - protected static class GadpAttributeChangeCallbackMap - extends GadpHandlerMap { - protected static final Class[] PARAMETER_CLASSES = new Class[] { Object.class }; - - public GadpAttributeChangeCallbackMap(Set> ifaces) { - super(GadpAttributeChangeCallback.class, PARAMETER_CLASSES); - for (Class iface : ifaces) { - registerInterface(iface); - } - } - - @Override - protected String getKey(GadpAttributeChangeCallback annot) { - return annot.value(); - } - } - protected static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); protected static final Map>, GadpEventHandlerMap> EVENT_HANDLER_MAPS_BY_COMPOSITION = new HashMap<>(); - protected static final Map>, GadpAttributeChangeCallbackMap> ATTRIBUTE_CHANGE_CALLBACKS_MAPS_BY_COMPOSITION = - new HashMap<>(); protected static GadpClientTargetObject makeModelProxy(GadpClient client, GadpClientTargetObject parent, String key, String typeHint, List ifaceNames) { @@ -166,7 +143,6 @@ public class DelegateGadpClientTargetObject private final List ifaceNames; private final List> ifaces; private final GadpEventHandlerMap eventHandlers; - private final GadpAttributeChangeCallbackMap attributeChangeCallbacks; protected Map memCache = null; // Becomes active if this is a TargetMemory protected Map regCache = null; // Becomes active if this is a TargtRegisterBank @@ -185,9 +161,6 @@ public class DelegateGadpClientTargetObject allMixins.add(GadpClientTargetObject.class); this.eventHandlers = EVENT_HANDLER_MAPS_BY_COMPOSITION.computeIfAbsent(allMixins, GadpEventHandlerMap::new); - this.attributeChangeCallbacks = - ATTRIBUTE_CHANGE_CALLBACKS_MAPS_BY_COMPOSITION.computeIfAbsent(allMixins, - GadpAttributeChangeCallbackMap::new); } @Override @@ -241,46 +214,13 @@ public class DelegateGadpClientTargetObject GadpValueUtils.getAttributeMap(this, deltaA.getAddedList()); changeElements(deltaE.getRemovedList(), List.of(), elementsAdded, "Updated"); - Delta attrDelta = - changeAttributes(deltaA.getRemovedList(), attributesAdded, "Updated"); - for (String name : attrDelta.getKeysRemoved()) { - handleAttributeChange(name, null); - } - for (Map.Entry a : attrDelta.added.entrySet()) { - handleAttributeChange(a.getKey(), a.getValue()); - } + changeAttributes(deltaA.getRemovedList(), attributesAdded, "Updated"); } protected void handleEvent(Gadp.EventNotification notify) { eventHandlers.handle(getProxy(), notify.getEvtCase(), notify); } - /** - * Translate the given attribute change into an interface-specific property change, if - * applicable, and invokes the appropriate listeners - * - * @implNote The model interface allow listening for attribute changes in general, as well as - * some interface-specific property changes. The convention in model is for such - * properties to be encoded as an attribute with a specified name, e.g., the - * 'accessible' attribute encodes the valcachedAue for - * {@link TargetAccessConditioned#getAccessibility()}, and changes will invoke - * {@link TargetAccessibilityListener#accessibilityChanged(TargetAccessConditioned, TargetAccessibility)}. - * Taking advantage of the general attribute getting/change-listening convention, GADP - * communicates only the attribute changes, and then invokes the corresponding - * interface-specific property change listeners on the client side. So long as the - * model implementation on the server side follows the convention, then the - * client-side proxies will obey the same. If not, then client-side behavior is - * undefined. - * - * @see GadpAttributeChangeCallback - * - * @param name the name of the attribute - * @param value the new value of the attribute - */ - protected void handleAttributeChange(String name, Object value) { - attributeChangeCallbacks.handle(getProxy(), name, value); - } - protected void assertValid() { if (!valid) { throw new IllegalStateException("Object is no longer valid: " + toString()); @@ -345,7 +285,7 @@ public class DelegateGadpClientTargetObject } @Override - protected void doInvalidate(TargetObject branch, String reason) { + public void doInvalidate(TargetObject branch, String reason) { client.removeProxy(path, reason); super.doInvalidate(branch, reason); } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClient.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClient.java index e92c7b7d0c..fced883167 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClient.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClient.java @@ -313,13 +313,8 @@ public class GadpClient extends AbstractDebuggerObjectModel } else if (old == ChannelState.ACTIVE && set == ChannelState.CLOSED) { listeners.fire.modelClosed(reason); - List copy; - synchronized (lock) { - copy = List.copyOf(modelProxies.values()); - } - for (GadpClientTargetObject proxy : copy) { - proxy.getDelegate().doInvalidate(root, "GADP Client disconnected"); - } + root.invalidateSubtree(root, "GADP Client disconnected"); + messageMatcher.flush(new DebuggerModelTerminatingException("GADP Client disconnected")); } } @@ -355,6 +350,10 @@ public class GadpClient extends AbstractDebuggerObjectModel protected CompletableFuture sendChecked(Message.Builder req, M exampleRep) { + if (channelState.get().isTerminate()) { + return CompletableFuture.failedFuture( + new DebuggerModelTerminatingException("GADP Client disconnected")); + } return sendCommand(req) .thenApply(msg -> require(exampleRep, checkError(msg))); //.thenCompose(msg -> flushEvents().thenApply(__ -> msg)); @@ -486,14 +485,13 @@ public class GadpClient extends AbstractDebuggerObjectModel public CompletableFuture close() { try { messageChannel.close(); - CompletableFuture.runAsync(() -> { - channelState.set(ChannelState.CLOSED, DebuggerModelClosedReason.normal()); - }, clientExecutor).exceptionally(ex -> { - Msg.error("Problem upon firing channel state change", ex); - return null; - }); + channelState.set(ChannelState.CLOSED, DebuggerModelClosedReason.normal()); return super.close(); } + catch (RejectedExecutionException e) { + reportError(this, "Client already closed", e); + return AsyncUtils.NIL; + } catch (IOException e) { return CompletableFuture.failedFuture(e); } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAccessConditioned.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAccessConditioned.java index 2304b2b91c..4404136753 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAccessConditioned.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetAccessConditioned.java @@ -15,16 +15,9 @@ */ package ghidra.dbg.gadp.client; -import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback; import ghidra.dbg.target.TargetAccessConditioned; public interface GadpClientTargetAccessConditioned extends GadpClientTargetObject, TargetAccessConditioned { - - @GadpAttributeChangeCallback(ACCESSIBLE_ATTRIBUTE_NAME) - default void handleAccessibleChanged(Object accessible) { - getDelegate().getListeners() - .fire(TargetAccessibilityListener.class) - .accessibilityChanged(this, (Boolean) accessible); - } + // Nothing to add } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointLocationContainer.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointLocationContainer.java new file mode 100644 index 0000000000..f0479e2d32 --- /dev/null +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointLocationContainer.java @@ -0,0 +1,23 @@ +/* ### + * 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 ghidra.dbg.target.TargetBreakpointLocationContainer; + +public interface GadpClientTargetBreakpointLocationContainer + extends GadpClientTargetObject, TargetBreakpointLocationContainer { + // Nothing to add +} diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpec.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpec.java index 145c049dbc..109a4a1b8a 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpec.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpec.java @@ -17,10 +17,8 @@ package ghidra.dbg.gadp.client; import java.util.concurrent.CompletableFuture; -import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback; import ghidra.dbg.gadp.protocol.Gadp; import ghidra.dbg.target.TargetBreakpointSpec; -import ghidra.dbg.util.ValueUtils; import ghidra.util.datastruct.ListenerSet; public interface GadpClientTargetBreakpointSpec @@ -57,15 +55,4 @@ public interface GadpClientTargetBreakpointSpec actions.remove(action); } } - - default boolean enabledFromObj(Object obj) { - return ValueUtils.expectBoolean(obj, this, ENABLED_ATTRIBUTE_NAME, false, true); - } - - @GadpAttributeChangeCallback(ENABLED_ATTRIBUTE_NAME) - default void handleEnabledChanged(Object enabled) { - getDelegate().getListeners() - .fire(TargetBreakpointSpecListener.class) - .breakpointToggled(this, enabledFromObj(enabled)); - } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointContainer.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpecContainer.java similarity index 92% rename from Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointContainer.java rename to Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpecContainer.java index b217a1dad7..7e2f22a096 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointContainer.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetBreakpointSpecContainer.java @@ -27,8 +27,8 @@ import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.program.model.address.AddressRange; import ghidra.util.datastruct.ListenerSet; -public interface GadpClientTargetBreakpointContainer - extends GadpClientTargetObject, TargetBreakpointContainer { +public interface GadpClientTargetBreakpointSpecContainer + extends GadpClientTargetObject, TargetBreakpointSpecContainer { @Override default CompletableFuture placeBreakpoint(AddressRange range, @@ -71,9 +71,7 @@ public interface GadpClientTargetBreakpointContainer Path bptPath = evt.getEffective(); TargetBreakpointLocation breakpoint = bptPath == null ? null : getModel().getProxy(bptPath.getEList(), true).as(TargetBreakpointLocation.class); - getDelegate().getListeners() - .fire(TargetBreakpointListener.class) - .breakpointHit(this, trapped, frame, spec, breakpoint); + getDelegate().getListeners().fire.breakpointHit(this, trapped, frame, spec, breakpoint); if (spec instanceof GadpClientTargetBreakpointSpec) { // If I don't have a cached proxy, then I don't have any listeners GadpClientTargetBreakpointSpec specObj = (GadpClientTargetBreakpointSpec) spec; diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetEventScope.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetEventScope.java index 9d4a348825..ca19515795 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetEventScope.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetEventScope.java @@ -33,8 +33,6 @@ public interface GadpClientTargetEventScope extends GadpClientTargetObject, Targ String description = evt.getDescription(); List parameters = GadpValueUtils.getValues(getModel(), evt.getParametersList()); - getDelegate().getListeners() - .fire(TargetEventScopeListener.class) - .event(this, thread, type, description, parameters); + getDelegate().getListeners().fire.event(this, thread, type, description, parameters); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetExecutionStateful.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetExecutionStateful.java index 1f38b061f7..9ce8206927 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetExecutionStateful.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetExecutionStateful.java @@ -15,22 +15,9 @@ */ package ghidra.dbg.gadp.client; -import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback; import ghidra.dbg.target.TargetExecutionStateful; -import ghidra.dbg.util.ValueUtils; public interface GadpClientTargetExecutionStateful extends GadpClientTargetObject, TargetExecutionStateful { - - default TargetExecutionState stateFromObj(Object obj) { - return ValueUtils.expectType(obj, TargetExecutionState.class, this, - STATE_ATTRIBUTE_NAME, TargetExecutionState.INACTIVE, true); - } - - @GadpAttributeChangeCallback(STATE_ATTRIBUTE_NAME) - default void handleStateChanged(Object state) { - getDelegate().getListeners() - .fire(TargetExecutionStateListener.class) - .executionStateChanged(this, stateFromObj(state)); - } + // Nothing to add } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetFocusScope.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetFocusScope.java index 3ce1dbf4fb..f52444e409 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetFocusScope.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetFocusScope.java @@ -18,12 +18,10 @@ package ghidra.dbg.gadp.client; import java.util.concurrent.CompletableFuture; import ghidra.dbg.error.DebuggerIllegalArgumentException; -import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback; import ghidra.dbg.gadp.protocol.Gadp; import ghidra.dbg.target.TargetFocusScope; import ghidra.dbg.target.TargetObject; import ghidra.dbg.util.PathUtils; -import ghidra.dbg.util.ValueUtils; public interface GadpClientTargetFocusScope extends GadpClientTargetObject, TargetFocusScope { @@ -41,16 +39,4 @@ public interface GadpClientTargetFocusScope extends GadpClientTargetObject, Targ Gadp.FocusReply.getDefaultInstance()) .thenApply(__ -> null); } - - default TargetObject focusFromObj(Object obj) { - return ValueUtils.expectType(obj, TargetObject.class, this, FOCUS_ATTRIBUTE_NAME, this, - true); - } - - @GadpAttributeChangeCallback(FOCUS_ATTRIBUTE_NAME) - default void handleFocusChanged(Object focus) { - getDelegate().getListeners() - .fire(TargetFocusScopeListener.class) - .focusChanged(this, focusFromObj(focus)); - } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterpreter.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterpreter.java index fc3c06ede6..91ea2b266c 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterpreter.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetInterpreter.java @@ -17,10 +17,8 @@ package ghidra.dbg.gadp.client; import java.util.concurrent.CompletableFuture; -import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback; import ghidra.dbg.gadp.protocol.Gadp; import ghidra.dbg.target.TargetInterpreter; -import ghidra.dbg.util.ValueUtils; public interface GadpClientTargetInterpreter extends GadpClientTargetObject, TargetInterpreter { @@ -44,15 +42,4 @@ public interface GadpClientTargetInterpreter extends GadpClientTargetObject, Tar Gadp.ExecuteReply.getDefaultInstance()) .thenApply(rep -> rep.getCaptured()); } - - default String promptFromObj(Object obj) { - return ValueUtils.expectType(obj, String.class, this, PROMPT_ATTRIBUTE_NAME, ">", true); - } - - @GadpAttributeChangeCallback(PROMPT_ATTRIBUTE_NAME) - default void handlePromptChanged(Object prompt) { - getDelegate().getListeners() - .fire(TargetInterpreterListener.class) - .promptChanged(this, promptFromObj(prompt)); - } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMemory.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMemory.java index 6dc613e00c..55ed3731cc 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMemory.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetMemory.java @@ -89,7 +89,7 @@ public interface GadpClientTargetMemory extends GadpClientTargetObject, TargetMe byte[] data = evt.getContent().toByteArray(); DelegateGadpClientTargetObject delegate = getDelegate(); delegate.getMemoryCache(address.getAddressSpace()).updateMemory(address.getOffset(), data); - delegate.getListeners().fire(TargetMemoryListener.class).memoryUpdated(this, address, data); + delegate.getListeners().fire.memoryUpdated(this, address, data); } @GadpEventHandler(Gadp.EventNotification.EvtCase.MEMORY_ERROR_EVENT) @@ -98,8 +98,7 @@ public interface GadpClientTargetMemory extends GadpClientTargetObject, TargetMe AddressRange range = GadpValueUtils.getAddressRange(getModel(), evt.getRange()); String message = evt.getMessage(); // Errors are not cached, but recorded in trace - getDelegate().getListeners() - .fire(TargetMemoryListener.class) - .memoryReadError(this, range, new DebuggerMemoryAccessException(message)); + getDelegate().getListeners().fire.memoryReadError(this, range, + new DebuggerMemoryAccessException(message)); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObject.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObject.java index 5e56c7ed24..7defc5592a 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObject.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetObject.java @@ -19,12 +19,9 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import ghidra.dbg.agent.SpiTargetObject; -import ghidra.dbg.gadp.client.annot.GadpAttributeChangeCallback; import ghidra.dbg.gadp.client.annot.GadpEventHandler; import ghidra.dbg.gadp.protocol.Gadp; import ghidra.dbg.target.TargetConsole.Channel; -import ghidra.dbg.target.TargetConsole.TargetConsoleListener; -import ghidra.dbg.util.ValueUtils; import ghidra.util.Msg; public interface GadpClientTargetObject extends SpiTargetObject { @@ -52,26 +49,14 @@ public interface GadpClientTargetObject extends SpiTargetObject { getDelegate().doClearCaches(); } - default String displayFromObj(Object obj) { - return ValueUtils.expectType(obj, String.class, this, DISPLAY_ATTRIBUTE_NAME, getName(), - false); - } - - @GadpAttributeChangeCallback(DISPLAY_ATTRIBUTE_NAME) - default void handleDisplayChanged(Object 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 @GadpEventHandler(Gadp.EventNotification.EvtCase.CONSOLE_OUTPUT_EVENT) default void handleConsoleOutputEvent(Gadp.EventNotification notification) { Gadp.ConsoleOutputEvent evt = notification.getConsoleOutputEvent(); int channelIndex = evt.getChannel(); Channel[] allChannels = Channel.values(); if (0 <= channelIndex && channelIndex < allChannels.length) { - getDelegate().getListeners() - .fire(TargetConsoleListener.class) - .consoleOutput(this, allChannels[channelIndex], evt.getData().toByteArray()); + getDelegate().getListeners().fire.consoleOutput(this, allChannels[channelIndex], + evt.getData().toByteArray()); } else { Msg.error(this, "Received output for unknown channel " + channelIndex + ": " + diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetRegisterBank.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetRegisterBank.java index e9cd3500a2..80506f56e5 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetRegisterBank.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetRegisterBank.java @@ -82,8 +82,6 @@ public interface GadpClientTargetRegisterBank extends GadpClientTargetObject, Ta Map updates = GadpValueUtils.getRegisterValueMap(evt.getValueList()); DelegateGadpClientTargetObject delegate = getDelegate(); delegate.getRegisterCache().putAll(updates); - delegate.getListeners() - .fire(TargetRegisterBankListener.class) - .registersUpdated(this, updates); + delegate.getListeners().fire.registersUpdated(this, updates); } } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetSectionContainer.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetSectionContainer.java new file mode 100644 index 0000000000..9c904c9d5e --- /dev/null +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetSectionContainer.java @@ -0,0 +1,23 @@ +/* ### + * 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 ghidra.dbg.target.TargetSectionContainer; + +public interface GadpClientTargetSectionContainer + extends GadpClientTargetObject, TargetSectionContainer { + // Nothing to add +} diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/annot/GadpAttributeChangeCallback.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetTogglable.java similarity index 74% rename from Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/annot/GadpAttributeChangeCallback.java rename to Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetTogglable.java index 02b1eaf302..a165f5e2b3 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/annot/GadpAttributeChangeCallback.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpClientTargetTogglable.java @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.dbg.gadp.client.annot; +package ghidra.dbg.gadp.client; -import java.lang.annotation.*; +import ghidra.dbg.target.TargetTogglable; -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface GadpAttributeChangeCallback { - String value(); +public interface GadpClientTargetTogglable extends GadpClientTargetObject, TargetTogglable { + // Nothing to add } diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java index 9203ccc2fe..e59b948cf2 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/client/GadpValueUtils.java @@ -30,7 +30,7 @@ import ghidra.dbg.gadp.protocol.Gadp; import ghidra.dbg.gadp.protocol.Gadp.ModelObjectDelta; import ghidra.dbg.target.TargetAttacher.TargetAttachKind; import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetEventScope.TargetEventType; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; @@ -38,7 +38,6 @@ import ghidra.dbg.target.TargetMethod; import ghidra.dbg.target.TargetMethod.ParameterDescription; import ghidra.dbg.target.TargetMethod.TargetParameterMap; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.TargetObject.TargetUpdateMode; import ghidra.dbg.target.TargetSteppable.TargetStepKind; import ghidra.dbg.target.TargetSteppable.TargetStepKindSet; import ghidra.dbg.util.CollectionUtils.Delta; @@ -69,9 +68,9 @@ public enum GadpValueUtils { case BK_WRITE: return TargetBreakpointKind.WRITE; case BK_EXECUTE: - return TargetBreakpointKind.EXECUTE; + return TargetBreakpointKind.HW_EXECUTE; case BK_SOFTWARE: - return TargetBreakpointKind.SOFTWARE; + return TargetBreakpointKind.SW_EXECUTE; default: throw new IllegalArgumentException(); } @@ -89,9 +88,9 @@ public enum GadpValueUtils { return Gadp.BreakKind.BK_READ; case WRITE: return Gadp.BreakKind.BK_WRITE; - case EXECUTE: + case HW_EXECUTE: return Gadp.BreakKind.BK_EXECUTE; - case SOFTWARE: + case SW_EXECUTE: return Gadp.BreakKind.BK_SOFTWARE; default: throw new IllegalArgumentException(); @@ -375,30 +374,6 @@ public enum GadpValueUtils { return TargetStringList.copyOf(list.getSList()); } - public static Gadp.UpdateMode makeUpdateMode(TargetUpdateMode mode) { - switch (mode) { - case SOLICITED: - return Gadp.UpdateMode.UM_SOLICITED; - case FIXED: - return Gadp.UpdateMode.UM_FIXED; - case UNSOLICITED: - default: - return Gadp.UpdateMode.UM_UNSOLICITED; - } - } - - public static TargetUpdateMode getUpdateMode(Gadp.UpdateMode mode) { - switch (mode) { - case UM_FIXED: - return TargetUpdateMode.FIXED; - case UM_SOLICITED: - return TargetUpdateMode.SOLICITED; - case UM_UNSOLICITED: - default: - return TargetUpdateMode.UNSOLICITED; - } - } - public static Class getValueType(Gadp.ValueType type) { switch (type) { case VT_VOID: @@ -433,8 +408,6 @@ public enum GadpValueUtils { return TargetPrimitiveDataType.class; case VT_DATA_TYPE: return TargetDataType.class; - case VT_UPDATE_MODE: - return TargetUpdateMode.class; case VT_PATH: return TargetObject.class; case VT_PATH_LIST: @@ -522,9 +495,6 @@ public enum GadpValueUtils { if (type == TargetDataType.class) { return Gadp.ValueType.VT_DATA_TYPE; } - if (type == TargetUpdateMode.class) { - return Gadp.ValueType.VT_UPDATE_MODE; - } if (type == TargetObject.class) { return Gadp.ValueType.VT_PATH; } @@ -613,9 +583,6 @@ public enum GadpValueUtils { } // TODO: TargetPrimitiveDataType? // TODO: TargetDataType? - else if (value instanceof TargetUpdateMode) { - b.setUpdateModeValue(makeUpdateMode((TargetUpdateMode) value)); - } else if (value instanceof TargetParameterMap) { b.setParametersValue(makeParameterList((TargetParameterMap) value)); } @@ -705,8 +672,6 @@ public enum GadpValueUtils { return TODO("Marhsalling types over GADP", value.getPrimitiveKindValue()); case DATA_TYPE_VALUE: return TODO("Marshalling types over GADP", value.getDataTypeValue()); - case UPDATE_MODE_VALUE: - return getUpdateMode(value.getUpdateModeValue()); case PARAMETERS_VALUE: return getParameters(model, value.getParametersValue()); case PATH_VALUE: diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpLocalDebuggerModelFactory.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpLocalDebuggerModelFactory.java index a08e91aa51..79e2dcbabe 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpLocalDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/AbstractGadpLocalDebuggerModelFactory.java @@ -145,7 +145,7 @@ public abstract class AbstractGadpLocalDebuggerModelFactory implements LocalDebu @Override public CompletableFuture close() { - return super.close().thenRun(() -> { + return super.close().whenComplete((v, e) -> { agentThread.process.destroy(); agentThread.interrupt(); }); diff --git a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/GadpClientHandler.java b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/GadpClientHandler.java index 1325550804..8ebaa844c8 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/GadpClientHandler.java +++ b/Ghidra/Debug/Debugger-gadp/src/main/java/ghidra/dbg/gadp/server/GadpClientHandler.java @@ -37,7 +37,6 @@ import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel; import ghidra.dbg.target.*; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetConsole.Channel; -import ghidra.dbg.target.TargetConsole.TargetTextConsoleListener; import ghidra.dbg.target.TargetEventScope.TargetEventType; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.XmlSchemaContext; @@ -78,7 +77,7 @@ public class GadpClientHandler } } - protected class ListenerForEvents implements DebuggerModelListener, TargetTextConsoleListener { + protected class ListenerForEvents implements DebuggerModelListener { @Override public void created(TargetObject object) { if (!sock.isOpen()) { @@ -175,7 +174,7 @@ public class GadpClientHandler } @Override - public void breakpointHit(TargetBreakpointContainer container, TargetObject trapped, + public void breakpointHit(TargetObject container, TargetObject trapped, TargetStackFrame frame, TargetBreakpointSpec spec, TargetBreakpointLocation breakpoint) { if (!sock.isOpen()) { @@ -209,7 +208,7 @@ public class GadpClientHandler } @Override - public void memoryReadError(TargetMemory memory, AddressRange range, + public void memoryReadError(TargetObject memory, AddressRange range, DebuggerMemoryAccessException e) { if (!sock.isOpen()) { return; @@ -225,7 +224,7 @@ public class GadpClientHandler } @Override - public void memoryUpdated(TargetMemory memory, Address address, byte[] data) { + public void memoryUpdated(TargetObject memory, Address address, byte[] data) { if (!sock.isOpen()) { return; } @@ -240,7 +239,7 @@ public class GadpClientHandler } @Override - public void registersUpdated(TargetRegisterBank bank, Map updates) { + public void registersUpdated(TargetObject bank, Map updates) { if (!sock.isOpen()) { return; } @@ -254,7 +253,7 @@ public class GadpClientHandler } @Override - public void event(TargetEventScope object, TargetThread eventThread, TargetEventType type, + public void event(TargetObject object, TargetThread eventThread, TargetEventType type, String description, List parameters) { if (!sock.isOpen()) { return; @@ -502,6 +501,7 @@ public class GadpClientHandler protected CompletableFuture processResync(int seqno, Gadp.ResyncRequest req) { List path = req.getPath().getEList(); return model.fetchModelObject(path).thenCompose(obj -> { + DebuggerObjectModel.requireNonNull(obj, path); return obj.resync(req.getAttributes(), req.getElements()); }).thenCompose(__ -> { return model.flushEvents(); @@ -541,7 +541,7 @@ public class GadpClientHandler } protected CompletableFuture performBreakCreate(Gadp.BreakCreateRequest req, - TargetBreakpointContainer breaks) { + TargetBreakpointSpecContainer breaks) { Set kinds = GadpValueUtils.getBreakKindSet(req.getKinds()); switch (req.getSpecCase()) { case EXPRESSION: @@ -556,8 +556,8 @@ public class GadpClientHandler } protected CompletableFuture processBreakCreate(int seqno, Gadp.BreakCreateRequest req) { - TargetBreakpointContainer breaks = - getObjectChecked(req.getPath()).as(TargetBreakpointContainer.class); + TargetBreakpointSpecContainer breaks = + getObjectChecked(req.getPath()).as(TargetBreakpointSpecContainer.class); return performBreakCreate(req, breaks).thenCompose(__ -> { return model.flushEvents(); }).thenCompose(__ -> { diff --git a/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto b/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto index 6e742176c0..bf254bea39 100644 --- a/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto +++ b/Ghidra/Debug/Debugger-gadp/src/main/proto/gadp.proto @@ -166,12 +166,6 @@ message DataType { bool const = 6; } -enum UpdateMode { - UM_UNSOLICITED = 0; - UM_SOLICITED = 1; - UM_FIXED = 2; -} - enum ValueType { VT_VOID = 0; VT_BOOL = 1; @@ -189,7 +183,6 @@ enum ValueType { VT_STEP_KIND_SET = 13; VT_PRIMITIVE_KIND = 14; VT_DATA_TYPE = 15; - VT_UPDATE_MODE = 16; VT_PATH = 17; VT_PATH_LIST = 18; VT_TYPE = 19; @@ -227,7 +220,6 @@ message Value { StepKindsSet step_kinds_value = 13; PrimitiveKind primitive_kind_value = 14; DataType data_type_value = 15; - UpdateMode update_mode_value = 16; Path path_value = 17; PathList path_list_value = 18; ModelObjectStub object_stub = 20; diff --git a/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/GadpClientServerTest.java b/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/GadpClientServerTest.java index 95bc68b62d..084236a8f7 100644 --- a/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/GadpClientServerTest.java +++ b/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/GadpClientServerTest.java @@ -18,6 +18,7 @@ package ghidra.dbg.gadp; import static org.junit.Assert.*; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; @@ -36,6 +37,7 @@ import com.google.protobuf.GeneratedMessageV3; import generic.ID; import generic.Unique; import ghidra.async.*; +import ghidra.dbg.AnnotatedDebuggerAttributeListener; import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.agent.*; import ghidra.dbg.attributes.TargetStringList; @@ -47,17 +49,15 @@ import ghidra.dbg.gadp.server.AbstractGadpServer; import ghidra.dbg.gadp.server.GadpClientHandler; import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener; import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; import ghidra.dbg.target.TargetMethod.ParameterDescription; import ghidra.dbg.target.TargetMethod.TargetParameterMap; -import ghidra.dbg.target.TargetObject.TargetObjectListener; -import ghidra.dbg.target.TargetObject.TargetUpdateMode; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; -import ghidra.dbg.util.*; -import ghidra.dbg.util.AttributesChangedListener.AttributesChangedInvocation; -import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation; +import ghidra.dbg.testutil.*; +import ghidra.dbg.testutil.AttributesChangedListener.AttributesChangedInvocation; +import ghidra.dbg.testutil.ElementsChangedListener.ElementsChangedInvocation; +import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.*; import ghidra.util.Msg; import ghidra.util.SystemUtilities; @@ -283,8 +283,7 @@ public class GadpClientServerTest { } @TargetObjectSchemaInfo(name = "Session") - public class TestGadpTargetSession extends DefaultTargetModelRoot - implements TargetFocusScope { + public class TestGadpTargetSession extends DefaultTargetModelRoot implements TargetFocusScope { protected final TestGadpTargetAvailableContainer available = new TestGadpTargetAvailableContainer(this); protected final TestGadpTargetProcessContainer processes = @@ -315,18 +314,14 @@ public class GadpClientServerTest { public void addLinks() throws Throwable { waitOn(available.fetchElements()); - links.setElements(List.of(), Map.of( - "1", available.getCachedElements().get("2"), - "2", available.getCachedElements().get("1")), - "Initialized"); + links.setElements(List.of(), Map.of("1", available.getCachedElements().get("2"), "2", + available.getCachedElements().get("1")), "Initialized"); changeAttributes(List.of(), List.of(links), Map.of(), "Adding links"); } public void setFocus(TestGadpTargetProcess process) { - changeAttributes(List.of(), List.of(), Map.of( - FOCUS_ATTRIBUTE_NAME, process), + changeAttributes(List.of(), List.of(), Map.of(FOCUS_ATTRIBUTE_NAME, process), "New Process"); - listeners.fire(TargetFocusScopeListener.class).focusChanged(this, process); } @Override @@ -363,12 +358,12 @@ public class GadpClientServerTest { } } - private static final TargetParameterMap PARAMS = TargetParameterMap.copyOf(Map.of( - "whom", ParameterDescription.create(String.class, "whom", true, "World", - "Whom to greet", "The person or people to greet"), - "others", ParameterDescription.create(TargetStringList.class, "others", - false, TargetStringList.of(), "Others to greet", - "List of other people to greet individually"))); + private static final TargetParameterMap PARAMS = TargetParameterMap.copyOf(Map.of("whom", + ParameterDescription.create(String.class, "whom", true, "World", "Whom to greet", + "The person or people to greet"), + "others", + ParameterDescription.create(TargetStringList.class, "others", false, TargetStringList.of(), + "Others to greet", "List of other people to greet individually"))); public class TestGadpTargetMethod extends TestTargetObject> implements TargetMethod { @@ -376,22 +371,20 @@ public class GadpClientServerTest { public TestGadpTargetMethod(TestTargetObject parent, String key) { super(parent.getModel(), parent, key, "Method"); - setAttributes(Map.of( - PARAMETERS_ATTRIBUTE_NAME, PARAMS, - RETURN_TYPE_ATTRIBUTE_NAME, Integer.class), - "Initialized"); + setAttributes(Map.of(PARAMETERS_ATTRIBUTE_NAME, PARAMS, RETURN_TYPE_ATTRIBUTE_NAME, + Integer.class), "Initialized"); } @Override public CompletableFuture invoke(Map arguments) { TestMethodInvocation invocation = new TestMethodInvocation(arguments); invocations.offer(invocation); - return invocation.thenApply(obj -> { - parent.changeAttributes(List.of(), Map.ofEntries( - Map.entry("greet(" + arguments.get("arg") + ")", obj)), + return model.gateFuture(invocation.thenApply(obj -> { + parent.changeAttributes(List.of(), + Map.ofEntries(Map.entry("greet(" + arguments.get("arg") + ")", obj)), "greet() invoked"); return obj; - }).thenCompose(model::gateFuture); + })); } } @@ -415,8 +408,7 @@ public class GadpClientServerTest { public class TestGadpTargetProcess extends TestTargetObject { public TestGadpTargetProcess(TestGadpTargetProcessContainer processes, int index) { - super(processes.getModel(), processes, - PathUtils.makeKey(PathUtils.makeIndex(index)), + super(processes.getModel(), processes, PathUtils.makeKey(PathUtils.makeIndex(index)), "Process"); } } @@ -427,17 +419,14 @@ public class GadpClientServerTest { public TestGadpTargetAvailableContainer(TestGadpTargetSession session) { super(session.getModel(), session, "Available", "AvailableContainer"); - setAttributes(List.of( - new TestGadpTargetMethod(this, "greet")), - Map.of(), "Initialized"); + setAttributes(List.of(new TestGadpTargetMethod(this, "greet")), Map.of(), + "Initialized"); } @Override public CompletableFuture requestElements(boolean refresh) { - setElements(List.of( - new TestGadpTargetAvailable(this, 1, "echo"), - new TestGadpTargetAvailable(this, 2, "dd")), - Map.of(), "Refreshed"); + setElements(List.of(new TestGadpTargetAvailable(this, 1, "echo"), + new TestGadpTargetAvailable(this, 2, "dd")), Map.of(), "Refreshed"); return super.requestElements(refresh); } } @@ -450,9 +439,7 @@ public class GadpClientServerTest { String cmd) { super(available.getModel(), available, PathUtils.makeKey(PathUtils.makeIndex(pid)), "Available"); - changeAttributes(List.of(), Map.of( - "pid", pid, - "cmd", cmd // + changeAttributes(List.of(), Map.of("pid", pid, "cmd", cmd // ), "Initialized"); } } @@ -706,14 +693,11 @@ public class GadpClientServerTest { assertEquals(PARAMS, method.getParameters()); assertEquals(Integer.class, method.getReturnType()); - CompletableFuture future = method.invoke(Map.of( - "whom", "GADP", - "others", TargetStringList.of("Alice", "Bob"))); + CompletableFuture future = method + .invoke(Map.of("whom", "GADP", "others", TargetStringList.of("Alice", "Bob"))); waitOn(invocations.count.waitValue(1)); TestMethodInvocation invocation = invocations.poll(); - assertEquals(Map.of( - "whom", "GADP", - "others", TargetStringList.of("Alice", "Bob")), + assertEquals(Map.of("whom", "GADP", "others", TargetStringList.of("Alice", "Bob")), invocation.args); invocation.complete(42); int result = (Integer) waitOn(future); @@ -780,11 +764,8 @@ public class GadpClientServerTest { waitOn(client.fetchObjectElements(List.of())); assertEquals(0, elements.size()); Map attributes = waitOn(client.fetchObjectAttributes(List.of())); - assertEquals(Set.of( - "Processes", "Available", - TargetObject.DISPLAY_ATTRIBUTE_NAME, - TargetObject.UPDATE_MODE_ATTRIBUTE_NAME), - attributes.keySet()); + assertEquals(Set.of("Processes", "Available", TargetObject.DISPLAY_ATTRIBUTE_NAME, + TargetObject.UPDATE_MODE_ATTRIBUTE_NAME), attributes.keySet()); Object procContAttr = attributes.get("Processes"); TargetObject procCont = (TargetObject) procContAttr; assertEquals(List.of("Processes"), procCont.getPath()); @@ -824,7 +805,7 @@ public class GadpClientServerTest { List invocations = new ArrayList<>(); // Any listener which calls .get on a child ref would do.... // This object-getting listener is the pattern that revealed this problem, though. - TargetObjectListener listener = new TargetObjectListener() { + DebuggerModelListener listener = new DebuggerModelListener() { @Override public void elementsChanged(TargetObject parent, Collection removed, Map added) { @@ -855,15 +836,16 @@ public class GadpClientServerTest { CompletableFuture> focusPath = new CompletableFuture<>(); AtomicBoolean failed = new AtomicBoolean(); - TargetFocusScopeListener focusListener = new TargetFocusScopeListener() { - @Override - public void focusChanged(TargetFocusScope object, TargetObject focused) { - Msg.info(this, "Focus changed to " + focused); - if (!focusPath.complete(focused.getPath())) { - failed.set(true); + DebuggerModelListener focusListener = + new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + @AttributeCallback(TargetFocusScope.FOCUS_ATTRIBUTE_NAME) + public void focusChanged(TargetObject object, TargetObject focused) { + Msg.info(this, "Focus changed to " + focused); + if (!focusPath.complete(focused.getPath())) { + failed.set(true); + } } - } - }; + }; AsynchronousSocketChannel socket = socketChannel(); try (ServerRunner runner = new ServerRunner()) { GadpClient client = new GadpClient("Test", socket); @@ -935,8 +917,7 @@ public class GadpClientServerTest { assertEquals(procContainer, eci.parent); assertEquals(List.of(), List.copyOf(eci.removed)); assertEquals(1, eci.added.size()); - Entry ent = - eci.added.entrySet().iterator().next(); + Entry ent = eci.added.entrySet().iterator().next(); assertEquals("0", ent.getKey()); assertEquals(List.of("Processes", "[0]"), ent.getValue().getPath()); } @@ -957,8 +938,7 @@ public class GadpClientServerTest { runner.server.getLocalAddress())); waitOn(client.connect()); - TargetObject availCont = - waitOn(client.fetchModelObject(PathUtils.parse("Available"))); + TargetObject availCont = waitOn(client.fetchModelObject(PathUtils.parse("Available"))); availCont.addListener(elemL); Map avail1 = waitOn(availCont.fetchElements()); assertEquals(2, avail1.size()); @@ -969,8 +949,7 @@ public class GadpClientServerTest { elemL.clear(); TestGadpTargetAvailableContainer ssAvail = runner.server.model.session.available; - ssAvail.setElements(List.of( - new TestGadpTargetAvailable(ssAvail, 1, "cat") // + ssAvail.setElements(List.of(new TestGadpTargetAvailable(ssAvail, 1, "cat") // ), "Changed"); waitOn(invL.count.waitValue(2)); @@ -998,9 +977,9 @@ public class GadpClientServerTest { Map, String> actualInv = invL.invocations.stream() .collect(Collectors.toMap(ii -> ID.of(ii.object), ii -> ii.reason)); - Map, String> expectedInv = Map.ofEntries( - Map.entry(ID.of(avail1.get("1")), "Replaced"), - Map.entry(ID.of(avail1.get("2")), "Changed")); + Map, String> expectedInv = + Map.ofEntries(Map.entry(ID.of(avail1.get("1")), "Replaced"), + Map.entry(ID.of(avail1.get("2")), "Changed")); assertEquals(expectedInv, actualInv); } } @@ -1019,36 +998,25 @@ public class GadpClientServerTest { TargetObject echoAvail = waitOn(client.fetchModelObject(PathUtils.parse("Available[1]"))); echoAvail.addListener(attrL); - assertEquals(Map.ofEntries( - Map.entry("pid", 1), - Map.entry("cmd", "echo"), - Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED), - Map.entry("_display", "[1]")), - waitOn(echoAvail.fetchAttributes())); + assertEquals(Map.ofEntries(Map.entry("pid", 1), Map.entry("cmd", "echo"), + Map.entry("_display", "[1]")), waitOn(echoAvail.fetchAttributes())); TestGadpTargetAvailable ssEchoAvail = runner.server.model.session.available.getCachedElements().get("1"); - ssEchoAvail.changeAttributes(List.of("pid"), Map.ofEntries( - Map.entry("cmd", "echo"), - Map.entry("args", "Hello, World!")), + ssEchoAvail.changeAttributes(List.of("pid"), + Map.ofEntries(Map.entry("cmd", "echo"), Map.entry("args", "Hello, World!")), "Changed"); waitOn(attrL.count.waitValue(1)); - assertEquals(Map.ofEntries( - Map.entry("cmd", "echo"), - Map.entry("args", "Hello, World!"), - Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED), - Map.entry("_display", "[1]")), - echoAvail.getCachedAttributes()); + assertEquals(Map.ofEntries(Map.entry("cmd", "echo"), Map.entry("args", "Hello, World!"), + Map.entry("_display", "[1]")), echoAvail.getCachedAttributes()); AttributesChangedInvocation changed = Unique.assertOne(attrL.invocations); assertSame(echoAvail, changed.parent); assertEquals(Set.of("pid"), Set.copyOf(changed.removed)); - assertEquals(Map.ofEntries( - Map.entry("args", "Hello, World!")), - changed.added); + assertEquals(Map.ofEntries(Map.entry("args", "Hello, World!")), changed.added); } } @@ -1063,8 +1031,7 @@ public class GadpClientServerTest { runner.server.getLocalAddress())); waitOn(client.connect()); - TargetObject availCont = - waitOn(client.fetchModelObject(PathUtils.parse("Available"))); + TargetObject availCont = waitOn(client.fetchModelObject(PathUtils.parse("Available"))); availCont.addListener(invL); for (TargetObject avail : waitOn(availCont.fetchElements()).values()) { avail.addListener(invL); @@ -1083,10 +1050,9 @@ public class GadpClientServerTest { assertEquals(Gadp.RootMessage.newBuilder() .setEventNotification(Gadp.EventNotification.newBuilder() - .setPath(Gadp.Path.newBuilder() - .addAllE(List.of("Available"))) - .setObjectInvalidateEvent(Gadp.ObjectInvalidateEvent.newBuilder() - .setReason("Clear"))) + .setPath(Gadp.Path.newBuilder().addAllE(List.of("Available"))) + .setObjectInvalidateEvent( + Gadp.ObjectInvalidateEvent.newBuilder().setReason("Clear"))) .build(), client.getMessageChannel().record.get(0).assertReceived()); } @@ -1107,21 +1073,13 @@ public class GadpClientServerTest { waitOn(client.fetchModelObject(PathUtils.parse("Available[1]"))); // TODO: This comes back null too often... echoAvail.addListener(attrL); - assertEquals(Map.ofEntries( - Map.entry("pid", 1), - Map.entry("cmd", "echo"), - Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED), - Map.entry("_display", "[1]")), - waitOn(echoAvail.fetchAttributes())); + assertEquals(Map.ofEntries(Map.entry("pid", 1), Map.entry("cmd", "echo"), + Map.entry("_display", "[1]")), waitOn(echoAvail.fetchAttributes())); TargetObject ddAvail = waitOn(client.fetchModelObject(PathUtils.parse("Available[2]"))); ddAvail.addListener(attrL); - assertEquals(Map.ofEntries( - Map.entry("pid", 2), - Map.entry("cmd", "dd"), - Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED), - Map.entry("_display", "[2]")), - waitOn(ddAvail.fetchAttributes())); + assertEquals(Map.ofEntries(Map.entry("pid", 2), Map.entry("cmd", "dd"), + Map.entry("_display", "[2]")), waitOn(ddAvail.fetchAttributes())); // NB: copy Map ssAvail = @@ -1130,21 +1088,18 @@ public class GadpClientServerTest { runner.server.model.session.available.changeElements(List.of("1"), List.of(), Map.of(), "1 is Gone"); // Should produce nothing - (ssAvail.get("1")).changeAttributes(List.of(), Map.ofEntries( - Map.entry("args", "Hello, World!")), - "Changed"); + (ssAvail.get("1")).changeAttributes(List.of(), + Map.ofEntries(Map.entry("args", "Hello, World!")), "Changed"); // Produce something, so we know we didn't get the other thing - (ssAvail.get("2")).changeAttributes(List.of(), List.of(), Map.of( - "args", "if=/dev/null"), - "Observe"); + (ssAvail.get("2")).changeAttributes(List.of(), List.of(), + Map.of("args", "if=/dev/null"), "Observe"); waitOn(attrL.count.waitValue(1)); AttributesChangedInvocation changed = Unique.assertOne(attrL.invocations); assertSame(ddAvail, changed.parent); assertEquals(Set.of(), Set.copyOf(changed.removed)); - assertEquals(Map.of( - "args", "if=/dev/null" // + assertEquals(Map.of("args", "if=/dev/null" // ), changed.added); } } @@ -1160,8 +1115,7 @@ public class GadpClientServerTest { runner.server.model.session.addLinks(); TargetObject canonical = waitOn(client.fetchModelObject(PathUtils.parse("Available[2]"))); - TargetObject link = - waitOn(client.fetchModelObject(PathUtils.parse("Links[1]"))); + TargetObject link = waitOn(client.fetchModelObject(PathUtils.parse("Links[1]"))); assertSame(canonical, link); assertEquals(PathUtils.parse("Available[2]"), link.getPath()); waitOn(client.close()); @@ -1183,8 +1137,7 @@ public class GadpClientServerTest { TargetObject linkVal = (TargetObject) waitOn(client.fetchModelValue(PathUtils.parse("Links[1]"))); assertTrue(linkVal instanceof TargetObject); - TargetObject linkObj = - waitOn(client.fetchModelObject(PathUtils.parse("Links[1]"))); + TargetObject linkObj = waitOn(client.fetchModelObject(PathUtils.parse("Links[1]"))); assertSame(linkVal, linkObj); TargetObject canonical = waitOn(client.fetchModelObject(PathUtils.parse("Available[2]"))); @@ -1302,28 +1255,24 @@ public class GadpClientServerTest { root.changeAttributes(List.of(), List.of(a), Map.of(), "Because"); runner.server.model.addModelRoot(root); waitOn(client.fetchModelRoot()); + waitOn(client.flushEvents()); - assertEquals(List.of( - new CallEntry("created", List.of( - client.getModelRoot())), - new CallEntry("attributesChanged", List.of( - client.getModelRoot(), Set.of(), Map.ofEntries( - Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED), - Map.entry("_display", "")))), - new CallEntry("created", List.of( - client.getModelObject(PathUtils.parse("a")))), - new CallEntry("attributesChanged", List.of( - client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries( - Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED), - Map.entry("_display", "a")))), - new CallEntry("attributesChanged", List.of( - client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries( - Map.entry("test", 6)))), - new CallEntry("attributesChanged", List.of( - client.getModelRoot(), Set.of(), Map.ofEntries( - Map.entry("a", client.getModelObject(PathUtils.parse("a")))))), - new CallEntry("rootAdded", List.of(client.getModelRoot()))), - listener.record); + assertEquals(List.of(new CallEntry("created", List.of(client.getModelRoot())), + new CallEntry("attributesChanged", + List.of(client.getModelRoot(), Set.of(), + Map.ofEntries(Map.entry("_display", "")))), + new CallEntry("created", List.of(client.getModelObject(PathUtils.parse("a")))), + new CallEntry("attributesChanged", + List.of(client.getModelObject(PathUtils.parse("a")), Set.of(), + Map.ofEntries(Map.entry("_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); } } @@ -1355,27 +1304,22 @@ public class GadpClientServerTest { waitOn(client.fetchModelRoot()); waitOn(client.flushEvents()); - assertEquals(List.of( - new CallEntry("created", List.of( - client.getModelRoot())), - new CallEntry("attributesChanged", List.of( - client.getModelRoot(), Set.of(), Map.ofEntries( - Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED), - Map.entry("_display", "")))), - new CallEntry("created", List.of( - client.getModelObject(PathUtils.parse("a")))), - new CallEntry("attributesChanged", List.of( - client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries( - Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED), - Map.entry("_display", "a")))), - new CallEntry("attributesChanged", List.of( - client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries( - Map.entry("test", 6)))), - new CallEntry("attributesChanged", List.of( - client.getModelRoot(), Set.of(), Map.ofEntries( - Map.entry("a", client.getModelObject(PathUtils.parse("a")))))), - new CallEntry("rootAdded", List.of(client.getModelRoot()))), - listener.record); + assertEquals(List.of(new CallEntry("created", List.of(client.getModelRoot())), + new CallEntry("attributesChanged", + List.of(client.getModelRoot(), Set.of(), + Map.ofEntries(Map.entry("_display", "")))), + new CallEntry("created", List.of(client.getModelObject(PathUtils.parse("a")))), + new CallEntry("attributesChanged", + List.of(client.getModelObject(PathUtils.parse("a")), Set.of(), + Map.ofEntries(Map.entry("_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); } } @@ -1408,27 +1352,22 @@ public class GadpClientServerTest { waitOn(client.fetchModelRoot()); waitOn(client.flushEvents()); - assertEquals(List.of( - new CallEntry("created", List.of( - client.getModelRoot())), + assertEquals(List.of(new CallEntry("created", List.of(client.getModelRoot())), new CallEntry("attributesChanged", List.of( // Defaults upon client-side construction - client.getModelRoot(), Set.of(), Map.ofEntries( - Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED), - Map.entry("_display", "")))), - new CallEntry("created", List.of( - client.getModelObject(PathUtils.parse("a")))), + client.getModelRoot(), Set.of(), + Map.ofEntries(Map.entry("_display", "")))), + new CallEntry("created", List.of(client.getModelObject(PathUtils.parse("a")))), new CallEntry("attributesChanged", List.of( // Defaults - client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries( - Map.entry("_update_mode", TargetUpdateMode.UNSOLICITED), - Map.entry("_display", "a")))), - new CallEntry("attributesChanged", List.of( - client.getModelObject(PathUtils.parse("a")), Set.of(), Map.ofEntries( - Map.entry("test", 6)))), - new CallEntry("attributesChanged", List.of( - client.getModelRoot(), Set.of(), Map.ofEntries( - Map.entry("a", client.getModelObject(PathUtils.parse("a")))))), - new CallEntry("rootAdded", List.of(client.getModelRoot()))), - listener.record); + client.getModelObject(PathUtils.parse("a")), Set.of(), + Map.ofEntries(Map.entry("_display", "a")))), + new CallEntry("attributesChanged", + List.of(client.getModelObject(PathUtils.parse("a")), Set.of(), + Map.ofEntries(Map.entry("test", 6)))), + new CallEntry("attributesChanged", + List.of(client.getModelRoot(), Set.of(), + Map.ofEntries( + Map.entry("a", client.getModelObject(PathUtils.parse("a")))))), + new CallEntry("rootAdded", List.of(client.getModelRoot()))), listener.record); } } } diff --git a/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/client/GadpClientTest.java b/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/client/GadpClientTest.java index a45ceec022..9656777ba5 100644 --- a/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/client/GadpClientTest.java +++ b/Ghidra/Debug/Debugger-gadp/src/test/java/ghidra/dbg/gadp/client/GadpClientTest.java @@ -38,8 +38,8 @@ import ghidra.dbg.gadp.GadpVersion; import ghidra.dbg.gadp.protocol.Gadp; import ghidra.dbg.gadp.util.AsyncProtobufMessageChannel; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.util.ElementsChangedListener; -import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation; +import ghidra.dbg.testutil.ElementsChangedListener; +import ghidra.dbg.testutil.ElementsChangedListener.ElementsChangedInvocation; import ghidra.dbg.util.PathUtils; import ghidra.util.Msg; import ghidra.util.SystemUtilities; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetAttributesContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetAttributesContainer.java index 348012e9f5..1f14b61c34 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetAttributesContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetAttributesContainer.java @@ -22,11 +22,15 @@ import ghidra.dbg.jdi.manager.JdiEventsListenerAdapter; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "AttributesContainer", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(type = Object.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "AttributesContainer", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(type = Object.class) + }, + canonicalContainer = true) public class JdiModelTargetAttributesContainer extends JdiModelTargetObjectImpl implements JdiEventsListenerAdapter { diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetBreakpointContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetBreakpointContainer.java index cb5440bb98..bc94ad9c44 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetBreakpointContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetBreakpointContainer.java @@ -25,7 +25,7 @@ import ghidra.dbg.jdi.manager.JdiCause; import ghidra.dbg.jdi.manager.JdiEventsListenerAdapter; import ghidra.dbg.jdi.manager.breakpoint.JdiBreakpointInfo; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; -import ghidra.dbg.target.TargetBreakpointContainer; +import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.schema.*; import ghidra.program.model.address.AddressRange; @@ -33,15 +33,15 @@ import ghidra.util.datastruct.WeakValueHashMap; @TargetObjectSchemaInfo( name = "BreakpointContainer", - elements = { // - @TargetElementType(type = JdiModelTargetBreakpointSpec.class) // + elements = { + @TargetElementType(type = JdiModelTargetBreakpointSpec.class) }, - attributes = { // - @TargetAttributeType(type = Void.class) // + attributes = { + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class JdiModelTargetBreakpointContainer extends JdiModelTargetObjectImpl implements - TargetBreakpointContainer, JdiEventsListenerAdapter { + TargetBreakpointSpecContainer, JdiEventsListenerAdapter { protected static final TargetBreakpointKindSet SUPPORTED_KINDS = TargetBreakpointKindSet.of(TargetBreakpointKind.values()); @@ -82,7 +82,7 @@ public class JdiModelTargetBreakpointContainer extends JdiModelTargetObjectImpl @Override public CompletableFuture placeBreakpoint(AddressRange range, Set kinds) { - if (kinds.contains(TargetBreakpointKind.SOFTWARE)) { + if (kinds.contains(TargetBreakpointKind.SW_EXECUTE)) { Location location = impl.getLocation(range.getMinAddress()); JdiModelTargetLocation targetLocation = (JdiModelTargetLocation) getTargetObject(location); @@ -100,14 +100,14 @@ public class JdiModelTargetBreakpointContainer extends JdiModelTargetObjectImpl Set kinds) { JdiModelTargetObject targetObject = getTargetObject(expression); if (targetObject != null) { - if (kinds.contains(TargetBreakpointKind.SOFTWARE) && + if (kinds.contains(TargetBreakpointKind.SW_EXECUTE) && targetObject instanceof JdiModelTargetLocation) { JdiModelTargetLocation targetLocation = (JdiModelTargetLocation) targetObject; JdiBreakpointInfo info = targetLocation.addBreakpoint(); breakpointCreated(info, JdiCause.Causes.UNCLAIMED); } if ((kinds.contains(TargetBreakpointKind.READ) || - kinds.contains(TargetBreakpointKind.EXECUTE)) && + kinds.contains(TargetBreakpointKind.HW_EXECUTE)) && targetObject instanceof JdiModelTargetField && targetVM.vm.canWatchFieldAccess()) { JdiModelTargetField targetField = (JdiModelTargetField) targetObject; JdiBreakpointInfo info = targetField.addAccessWatchpoint(); diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetBreakpointSpec.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetBreakpointSpec.java index 8510766aec..76c3af34b9 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetBreakpointSpec.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetBreakpointSpec.java @@ -20,24 +20,23 @@ import java.util.concurrent.CompletableFuture; import ghidra.dbg.jdi.manager.breakpoint.JdiBreakpointInfo; import ghidra.dbg.jdi.model.iface1.JdiModelTargetDeletable; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.target.TargetBreakpointLocation; import ghidra.dbg.target.TargetBreakpointSpec; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.dbg.target.schema.TargetObjectSchemaInfo; -import ghidra.dbg.util.CollectionUtils.Delta; import ghidra.util.datastruct.ListenerSet; @TargetObjectSchemaInfo( name = "BreakpointSpec", - attributes = { // - @TargetAttributeType( // - name = TargetBreakpointSpec.CONTAINER_ATTRIBUTE_NAME, // - type = JdiModelTargetBreakpointContainer.class), // - @TargetAttributeType( // - name = TargetBreakpointLocation.SPEC_ATTRIBUTE_NAME, // - type = JdiModelTargetBreakpointSpec.class), // - @TargetAttributeType(type = Void.class) // + attributes = { + @TargetAttributeType( + name = TargetBreakpointSpec.CONTAINER_ATTRIBUTE_NAME, + type = JdiModelTargetBreakpointContainer.class), + @TargetAttributeType( + name = TargetBreakpointLocation.SPEC_ATTRIBUTE_NAME, + type = JdiModelTargetBreakpointSpec.class), + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class JdiModelTargetBreakpointSpec extends JdiModelTargetObjectImpl @@ -78,7 +77,7 @@ public class JdiModelTargetBreakpointSpec extends JdiModelTargetObjectImpl protected TargetBreakpointKindSet computeKinds(JdiBreakpointInfo from) { switch (from.getType()) { case BREAKPOINT: - return TargetBreakpointKindSet.of(TargetBreakpointKind.SOFTWARE); + return TargetBreakpointKindSet.of(TargetBreakpointKind.SW_EXECUTE); case MODIFICATION_WATCHPOINT: return TargetBreakpointKindSet.of(TargetBreakpointKind.WRITE); case ACCESS_WATCHPOINT: @@ -111,18 +110,11 @@ public class JdiModelTargetBreakpointSpec extends JdiModelTargetObjectImpl protected void updateAttributesFromInfo(String reason) { boolean enabled = info.isEnabled(); - Delta delta = changeAttributes(List.of(), List.of(), Map.of( // + changeAttributes(List.of(), List.of(), Map.of( // ENABLED_ATTRIBUTE_NAME, enabled, // KINDS_ATTRIBUTE_NAME, kinds = computeKinds(info), // DISPLAY_ATTRIBUTE_NAME, display = getDisplay() // ), reason); - // TODO: These attribute-specific conveniences should be done by DTO. - if (delta.added.containsKey(ENABLED_ATTRIBUTE_NAME)) { - listeners.fire(TargetBreakpointSpecListener.class).breakpointToggled(this, enabled); - } - if (delta.added.containsKey(DISPLAY_ATTRIBUTE_NAME)) { - listeners.fire.displayChanged(this, display); - } } protected CompletableFuture updateInfo(JdiBreakpointInfo oldInfo, diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetClassContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetClassContainer.java index 55e411fc99..2ba22fb6eb 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetClassContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetClassContainer.java @@ -26,11 +26,15 @@ import ghidra.async.AsyncUtils; import ghidra.dbg.target.schema.*; import ghidra.util.Msg; -@TargetObjectSchemaInfo(name = "ClassContainer", elements = { // - @TargetElementType(type = JdiModelTargetReferenceType.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "ClassContainer", + elements = { + @TargetElementType(type = JdiModelTargetReferenceType.class) + }, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetClassContainer extends JdiModelTargetObjectImpl { protected final JdiModelTargetVM vm; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConnector.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConnector.java index 3721b570d9..ee42321606 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConnector.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConnector.java @@ -74,8 +74,7 @@ public class JdiModelTargetConnector extends JdiModelTargetObjectImpl "Default Arguments", cx.defaultArguments(), // "Transport", cx.transport(), // TargetMethod.PARAMETERS_ATTRIBUTE_NAME, - paramDescs = TargetParameterMap.copyOf(computeParameters()), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + paramDescs = TargetParameterMap.copyOf(computeParameters()) // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConstantPool.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConstantPool.java index 30a1fc58c2..0646277be3 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConstantPool.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetConstantPool.java @@ -22,19 +22,21 @@ import java.util.concurrent.CompletableFuture; import ghidra.dbg.target.TargetMemoryRegion; import ghidra.dbg.target.TargetSection; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.program.model.address.AddressRange; @TargetObjectSchemaInfo( name = "ConstantPool", - elements = { // - @TargetElementType(type = Void.class) // + elements = { + @TargetElementType(type = Void.class) }, - attributes = { // - @TargetAttributeType(type = Void.class) // + elementResync = ResyncMode.ONCE, + attributes = { + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class JdiModelTargetConstantPool extends JdiModelTargetObjectImpl implements // - //TargetMemory, + //TargetMemory TargetMemoryRegion, TargetSection { private AddressRange range; @@ -52,8 +54,7 @@ public class JdiModelTargetConstantPool extends JdiModelTargetObjectImpl impleme MODULE_ATTRIBUTE_NAME, parent.getClassType(), // READABLE_ATTRIBUTE_NAME, true, // MEMORY_ATTRIBUTE_NAME, parent, // - TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, range, // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, range // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetElementsContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetElementsContainer.java index d3174902b3..cd2bb4b8c0 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetElementsContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetElementsContainer.java @@ -24,14 +24,20 @@ import ghidra.dbg.jdi.manager.JdiEventsListenerAdapter; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.dbg.util.PathUtils; import ghidra.util.datastruct.WeakValueHashMap; -@TargetObjectSchemaInfo(name = "ElementsContainer", elements = { // - @TargetElementType(type = JdiModelTargetObject.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "ElementsContainer", + elements = { + @TargetElementType(type = JdiModelTargetObject.class) + }, + elementResync = ResyncMode.ONCE, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetElementsContainer extends JdiModelTargetObjectImpl implements JdiEventsListenerAdapter { diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetField.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetField.java index 5c1c4ba9c4..bd345f99d0 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetField.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetField.java @@ -26,12 +26,15 @@ import ghidra.dbg.jdi.manager.breakpoint.JdiBreakpointInfo; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "Field", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(name = "Attributes", type = JdiModelTargetAttributesContainer.class), // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo( + name = "Field", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(name = "Attributes", type = JdiModelTargetAttributesContainer.class), + @TargetAttributeType(type = Void.class) + }) public class JdiModelTargetField extends JdiModelTargetObjectImpl { protected final Field field; @@ -46,8 +49,7 @@ public class JdiModelTargetField extends JdiModelTargetObjectImpl { changeAttributes(List.of(), List.of(), Map.of( // DISPLAY_ATTRIBUTE_NAME, getDisplay(), // - TYPE_ATTRIBUTE_NAME, field.typeName(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + TYPE_ATTRIBUTE_NAME, field.typeName() // ), "Initialized"); } @@ -101,6 +103,7 @@ public class JdiModelTargetField extends JdiModelTargetObjectImpl { return CompletableFuture.completedFuture(null); } + @Override public CompletableFuture init() { return CompletableFuture.completedFuture(null); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetFieldContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetFieldContainer.java index a7b598c644..dc06affded 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetFieldContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetFieldContainer.java @@ -23,12 +23,18 @@ import com.sun.jdi.Field; import ghidra.async.AsyncFence; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; -@TargetObjectSchemaInfo(name = "FieldsContainer", elements = { // - @TargetElementType(type = JdiModelTargetField.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "FieldsContainer", + elements = { + @TargetElementType(type = JdiModelTargetField.class) + }, + elementResync = ResyncMode.ONCE, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetFieldContainer extends JdiModelTargetObjectImpl { // NOTE: -file-list-shared-libraries omits the main module and system-supplied DSO. diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocalVariable.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocalVariable.java index f55454e282..a89400d53c 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocalVariable.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocalVariable.java @@ -23,15 +23,18 @@ import com.sun.jdi.LocalVariable; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "LocalVariable", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(name = "Attributes", type = JdiModelTargetAttributesContainer.class), // - @TargetAttributeType(name = "Generic Signature", type = String.class), // - @TargetAttributeType(name = "Signature", type = String.class), // - @TargetAttributeType(name = "Type", type = String.class, required = true), // - @TargetAttributeType(type = Void.class) // -}) +@TargetObjectSchemaInfo( + name = "LocalVariable", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(name = "Attributes", type = JdiModelTargetAttributesContainer.class), + @TargetAttributeType(name = "Generic Signature", type = String.class), + @TargetAttributeType(name = "Signature", type = String.class), + @TargetAttributeType(name = "Type", type = String.class, required = true), + @TargetAttributeType(type = Void.class) + }) public class JdiModelTargetLocalVariable extends JdiModelTargetObjectImpl { String IS_ARGUMENT_ATTRIBUTE_NAME = "IsArg"; @@ -47,8 +50,7 @@ public class JdiModelTargetLocalVariable extends JdiModelTargetObjectImpl { changeAttributes(List.of(), List.of(), Map.of( // DISPLAY_ATTRIBUTE_NAME, var.name(), // - VISIBLE_TYPE_ATTRIBUTE_NAME, var.typeName(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + VISIBLE_TYPE_ATTRIBUTE_NAME, var.typeName() // ), "Initialized"); } @@ -89,6 +91,7 @@ public class JdiModelTargetLocalVariable extends JdiModelTargetObjectImpl { return CompletableFuture.completedFuture(null); } + @Override public CompletableFuture init() { return CompletableFuture.completedFuture(null); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocalVariableContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocalVariableContainer.java index ed20df1c9e..c1ce69b681 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocalVariableContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocalVariableContainer.java @@ -25,11 +25,15 @@ import ghidra.async.AsyncFence; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "LocalVariableContainer", elements = { // - @TargetElementType(type = JdiModelTargetLocalVariable.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "LocalVariableContainer", + elements = { + @TargetElementType(type = JdiModelTargetLocalVariable.class) + }, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetLocalVariableContainer extends JdiModelTargetObjectImpl { private List vars; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocation.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocation.java index 32f64d102a..2b4cedd44a 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocation.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocation.java @@ -30,15 +30,18 @@ import ghidra.dbg.target.schema.*; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRange; -@TargetObjectSchemaInfo(name = "Location", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(name = "Method", type = String.class, required = true, fixed = true), // - @TargetAttributeType(name = "Line", type = Integer.class, required = true, fixed = true), // - @TargetAttributeType(name = "Index", type = Long.class, required = true, fixed = true), // - @TargetAttributeType(name = "Address", type = String.class, required = true, fixed = true), // - @TargetAttributeType(type = Object.class) // -}) +@TargetObjectSchemaInfo( + name = "Location", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(name = "Method", type = String.class, required = true, fixed = true), + @TargetAttributeType(name = "Line", type = Integer.class, required = true, fixed = true), + @TargetAttributeType(name = "Index", type = Long.class, required = true, fixed = true), + @TargetAttributeType(name = "Address", type = String.class, required = true, fixed = true), + @TargetAttributeType(type = Object.class) // + }) public class JdiModelTargetLocation extends JdiModelTargetObjectImpl { public static String getUniqueId(Location obj) { @@ -63,8 +66,7 @@ public class JdiModelTargetLocation extends JdiModelTargetObjectImpl { "Method", location.method().name(), // "Line", location.lineNumber(), // "Index", location.codeIndex(), // - "Address", Long.toHexString(address.getOffset()), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + "Address", Long.toHexString(address.getOffset()) // ), "Initialized"); } @@ -92,6 +94,7 @@ public class JdiModelTargetLocation extends JdiModelTargetObjectImpl { return CompletableFuture.completedFuture(null); } + @Override public CompletableFuture init() { return CompletableFuture.completedFuture(null); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocationContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocationContainer.java index 0212ee01d5..0da50507bc 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocationContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetLocationContainer.java @@ -25,11 +25,15 @@ import ghidra.async.AsyncFence; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "LocationContainer", elements = { // - @TargetElementType(type = JdiModelTargetLocation.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "LocationContainer", + elements = { + @TargetElementType(type = JdiModelTargetLocation.class) + }, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetLocationContainer extends JdiModelTargetObjectImpl { private List locations; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetMethod.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetMethod.java index f57d9292eb..d5d85d49e1 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetMethod.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetMethod.java @@ -23,12 +23,15 @@ import com.sun.jdi.*; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "Method", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(name = "Attributes", type = JdiModelTargetAttributesContainer.class), // - @TargetAttributeType(type = Object.class) // -}) +@TargetObjectSchemaInfo( + name = "Method", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(name = "Attributes", type = JdiModelTargetAttributesContainer.class), + @TargetAttributeType(type = Object.class) + }) public class JdiModelTargetMethod extends JdiModelTargetObjectImpl { protected final Method method; @@ -46,8 +49,7 @@ public class JdiModelTargetMethod extends JdiModelTargetObjectImpl { this.method = method; changeAttributes(List.of(), List.of(), Map.of( // - DISPLAY_ATTRIBUTE_NAME, getDisplay(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, getDisplay() // ), "Initialized"); } @@ -162,6 +164,7 @@ public class JdiModelTargetMethod extends JdiModelTargetObjectImpl { return CompletableFuture.completedFuture(null); } + @Override public CompletableFuture init() { return CompletableFuture.completedFuture(null); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetMethodContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetMethodContainer.java index 41bbcb07b9..6065963478 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetMethodContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetMethodContainer.java @@ -23,12 +23,18 @@ import com.sun.jdi.Method; import ghidra.async.AsyncFence; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; -@TargetObjectSchemaInfo(name = "TargetMethodContainer", elements = { // - @TargetElementType(type = JdiModelTargetMethod.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "TargetMethodContainer", + elements = { + @TargetElementType(type = JdiModelTargetMethod.class) + }, + elementResync = ResyncMode.ONCE, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetMethodContainer extends JdiModelTargetObjectImpl { protected final JdiModelTargetReferenceType reftype; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModule.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModule.java index 608a61c75d..bf3f4b0605 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModule.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModule.java @@ -23,12 +23,15 @@ import com.sun.jdi.ModuleReference; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "Module", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(name = "UID", type = Long.class, fixed = true), // - @TargetAttributeType(type = Object.class) // -}) +@TargetObjectSchemaInfo( + name = "Module", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(name = "UID", type = Long.class, fixed = true), + @TargetAttributeType(type = Object.class) + }) public class JdiModelTargetModule extends JdiModelTargetObjectReference { public static String getUniqueId(ModuleReference module) { @@ -45,11 +48,11 @@ public class JdiModelTargetModule extends JdiModelTargetObjectReference { this.module = module; changeAttributes(List.of(), List.of(), Map.of( // - DISPLAY_ATTRIBUTE_NAME, getUniqueId(module), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, getUniqueId(module) // ), "Initialized"); } + @Override public CompletableFuture init() { return CompletableFuture.completedFuture(null); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModuleContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModuleContainer.java index 7fcfb6f397..a46bac36f7 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModuleContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetModuleContainer.java @@ -27,16 +27,18 @@ import ghidra.dbg.error.DebuggerUserException; import ghidra.dbg.target.TargetModule; import ghidra.dbg.target.TargetModuleContainer; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.lifecycle.Internal; import ghidra.util.Msg; @TargetObjectSchemaInfo( name = "TargetModuleContainer", - elements = { // - @TargetElementType(type = JdiModelTargetModule.class) // + elements = { + @TargetElementType(type = JdiModelTargetModule.class) }, - attributes = { // - @TargetAttributeType(type = Void.class) // + elementResync = ResyncMode.ONCE, + attributes = { + @TargetAttributeType(type = Void.class) }, canonicalContainer = true) public class JdiModelTargetModuleContainer extends JdiModelTargetObjectImpl diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectImpl.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectImpl.java index 56c6f1584c..6b346ea09f 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectImpl.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectImpl.java @@ -58,8 +58,7 @@ public class JdiModelTargetObjectImpl extends } changeAttributes(List.of(), List.of(), Map.of( // - DISPLAY_ATTRIBUTE_NAME, display = getDisplay(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, display = getDisplay() // ), "Initialized"); } @@ -82,8 +81,7 @@ public class JdiModelTargetObjectImpl extends } changeAttributes(List.of(), List.of(), Map.of( // - DISPLAY_ATTRIBUTE_NAME, display = getDisplay(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, display = getDisplay() // ), "Initialized"); } @@ -170,7 +168,6 @@ public class JdiModelTargetObjectImpl extends changeAttributes(List.of(), List.of(), Map.of( // MODIFIED_ATTRIBUTE_NAME, modified // ), "Refreshed"); - listeners.fire.displayChanged(this, getDisplay()); } } @@ -179,4 +176,10 @@ public class JdiModelTargetObjectImpl extends MODIFIED_ATTRIBUTE_NAME, false // ), "Refreshed"); } + + public TargetObject searchForSuitable(Class type) { + List pathToClass = model.getRootSchema().searchForSuitable(type, path); + return model.getModelObject(pathToClass); + } + } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectReference.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectReference.java index e2192705ea..c3e34143eb 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectReference.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectReference.java @@ -24,12 +24,15 @@ import com.sun.jdi.*; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "ObjectReference", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(name = "UID", type = Long.class, required = true, fixed = true), // - @TargetAttributeType(type = Object.class) // -}) +@TargetObjectSchemaInfo( + name = "ObjectReference", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(name = "UID", type = Long.class, required = true, fixed = true), + @TargetAttributeType(type = Object.class) + }) public class JdiModelTargetObjectReference extends JdiModelTargetValue { private static final long MAX_REFERRERS = 100; @@ -51,8 +54,7 @@ public class JdiModelTargetObjectReference extends JdiModelTargetValue { changeAttributes(List.of(), List.of(), Map.of( // DISPLAY_ATTRIBUTE_NAME, getDisplay(), // - "UID", objref.uniqueID(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + "UID", objref.uniqueID() // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectReferenceContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectReferenceContainer.java index f498b936fd..a7675e3f57 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectReferenceContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetObjectReferenceContainer.java @@ -24,12 +24,18 @@ import com.sun.jdi.ObjectReference; import ghidra.async.AsyncFence; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; -@TargetObjectSchemaInfo(name = "TargetObjectReferenceContainer", elements = { // - @TargetElementType(type = JdiModelTargetObjectReference.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "TargetObjectReferenceContainer", + elements = { + @TargetElementType(type = JdiModelTargetObjectReference.class) + }, + elementResync = ResyncMode.ONCE, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetObjectReferenceContainer extends JdiModelTargetObjectImpl { protected final List refs; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetProcess.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetProcess.java index 95e8fb7693..d17397c305 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetProcess.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetProcess.java @@ -25,7 +25,6 @@ import ghidra.dbg.jdi.model.iface1.JdiModelSelectableObject; import ghidra.dbg.jdi.model.iface1.JdiModelTargetConsole; import ghidra.dbg.target.TargetConsole; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; -import ghidra.dbg.target.TargetInterpreter.TargetInterpreterListener; import ghidra.dbg.target.schema.*; import ghidra.lifecycle.Internal; import ghidra.util.Msg; @@ -68,8 +67,7 @@ public class JdiModelTargetProcess extends JdiModelTargetObjectImpl changeAttributes(List.of(), List.of(), Map.of( // STATE_ATTRIBUTE_NAME, convertState(process.isAlive()), // - DISPLAY_ATTRIBUTE_NAME, getDisplay(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + DISPLAY_ATTRIBUTE_NAME, getDisplay() // ), "Initialized"); } @@ -108,7 +106,7 @@ public class JdiModelTargetProcess extends JdiModelTargetObjectImpl default: throw new AssertionError(); } - listeners.fire(TargetInterpreterListener.class).consoleOutput(this, channel, out); + listeners.fire.consoleOutput(this, channel, out); } private void readStream(InputStream in, TargetConsole.Channel channel) { diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetReferenceType.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetReferenceType.java index 90ba642341..8833ced9f0 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetReferenceType.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetReferenceType.java @@ -78,8 +78,7 @@ public class JdiModelTargetReferenceType extends JdiModelTargetType implements T DISPLAY_ATTRIBUTE_NAME, reftype.name(), // SHORT_DISPLAY_ATTRIBUTE_NAME, reftype.name(), // RANGE_ATTRIBUTE_NAME, new AddressRangeImpl(zero, zero), // - MODULE_NAME_ATTRIBUTE_NAME, reftype.name(), // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + MODULE_NAME_ATTRIBUTE_NAME, reftype.name() // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetRegisterContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetRegisterContainer.java index 153bdbf40a..062562707d 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetRegisterContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetRegisterContainer.java @@ -121,7 +121,7 @@ public class JdiModelTargetRegisterContainer extends JdiModelTargetObjectImpl map.put(retAddr.getIndex(), bytes); } if (!map.isEmpty()) { - listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, map); + listeners.fire.registersUpdated(this, map); } return CompletableFuture.completedFuture(map); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetRoot.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetRoot.java index d7a70fe894..b2d13853bf 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetRoot.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetRoot.java @@ -30,11 +30,7 @@ import ghidra.dbg.jdi.manager.*; import ghidra.dbg.jdi.model.iface1.*; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener; -import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener; -import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; import ghidra.dbg.target.TargetMethod.TargetParameterMap; -import ghidra.dbg.target.TargetObject.TargetUpdateMode; import ghidra.dbg.target.schema.*; import ghidra.util.Msg; @@ -50,26 +46,26 @@ import ghidra.util.Msg; */ @TargetObjectSchemaInfo( name = "Debugger", - elements = { // - @TargetElementType(type = Void.class) // + elements = { + @TargetElementType(type = Void.class) }, - attributes = { // + attributes = { @TargetAttributeType( name = "Attributes", type = JdiModelTargetAttributesContainer.class, required = true, - fixed = true), // + fixed = true), @TargetAttributeType( name = "Connectors", type = JdiModelTargetConnectorContainer.class, required = true, - fixed = true), // + fixed = true), @TargetAttributeType( name = "VirtualMachines", type = JdiModelTargetVMContainer.class, required = true, - fixed = true), // - @TargetAttributeType(type = Void.class) // + fixed = true), + @TargetAttributeType(type = Void.class) }) public class JdiModelTargetRoot extends DefaultTargetModelRoot implements // JdiModelTargetAccessConditioned, // @@ -113,8 +109,7 @@ public class JdiModelTargetRoot extends DefaultTargetModelRoot implements // ), Map.of( // ACCESSIBLE_ATTRIBUTE_NAME, accessible, // DISPLAY_ATTRIBUTE_NAME, display, // - TargetMethod.PARAMETERS_ATTRIBUTE_NAME, TargetCmdLineLauncher.PARAMETERS, // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + TargetMethod.PARAMETERS_ATTRIBUTE_NAME, TargetCmdLineLauncher.PARAMETERS // ), "Initialized"); impl.getManager().addEventsListener(null, this); @@ -188,7 +183,6 @@ public class JdiModelTargetRoot extends DefaultTargetModelRoot implements // ACCESSIBLE_ATTRIBUTE_NAME, accessible // ), "Accessibility changed"); } - listeners.fire(TargetAccessibilityListener.class).accessibilityChanged(this, accessible); } @Override @@ -268,7 +262,6 @@ public class JdiModelTargetRoot extends DefaultTargetModelRoot implements // changeAttributes(List.of(), List.of(), Map.of( // FOCUS_ATTRIBUTE_NAME, focus // ), "Focus changed"); - listeners.fire(TargetFocusScopeListener.class).focusChanged(this, sel); return true; } return false; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetSection.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetSection.java index adfd9af53a..8ee917c73b 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetSection.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetSection.java @@ -35,7 +35,7 @@ import ghidra.program.model.address.AddressRange; @TargetAttributeType(type = Void.class) }) public class JdiModelTargetSection extends JdiModelTargetObjectImpl implements // - //TargetMemory, + //TargetMemory, TargetMemoryRegion, TargetSection { protected final Method method; @@ -52,8 +52,7 @@ public class JdiModelTargetSection extends JdiModelTargetObjectImpl implements / DISPLAY_ATTRIBUTE_NAME, getDisplay(), // MODULE_ATTRIBUTE_NAME, parent.getClassType(), // READABLE_ATTRIBUTE_NAME, true, // - MEMORY_ATTRIBUTE_NAME, parent, TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, range, // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + MEMORY_ATTRIBUTE_NAME, parent, TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, range // ), "Initialized"); } @@ -66,8 +65,7 @@ public class JdiModelTargetSection extends JdiModelTargetObjectImpl implements / changeAttributes(List.of(), List.of(), Map.of( // DISPLAY_ATTRIBUTE_NAME, getDisplay(), // MODULE_ATTRIBUTE_NAME, parent.getClassType(), // - TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, range, // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, range // ), "Initialized"); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetSectionContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetSectionContainer.java index 890abb9d46..74ef0a3d8c 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetSectionContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetSectionContainer.java @@ -125,12 +125,12 @@ public class JdiModelTargetSectionContainer extends JdiModelTargetObjectImpl bytes[i] = (byte) 0xFF; } } - listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, bytes); + listeners.fire.memoryUpdated(this, address, bytes); return CompletableFuture.completedFuture(bytes); } if (addressSpace.equals(impl.getAddressSpace("constantPool"))) { byte[] bytes = constantPool.getPool(); - listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, bytes); + listeners.fire.memoryUpdated(this, address, bytes); return CompletableFuture.completedFuture(bytes); } throw new RuntimeException(); diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetStackFrame.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetStackFrame.java index 80930d3e42..3b3bd67d7f 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetStackFrame.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetStackFrame.java @@ -18,32 +18,23 @@ package ghidra.dbg.jdi.model; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import com.sun.jdi.*; -import ghidra.async.AsyncUtils; -import ghidra.async.TypeSpec; -import ghidra.dbg.DebugModelConventions; import ghidra.dbg.jdi.manager.JdiCause; import ghidra.dbg.jdi.manager.JdiEventsListenerAdapter; import ghidra.dbg.jdi.model.iface1.JdiModelSelectableObject; import ghidra.dbg.jdi.model.iface1.JdiModelTargetFocusScope; +import ghidra.dbg.target.TargetFocusScope; import ghidra.dbg.target.TargetStackFrame; import ghidra.dbg.target.schema.*; import ghidra.program.model.address.Address; import ghidra.util.Msg; -@TargetObjectSchemaInfo( - name = "StackFrame", - elements = { - @TargetElementType(type = Void.class) - }, - attributes = { - @TargetAttributeType(type = Object.class) - }) -public class JdiModelTargetStackFrame extends JdiModelTargetObjectImpl - implements TargetStackFrame, // +@TargetObjectSchemaInfo(name = "StackFrame", elements = { + @TargetElementType(type = Void.class) }, attributes = { + @TargetAttributeType(type = Object.class) }) +public class JdiModelTargetStackFrame extends JdiModelTargetObjectImpl implements TargetStackFrame, // //TargetRegisterBank, // JdiEventsListenerAdapter, // JdiModelSelectableObject { @@ -130,13 +121,7 @@ public class JdiModelTargetStackFrame extends JdiModelTargetObjectImpl @Override public void threadSelected(ThreadReference eventThread, StackFrame eventFrame, JdiCause cause) { if (eventThread.equals(thread.thread) && eventFrame.equals(frame)) { - AtomicReference scope = new AtomicReference<>(); - AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - DebugModelConventions.findSuitable(JdiModelTargetFocusScope.class, this) - .handle(seq::next); - }, scope).then(seq -> { - scope.get().setFocus(this); - }).finish(); + ((JdiModelTargetFocusScope) searchForSuitable(TargetFocusScope.class)).setFocus(this); } } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThread.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThread.java index 3a40880409..e7a80234fc 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThread.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThread.java @@ -17,45 +17,31 @@ package ghidra.dbg.jdi.model; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import com.sun.jdi.*; import com.sun.jdi.event.*; import com.sun.jdi.request.EventRequestManager; import com.sun.jdi.request.StepRequest; -import ghidra.async.*; -import ghidra.dbg.DebugModelConventions; +import ghidra.async.AsyncFence; import ghidra.dbg.jdi.manager.*; import ghidra.dbg.jdi.model.iface1.*; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; +import ghidra.dbg.target.TargetFocusScope; import ghidra.dbg.target.TargetThread; import ghidra.dbg.target.schema.*; import ghidra.lifecycle.Internal; import ghidra.util.Msg; -@TargetObjectSchemaInfo( - name = "Thread", - elements = { // - @TargetElementType(type = Void.class) - }, - attributes = { +@TargetObjectSchemaInfo(name = "Thread", elements = { // + @TargetElementType(type = Void.class) }, attributes = { @TargetAttributeType(name = "Attributes", type = JdiModelTargetAttributesContainer.class), - @TargetAttributeType( - name = "Registers", - type = JdiModelTargetRegisterContainer.class, - required = true, - fixed = true), - @TargetAttributeType( - name = "Stack", - type = JdiModelTargetStack.class, - required = true, - fixed = true), + @TargetAttributeType(name = "Registers", type = JdiModelTargetRegisterContainer.class, required = true, fixed = true), + @TargetAttributeType(name = "Stack", type = JdiModelTargetStack.class, required = true, fixed = true), @TargetAttributeType(name = "Status", type = Integer.class), @TargetAttributeType(name = "UID", type = Long.class, fixed = true), @TargetAttributeType(type = Object.class) // - }, - canonicalContainer = true) +}, canonicalContainer = true) public class JdiModelTargetThread extends JdiModelTargetObjectReference implements // TargetThread, // JdiModelTargetAccessConditioned, // @@ -112,7 +98,6 @@ public class JdiModelTargetThread extends JdiModelTargetObjectReference implemen SUPPORTED_STEP_KINDS_ATTRIBUTE_NAME, SUPPORTED_KINDS, // DISPLAY_ATTRIBUTE_NAME, display = getDisplay() // ), "Initialized"); - listeners.fire(TargetExecutionStateListener.class).executionStateChanged(this, targetState); getManager().addStateListener(thread.virtualMachine(), accessListener); } @@ -277,13 +262,7 @@ public class JdiModelTargetThread extends JdiModelTargetObjectReference implemen @Override public void threadSelected(ThreadReference eventThread, StackFrame frame, JdiCause cause) { if (eventThread.equals(thread) && frame == null) { - AtomicReference scope = new AtomicReference<>(); - AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - DebugModelConventions.findSuitable(JdiModelTargetFocusScope.class, this) - .handle(seq::next); - }, scope).then(seq -> { - scope.get().setFocus(this); - }).finish(); + ((JdiModelTargetFocusScope) searchForSuitable(TargetFocusScope.class)).setFocus(this); } } @@ -303,7 +282,6 @@ public class JdiModelTargetThread extends JdiModelTargetObjectReference implemen changeAttributes(List.of(), List.of(), Map.of( // STATE_ATTRIBUTE_NAME, targetState // ), "Refreshed"); - listeners.fire(TargetExecutionStateListener.class).executionStateChanged(this, targetState); } protected CompletableFuture update() { diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThreadContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThreadContainer.java index d61d15c55b..6e0ac65f02 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThreadContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThreadContainer.java @@ -77,9 +77,8 @@ public class JdiModelTargetThreadContainer extends JdiModelTargetObjectImpl TargetExecutionState targetState = targetThread.convertState(state); targetThread.threadStateChanged(targetState); TargetEventType eventType = getEventType(reason); - getListeners().fire(TargetEventScopeListener.class) - .event(this, targetThread, eventType, - "Thread " + targetThread.getName() + " state changed", List.of(targetThread)); + getListeners().fire.event(this, targetThread, eventType, + "Thread " + targetThread.getName() + " state changed", List.of(targetThread)); } private TargetEventType getEventType(JdiReason reason) { diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThreadGroupContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThreadGroupContainer.java index 80b6d2e131..2e55f4a336 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThreadGroupContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetThreadGroupContainer.java @@ -28,11 +28,15 @@ import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; import ghidra.util.datastruct.WeakValueHashMap; -@TargetObjectSchemaInfo(name = "ThreadGroupContainer", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(type = JdiModelTargetThreadGroupContainer.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "ThreadGroupContainer", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(type = JdiModelTargetThreadGroupContainer.class) + }, + canonicalContainer = true) public class JdiModelTargetThreadGroupContainer extends JdiModelTargetObjectImpl implements JdiEventsListenerAdapter { diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetType.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetType.java index 2f0324c91f..2b03d43ce8 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetType.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetType.java @@ -23,12 +23,16 @@ import com.sun.jdi.Type; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "Type", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(name = "Signature", type = String.class), // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "Type", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(name = "Signature", type = String.class), + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetType extends JdiModelTargetObjectImpl { protected final Type type; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetTypeContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetTypeContainer.java index 6917df0ce9..5e15cb7ecd 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetTypeContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetTypeContainer.java @@ -24,12 +24,18 @@ import com.sun.jdi.Type; import ghidra.async.AsyncFence; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; -@TargetObjectSchemaInfo(name = "TargetTypeContainer", elements = { // - @TargetElementType(type = JdiModelTargetType.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "TargetTypeContainer", + elements = { + @TargetElementType(type = JdiModelTargetType.class) + }, + elementResync = ResyncMode.ONCE, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetTypeContainer extends JdiModelTargetObjectImpl { private List types; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVM.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVM.java index f64936033f..030bbe99ce 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVM.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVM.java @@ -18,7 +18,6 @@ package ghidra.dbg.jdi.model; import java.lang.ProcessHandle.Info; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import com.sun.jdi.PathSearchingVirtualMachine; import com.sun.jdi.VirtualMachine; @@ -27,16 +26,14 @@ import com.sun.jdi.connect.Connector.Argument; import com.sun.jdi.event.*; import com.sun.jdi.request.*; -import ghidra.async.*; -import ghidra.dbg.DebugModelConventions; +import ghidra.async.AsyncFence; import ghidra.dbg.jdi.manager.*; import ghidra.dbg.jdi.manager.impl.JdiManagerImpl; import ghidra.dbg.jdi.model.iface1.*; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.*; import ghidra.dbg.target.TargetMethod.TargetParameterMap; -import ghidra.dbg.target.schema.TargetAttributeType; -import ghidra.dbg.target.schema.TargetObjectSchemaInfo; +import ghidra.dbg.target.schema.*; import ghidra.lifecycle.Internal; /** @@ -46,34 +43,14 @@ import ghidra.lifecycle.Internal; * UI perspective, it doesn't make sense semantically. */ @TargetObjectSchemaInfo(name = "VM", elements = { -// @TargetElementType(type = Void.class) -}, - attributes = { + @TargetElementType(type = Void.class) }, attributes = { @TargetAttributeType(name = "Attributes", type = JdiModelTargetAttributesContainer.class), - @TargetAttributeType( - name = "Breakpoints", - type = JdiModelTargetBreakpointContainer.class, - fixed = true), - @TargetAttributeType( - name = "Classes", - type = JdiModelTargetClassContainer.class, - fixed = true), - @TargetAttributeType( - name = "Modules", - type = JdiModelTargetModuleContainer.class, - fixed = true), - @TargetAttributeType( - name = "Threads", - type = JdiModelTargetThreadContainer.class, - required = true, - fixed = true), - @TargetAttributeType( - name = "ThreadGroups", - type = JdiModelTargetThreadGroupContainer.class, - fixed = true), - @TargetAttributeType(type = Object.class) - }, - canonicalContainer = true) + @TargetAttributeType(name = "Breakpoints", type = JdiModelTargetBreakpointContainer.class, fixed = true), + @TargetAttributeType(name = "Classes", type = JdiModelTargetClassContainer.class, fixed = true), + @TargetAttributeType(name = "Modules", type = JdiModelTargetModuleContainer.class, fixed = true), + @TargetAttributeType(name = "Threads", type = JdiModelTargetThreadContainer.class, required = true, fixed = true), + @TargetAttributeType(name = "ThreadGroups", type = JdiModelTargetThreadGroupContainer.class, fixed = true), + @TargetAttributeType(type = Object.class) }, canonicalContainer = true) public class JdiModelTargetVM extends JdiModelTargetObjectImpl implements // TargetProcess, // TargetAggregate, // @@ -162,11 +139,8 @@ public class JdiModelTargetVM extends JdiModelTargetObjectImpl implements // ARCH_ATTRIBUTE_NAME, vm.name(), // DEBUGGER_ATTRIBUTE_NAME, vm.description(), // OS_ATTRIBUTE_NAME, "JRE " + vm.version(), // - TargetMethod.PARAMETERS_ATTRIBUTE_NAME, TargetCmdLineLauncher.PARAMETERS, // - UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.FIXED // + TargetMethod.PARAMETERS_ATTRIBUTE_NAME, TargetCmdLineLauncher.PARAMETERS // ), "Initialized"); - listeners.fire(TargetExecutionStateListener.class) - .executionStateChanged(this, TargetExecutionState.ALIVE); if (process != null) { changeAttributes(List.of(), List.of( // @@ -297,8 +271,6 @@ public class JdiModelTargetVM extends JdiModelTargetObjectImpl implements // DISPLAY_ATTRIBUTE_NAME, updateDisplay() // ), "Started"); } - listeners.fire(TargetExecutionStateListener.class) - .executionStateChanged(this, TargetExecutionState.ALIVE); vmSelected(vm, JdiCause.Causes.UNCLAIMED); }); } @@ -317,20 +289,12 @@ public class JdiModelTargetVM extends JdiModelTargetObjectImpl implements // DISPLAY_ATTRIBUTE_NAME, updateDisplay() // ), "Exited"); } - listeners.fire(TargetExecutionStateListener.class) - .executionStateChanged(this, TargetExecutionState.TERMINATED); } @Override public void vmSelected(VirtualMachine eventVM, JdiCause cause) { if (eventVM.equals(vm)) { - AtomicReference scope = new AtomicReference<>(); - AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { - DebugModelConventions.findSuitable(JdiModelTargetFocusScope.class, this) - .handle(seq::next); - }, scope).then(seq -> { - scope.get().setFocus(this); - }).finish(); + ((JdiModelTargetFocusScope) searchForSuitable(TargetFocusScope.class)).setFocus(this); } } @@ -338,7 +302,6 @@ public class JdiModelTargetVM extends JdiModelTargetObjectImpl implements // changeAttributes(List.of(), List.of(), Map.of( // STATE_ATTRIBUTE_NAME, targetState // ), reason.desc()); - listeners.fire(TargetExecutionStateListener.class).executionStateChanged(this, targetState); } @Override diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVMContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVMContainer.java index df66948dce..fe65e56ff8 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVMContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetVMContainer.java @@ -26,7 +26,6 @@ import com.sun.jdi.event.*; import ghidra.async.AsyncUtils; import ghidra.dbg.jdi.manager.JdiCause; import ghidra.dbg.jdi.manager.JdiEventsListenerAdapter; -import ghidra.dbg.target.TargetEventScope.TargetEventScopeListener; import ghidra.dbg.target.TargetEventScope.TargetEventType; import ghidra.dbg.target.schema.*; import ghidra.util.Msg; @@ -57,11 +56,8 @@ public class JdiModelTargetVMContainer extends JdiModelTargetObjectImpl // TODO: Move PROCESS_CREATED here to restore proper order of event reporting // Pending some client-side changes to handle architecture selection, though. target.started(vm.name()).thenAccept(__ -> { - session.getListeners() - .fire(TargetEventScopeListener.class) - .event(session, null, TargetEventType.PROCESS_CREATED, - "VM " + vm.name() + " started " + vm.process() + " pid=" + vm.name(), - List.of(vm)); + session.getListeners().fire.event(session, null, TargetEventType.PROCESS_CREATED, + "VM " + vm.name() + " started " + vm.process() + " pid=" + vm.name(), List.of(vm)); }).exceptionally(ex -> { Msg.error(this, "Could not notify vm started", ex); return null; @@ -74,10 +70,8 @@ public class JdiModelTargetVMContainer extends JdiModelTargetObjectImpl public void vmDied(VMDeathEvent event, JdiCause cause) { VirtualMachine vm = event.virtualMachine(); JdiModelTargetVM tgtVM = vmsById.get(vm.name()); - session.getListeners() - .fire(TargetEventScopeListener.class) - .event(session, null, TargetEventType.PROCESS_EXITED, "VM " + vm.name(), - List.of(tgtVM)); + session.getListeners().fire.event(session, null, TargetEventType.PROCESS_EXITED, + "VM " + vm.name(), List.of(tgtVM)); tgtVM.exited(vm); synchronized (this) { vmsById.remove(vm.name()); @@ -105,10 +99,8 @@ public class JdiModelTargetVMContainer extends JdiModelTargetObjectImpl return; } JdiModelTargetThread targetThread = vm.threads.threadCreated(thread); - session.getListeners() - .fire(TargetEventScopeListener.class) - .event(session, targetThread, TargetEventType.THREAD_CREATED, - "Thread " + thread.name() + " started", List.of(targetThread)); + session.getListeners().fire.event(session, targetThread, TargetEventType.THREAD_CREATED, + "Thread " + thread.name() + " started", List.of(targetThread)); } @Override @@ -116,10 +108,8 @@ public class JdiModelTargetVMContainer extends JdiModelTargetObjectImpl ThreadReference thread = event.thread(); JdiModelTargetVM tgtVM = vmsById.get(thread.virtualMachine().name()); JdiModelTargetThread targetThread = tgtVM.threads.threadsById.get(thread.name()); - session.getListeners() - .fire(TargetEventScopeListener.class) - .event(session, targetThread, TargetEventType.THREAD_EXITED, - "Thread " + thread.name() + " exited", List.of(targetThread)); + session.getListeners().fire.event(session, targetThread, TargetEventType.THREAD_EXITED, + "Thread " + thread.name() + " exited", List.of(targetThread)); tgtVM.threads.threadExited(thread); } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValue.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValue.java index d92e16fc94..84ca356af8 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValue.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValue.java @@ -21,11 +21,15 @@ import com.sun.jdi.Value; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; -@TargetObjectSchemaInfo(name = "Value", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "Value", + elements = { + @TargetElementType(type = Void.class) + }, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetValue extends JdiModelTargetObjectImpl { protected final Value value; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValueContainer.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValueContainer.java index d458cbc900..adad690032 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValueContainer.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValueContainer.java @@ -24,12 +24,18 @@ import com.sun.jdi.Value; import ghidra.async.AsyncFence; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; -@TargetObjectSchemaInfo(name = "TargetValueContainer", elements = { // - @TargetElementType(type = JdiModelTargetValue.class) // -}, attributes = { // - @TargetAttributeType(type = Void.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "TargetValueContainer", + elements = { + @TargetElementType(type = JdiModelTargetValue.class) + }, + elementResync = ResyncMode.ONCE, + attributes = { + @TargetAttributeType(type = Void.class) + }, + canonicalContainer = true) public class JdiModelTargetValueContainer extends JdiModelTargetObjectImpl { private List values; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValueMap.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValueMap.java index 11fd515737..a5a1cee241 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValueMap.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/JdiModelTargetValueMap.java @@ -24,12 +24,18 @@ import com.sun.jdi.Value; import ghidra.async.AsyncFence; import ghidra.dbg.jdi.model.iface2.JdiModelTargetObject; import ghidra.dbg.target.schema.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; -@TargetObjectSchemaInfo(name = "TargetValueMap", elements = { // - @TargetElementType(type = Void.class) // -}, attributes = { // - @TargetAttributeType(type = JdiModelTargetValue.class) // -}, canonicalContainer = true) +@TargetObjectSchemaInfo( + name = "TargetValueMap", + elements = { + @TargetElementType(type = Void.class) + }, + elementResync = ResyncMode.ONCE, + attributes = { + @TargetAttributeType(type = JdiModelTargetValue.class) + }, + canonicalContainer = true) public class JdiModelTargetValueMap extends JdiModelTargetObjectImpl { private Map values; diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface1/JdiModelTargetExecutionStateful.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface1/JdiModelTargetExecutionStateful.java index ecd0ee4b24..15f6a8b19c 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface1/JdiModelTargetExecutionStateful.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface1/JdiModelTargetExecutionStateful.java @@ -35,7 +35,6 @@ public interface JdiModelTargetExecutionStateful changeAttributes(List.of(), Map.of( // STATE_ATTRIBUTE_NAME, state // ), reason); - getListeners().fire(TargetExecutionStateListener.class).executionStateChanged(this, state); } } diff --git a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface2/JdiModelTargetObject.java b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface2/JdiModelTargetObject.java index 14b150bc35..708cce55fc 100644 --- a/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface2/JdiModelTargetObject.java +++ b/Ghidra/Debug/Debugger-jpda/src/main/java/ghidra/dbg/jdi/model/iface2/JdiModelTargetObject.java @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import com.sun.jdi.*; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.agent.InvalidatableTargetObjectIf; import ghidra.dbg.jdi.manager.JdiManager; import ghidra.dbg.jdi.model.*; @@ -55,7 +56,7 @@ public interface JdiModelTargetObject extends TargetObject, InvalidatableTargetO public Delta changeAttributes(List remove, Map add, String reason); - public ListenerSet getListeners(); + public ListenerSet getListeners(); public default JdiModelTargetObject getInstance(Mirror object) { JdiModelTargetObject targetObject = getTargetObject(object); diff --git a/Ghidra/Debug/Debugger-jpda/src/test/java/ghidra/dbg/jdi/model/JdiModelTest.java b/Ghidra/Debug/Debugger-jpda/src/test/java/ghidra/dbg/jdi/model/JdiModelTest.java index 225bdbc19c..14ea9e6445 100644 --- a/Ghidra/Debug/Debugger-jpda/src/test/java/ghidra/dbg/jdi/model/JdiModelTest.java +++ b/Ghidra/Debug/Debugger-jpda/src/test/java/ghidra/dbg/jdi/model/JdiModelTest.java @@ -25,7 +25,7 @@ import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.jdi.JdiExperimentsTest; import ghidra.dbg.target.*; import ghidra.dbg.target.TargetMethod.ParameterDescription; -import ghidra.dbg.util.DebuggerModelTestUtils; +import ghidra.dbg.testutil.DebuggerModelTestUtils; import ghidra.dbg.util.PathUtils; import ghidra.util.Msg; diff --git a/Ghidra/Debug/Debugger/garbage/DefaultTraceRecorderPre666Onto741.java b/Ghidra/Debug/Debugger/garbage/DefaultTraceRecorderPre666Onto741.java new file mode 100644 index 0000000000..3e22d51411 --- /dev/null +++ b/Ghidra/Debug/Debugger/garbage/DefaultTraceRecorderPre666Onto741.java @@ -0,0 +1,2598 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.nio.ByteBuffer; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import com.google.common.collect.*; + +import ghidra.app.plugin.core.debug.mapping.*; +import ghidra.app.services.TraceRecorder; +import ghidra.app.services.TraceRecorderListener; +import ghidra.async.*; +import ghidra.async.AsyncLazyMap.KeyedFuture; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AllRequiredAccess; +import ghidra.dbg.DebugModelConventions.SubTreeListenerAdapter; +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.attributes.TargetObjectList; +import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.dbg.error.DebuggerModelAccessException; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.util.*; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.*; +import ghidra.program.model.data.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.symbol.SourceType; +import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.breakpoint.*; +import ghidra.trace.model.data.TraceBasedDataTypeManager; +import ghidra.trace.model.listing.TraceCodeManager; +import ghidra.trace.model.memory.*; +import ghidra.trace.model.modules.*; +import ghidra.trace.model.program.TraceProgramView; +import ghidra.trace.model.stack.*; +import ghidra.trace.model.symbol.*; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.trace.model.time.TraceTimeManager; +import ghidra.util.*; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.datastruct.ListenerSet; +import ghidra.util.exception.DuplicateNameException; +import ghidra.util.exception.InvalidInputException; +import ghidra.util.task.TaskMonitor; + +public class DefaultTraceRecorderOnto741 implements TraceRecorder { + + private static final boolean LOG_STACK_TRACE = false; + // For large memory captures + private static final int BLOCK_SIZE = 4096; + private static final long BLOCK_MASK = -1L << 12; + + static final PathMatcher HARDCODED_MATCHER = new PathMatcher() { + { + // Paths for GDB + addPattern(PathUtils.parse("Breakpoints[].")); + addPattern(PathUtils.parse("Inferiors[].Memory[]")); + addPattern(PathUtils.parse("Inferiors[].Modules[].Sections[]")); + addPattern(PathUtils.parse("Inferiors[].Registers[]")); + addPattern(PathUtils.parse("Inferiors[].Threads[]")); + addPattern(PathUtils.parse("Inferiors[].Threads[].Stack[]")); + + // Paths for dbgeng + addPattern(PathUtils.parse("Sessions[].Processes[].Memory[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Modules[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Registers[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Stack[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Debug.Breakpoints[]")); + + // (Additional) paths for dbgmodel + addPattern(PathUtils.parse("Sessions[].Attributes")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Stack.Frames[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].TTD.Position")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Registers.User.")); + + // Paths for JDI + addPattern(PathUtils.parse("VirtualMachines[]")); + addPattern(PathUtils.parse("VirtualMachines[].Breakpoints")); + addPattern(PathUtils.parse("VirtualMachines[].Classes[]")); + addPattern(PathUtils.parse("VirtualMachines[].Classes[].Sections[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[].Registers[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[].Stack[]")); + + } + }; + + protected static class PermanentTransaction implements AutoCloseable { + static PermanentTransaction start(Trace trace, String description) { + UndoableTransaction tid = null; + try { + tid = UndoableTransaction.start(trace, description, true); + } + catch (Throwable t) { + tid.close(); + return ExceptionUtils.rethrow(t); + } + return new PermanentTransaction(trace, tid); + } + + private final Trace trace; + private final UndoableTransaction tid; + + public PermanentTransaction(Trace trace, UndoableTransaction tid) { + this.trace = trace; + this.tid = tid; + } + + @Override + public void close() { + tid.close(); + trace.clearUndo(); + } + } + + protected final AsyncLazyMap accessibilityByRegBank = + new AsyncLazyMap<>(new HashMap<>(), this::fetchRegAccessibility) { + public AllRequiredAccess remove(TargetRegisterBank key) { + AllRequiredAccess acc = super.remove(key); + if (acc != null) { + acc.removeChangeListener(listenerRegAccChanged); + } + return acc; + } + }; + protected final Map byRegion = new HashMap<>(); + protected final AsyncLazyMap accessibilityByMemory = + new AsyncLazyMap<>(new HashMap<>(), this::fetchMemAccessibility) { + public AllRequiredAccess remove(TargetMemory key) { + AllRequiredAccess acc = super.remove(key); + if (acc != null) { + acc.removeChangeListener(processMemory.memAccListeners.fire); + } + return acc; + } + }; + + protected CompletableFuture fetchRegAccessibility(TargetRegisterBank bank) { + return DebugModelConventions.trackAccessibility(bank).thenApply(acc -> { + acc.addChangeListener(listenerRegAccChanged); + return acc; + }); + } + + protected CompletableFuture fetchMemAccessibility(TargetMemory mem) { + return DebugModelConventions.trackAccessibility(mem).thenApply(acc -> { + acc.addChangeListener(processMemory.memAccListeners.fire); + return acc; + }); + } + + /** + * Get accessible memory, as viewed in the trace + * + * @param pred an additional predicate applied via "AND" with accessibility + * @return the computed set + */ + protected AddressSet getAccessibleMemory(Predicate pred) { + synchronized (accessibilityByMemory) { + // TODO: Might accomplish by using listeners and tracking the accessible set + AddressSet accessible = new AddressSet(); + for (Entry ent : byRegion.entrySet()) { + TargetMemory mem = ent.getValue(); + if (!pred.test(mem)) { + continue; + } + AllRequiredAccess acc = accessibilityByMemory.getCompletedMap().get(mem); + if (acc == null || !acc.getAllAccessibility()) { + continue; + } + accessible.add(memMapper.targetToTrace(ent.getKey().getRange())); + } + return accessible; + } + } + + protected class ComposedMemory { + protected final ComposedMemory chain; + + protected final NavigableMap byMin = new TreeMap<>(); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected final ListenerSet> memAccListeners = + new ListenerSet(TriConsumer.class); + + public ComposedMemory() { + this.chain = null; + } + + public ComposedMemory(ComposedMemory chain) { + this.chain = chain; + } + + protected void addRegion(TargetMemoryRegion region, TargetMemory memory) { + synchronized (accessibilityByMemory) { + TargetMemory old = byRegion.put(region, memory); + assert old == null; + byMin.put(region.getRange().getMinAddress(), region); + accessibilityByMemory.get(memory).exceptionally(e -> { + e = AsyncUtils.unwrapThrowable(e); + Msg.error(this, "Could not track memory accessibility: " + e.getMessage()); + return null; + }); + } + } + + protected boolean removeRegion(TargetObject invalid) { + if (!(invalid instanceof TargetMemoryRegion)) { + return false; + } + synchronized (accessibilityByMemory) { + TargetMemoryRegion invRegion = (TargetMemoryRegion) invalid; + TargetMemory old = byRegion.remove(invRegion); + assert old != null; + byMin.remove(invRegion.getRange().getMinAddress()); + if (!old.isValid() || !byRegion.containsValue(old)) { + accessibilityByMemory.remove(old); + } + return true; + } + } + + protected AllRequiredAccess findChainedMemoryAccess(TargetMemoryRegion region) { + synchronized (accessibilityByMemory) { + TargetMemory mem = byRegion.get(region); + if (mem != null) { + return accessibilityByMemory.getCompletedMap().get(mem); + } + return chain == null ? null : chain.findChainedMemoryAccess(region); + } + } + + protected Entry findChainedFloor(Address address) { + synchronized (accessibilityByMemory) { + Entry myFloor = byMin.floorEntry(address); + Entry byChain = + chain == null ? null : chain.findChainedFloor(address); + if (byChain == null) { + return myFloor; + } + if (myFloor == null) { + return byChain; + } + int c = myFloor.getKey().compareTo(byChain.getKey()); + if (c < 0) { + return byChain; + } + return myFloor; + } + } + + protected AddressRange align(Address address, int length) { + AddressSpace space = address.getAddressSpace(); + long offset = address.getOffset(); + Address start = space.getAddress(offset & BLOCK_MASK); + Address end = space.getAddress(((offset + length - 1) & BLOCK_MASK) + BLOCK_SIZE - 1); + return new AddressRangeImpl(start, end); + } + + protected AddressRange alignWithLimit(Address address, int length, + TargetMemoryRegion limit) { + return align(address, length).intersect(limit.getRange()); + } + + protected AddressRange alignAndLimitToFloor(Address address, int length) { + Entry floor = findChainedFloor(address); + if (floor == null) { + return null; + } + return alignWithLimit(address, length, floor.getValue()); + } + + protected AddressRange alignWithOptionalLimit(Address address, int length, + TargetMemoryRegion limit) { + if (limit == null) { + return alignAndLimitToFloor(address, length); + } + return alignWithLimit(address, length, limit); + } + + protected CompletableFuture readMemory(Address address, int length) { + synchronized (accessibilityByMemory) { + Entry floor = findChainedFloor(address); + if (floor == null) { + throw new IllegalArgumentException( + "address " + address + " is not in any known region"); + } + Address max; + try { + max = address.addNoWrap(length - 1); + } + catch (AddressOverflowException e) { + throw new IllegalArgumentException("read extends beyond the address space"); + } + if (!floor.getValue().getRange().contains(max)) { + throw new IllegalArgumentException("read extends beyond a single region"); + } + TargetMemory mem = byRegion.get(floor.getValue()); + if (mem != null) { + return mem.readMemory(address, length); + } + return CompletableFuture.completedFuture(new byte[0]); + } + } + + protected CompletableFuture writeMemory(Address address, byte[] data) { + synchronized (accessibilityByMemory) { + Entry floor = findChainedFloor(address); + if (floor == null) { + throw new IllegalArgumentException( + "address " + address + " is not in any known region"); + } + Address max; + try { + max = address.addNoWrap(data.length - 1); + } + catch (AddressOverflowException e) { + throw new IllegalArgumentException("read extends beyond the address space"); + } + if (!floor.getValue().getRange().contains(max)) { + throw new IllegalArgumentException("read extends beyond a single region"); + } + TargetMemory mem = byRegion.get(floor.getValue()); + if (mem != null) { + return mem.writeMemory(address, data); + } + throw new IllegalArgumentException("read starts outside any address space"); + } + } + } + + protected static class ThreadMap { + protected final Map byTargetThread = new HashMap<>(); + protected final Map byTraceThread = new HashMap<>(); + + public void put(ThreadRecorder rec) { + byTargetThread.put(rec.targetThread, rec); + byTraceThread.put(rec.traceThread, rec); + } + + public ThreadRecorder getForSuccessor(TargetObject successor) { + while (successor != null) { + ThreadRecorder rec = byTargetThread.get(successor); + if (rec != null) { + return rec; + } + successor = successor.getParent(); + } + return null; + } + + public ThreadRecorder get(TargetThread thread) { + return byTargetThread.get(thread); + } + + public ThreadRecorder get(TargetObject maybeThread) { + return byTargetThread.get(maybeThread); + } + + public ThreadRecorder get(TraceThread thread) { + return byTraceThread.get(thread); + } + + public void remove(ThreadRecorder rec) { + ThreadRecorder rByTarget = byTargetThread.remove(rec.targetThread); + ThreadRecorder rByTrace = byTraceThread.remove(rec.traceThread); + assert rec == rByTarget; + assert rec == rByTrace; + } + + public Collection recorders() { + return byTargetThread.values(); + } + } + + protected static AddressSetView expandToBlocks(AddressSetView asv) { + AddressSet result = new AddressSet(); + // Not terribly efficient, but this is one range most of the time + for (AddressRange range : asv) { + AddressSpace space = range.getAddressSpace(); + Address min = space.getAddress(range.getMinAddress().getOffset() & BLOCK_MASK); + Address max = space.getAddress(range.getMaxAddress().getOffset() | ~BLOCK_MASK); + result.add(new AddressRangeImpl(min, max)); + } + return result; + } + + protected static AddressRange range(Address min, Integer length) { + if (length == null) { + length = 1; + } + try { + return new AddressRangeImpl(min, length); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + } + + protected static String nameBreakpoint(TargetBreakpointLocation bpt) { + if (bpt instanceof TargetBreakpointSpec) { + return bpt.getIndex(); + } + return bpt.getSpecification().getIndex() + "." + bpt.getIndex(); + } + + protected static int getFrameLevel(TargetStackFrame frame) { + // TODO: A fair assumption? frames are elements with numeric base-10 indices + return Integer.decode(frame.getIndex()); + } + + protected class ThreadRecorder { + protected final TargetThread targetThread; + protected final TraceThread traceThread; + protected DebuggerRegisterMapper regMapper; + protected TargetRegister pcReg; + protected TargetRegister spReg; + protected Map regs = new HashMap<>(); + protected NavigableMap stack = + Collections.synchronizedNavigableMap(new TreeMap<>()); + protected final ComposedMemory threadMemory = new ComposedMemory(processMemory); + protected TargetBreakpointContainer threadBreakpointContainer; + protected TargetExecutionState state = TargetExecutionState.ALIVE; + + protected ThreadRecorder(TargetThread targetThread, TraceThread traceThread) { + this.targetThread = targetThread; + this.traceThread = traceThread; + + if (targetThread instanceof TargetExecutionStateful) { + TargetExecutionStateful stateful = (TargetExecutionStateful) targetThread; + state = stateful.getExecutionState(); + } + } + + protected synchronized CompletableFuture initRegMapper( + TargetRegisterContainer registers) { + /** + * TODO: At the moment, this assumes the recorded thread has one register container, or + * at least that all register banks in the thread use the same register container + * (descriptors). If this becomes a problem, then we'll need to keep a separate register + * mapper per register container. This would likely also require some notion of multiple + * languages in the mapper (seems an unlikely design choice). NOTE: In cases where a + * single process may (at least appear to) execute multiple languages, the model should + * strive to present the registers of the physical machine, as they are most likely + * uniform across the process, not those being emulated in the moment. In cases where an + * abstract machine is involved, it is probably more fitting to present separate + * containers (likely provided by separate models) than to present both the physical and + * abstract machine in the same target. + * + *

+ * TODO: Should I formalize that only one register container is present in a recorded + * thread? This seems counter to the model's flexibility. Traces allow polyglot + * disassembly, but not polyglot register spaces. + */ + /*if (regMapper != null) { + return AsyncUtils.NIL; + }*/ + return regMappers.get(registers).thenAccept(rm -> { + synchronized (this) { + regMapper = rm; + Language language = trace.getBaseLanguage(); + pcReg = regMapper.traceToTarget(language.getProgramCounter()); + spReg = regMapper.traceToTarget(trace.getBaseCompilerSpec().getStackPointer()); + extraRegs = new LinkedHashSet<>(); + for (String rn : mapper.getExtraRegNames()) { + Register traceReg = language.getRegister(rn); + if (traceReg == null) { + Msg.error(this, + "Mapper's extra register '" + rn + "' is not in the language!"); + continue; + } + TargetRegister targetReg = regMapper.traceToTarget(traceReg); + if (targetReg == null) { + Msg.error(this, + "Mapper's extra register '" + traceReg + "' is not mappable!"); + continue; + } + extraRegs.add(targetReg); + } + } + listenerForRecord.retroOfferRegMapperDependents(); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize register mapper", ex); + return null; + }); + } + + protected void regMapperAmended(DebuggerRegisterMapper rm, TargetRegister reg, + boolean removed) { + boolean doUpdateRegs = false; + String name = reg.getIndex(); + synchronized (this) { + if (regMapper != rm) { + return; + } + TargetRegister newPcReg = + regMapper.traceToTarget(trace.getBaseLanguage().getProgramCounter()); + if (pcReg != newPcReg) { + pcReg = newPcReg; + doUpdateRegs |= pcReg != null; + } + TargetRegister newSpReg = + regMapper.traceToTarget(trace.getBaseCompilerSpec().getStackPointer()); + if (spReg != newSpReg) { + spReg = newSpReg; + doUpdateRegs |= spReg != null; + } + if (mapper.getExtraRegNames().contains(name)) { + if (removed) { + extraRegs.remove(reg); + } + else { + extraRegs.add(reg); + } + doUpdateRegs = true; + } + } + if (removed) { + return; + } + TargetRegisterBank bank = regs.get(0); + if (bank != null) { + byte[] cachedVal = bank.getCachedRegisters().get(name); + if (cachedVal != null) { + recordRegisterValues(bank, Map.of(name, cachedVal)); + } + if (doUpdateRegs) { + updateRegsMem(null); + } + } + // TODO: This may be too heavy-handed + // listenerForRecord.retroOfferRegMapperDependents(); + } + + protected int getSuccessorFrameLevel(TargetObject successor) { + NavigableSet observedPathLengths = new TreeSet<>(); + for (TargetStackFrame frame : stack.values()) { + observedPathLengths.add(frame.getPath().size()); + } + List path = successor.getPath(); + for (int l : observedPathLengths.descendingSet()) { + if (l > path.size()) { + continue; + } + List sub = path.subList(0, l); + if (!PathUtils.isIndex(sub)) { + continue; + } + int index = Integer.decode(PathUtils.getIndex(sub)); + TargetStackFrame frame = stack.get(index); + if (frame == null || !Objects.equals(sub, frame.getPath())) { + continue; + } + return index; + } + return 0; + } + + CompletableFuture doFetchAndInitRegMapper(TargetRegisterBank bank) { + int frameLevel = getSuccessorFrameLevel(bank); + TargetRegisterContainer descs = bank.getDescriptions(); + if (descs == null) { + Msg.error(this, "Cannot create mapper, yet: Descriptions is null."); + return AsyncUtils.NIL; + } + return initRegMapper(descs).thenAccept(__ -> { + if (frameLevel == 0) { + recordRegisterValues(bank, bank.getCachedRegisters()); + updateRegsMem(null); + } + listeners.fire.registerBankMapped(DefaultTraceRecorder.this); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize register mapper", ex); + return null; + }); + } + + protected void offerRegisters(TargetRegisterBank newRegs) { + int frameLevel = getSuccessorFrameLevel(newRegs); + if (regs.isEmpty()) { + // TODO: Technically, each frame may need its own mapper.... + doFetchAndInitRegMapper(newRegs); + } + + TargetRegisterBank oldRegs = regs.put(frameLevel, newRegs); + if (oldRegs == newRegs) { + return; + } + + synchronized (accessibilityByRegBank) { + if (oldRegs != null) { + accessibilityByRegBank.remove(oldRegs); + } + accessibilityByRegBank.get(newRegs).exceptionally(e -> { + e = AsyncUtils.unwrapThrowable(e); + Msg.error(this, "Could not track register accessibility: " + e.getMessage()); + return null; + }); + } + } + + protected void offerStackFrame(TargetStackFrame frame) { + stack.put(getFrameLevel(frame), frame); + recordFrame(frame); + } + + protected void offerThreadRegion(TargetMemoryRegion region) { + TargetMemory mem = region.getMemory(); + threadMemory.addRegion(region, mem); + initMemMapper(mem); + // TODO: Add region to trace memory manager (when allowed for threads) + updateRegsMem(region).exceptionally(ex -> { + Msg.error(this, "Could not add thread memory region", ex); + return null; + }); + } + + protected void offerThreadBreakpointContainer(TargetBreakpointContainer bc) { + if (threadBreakpointContainer != null) { + Msg.warn(this, "Thread already has a breakpoint container"); + } + threadBreakpointContainer = bc; + } + + /** + * Inform the recorder the given object is no longer valid + * + * @param invalid the invalidated object + * @return true if this recorder should be invalidated, too + */ + protected synchronized boolean objectRemoved(TargetObject invalid) { + if (checkThreadRemoved(invalid)) { + return true; + } + if (checkRegistersRemoved(invalid)) { + //return false; + // Regs could also be a stack frame + } + if (checkStackFrameRemoved(invalid)) { + return false; + } + if (threadMemory.removeRegion(invalid)) { + return false; + } + Msg.trace(this, "Ignored removed object: " + invalid); + return false; + } + + protected boolean checkThreadRemoved(TargetObject invalid) { + if (targetThread == invalid) { + threadDestroyed(); + return true; + } + return false; + } + + protected boolean checkRegistersRemoved(TargetObject invalid) { + synchronized (accessibilityByRegBank) { + if (regs.values().remove(invalid)) { + accessibilityByRegBank.remove((TargetRegisterBank) invalid); + return true; + } + return false; + } + } + + protected boolean checkStackFrameRemoved(TargetObject invalid) { + if (stack.values().remove(invalid)) { + popStack(); + return true; + } + return false; + } + + protected Address pcFromStack() { + TargetStackFrame frame = stack.get(0); + if (frame == null) { + return null; + } + return frame.getProgramCounter(); + } + + protected boolean checkReadCondition(Address traceAddress) { + /** + * TODO: This heuristic doesn't really belong here, but I have to implement it here so + * that it doesn't "override" the listing's implementation. Once watches are + * implemented, we should be able to drop this garbage. + */ + TraceMemoryRegion region = + memoryManager.getRegionContaining(snapshot.getKey(), traceAddress); + if (region == null) { + return false; + } + if (region.isWrite()) { + return true; + } + Entry ent = + memoryManager.getMostRecentStateEntry(snapshot.getKey(), traceAddress); + if (ent == null) { + return true; + } + if (ent.getValue() == TraceMemoryState.KNOWN) { + return false; + } + return true; + } + + protected CompletableFuture readAlignedConditionally(String name, Address targetAddress, + TargetMemoryRegion limit) { + if (targetAddress == null) { + return AsyncUtils.NIL; + } + Address traceAddress = memMapper.targetToTrace(targetAddress); + if (traceAddress == null) { + return AsyncUtils.NIL; + } + if (!checkReadCondition(traceAddress)) { + return AsyncUtils.NIL; + } + AddressRange targetRange = threadMemory.alignWithOptionalLimit(targetAddress, 1, limit); + if (targetRange == null) { + return AsyncUtils.NIL; + } + TimedMsg.info(this, + " Reading memory at " + name + " (" + targetAddress + " -> " + targetRange + ")"); + // NOTE: Recorder takes data via memoryUpdated callback + // TODO: In that callback, sort out process memory from thread memory? + return threadMemory + .readMemory(targetRange.getMinAddress(), (int) targetRange.getLength()) + .exceptionally(ex -> { + Msg.error(this, "Could not read memory at " + name, ex); + return null; + }); + } + + Address registerValueToTargetAddress(TargetRegister reg, byte[] value) { + /** + * TODO: This goes around the horn and back just to select a default address space. We + * should really just go directly to target address space. + */ + RegisterValue rv = regMapper.targetToTrace(reg, value); + if (rv == null) { + return null; + } + Address traceAddress = trace.getBaseLanguage() + .getDefaultSpace() + .getAddress(rv.getUnsignedValue().longValue()); + return memMapper.traceToTarget(traceAddress); + } + + protected CompletableFuture updateRegsMem(TargetMemoryRegion limit) { + TargetRegisterBank bank; + TargetRegister pc; + TargetRegister sp; + Set toRead = new LinkedHashSet<>(); + synchronized (DefaultTraceRecorder.this) { + if (regMapper == null) { + return AsyncUtils.NIL; + } + bank = regs.get(0); + pc = pcReg; + sp = spReg; + toRead.addAll(extraRegs); + toRead.add(sp); + toRead.add(pc); + } + if (bank == null || pc == null || sp == null) { + return AsyncUtils.NIL; + } + TimedMsg.info(this, "Reading " + toRead + " of " + targetThread); + return bank.readRegisters(toRead).thenCompose(vals -> { + synchronized (DefaultTraceRecorder.this) { + if (memMapper == null) { + return AsyncUtils.NIL; + } + } + if (threadMemory == null) { + return AsyncUtils.NIL; + } + AsyncFence fence = new AsyncFence(); + + Address pcTargetAddr = pcFromStack(); + if (pcTargetAddr == null) { + pcTargetAddr = registerValueToTargetAddress(pcReg, vals.get(pcReg.getIndex())); + } + fence.include(readAlignedConditionally("PC", pcTargetAddr, limit)); + + Address spTargetAddr = + registerValueToTargetAddress(spReg, vals.get(spReg.getIndex())); + fence.include(readAlignedConditionally("SP", spTargetAddr, limit)); + + return fence.ready(); + }).exceptionally(ex -> { + if (LOG_STACK_TRACE) { + Msg.error(this, "Could not read registers", ex); + } + else { + Msg.error(this, "Could not read registers"); + } + return null; + }); + } + + public void stateChanged(final TargetExecutionState newState) { + if (newState == TargetExecutionState.STOPPED) { + updateRegsMem(null); + } + state = newState; + } + + public void threadDestroyed() { + String path = PathUtils.toString(targetThread.getPath()); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, path + " destroyed")) { + // TODO: Should it be key - 1 + // Perhaps, since the thread should not exist + // But it could imply earlier destruction than actually observed + traceThread.setDestructionSnap(snapshot.getKey()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be shrinking + } + } + + public void recordRegisterValues(TargetObject bank, Map updates) { + synchronized (DefaultTraceRecorder.this) { + if (regMapper == null) { + return; + } + } + int frameLevel = getSuccessorFrameLevel(bank); + TimedMsg.info(this, "Reg values changed: " + updates.keySet()); + try (PermanentTransaction tid = PermanentTransaction.start(trace, + "Registers changed in " + PathUtils.toString(bank.getPath()))) { + TraceMemoryRegisterSpace regSpace = + memoryManager.getMemoryRegisterSpace(traceThread, frameLevel, true); + for (Entry ent : updates.entrySet()) { + RegisterValue rv = regMapper.targetToTrace(ent.getKey(), ent.getValue()); + if (rv == null) { + continue; // mapper does not know this register.... + } + regSpace.setValue(snapshot.getKey(), rv); + if (rv.getRegister() == trace.getBaseLanguage().getProgramCounter() && + pcFromStack() == null) { + Address pcTargetAddr = registerValueToTargetAddress(pcReg, ent.getValue()); + readAlignedConditionally("PC", pcTargetAddr, null); // NB: Reports errors + } + if (rv.getRegister() == trace.getBaseCompilerSpec().getStackPointer()) { + Address spTargetAddr = registerValueToTargetAddress(spReg, ent.getValue()); + readAlignedConditionally("SP", spTargetAddr, null); // NB: Reports errors + } + } + } + } + + public void recordFrame(TargetStackFrame frame) { + recordFrame(frame, frame.getProgramCounter()); + } + + public void doRecordFrame(TraceStack traceStack, int frameLevel, Address pc) { + TraceStackFrame traceFrame = traceStack.getFrame(frameLevel, true); + traceFrame.setProgramCounter(pc); + } + + public void recordFrame(TargetStackFrame frame, Address pc) { + synchronized (DefaultTraceRecorder.this) { + if (memMapper == null) { + return; + } + Address tracePc = pc == null ? null : memMapper.targetToTrace(pc); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Stack frame added")) { + TraceStack traceStack = + stackManager.getStack(traceThread, snapshot.getKey(), true); + doRecordFrame(traceStack, getFrameLevel(frame), tracePc); + } + } + } + + protected int stackDepth() { + return stack.isEmpty() ? 0 : stack.lastKey() + 1; + } + + public void recordStack() { + synchronized (DefaultTraceRecorder.this) { + if (memMapper == null) { + return; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Stack changed")) { + TraceStack traceStack = + stackManager.getStack(traceThread, snapshot.getKey(), true); + traceStack.setDepth(stackDepth(), false); + for (Map.Entry ent : stack.entrySet()) { + Address tracePc = + memMapper.targetToTrace(ent.getValue().getProgramCounter()); + doRecordFrame(traceStack, ent.getKey(), tracePc); + } + } + } + } + + public void popStack() { + synchronized (DefaultTraceRecorder.this) { + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Stack popped")) { + TraceStack traceStack = + stackManager.getStack(traceThread, snapshot.getKey(), true); + traceStack.setDepth(stackDepth(), false); + } + } + } + + public void onThreadBreakpointContainers( + Consumer action) { + if (threadBreakpointContainer == null) { + return; + } + action.accept(threadBreakpointContainer); + } + } + + protected class EffectiveBreakpointResolver { + private final TargetBreakpointLocation bpt; + private TargetBreakpointSpec spec; + private boolean affectsProcess = false; + private final Set threadsAffected = new LinkedHashSet<>(); + + public EffectiveBreakpointResolver(TargetBreakpointLocation bpt) { + this.bpt = bpt; + } + + public CompletableFuture resolve() { + AsyncFence fence = new AsyncFence(); + this.spec = bpt.getSpecification(); + + for (TargetObject ref : bpt.getAffects()) { + if (ref.equals(target)) { + affectsProcess = true; + } + else { + fence.include(resolveThread(ref)); + } + } + return fence.ready(); + } + + // TODO: If affects is empty/null, also try to default to the containing process + private CompletableFuture resolveThread(TargetObject ref) { + return DebugModelConventions.findThread(ref).thenAccept(thread -> { + if (thread == null) { + Msg.error(this, + "Could not find process or thread from breakpoint-affected object: " + ref); + return; + } + if (!ref.equals(thread)) { + Msg.warn(this, "Effective breakpoint should apply to process or threads. Got " + + ref + ". Resolved to " + thread); + return; + } + if (!PathUtils.isAncestor(target.getPath(), thread.getPath())) { + /** + * Perfectly normal if the breakpoint container is outside the process + * container. Don't record such in this trace, though. + */ + return; + } + ThreadRecorder rec = listenerForRecord.getOrCreateThreadRecorder(thread); + synchronized (threadsAffected) { + threadsAffected.add(rec.traceThread); + } + }).exceptionally(ex -> { + Msg.error(this, "Error resolving thread from breakpoint-affected object: " + ref); + return null; + }); + } + + public void applyChecksAndConventions() { + if (affectsProcess && !threadsAffected.isEmpty()) { + Msg.warn(this, "Breakpoint affects process and individual threads?: " + bpt); + threadsAffected.clear(); + } + // Check ancestry for "affects" + if (!affectsProcess && threadsAffected.isEmpty()) { + if (PathUtils.isAncestor(target.getPath(), bpt.getPath())) { + for (ThreadRecorder rec : threadMap.byTargetThread.values()) { + if (PathUtils.isAncestor(rec.targetThread.getPath(), bpt.getPath())) { + threadsAffected.add(rec.traceThread); + break; // Only one thread could be its ancestor + } + } + if (threadsAffected.isEmpty()) { + affectsProcess = true; + } + } + } + } + } + + public class ListenerForRecord extends SubTreeListenerAdapter implements DebuggerModelListener { + + //protected final Map modulesByName = new HashMap<>(); + protected final Set breakpoints = new HashSet<>(); + + @Override + protected boolean checkFire(TargetObject object) { + return true; + } + + @Override + protected boolean checkDescend(TargetObject ref) { + // NOTE, cannot return false on match, since it could be a prefix of another + if (HARDCODED_MATCHER.successorCouldMatch(ref.getPath())) { + return true; + } + return false; + } + + // TODO: Move this into conventions? + protected CompletableFuture findThreadOrProcess(TargetObject successor) { + return new DebugModelConventions.AncestorTraversal(successor) { + @Override + protected Result check(TargetObject obj) { + if (obj.isRoot()) { + return Result.FOUND; + } + if (obj instanceof TargetThread) { + return Result.FOUND; + } + if (obj instanceof TargetProcess) { + return Result.FOUND; + } + return Result.CONTINUE; + } + + @Override + protected TargetObject finish(TargetObject obj) { + return obj; + } + }.start(); + } + + @Override + protected void objectAdded(TargetObject added) { + if (!valid) { + return; + } + if (added instanceof TargetThread) { + getOrCreateThreadRecorder((TargetThread) added); + } + if (added instanceof TargetStack) { + // Actually, this may not matter + } + // Do stack frame first, since bank would be it or child. + // Need frames indexed first to determine level of bank + if (added instanceof TargetStackFrame) { + ThreadRecorder rec = threadMap.getForSuccessor(added); + if (rec == null) { + Msg.error(this, "Frame without thread?: " + added); + } + else { + rec.offerStackFrame((TargetStackFrame) added); + } + } + if (added instanceof TargetRegisterBank) { + ThreadRecorder rec = threadMap.getForSuccessor(added); + if (rec == null) { + Msg.error(this, "Bank without thread?: " + added); + } + else { + rec.offerRegisters((TargetRegisterBank) added); + } + } + if (added instanceof TargetRegisterContainer) { + // These are picked up when a bank is added with these descriptions + } + if (added instanceof TargetRegister) { + TargetRegister reg = (TargetRegister) added; + regMappers.get(reg.getContainer()).thenAccept(rm -> { + rm.targetRegisterAdded(reg); + for (ThreadRecorder rec : threadMap.byTargetThread.values()) { + rec.regMapperAmended(rm, reg, false); + } + }); + } + if (added instanceof TargetMemory) { + initMemMapper((TargetMemory) added); + } + if (added instanceof TargetMemoryRegion) { + TargetMemoryRegion region = (TargetMemoryRegion) added; + findThreadOrProcess(added).thenAccept(obj -> { + if (obj == target) { + offerProcessRegion(region); + return; + } + if (obj instanceof TargetThread) { + ThreadRecorder rec = getOrCreateThreadRecorder((TargetThread) obj); + rec.offerThreadRegion(region); + } + }).exceptionally(ex -> { + Msg.error(this, "Error recording memory region", ex); + return null; + }); + } + if (added instanceof TargetModule) { + TargetModule module = (TargetModule) added; + offerProcessModule(module); + } + if (added instanceof TargetSection) { + TargetSection section = (TargetSection) added; + offerProcessModuleSection(section.getModule(), section); + } + if (added instanceof TargetBreakpointContainer) { + TargetBreakpointContainer breaks = (TargetBreakpointContainer) added; + findThreadOrProcess(added).thenAccept(obj -> { + if (obj == target) { + offerProcessBreakpointContainer(breaks); + return; + } + if (obj.isRoot()) { + return; + } + ThreadRecorder rec = getOrCreateThreadRecorder((TargetThread) obj); + rec.offerThreadBreakpointContainer(breaks); + }).exceptionally(ex -> { + Msg.error(this, "Error recording breakpoint container", ex); + return null; + }); + } + if (added instanceof TargetBreakpointSpec) { + // I don't think this matters. UI for live recording only. + } + if (added instanceof TargetBreakpointLocation) { + TargetBreakpointLocation bpt = (TargetBreakpointLocation) added; + breakpoints.add(bpt); + offerEffectiveBreakpoint(bpt); + } + } + + @Override + protected void objectRemoved(TargetObject removed) { + if (!valid) { + return; + } + if (target == removed) { + stopRecording(); + return; + } + if (removed instanceof TargetRegisterContainer) { + regMappers.remove((TargetRegisterContainer) removed); + } + if (removed instanceof TargetRegister) { + TargetRegister reg = (TargetRegister) removed; + DebuggerRegisterMapper rm = regMappers.getCompletedMap().get(reg.getContainer()); + if (rm == null) { + return; + } + rm.targetRegisterRemoved(reg); + for (ThreadRecorder rec : threadMap.byTargetThread.values()) { + rec.regMapperAmended(rm, reg, true); + } + } + if (removed instanceof TargetMemoryRegion) { + TargetMemoryRegion region = (TargetMemoryRegion) removed; + if (processMemory.removeRegion(region)) { + removeProcessRegion(region); + return; + } + // Allow removal notice to fall through to thread recorders + } + if (removed instanceof TargetModule) { + TargetModule module = (TargetModule) removed; + removeProcessModule(module); + return; + } + if (removed instanceof TargetBreakpointLocation) { + TargetBreakpointLocation bpt = (TargetBreakpointLocation) removed; + breakpoints.remove(bpt); + removeEffectiveBreakpoint(bpt); + return; + } + synchronized (threadMap) { + for (Iterator it = threadMap.recorders().iterator(); it + .hasNext();) { + ThreadRecorder rec = it.next(); + if (rec.objectRemoved(removed)) { + it.remove(); + } + } + } + } + + protected boolean successor(TargetObject ref) { + return PathUtils.isAncestor(target.getPath(), ref.getPath()); + } + + protected boolean anyRef(Collection parameters) { + for (Object p : parameters) { + if (!(p instanceof TargetObject)) { + continue; + } + return true; + } + return false; + } + + protected boolean anySuccessor(Collection parameters) { + for (Object p : parameters) { + if (!(p instanceof TargetObject)) { + continue; + } + TargetObject ref = (TargetObject) p; + if (!successor(ref)) { + continue; + } + return true; + } + return false; + } + + protected boolean eventApplies(TargetObject eventThread, TargetEventType type, + List parameters) { + if (type == TargetEventType.RUNNING) { + return false; + /** + * TODO: Perhaps some configuration for this later. It's kind of interesting to + * record the RUNNING event time, but it gets pedantic when these exist between + * steps. + */ + } + if (eventThread != null) { + return successor(eventThread); + } + if (anyRef(parameters)) { + return anySuccessor(parameters); + } + return true; // Some session-wide event, I suppose + } + + @Override + public void event(TargetObject object, TargetThread eventThread, TargetEventType type, + String description, List parameters) { + if (!valid) { + return; + } + TimedMsg.info(this, "Event: " + type + " thread=" + eventThread + " description=" + + description + " params=" + parameters); + // Just use this to step the snaps. Creation/destruction still handled in add/remove + if (!eventApplies(eventThread, type, parameters)) { + return; + } + ThreadRecorder rec = threadMap.get(eventThread); + createSnapshot(description, rec == null ? null : rec.traceThread, null); + + if (type == TargetEventType.THREAD_CREATED) { + if (rec == null) { + return; + } + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Adjust thread creation", true)) { + rec.traceThread.setCreationSnap(snapshot.getKey()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be shrinking + } + } + else if (type == TargetEventType.MODULE_LOADED) { + Object p0 = parameters.get(0); + if (!(p0 instanceof TargetObject)) { + return; + } + TargetObject obj = (TargetObject) p0; + if (!(obj instanceof TargetModule)) { + return; + } + TargetModule mod = (TargetModule) obj; + TraceModule traceModule = getTraceModule(mod); + if (traceModule == null) { + return; + } + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Adjust module load", true)) { + traceModule.setLoadedSnap(snapshot.getKey()); + } + catch (DuplicateNameException e) { + Msg.error(this, "Could not set module loaded snap", e); + } + } + } + + @Override + public void attributesChanged(TargetObject parent, Collection removed, + Map added) { + super.attributesChanged(parent, removed, added); + if (!valid) { + return; + } + // Dispatch attribute changes which don't have "built-in" events. + if (parent instanceof TargetBreakpointLocation) { + if (added.containsKey(TargetBreakpointLocation.LENGTH_ATTRIBUTE_NAME)) { + breakpointLengthChanged((TargetBreakpointLocation) parent, + (Integer) added.get(TargetBreakpointLocation.LENGTH_ATTRIBUTE_NAME)); + } + } + if (parent instanceof TargetStackFrame) { + if (added.containsKey(TargetStackFrame.PC_ATTRIBUTE_NAME)) { + framePcUpdated((TargetStackFrame) parent); + } + } + if (parent instanceof TargetRegisterBank) { + if (added.containsKey(TargetRegisterBank.DESCRIPTIONS_ATTRIBUTE_NAME)) { + ThreadRecorder rec = threadMap.getForSuccessor(parent); + if (rec != null) { + rec.doFetchAndInitRegMapper((TargetRegisterBank) parent); + } + } + } + // This should be fixed at construction. + /*if (parent instanceof TargetModule) { + if (added.containsKey(TargetModule.BASE_ATTRIBUTE_NAME)) { + moduleBaseUpdated((TargetModule) parent, + (Address) added.get(TargetModule.BASE_ATTRIBUTE_NAME)); + } + }*/ + } + + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + public void executionStateChanged(TargetObject stateful, TargetExecutionState state) { + if (!valid) { + return; + } + TimedMsg.info(this, "State " + state + " for " + stateful); + findThreadOrProcess(stateful).thenAccept(threadOrProcess -> { + if (threadOrProcess == target && state == TargetExecutionState.TERMINATED) { + stopRecording(); + return; + } + ThreadRecorder rec = null; + synchronized (threadMap) { + if (threadOrProcess instanceof TargetThread) { + rec = threadMap.get((TargetThread) threadOrProcess); + } + } + if (rec != null) { + rec.stateChanged(state); + } + // Else we'll discover it and sync state later + }); + } + + protected ThreadRecorder getOrCreateThreadRecorder(TargetThread thread) { + synchronized (threadMap) { + ThreadRecorder rec = threadMap.get(thread); + if (rec != null) { + return rec; + } + TraceThread traceThread; + String path = PathUtils.toString(thread.getPath()); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, path + " created")) { + // Note, if THREAD_CREATED is emitted, it will adjust the creation snap + traceThread = threadManager.createThread(path, thread.getShortDisplay(), + snapshot.getKey()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be a new thread in model + } + rec = new ThreadRecorder(thread, traceThread); + threadMap.put(rec); + return rec; + } + } + + @Override + public void registersUpdated(TargetObject bank, Map updates) { + if (!valid) { + return; + } + ThreadRecorder rec = threadMap.getForSuccessor(bank); + if (rec == null) { + return; + } + rec.recordRegisterValues(bank, updates); + } + + @Override + public void memoryUpdated(TargetObject memory, Address address, byte[] data) { + if (!valid) { + return; + } + synchronized (DefaultTraceRecorder.this) { + if (memMapper == null) { + Msg.warn(this, "Received memory write before a region has been added"); + return; + } + } + Address traceAddr = memMapper.targetToTrace(address); + long snap = snapshot.getKey(); + TimedMsg.info(this, "Memory updated: " + address + " (" + data.length + ")"); + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Memory observed")) { + ByteBuffer newBytes = ByteBuffer.wrap(data); + memoryManager.putBytes(snap, traceAddr, newBytes); + } + } + + @Override + public void memoryReadError(TargetObject memory, AddressRange range, + DebuggerMemoryAccessException e) { + if (!valid) { + return; + } + Msg.error(this, "Error reading range " + range, e); + Address traceMin = memMapper.targetToTrace(range.getMinAddress()); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Memory read error")) { + memoryManager.setState(snapshot.getKey(), traceMin, TraceMemoryState.ERROR); + // TODO: Bookmark to describe error? + } + } + + @AttributeCallback(TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME) + public void breakpointToggled(TargetObject obj, boolean enabled) { + if (!valid) { + return; + } + TargetBreakpointSpec spec = (TargetBreakpointSpec) obj; + spec.getLocations().thenAccept(bpts -> { + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Breakpoint toggled")) { + for (TargetBreakpointLocation eb : bpts) { + TraceBreakpoint traceBpt = getTraceBreakpoint(eb); + if (traceBpt == null) { + String path = PathUtils.toString(eb.getPath()); + Msg.warn(this, "Cannot find toggled trace breakpoint for " + path); + continue; + } + // Verify attributes match? Eh. If they don't, someone has fiddled with it. + traceBpt.splitWithEnabled(snapshot.getKey(), enabled); + } + } + }).exceptionally(ex -> { + Msg.error(this, "Error recording toggled breakpoint spec: " + spec, ex); + return null; + }); + } + + protected void breakpointLengthChanged(TargetBreakpointLocation bpt, int length) { + Address traceAddr = memMapper.targetToTrace(bpt.getAddress()); + String path = PathUtils.toString(bpt.getPath()); + for (TraceBreakpoint traceBpt : breakpointManager.getBreakpointsByPath(path)) { + if (traceBpt.getLength() == length) { + continue; // Nothing to change + } + // TODO: Verify all other attributes match? + // TODO: Should this be allowed to happen? + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Breakpoint length changed")) { + long snap = snapshot.getKey(); + if (traceBpt.getPlacedSnap() == snap) { + traceBpt.delete(); + } + else { + traceBpt.setClearedSnap(snap - 1); + } + breakpointManager.placeBreakpoint(path, snap, range(traceAddr, length), + traceBpt.getThreads(), traceBpt.getKinds(), traceBpt.isEnabled(), + traceBpt.getComment()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Split, and length matters not + } + } + } + + protected void framePcUpdated(TargetStackFrame frame) { + ThreadRecorder rec = threadMap.getForSuccessor(frame); + // Yes, entire stack, otherwise, the stack seems to be just one deep. + rec.recordStack(); + } + + protected void stackUpdated(TargetStack stack) { + ThreadRecorder rec = threadMap.getForSuccessor(stack); + rec.recordStack(); + } + + @AttributeCallback(TargetFocusScope.FOCUS_ATTRIBUTE_NAME) + public void focusChanged(TargetObject object, TargetObject focused) { + if (!valid) { + return; + } + if (PathUtils.isAncestor(target.getPath(), focused.getPath())) { + curFocus = focused; + } + } + + protected void retroOfferRegMapperDependents() { + List copy; + synchronized (objects) { + copy = List.copyOf(threadMap.byTargetThread.values()); + } + for (ThreadRecorder rec : copy) { + TargetRegisterBank bank = rec.regs.get(0); + if (bank != null) { + rec.recordRegisterValues(bank, bank.getCachedRegisters()); + rec.updateRegsMem(null); + } + } + } + + protected void retroOfferMemMapperDependents() { + List copy; + synchronized (objects) { + copy = List.copyOf(objects.values()); + } + synchronized (DefaultTraceRecorder.this) { + for (TargetObject obj : copy) { + if (obj instanceof TargetModule) { + offerProcessModule((TargetModule) obj); + } + if (obj instanceof TargetSection) { + TargetSection section = (TargetSection) obj; + offerProcessModuleSection(section.getModule(), section); + } + if (obj instanceof TargetBreakpointLocation) { + offerEffectiveBreakpoint((TargetBreakpointLocation) obj); + } + if (obj instanceof TargetStack) { + stackUpdated((TargetStack) obj); + } + } + } + } + + public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { + synchronized (objects) { + return (TargetMemoryRegion) objects.get(PathUtils.parse(region.getPath())); + } + } + + public TargetModule getTargetModule(TraceModule module) { + synchronized (objects) { + return (TargetModule) objects.get(PathUtils.parse(module.getPath())); + } + } + + public TargetSection getTargetSection(TraceSection section) { + synchronized (objects) { + return (TargetSection) objects.get(PathUtils.parse(section.getPath())); + } + } + + public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { + synchronized (objects) { + return (TargetBreakpointLocation) objects.get(PathUtils.parse(bpt.getPath())); + } + } + + public List collectBreakpoints(TargetThread thread) { + synchronized (objects) { + return breakpoints.stream().filter(bpt -> { + TargetObjectList affects = bpt.getAffects(); + // N.B. in case thread is null (process), affects.contains(thread) is always false + return affects.isEmpty() || affects.contains(thread) || + affects.contains(target); + }).collect(Collectors.toList()); + } + } + + protected void onProcessBreakpointContainers( + Consumer action) { + synchronized (objects) { + if (processBreakpointContainer == null) { + for (TargetThread thread : threadsView) { + onThreadBreakpointContainers(thread, action); + } + } + else { + action.accept(processBreakpointContainer); + } + } + } + + protected void onThreadBreakpointContainers(TargetThread thread, + Consumer action) { + synchronized (objects) { + getOrCreateThreadRecorder(thread).onThreadBreakpointContainers(action); + } + } + + protected void onBreakpointContainers(TargetThread thread, + Consumer action) { + if (thread == null) { + onProcessBreakpointContainers(action); + } + else { + onThreadBreakpointContainers(thread, action); + } + } + } + + protected final DebuggerModelServicePlugin plugin; + protected final PluginTool tool; + protected final Trace trace; + protected final TargetObject target; + protected final ComposedMemory processMemory = new ComposedMemory(); + protected TargetBreakpointContainer processBreakpointContainer; + + protected final TraceBreakpointManager breakpointManager; + protected final TraceCodeManager codeManager; + protected final TraceBasedDataTypeManager dataTypeManager; + protected final TraceEquateManager equateManager; + protected final TraceMemoryManager memoryManager; + protected final TraceModuleManager moduleManager; + protected final TraceStackManager stackManager; + protected final TraceSymbolManager symbolManager; + protected final TraceThreadManager threadManager; + protected final TraceTimeManager timeManager; + + protected final AbstractDebuggerTargetTraceMapper mapper; + protected DebuggerMemoryMapper memMapper; + protected AsyncLazyMap regMappers; + protected final TargetDataTypeConverter typeConverter; + protected Collection extraRegs; + // TODO: Support automatic recording of user-specified extra registers... + // NOTE: Probably via watches, once we have those + // TODO: Probably move all the auto-reads into watches + + protected final ListenerSet listeners = + new ListenerSet<>(TraceRecorderListener.class); + protected final TriConsumer listenerRegAccChanged = + this::registerAccessibilityChanged; + protected final TriConsumer listenerProcMemAccChanged = + this::processMemoryAccessibilityChanged; + + private final ListenerForRecord listenerForRecord; + + protected final ThreadMap threadMap = new ThreadMap(); + protected final Set threadsView = + Collections.unmodifiableSet(threadMap.byTargetThread.keySet()); + protected final BiMap processBreakpointsMap = + HashBiMap.create(); + + protected final AsyncLazyValue lazyInit = new AsyncLazyValue<>(this::doInit); + + protected TraceSnapshot snapshot = null; + private boolean valid = true; + + protected TargetFocusScope focusScope; + protected TargetObject curFocus; + + public DefaultTraceRecorder(DebuggerModelServicePlugin plugin, Trace trace, TargetObject target, + AbstractDebuggerTargetTraceMapper mapper) { + this.plugin = plugin; + this.tool = plugin.getTool(); + this.trace = trace; + this.target = target; + + this.breakpointManager = trace.getBreakpointManager(); + this.codeManager = trace.getCodeManager(); + this.dataTypeManager = trace.getDataTypeManager(); + this.equateManager = trace.getEquateManager(); + this.memoryManager = trace.getMemoryManager(); + this.moduleManager = trace.getModuleManager(); + this.stackManager = trace.getStackManager(); + this.symbolManager = trace.getSymbolManager(); + this.threadManager = trace.getThreadManager(); + this.timeManager = trace.getTimeManager(); + + this.mapper = mapper; + this.regMappers = + new AsyncLazyMap<>(new HashMap<>(), descs -> mapper.offerRegisters(descs)); + this.typeConverter = new TargetDataTypeConverter(trace.getDataTypeManager()); + + this.listenerForRecord = new ListenerForRecord(); + + processMemory.memAccListeners.add(listenerProcMemAccChanged); + + trace.addConsumer(this); + } + + protected void registerAccessibilityChanged(boolean old, boolean acc, Void __) { + listeners.fire.registerAccessibilityChanged(this); + } + + protected void processMemoryAccessibilityChanged(boolean old, boolean acc, Void __) { + listeners.fire.processMemoryAccessibilityChanged(this); + } + + @Override + public CompletableFuture init() { + return lazyInit.request(); + } + + protected CompletableFuture doInit() { + createSnapshot("Started recording " + PathUtils.toString(target.getPath()) + " in " + + target.getModel(), null, null); + AsyncFence fence = new AsyncFence(); + CompletableFuture futureBreaks = + DebugModelConventions.findSuitable(TargetBreakpointContainer.class, target); + fence.include(futureBreaks.thenAccept(breaks -> { + if (breaks != null && !PathUtils.isAncestor(target.getPath(), breaks.getPath())) { + offerProcessBreakpointContainer(breaks); // instead of objectAdded + listenerForRecord.addListenerAndConsiderSuccessors(breaks); + } + }).exceptionally(e -> { + Msg.error(this, "Could not search for breakpoint container", e); + return null; + })); + + CompletableFuture futureEvents = + DebugModelConventions.findSuitable(TargetEventScope.class, target); + fence.include(futureEvents.thenAccept(events -> { + if (events != null && !PathUtils.isAncestor(target.getPath(), events.getPath())) { + // Don't descend. Scope may be the entire session. + listenerForRecord.addListener(events); + } + }).exceptionally(e -> { + Msg.warn(this, "Could not search for event scope", e); + return null; + })); + + CompletableFuture futureFocus = + DebugModelConventions.findSuitable(TargetFocusScope.class, target); + fence.include(futureFocus.thenAccept(focus -> { + if (focus != null && !PathUtils.isAncestor(target.getPath(), focus.getPath())) { + // Don't descend. Scope may be the entire session. + offerFocusScope(focus); + listenerForRecord.addListener(focus); + } + }).exceptionally(e -> { + Msg.error(this, "Could not search for focus scope", e); + return null; + })); + return fence.ready().thenAccept(__ -> { + listenerForRecord.objectAdded(target); // TODO: This seems wrong + listenerForRecord.addListenerAndConsiderSuccessors(target); + }); + } + + protected synchronized void doAdvanceSnap(String description, TraceThread eventThread) { + snapshot = timeManager.createSnapshot(description); + snapshot.setEventThread(eventThread); + } + + @Override + public TraceSnapshot forceSnapshot() { + createSnapshot("User-forced snapshot", null, null); + return snapshot; + } + + protected void createSnapshot(String description, TraceThread eventThread, + PermanentTransaction tid) { + if (tid != null) { + doAdvanceSnap(description, eventThread); + listeners.fire.snapAdvanced(this, snapshot.getKey()); + return; + } + try (PermanentTransaction tid2 = PermanentTransaction.start(trace, description)) { + doAdvanceSnap(description, eventThread); + } + listeners.fire.snapAdvanced(this, snapshot.getKey()); + } + + // TODO: This could probably be discovered by the offer and passed in at construction + protected synchronized CompletableFuture initMemMapper(TargetMemory memory) { + /** + * TODO: At the moment, there's no real dependency on the memory. When there is, see that + * additional memories can be incorporated into the mapper, and stale ones removed. + * Alternatively, formalize that there is no possible dependency on memory. + */ + if (memMapper != null) { + return AsyncUtils.NIL; + } + return mapper.offerMemory(memory).thenAccept(mm -> { + synchronized (this) { + memMapper = mm; + } + listenerForRecord.retroOfferMemMapperDependents(); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize memory mapper", ex); + return null; + }); + } + + protected Collection getTraceFlags(TargetMemoryRegion region) { + Collection flags = new HashSet<>(); + if (region.isReadable()) { + flags.add(TraceMemoryFlag.READ); + } + if (region.isWritable()) { + flags.add(TraceMemoryFlag.WRITE); + } + if (region.isExecutable()) { + flags.add(TraceMemoryFlag.EXECUTE); + } + // TODO: Volatile? Can any debugger report that? + return flags; + } + + protected void offerProcessRegion(TargetMemoryRegion region) { + TargetMemory mem = region.getMemory(); + processMemory.addRegion(region, mem); + initMemMapper(mem); + synchronized (this) { + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Memory region added")) { + String path = PathUtils.toString(region.getPath()); + TraceMemoryRegion traceRegion = + memoryManager.getLiveRegionByPath(snapshot.getKey(), path); + if (traceRegion != null) { + Msg.warn(this, "Region " + path + " already recorded"); + return; + } + traceRegion = memoryManager.addRegion(path, Range.atLeast(snapshot.getKey()), + memMapper.targetToTrace(region.getRange()), getTraceFlags(region)); + traceRegion.setName(region.getName()); + } + catch (TraceOverlappedRegionException e) { + Msg.error(this, "Failed to create region due to overlap", e); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Just checked for existing + } + } + updateAllThreadsRegsMem(region).exceptionally(ex -> { + Msg.error(this, "Could not add process memory region", ex); + return null; + }); + } + + protected synchronized void removeProcessRegion(TargetMemoryRegion region) { + // Already removed from processMemory. That's how we knew to go here. + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Memory region removed")) { + String path = PathUtils.toString(region.getPath()); + long snap = snapshot.getKey(); + TraceMemoryRegion traceRegion = memoryManager.getLiveRegionByPath(snap, path); + if (traceRegion == null) { + Msg.warn(this, "Could not find region " + path + " in trace to remove"); + return; + } + traceRegion.setDestructionSnap(snap - 1); + } + catch (DuplicateNameException | TraceOverlappedRegionException e) { + throw new AssertionError(e); // Region is shrinking in time + } + } + + protected void recordBreakpoint(TargetBreakpointSpec spec, TargetBreakpointLocation bpt, + Set traceThreads) { + synchronized (this) { + if (memMapper == null) { + throw new IllegalStateException( + "No memory mapper! Have not recorded a region, yet."); + } + } + String path = PathUtils.toString(bpt.getPath()); + String name = nameBreakpoint(bpt); + Address traceAddr = memMapper.targetToTrace(bpt.getAddress()); + AddressRange traceRange = range(traceAddr, bpt.getLength()); + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Breakpoint placed")) { + boolean enabled = spec.isEnabled(); + Set traceKinds = + TraceRecorder.targetToTraceBreakpointKinds(spec.getKinds()); + TraceBreakpoint traceBpt = breakpointManager.placeBreakpoint(path, snapshot.getKey(), + traceRange, traceThreads, traceKinds, enabled, spec.getExpression()); + traceBpt.setName(name); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be new to model, or already cleared + } + } + + protected void offerProcessBreakpointContainer(TargetBreakpointContainer bc) { + if (processBreakpointContainer != null) { + Msg.warn(this, "Already have a breakpoint container for this process"); + } + processBreakpointContainer = bc; + } + + protected void offerFocusScope(TargetFocusScope scope) { + if (this.focusScope != null) { + Msg.warn(this, "Already have a focus scope: " + this.focusScope); + } + this.focusScope = scope; + } + + protected synchronized TraceModule offerProcessModule(TargetModule module) { + if (memMapper == null) { + return null; + } + + String path = PathUtils.toString(module.getPath()); + TraceModule traceModule = moduleManager.getLoadedModuleByPath(snapshot.getKey(), path); + if (traceModule != null) { + return traceModule; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Module " + path + " loaded")) { + AddressRange targetRange = module.getRange(); + AddressRange traceRange = + targetRange == null ? null : memMapper.targetToTrace(targetRange); + traceModule = moduleManager.addLoadedModule(path, module.getModuleName(), traceRange, + snapshot.getKey()); + return traceModule; + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // We checked for existing by path + } + } + + protected synchronized TraceSection offerProcessModuleSection(TargetModule module, + TargetSection section) { + if (memMapper == null) { + return null; + } + String path = PathUtils.toString(section.getPath()); + TraceModule traceModule = offerProcessModule(module); + TraceSection traceSection = moduleManager.getLoadedSectionByPath(snapshot.getKey(), path); + if (traceSection != null) { + Msg.warn(this, path + " already recorded"); + return traceSection; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Section " + path + " added")) { + AddressRange targetRange = section.getRange(); + AddressRange traceRange = memMapper.targetToTrace(targetRange); + traceSection = traceModule.addSection(path, section.getIndex(), traceRange); + return traceSection; + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // We checked for existing by name + } + } + + protected synchronized void removeProcessModule(TargetModule module) { + String path = PathUtils.toString(module.getPath()); + long snap = snapshot.getKey(); + TraceThread eventThread = snapshot.getEventThread(); + TraceModule traceModule = moduleManager.getLoadedModuleByPath(snap, path); + if (traceModule == null) { + Msg.warn(this, "unloaded " + path + " is not in the trace"); + return; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Module " + path + " unloaded")) { + if (traceModule.getLoadedSnap() == snap) { + Msg.warn(this, "Observed module unload in the same snap as its load"); + createSnapshot("WARN: Module removed", eventThread, tid); + snap = snapshot.getKey(); + } + traceModule.setUnloadedSnap(snap - 1); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Module lifespan should be shrinking + } + } + + // NB: No removeProcessModuleSection, because sections should be immutable + // They are removed when the module is removed + + protected void offerEffectiveBreakpoint(TargetBreakpointLocation bpt) { + synchronized (this) { + if (memMapper == null) { + return; + } + } + EffectiveBreakpointResolver resolver = new EffectiveBreakpointResolver(bpt); + resolver.resolve().thenAccept(__ -> { + if (resolver.affectsProcess || !resolver.threadsAffected.isEmpty()) { + recordBreakpoint(resolver.spec, bpt, resolver.threadsAffected); + } + }).exceptionally(ex -> { + Msg.error(this, "Could record target breakpoint: " + bpt, ex); + return null; + }); + } + + protected void removeEffectiveBreakpoint(TargetBreakpointLocation bpt) { + String path = PathUtils.toString(bpt.getPath()); + long snap = snapshot.getKey(); + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Breakpoint deleted")) { + for (TraceBreakpoint traceBpt : breakpointManager.getBreakpointsByPath(path)) { + if (traceBpt.getPlacedSnap() > snap) { + Msg.error(this, + "Tracked, now removed breakpoint was placed in the future? " + bpt); + } + else if (traceBpt.getPlacedSnap() == snap) { + // TODO: I forget if this is allowed for DBTrace iteration + traceBpt.delete(); + } + else { + traceBpt.setClearedSnap(snap - 1); + } + } + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Lifespan in shrinking + } + } + + protected CompletableFuture updateAllThreadsRegsMem(TargetMemoryRegion limit) { + AsyncFence fence = new AsyncFence(); + for (ThreadRecorder rec : threadMap.recorders()) { + fence.include(rec.updateRegsMem(limit)); + } + return fence.ready(); + } + + @Override + public TargetObject getTarget() { + return target; + } + + @Override + public Trace getTrace() { + return trace; + } + + @Override + public long getSnap() { + return snapshot.getKey(); + } + + @Override + public boolean isRecording() { + return valid; + } + + @Override + public void stopRecording() { + invalidate(); + listeners.fire.recordingStopped(this); + } + + @Override + public void addListener(TraceRecorderListener l) { + listeners.add(l); + } + + @Override + public void removeListener(TraceRecorderListener l) { + listeners.remove(l); + } + + @Override + public boolean isViewAtPresent(TraceProgramView view) { + if (!valid) { + return false; + } + if (!Objects.equals(trace, view.getTrace())) { + return false; + } + if (snapshot.getKey() != view.getSnap()) { + return false; + } + return true; + } + + @Override + public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { + return listenerForRecord.getTargetBreakpoint(bpt); + } + + @Override + public TraceBreakpoint getTraceBreakpoint(TargetBreakpointLocation bpt) { + String path = PathUtils.toString(bpt.getPath()); + return breakpointManager.getPlacedBreakpointByPath(snapshot.getKey(), path); + } + + @Override + public List collectBreakpointContainers(TargetThread thread) { + List result = new ArrayList<>(); + listenerForRecord.onBreakpointContainers(thread, result::add); + return result; + } + + @Override + public List collectBreakpoints(TargetThread thread) { + return listenerForRecord.collectBreakpoints(thread); + } + + @Override + public Set getSupportedBreakpointKinds() { + Set tKinds = new HashSet<>(); + listenerForRecord.onBreakpointContainers(null, cont -> { + tKinds.addAll(cont.getSupportedBreakpointKinds()); + }); + return TraceRecorder.targetToTraceBreakpointKinds(tKinds); + } + + @Override + public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { + return listenerForRecord.getTargetMemoryRegion(region); + } + + @Override + public TraceMemoryRegion getTraceMemoryRegion(TargetMemoryRegion region) { + String path = PathUtils.toString(region.getPath()); + return memoryManager.getLiveRegionByPath(snapshot.getKey(), path); + } + + @Override + public TargetModule getTargetModule(TraceModule module) { + return listenerForRecord.getTargetModule(module); + } + + @Override + public TraceModule getTraceModule(TargetModule module) { + String path = PathUtils.toString(module.getPath()); + return moduleManager.getLoadedModuleByPath(snapshot.getKey(), path); + } + + @Override + public TargetSection getTargetSection(TraceSection section) { + return listenerForRecord.getTargetSection(section); + } + + @Override + public TraceSection getTraceSection(TargetSection section) { + String path = PathUtils.toString(section.getPath()); + return moduleManager.getLoadedSectionByPath(snapshot.getKey(), path); + } + + @Override + public TargetThread getTargetThread(TraceThread thread) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.targetThread; + } + + @Override + public TargetExecutionState getTargetThreadState(TargetThread thread) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.state; + } + + @Override + public TargetExecutionState getTargetThreadState(TraceThread thread) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.state; + } + + @Override + public boolean isRegisterBankAccessible(TargetRegisterBank bank) { + if (bank == null) { + return false; + } + synchronized (accessibilityByRegBank) { + KeyedFuture future = accessibilityByRegBank.get(bank); + if (future == null) { + return false; + } + AllRequiredAccess acc = future.getNow(null); + if (acc == null) { + return false; + } + return acc.get(); + } + } + + @Override + public boolean isRegisterBankAccessible(TraceThread thread, int frameLevel) { + return isRegisterBankAccessible(getTargetRegisterBank(thread, frameLevel)); + } + + @Override + public TargetRegisterBank getTargetRegisterBank(TraceThread thread, int frameLevel) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.regs.get(frameLevel); + } + + @Override + public Set getLiveTargetThreads() { + return threadsView; + } + + @Override + public TraceThread getTraceThread(TargetThread thread) { + ThreadRecorder rec = threadMap.byTargetThread.get(thread); + return rec == null ? null : rec.traceThread; + } + + @Override + public TraceThread getTraceThreadForSuccessor(TargetObject successor) { + ThreadRecorder rec = threadMap.getForSuccessor(successor); + return rec == null ? null : rec.traceThread; + } + + protected TraceStackFrame getTraceStackFrame(TraceThread thread, int level) { + TraceStack stack = trace.getStackManager().getLatestStack(thread, snapshot.getKey()); + if (stack == null) { + return null; + } + return stack.getFrame(level, false); + } + + @Override + public TraceStackFrame getTraceStackFrame(TargetStackFrame frame) { + ThreadRecorder rec = threadMap.getForSuccessor(frame); + if (rec == null) { + return null; + } + int level = getFrameLevel(frame); + if (rec.stack.get(level) != frame) { + return null; + } + return getTraceStackFrame(rec.traceThread, level); + } + + @Override + public TraceStackFrame getTraceStackFrameForSuccessor(TargetObject successor) { + ThreadRecorder rec = threadMap.getForSuccessor(successor); + if (rec == null) { + return null; + } + int level = rec.getSuccessorFrameLevel(successor); + return getTraceStackFrame(rec.traceThread, level); + } + + @Override + public TargetStackFrame getTargetStackFrame(TraceThread thread, int frameLevel) { + ThreadRecorder rec = threadMap.get(thread); + if (rec == null) { + return null; + } + return rec.stack.get(frameLevel); + } + + @Override + public DebuggerMemoryMapper getMemoryMapper() { + return memMapper; + } + + @Override + public DebuggerRegisterMapper getRegisterMapper(TraceThread thread) { + ThreadRecorder rec = threadMap.get(thread); + if (rec == null) { + return null; + } + return rec.regMapper; + } + + @Override + public AddressSetView getAccessibleProcessMemory() { + // TODO: Efficiently distinguish which memory is process vs. thread + return getAccessibleMemory(mem -> true); + } + + protected void invalidate() { + valid = false; + listenerForRecord.dispose(); + trace.release(this); + } + + protected TraceThread findLiveThreadByName(String name) { + for (TraceThread traceThread : threadManager.getThreadsByPath(name)) { + if (traceThread != null && traceThread.isAlive()) { + return traceThread; + } + } + return null; + } + + @Override + public CompletableFuture captureThreadRegisters(TraceThread thread, int frameLevel, + Set registers) { + DebuggerRegisterMapper regMapper = getRegisterMapper(thread); + if (regMapper == null) { + throw new IllegalStateException("Have not found register descriptions for " + thread); + } + if (!regMapper.getRegistersOnTarget().containsAll(registers)) { + throw new IllegalArgumentException( + "All given registers must be recognized by the target"); + } + if (registers.isEmpty()) { + return AsyncUtils.NIL; + } + List tRegs = + registers.stream().map(regMapper::traceToTarget).collect(Collectors.toList()); + + TargetRegisterBank bank = getTargetRegisterBank(thread, frameLevel); + if (bank == null) { + throw new IllegalArgumentException( + "Given thread and frame level does not have a live register bank"); + } + // NOTE: Cache update, if applicable, will cause recorder to write values to trace + return bank.readRegisters(tRegs).thenApply(__ -> null); + } + + @Override + public CompletableFuture writeThreadRegisters(TraceThread thread, int frameLevel, + Map values) { + DebuggerRegisterMapper regMapper = getRegisterMapper(thread); + if (regMapper == null) { + throw new IllegalStateException("Have not found register descriptions for " + thread); + } + if (!regMapper.getRegistersOnTarget().containsAll(values.keySet())) { + throw new IllegalArgumentException( + "All given registers must be recognized by the target"); + } + if (values.isEmpty()) { + return AsyncUtils.NIL; + } + Map tVals = values.entrySet().stream().map(ent -> { + if (ent.getKey() != ent.getValue().getRegister()) { + throw new IllegalArgumentException("register name mismatch in value"); + } + return regMapper.traceToTarget(ent.getValue()); + }).collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + TargetRegisterBank bank = getTargetRegisterBank(thread, frameLevel); + if (bank == null) { + throw new IllegalArgumentException( + "Given thread and frame level does not have a live register bank"); + } + // NOTE: Model + recorder will cause applicable trace updates + return bank.writeRegistersNamed(tVals).thenApply(__ -> null); + } + + @Override + public CompletableFuture readProcessMemory(Address start, int length) { + Address tStart = memMapper.traceToTarget(start); + return processMemory.readMemory(tStart, length); + } + + @Override + public CompletableFuture writeProcessMemory(Address start, byte[] data) { + Address tStart = memMapper.traceToTarget(start); + return processMemory.writeMemory(tStart, data); + } + + @Override + public CompletableFuture captureProcessMemory(AddressSetView set, TaskMonitor monitor) { + if (set.isEmpty()) { + return AsyncUtils.NIL; + } + // TODO: Figure out how to display/select per-thread memory. + // Probably need a thread parameter passed in then? + // NOTE: That thread memory will already be chained to process memory. Good. + + int total = 0; + AddressSetView expSet = + expandToBlocks(set).intersect(memoryManager.getRegionsAddressSet(snapshot.getKey())); + for (AddressRange r : expSet) { + total += Long.divideUnsigned(r.getLength() + BLOCK_SIZE - 1, BLOCK_SIZE); + } + monitor.initialize(total); + monitor.setMessage("Capturing memory"); + // TODO: Read blocks in parallel? Probably NO. Tends to overload the agent. + return AsyncUtils.each(TypeSpec.VOID, expSet.iterator(), (r, loop) -> { + AddressRangeChunker it = new AddressRangeChunker(r, BLOCK_SIZE); + AsyncUtils.each(TypeSpec.VOID, it.iterator(), (vRng, inner) -> { + // The listener in the recorder will copy to the Trace. + monitor.incrementProgress(1); + AddressRange tRng = memMapper.traceToTarget(vRng); + processMemory.readMemory(tRng.getMinAddress(), (int) tRng.getLength()) + .thenApply(b -> !monitor.isCancelled()) + .handle(inner::repeatWhile); + }).exceptionally(e -> { + Msg.error(this, "Error reading range " + r + ": " + e); + // NOTE: Above may double log, since recorder listens for errors, too + return null; // Continue looping on errors + }).thenApply(v -> !monitor.isCancelled()).handle(loop::repeatWhile); + }); + } + + @Override + public CompletableFuture captureDataTypes(TargetDataTypeNamespace namespace, + TaskMonitor monitor) { + if (!valid) { + return AsyncUtils.NIL; + } + String path = PathUtils.toString(namespace.getPath()); + monitor.setMessage("Capturing data types for " + path); + return namespace.getTypes().thenCompose(types -> { + monitor.initialize(types.size()); + AsyncFence fence = new AsyncFence(); + List converted = new ArrayList<>(); + for (TargetNamedDataType type : types) { + if (monitor.isCancelled()) { + fence.ready().cancel(false); + return AsyncUtils.nil(); + } + monitor.incrementProgress(1); + fence.include(typeConverter.convertTargetDataType(type).thenAccept(converted::add)); + } + return fence.ready().thenApply(__ -> converted); + }).thenAccept(converted -> { + if (converted == null) { + return; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Capture data types for " + path)) { + // NOTE: createCategory is actually getOrCreate + Category category = dataTypeManager.createCategory(new CategoryPath("/" + path)); + for (DataType dataType : converted) { + category.addDataType(dataType, DataTypeConflictHandler.DEFAULT_HANDLER); + } + } + }); + } + + @Override + public CompletableFuture captureDataTypes(TraceModule module, TaskMonitor monitor) { + TargetModule targetModule = getTargetModule(module); + if (targetModule == null) { + Msg.error(this, "Module " + module + " is not loaded"); + return AsyncUtils.NIL; + } + CompletableFuture> future = + targetModule.fetchChildrenSupporting(TargetDataTypeNamespace.class); + // NOTE: I should expect exactly one namespace... + return future.thenCompose(namespaces -> { + AsyncFence fence = new AsyncFence(); + for (TargetDataTypeNamespace ns : namespaces.values()) { + fence.include(captureDataTypes(ns, monitor)); + } + return fence.ready(); + }); + } + + private TraceNamespaceSymbol createNamespaceIfAbsent(String path) { + try { + return symbolManager.namespaces() + .add(path, symbolManager.getGlobalNamespace(), SourceType.IMPORTED); + } + catch (DuplicateNameException e) { + Msg.info(this, "Namespace for module " + path + + " already exists or another exists with a conflicting name. Using the existing one: " + + e); + TraceNamespaceSymbol ns = symbolManager.namespaces().getGlobalNamed(path); + if (ns != null) { + return ns; + } + Msg.error(this, "Existing namespace for " + path + + " is not a plain namespace. Using global namespace."); + return symbolManager.getGlobalNamespace(); + } + catch (InvalidInputException | IllegalArgumentException e) { + Msg.error(this, + "Could not create namespace for new module: " + path + ". Using global namespace.", + e); + return symbolManager.getGlobalNamespace(); + } + } + + @Override + public CompletableFuture captureSymbols(TargetSymbolNamespace namespace, + TaskMonitor monitor) { + if (!valid) { + return AsyncUtils.NIL; + } + String path = PathUtils.toString(namespace.getPath()); + monitor.setMessage("Capturing symbols for " + path); + return namespace.getSymbols().thenAccept(symbols -> { + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Capture types and symbols for " + path)) { + TraceNamespaceSymbol ns = createNamespaceIfAbsent(path); + monitor.setMessage("Capturing symbols for " + path); + monitor.initialize(symbols.size()); + for (TargetSymbol sym : symbols) { + if (monitor.isCancelled()) { + return; + } + monitor.incrementProgress(1); + String symName = sym.getIndex(); + if (sym.isConstant()) { + // TODO: Equate namespaces? + TraceEquate equate = equateManager.getByName(symName); + long symVal = sym.getValue().getOffset(); + if (equate != null && equate.getValue() == symVal) { + continue; + } + try { + equateManager.create(symName, symVal); + } + catch (DuplicateNameException | IllegalArgumentException e) { + Msg.error(this, "Could not create equate: " + symName, e); + } + continue; + } + Address addr = memMapper.targetToTrace(sym.getValue()); + try { + symbolManager.labels() + .create(snapshot.getKey(), null, addr, symName, ns, + SourceType.IMPORTED); + } + catch (InvalidInputException e) { + Msg.error(this, "Could not add module symbol " + sym + ": " + e); + } + /** + * TODO: Lay down data type, if present + * + * TODO: Interpret "address" type correctly. A symbol with this type is itself + * the pointer. In other words, it is not specifying the type to lay down in + * memory. + */ + } + } + }); + } + + @Override + public CompletableFuture captureSymbols(TraceModule module, TaskMonitor monitor) { + TargetModule targetModule = getTargetModule(module); + if (targetModule == null) { + Msg.error(this, "Module " + module + " is not loaded"); + return AsyncUtils.NIL; + } + CompletableFuture> future = + targetModule.fetchChildrenSupporting(TargetSymbolNamespace.class); + // NOTE: I should expect exactly one namespace... + return future.thenCompose(namespaces -> { + AsyncFence fence = new AsyncFence(); + for (TargetSymbolNamespace ns : namespaces.values()) { + fence.include(captureSymbols(ns, monitor)); + } + return fence.ready(); + }); + } + + @Override + public boolean isSupportsFocus() { + return focusScope != null; + } + + @Override + public TargetObject getFocus() { + if (curFocus == null) { + if (focusScope == null) { + return null; + } + TargetObject focus = focusScope.getFocus(); + if (focus == null || !PathUtils.isAncestor(target.getPath(), focus.getPath())) { + return null; + } + curFocus = focus; + } + return curFocus; + } + + @Override + public CompletableFuture requestFocus(TargetObject focus) { + if (!isSupportsFocus()) { + return CompletableFuture + .failedFuture(new IllegalArgumentException("Target does not support focus")); + } + if (!PathUtils.isAncestor(target.getPath(), focus.getPath())) { + return CompletableFuture.failedFuture(new IllegalArgumentException( + "Requested focus path is not a successor of the target")); + } + if (!PathUtils.isAncestor(focusScope.getPath(), focus.getPath())) { + return CompletableFuture.failedFuture(new IllegalArgumentException( + "Requested focus path is not a successor of the focus scope")); + } + return focusScope.requestFocus(focus).thenApply(__ -> true).exceptionally(ex -> { + ex = AsyncUtils.unwrapThrowable(ex); + if (ex instanceof DebuggerModelAccessException) { + String msg = "Could not focus " + focus + ": " + ex.getMessage(); + Msg.info(this, msg); + plugin.getTool().setStatusInfo(msg); + } + Msg.showError(this, null, "Focus Sync", "Could not focus " + focus, ex); + return false; + }); + } + + @Override + public ListenerForRecord getListenerForRecord() { + return listenerForRecord; + } +} diff --git a/Ghidra/Debug/Debugger/garbage/DefaultTraceRecorderPre666_740.java b/Ghidra/Debug/Debugger/garbage/DefaultTraceRecorderPre666_740.java new file mode 100644 index 0000000000..a815dcd177 --- /dev/null +++ b/Ghidra/Debug/Debugger/garbage/DefaultTraceRecorderPre666_740.java @@ -0,0 +1,2602 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.nio.ByteBuffer; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import com.google.common.collect.*; + +import ghidra.app.plugin.core.debug.mapping.*; +import ghidra.app.services.TraceRecorder; +import ghidra.app.services.TraceRecorderListener; +import ghidra.async.*; +import ghidra.async.AsyncLazyMap.KeyedFuture; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AllRequiredAccess; +import ghidra.dbg.DebugModelConventions.SubTreeListenerAdapter; +import ghidra.dbg.attributes.TargetObjectList; +import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.dbg.error.DebuggerModelAccessException; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointSpecListener; +import ghidra.dbg.target.TargetEventScope.TargetEventScopeListener; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionStateListener; +import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener; +import ghidra.dbg.target.TargetMemory.TargetMemoryListener; +import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener; +import ghidra.dbg.util.*; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.*; +import ghidra.program.model.data.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.symbol.SourceType; +import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.breakpoint.*; +import ghidra.trace.model.data.TraceBasedDataTypeManager; +import ghidra.trace.model.listing.TraceCodeManager; +import ghidra.trace.model.memory.*; +import ghidra.trace.model.modules.*; +import ghidra.trace.model.program.TraceProgramView; +import ghidra.trace.model.stack.*; +import ghidra.trace.model.symbol.*; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.trace.model.time.TraceTimeManager; +import ghidra.util.*; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.datastruct.ListenerSet; +import ghidra.util.exception.DuplicateNameException; +import ghidra.util.exception.InvalidInputException; +import ghidra.util.task.TaskMonitor; + +public class DefaultTraceRecorderPre666_740 implements TraceRecorder { + + private static final boolean LOG_STACK_TRACE = false; + // For large memory captures + private static final int BLOCK_SIZE = 4096; + private static final long BLOCK_MASK = -1L << 12; + + static final PathMatcher HARDCODED_MATCHER = new PathMatcher() { + { + // Paths for GDB + addPattern(PathUtils.parse("Breakpoints[].")); + addPattern(PathUtils.parse("Inferiors[].Memory[]")); + addPattern(PathUtils.parse("Inferiors[].Modules[].Sections[]")); + addPattern(PathUtils.parse("Inferiors[].Registers[]")); + addPattern(PathUtils.parse("Inferiors[].Threads[]")); + addPattern(PathUtils.parse("Inferiors[].Threads[].Stack[]")); + + // Paths for dbgeng + addPattern(PathUtils.parse("Sessions[].Processes[].Memory[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Modules[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Registers[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Stack[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Debug.Breakpoints[]")); + + // (Additional) paths for dbgmodel + addPattern(PathUtils.parse("Sessions[].Attributes")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Stack.Frames[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].TTD.Position")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Registers.User.")); + + // Paths for JDI + addPattern(PathUtils.parse("VirtualMachines[]")); + addPattern(PathUtils.parse("VirtualMachines[].Breakpoints")); + addPattern(PathUtils.parse("VirtualMachines[].Classes[]")); + addPattern(PathUtils.parse("VirtualMachines[].Classes[].Sections[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[].Registers[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[].Stack[]")); + + } + }; + + protected static class PermanentTransaction implements AutoCloseable { + static PermanentTransaction start(Trace trace, String description) { + UndoableTransaction tid = null; + try { + tid = UndoableTransaction.start(trace, description, true); + } + catch (Throwable t) { + tid.close(); + return ExceptionUtils.rethrow(t); + } + return new PermanentTransaction(trace, tid); + } + + private final Trace trace; + private final UndoableTransaction tid; + + public PermanentTransaction(Trace trace, UndoableTransaction tid) { + this.trace = trace; + this.tid = tid; + } + + @Override + public void close() { + tid.close(); + trace.clearUndo(); + } + } + + protected final AsyncLazyMap accessibilityByRegBank = + new AsyncLazyMap<>(new HashMap<>(), this::fetchRegAccessibility) { + public AllRequiredAccess remove(TargetRegisterBank key) { + AllRequiredAccess acc = super.remove(key); + if (acc != null) { + acc.removeChangeListener(listenerRegAccChanged); + } + return acc; + } + }; + protected final Map byRegion = new HashMap<>(); + protected final AsyncLazyMap accessibilityByMemory = + new AsyncLazyMap<>(new HashMap<>(), this::fetchMemAccessibility) { + public AllRequiredAccess remove(TargetMemory key) { + AllRequiredAccess acc = super.remove(key); + if (acc != null) { + acc.removeChangeListener(processMemory.memAccListeners.fire); + } + return acc; + } + }; + + protected CompletableFuture fetchRegAccessibility( + TargetRegisterBank bank) { + return DebugModelConventions.trackAccessibility(bank).thenApply(acc -> { + acc.addChangeListener(listenerRegAccChanged); + return acc; + }); + } + + protected CompletableFuture fetchMemAccessibility(TargetMemory mem) { + return DebugModelConventions.trackAccessibility(mem).thenApply(acc -> { + acc.addChangeListener(processMemory.memAccListeners.fire); + return acc; + }); + } + + /** + * Get accessible memory, as viewed in the trace + * + * @param pred an additional predicate applied via "AND" with accessibility + * @return the computed set + */ + protected AddressSet getAccessibleMemory(Predicate pred) { + synchronized (accessibilityByMemory) { + // TODO: Might accomplish by using listeners and tracking the accessible set + AddressSet accessible = new AddressSet(); + for (Entry ent : byRegion.entrySet()) { + TargetMemory mem = ent.getValue(); + if (!pred.test(mem)) { + continue; + } + AllRequiredAccess acc = accessibilityByMemory.getCompletedMap().get(mem); + if (acc == null || !acc.getAllAccessibility()) { + continue; + } + accessible.add(memMapper.targetToTrace(ent.getKey().getRange())); + } + return accessible; + } + } + + protected class ComposedMemory { + protected final ComposedMemory chain; + + protected final NavigableMap byMin = new TreeMap<>(); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected final ListenerSet> memAccListeners = + new ListenerSet(TriConsumer.class); + + public ComposedMemory() { + this.chain = null; + } + + public ComposedMemory(ComposedMemory chain) { + this.chain = chain; + } + + protected void addRegion(TargetMemoryRegion region, TargetMemory memory) { + synchronized (accessibilityByMemory) { + TargetMemory old = byRegion.put(region, memory); + assert old == null; + byMin.put(region.getRange().getMinAddress(), region); + accessibilityByMemory.get(memory).exceptionally(e -> { + e = AsyncUtils.unwrapThrowable(e); + Msg.error(this, "Could not track memory accessibility: " + e.getMessage()); + return null; + }); + } + } + + protected boolean removeRegion(TargetObject invalid) { + if (!(invalid instanceof TargetMemoryRegion)) { + return false; + } + synchronized (accessibilityByMemory) { + TargetMemoryRegion invRegion = (TargetMemoryRegion) invalid; + TargetMemory old = byRegion.remove(invRegion); + assert old != null; + byMin.remove(invRegion.getRange().getMinAddress()); + if (!old.isValid() || !byRegion.containsValue(old)) { + accessibilityByMemory.remove(old); + } + return true; + } + } + + protected AllRequiredAccess findChainedMemoryAccess(TargetMemoryRegion region) { + synchronized (accessibilityByMemory) { + TargetMemory mem = byRegion.get(region); + if (mem != null) { + return accessibilityByMemory.getCompletedMap().get(mem); + } + return chain == null ? null : chain.findChainedMemoryAccess(region); + } + } + + protected Entry findChainedFloor(Address address) { + synchronized (accessibilityByMemory) { + Entry myFloor = byMin.floorEntry(address); + Entry byChain = + chain == null ? null : chain.findChainedFloor(address); + if (byChain == null) { + return myFloor; + } + if (myFloor == null) { + return byChain; + } + int c = myFloor.getKey().compareTo(byChain.getKey()); + if (c < 0) { + return byChain; + } + return myFloor; + } + } + + protected AddressRange align(Address address, int length) { + AddressSpace space = address.getAddressSpace(); + long offset = address.getOffset(); + Address start = space.getAddress(offset & BLOCK_MASK); + Address end = space.getAddress(((offset + length - 1) & BLOCK_MASK) + BLOCK_SIZE - 1); + return new AddressRangeImpl(start, end); + } + + protected AddressRange alignWithLimit(Address address, int length, + TargetMemoryRegion limit) { + return align(address, length).intersect(limit.getRange()); + } + + protected AddressRange alignAndLimitToFloor(Address address, int length) { + Entry floor = findChainedFloor(address); + if (floor == null) { + return null; + } + return alignWithLimit(address, length, floor.getValue()); + } + + protected AddressRange alignWithOptionalLimit(Address address, int length, + TargetMemoryRegion limit) { + if (limit == null) { + return alignAndLimitToFloor(address, length); + } + return alignWithLimit(address, length, limit); + } + + protected CompletableFuture readMemory(Address address, int length) { + synchronized (accessibilityByMemory) { + Entry floor = findChainedFloor(address); + if (floor == null) { + throw new IllegalArgumentException( + "address " + address + " is not in any known region"); + } + Address max; + try { + max = address.addNoWrap(length - 1); + } + catch (AddressOverflowException e) { + throw new IllegalArgumentException("read extends beyond the address space"); + } + if (!floor.getValue().getRange().contains(max)) { + throw new IllegalArgumentException("read extends beyond a single region"); + } + TargetMemory mem = byRegion.get(floor.getValue()); + if (mem != null) { + return mem.readMemory(address, length); + } + return CompletableFuture.completedFuture(new byte[0]); + } + } + + protected CompletableFuture writeMemory(Address address, byte[] data) { + synchronized (accessibilityByMemory) { + Entry floor = findChainedFloor(address); + if (floor == null) { + throw new IllegalArgumentException( + "address " + address + " is not in any known region"); + } + Address max; + try { + max = address.addNoWrap(data.length - 1); + } + catch (AddressOverflowException e) { + throw new IllegalArgumentException("read extends beyond the address space"); + } + if (!floor.getValue().getRange().contains(max)) { + throw new IllegalArgumentException("read extends beyond a single region"); + } + TargetMemory mem = byRegion.get(floor.getValue()); + if (mem != null) { + return mem.writeMemory(address, data); + } + throw new IllegalArgumentException("read starts outside any address space"); + } + } + } + + protected static class ThreadMap { + protected final Map byTargetThread = new HashMap<>(); + protected final Map byTraceThread = new HashMap<>(); + + public void put(ThreadRecorder rec) { + byTargetThread.put(rec.targetThread, rec); + byTraceThread.put(rec.traceThread, rec); + } + + public ThreadRecorder getForSuccessor(TargetObject successor) { + while (successor != null) { + ThreadRecorder rec = byTargetThread.get(successor); + if (rec != null) { + return rec; + } + successor = successor.getParent(); + } + return null; + } + + public ThreadRecorder get(TargetThread thread) { + return byTargetThread.get(thread); + } + + public ThreadRecorder get(TargetObject maybeThread) { + return byTargetThread.get(maybeThread); + } + + public ThreadRecorder get(TraceThread thread) { + return byTraceThread.get(thread); + } + + public void remove(ThreadRecorder rec) { + ThreadRecorder rByTarget = byTargetThread.remove(rec.targetThread); + ThreadRecorder rByTrace = byTraceThread.remove(rec.traceThread); + assert rec == rByTarget; + assert rec == rByTrace; + } + + public Collection recorders() { + return byTargetThread.values(); + } + } + + protected static AddressSetView expandToBlocks(AddressSetView asv) { + AddressSet result = new AddressSet(); + // Not terribly efficient, but this is one range most of the time + for (AddressRange range : asv) { + AddressSpace space = range.getAddressSpace(); + Address min = space.getAddress(range.getMinAddress().getOffset() & BLOCK_MASK); + Address max = space.getAddress(range.getMaxAddress().getOffset() | ~BLOCK_MASK); + result.add(new AddressRangeImpl(min, max)); + } + return result; + } + + protected static AddressRange range(Address min, Integer length) { + if (length == null) { + length = 1; + } + try { + return new AddressRangeImpl(min, length); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + } + + protected static String nameBreakpoint(TargetBreakpointLocation bpt) { + if (bpt instanceof TargetBreakpointSpec) { + return bpt.getIndex(); + } + return bpt.getSpecification().getIndex() + "." + bpt.getIndex(); + } + + protected static int getFrameLevel(TargetStackFrame frame) { + // TODO: A fair assumption? frames are elements with numeric base-10 indices + return Integer.decode(frame.getIndex()); + } + + protected class ThreadRecorder { + protected final TargetThread targetThread; + protected final TraceThread traceThread; + protected DebuggerRegisterMapper regMapper; + protected TargetRegister pcReg; + protected TargetRegister spReg; + protected Map regs = new HashMap<>(); + protected NavigableMap stack = + Collections.synchronizedNavigableMap(new TreeMap<>()); + protected final ComposedMemory threadMemory = new ComposedMemory(processMemory); + protected TargetBreakpointContainer threadBreakpointContainer; + protected TargetExecutionState state = TargetExecutionState.ALIVE; + + protected ThreadRecorder(TargetThread targetThread, TraceThread traceThread) { + this.targetThread = targetThread; + this.traceThread = traceThread; + + if (targetThread instanceof TargetExecutionStateful) { + TargetExecutionStateful stateful = (TargetExecutionStateful) targetThread; + state = stateful.getExecutionState(); + } + } + + protected synchronized CompletableFuture initRegMapper( + TargetRegisterContainer registers) { + /** + * TODO: At the moment, this assumes the recorded thread has one register container, or + * at least that all register banks in the thread use the same register container + * (descriptors). If this becomes a problem, then we'll need to keep a separate register + * mapper per register container. This would likely also require some notion of multiple + * languages in the mapper (seems an unlikely design choice). NOTE: In cases where a + * single process may (at least appear to) execute multiple languages, the model should + * strive to present the registers of the physical machine, as they are most likely + * uniform across the process, not those being emulated in the moment. In cases where an + * abstract machine is involved, it is probably more fitting to present separate + * containers (likely provided by separate models) than to present both the physical and + * abstract machine in the same target. + * + *

+ * TODO: Should I formalize that only one register container is present in a recorded + * thread? This seems counter to the model's flexibility. Traces allow polyglot + * disassembly, but not polyglot register spaces. + */ + /*if (regMapper != null) { + return AsyncUtils.NIL; + }*/ + return regMappers.get(registers).thenAccept(rm -> { + synchronized (this) { + regMapper = rm; + Language language = trace.getBaseLanguage(); + pcReg = regMapper.traceToTarget(language.getProgramCounter()); + spReg = regMapper.traceToTarget(trace.getBaseCompilerSpec().getStackPointer()); + extraRegs = new LinkedHashSet<>(); + for (String rn : mapper.getExtraRegNames()) { + Register traceReg = language.getRegister(rn); + if (traceReg == null) { + Msg.error(this, + "Mapper's extra register '" + rn + "' is not in the language!"); + continue; + } + TargetRegister targetReg = regMapper.traceToTarget(traceReg); + if (targetReg == null) { + Msg.error(this, + "Mapper's extra register '" + traceReg + "' is not mappable!"); + continue; + } + extraRegs.add(targetReg); + } + } + listenerForRecord.retroOfferRegMapperDependents(); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize register mapper", ex); + return null; + }); + } + + protected void regMapperAmended(DebuggerRegisterMapper rm, TargetRegister reg, + boolean removed) { + boolean doUpdateRegs = false; + String name = reg.getIndex(); + synchronized (this) { + if (regMapper != rm) { + return; + } + TargetRegister newPcReg = + regMapper.traceToTarget(trace.getBaseLanguage().getProgramCounter()); + if (pcReg != newPcReg) { + pcReg = newPcReg; + doUpdateRegs |= pcReg != null; + } + TargetRegister newSpReg = + regMapper.traceToTarget(trace.getBaseCompilerSpec().getStackPointer()); + if (spReg != newSpReg) { + spReg = newSpReg; + doUpdateRegs |= spReg != null; + } + if (mapper.getExtraRegNames().contains(name)) { + if (removed) { + extraRegs.remove(reg); + } + else { + extraRegs.add(reg); + } + doUpdateRegs = true; + } + } + if (removed) { + return; + } + TargetRegisterBank bank = regs.get(0); + if (bank != null) { + byte[] cachedVal = bank.getCachedRegisters().get(name); + if (cachedVal != null) { + recordRegisterValues(bank, Map.of(name, cachedVal)); + } + if (doUpdateRegs) { + updateRegsMem(null); + } + } + // TODO: This may be too heavy-handed + // listenerForRecord.retroOfferRegMapperDependents(); + } + + protected int getSuccessorFrameLevel(TargetObject successor) { + NavigableSet observedPathLengths = new TreeSet<>(); + for (TargetStackFrame frame : stack.values()) { + observedPathLengths.add(frame.getPath().size()); + } + List path = successor.getPath(); + for (int l : observedPathLengths.descendingSet()) { + if (l > path.size()) { + continue; + } + List sub = path.subList(0, l); + if (!PathUtils.isIndex(sub)) { + continue; + } + int index = Integer.decode(PathUtils.getIndex(sub)); + TargetStackFrame frame = stack.get(index); + if (frame == null || !Objects.equals(sub, frame.getPath())) { + continue; + } + return index; + } + return 0; + } + + CompletableFuture doFetchAndInitRegMapper(TargetRegisterBank bank) { + int frameLevel = getSuccessorFrameLevel(bank); + TargetRegisterContainer descs = bank.getDescriptions(); + if (descs == null) { + Msg.error(this, "Cannot create mapper, yet: Descriptions is null."); + return AsyncUtils.NIL; + } + return initRegMapper(descs).thenAccept(__ -> { + if (frameLevel == 0) { + recordRegisterValues(bank, bank.getCachedRegisters()); + updateRegsMem(null); + } + listeners.fire.registerBankMapped(DefaultTraceRecorderPre666_740.this); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize register mapper", ex); + return null; + }); + } + + protected void offerRegisters(TargetRegisterBank newRegs) { + int frameLevel = getSuccessorFrameLevel(newRegs); + if (regs.isEmpty()) { + // TODO: Technically, each frame may need its own mapper.... + doFetchAndInitRegMapper(newRegs); + } + + TargetRegisterBank oldRegs = regs.put(frameLevel, newRegs); + if (oldRegs == newRegs) { + return; + } + + synchronized (accessibilityByRegBank) { + if (oldRegs != null) { + accessibilityByRegBank.remove(oldRegs); + } + accessibilityByRegBank.get(newRegs).exceptionally(e -> { + e = AsyncUtils.unwrapThrowable(e); + Msg.error(this, "Could not track register accessibility: " + e.getMessage()); + return null; + }); + } + } + + protected void offerStackFrame(TargetStackFrame frame) { + stack.put(getFrameLevel(frame), frame); + recordFrame(frame); + } + + protected void offerThreadRegion(TargetMemoryRegion region) { + TargetMemory mem = region.getMemory(); + threadMemory.addRegion(region, mem); + initMemMapper(mem); + // TODO: Add region to trace memory manager (when allowed for threads) + updateRegsMem(region).exceptionally(ex -> { + Msg.error(this, "Could not add thread memory region", ex); + return null; + }); + } + + protected void offerThreadBreakpointContainer(TargetBreakpointContainer bc) { + if (threadBreakpointContainer != null) { + Msg.warn(this, "Thread already has a breakpoint container"); + } + threadBreakpointContainer = bc; + } + + /** + * Inform the recorder the given object is no longer valid + * + * @param invalid the invalidated object + * @return true if this recorder should be invalidated, too + */ + protected synchronized boolean objectRemoved(TargetObject invalid) { + if (checkThreadRemoved(invalid)) { + return true; + } + if (checkRegistersRemoved(invalid)) { + //return false; + // Regs could also be a stack frame + } + if (checkStackFrameRemoved(invalid)) { + return false; + } + if (threadMemory.removeRegion(invalid)) { + return false; + } + Msg.trace(this, "Ignored removed object: " + invalid); + return false; + } + + protected boolean checkThreadRemoved(TargetObject invalid) { + if (targetThread == invalid) { + threadDestroyed(); + return true; + } + return false; + } + + protected boolean checkRegistersRemoved(TargetObject invalid) { + synchronized (accessibilityByRegBank) { + if (regs.values().remove(invalid)) { + accessibilityByRegBank.remove((TargetRegisterBank) invalid); + return true; + } + return false; + } + } + + protected boolean checkStackFrameRemoved(TargetObject invalid) { + if (stack.values().remove(invalid)) { + popStack(); + return true; + } + return false; + } + + protected Address pcFromStack() { + TargetStackFrame frame = stack.get(0); + if (frame == null) { + return null; + } + return frame.getProgramCounter(); + } + + protected boolean checkReadCondition(Address traceAddress) { + /** + * TODO: This heuristic doesn't really belong here, but I have to implement it here so + * that it doesn't "override" the listing's implementation. Once watches are + * implemented, we should be able to drop this garbage. + */ + TraceMemoryRegion region = + memoryManager.getRegionContaining(snapshot.getKey(), traceAddress); + if (region == null) { + return false; + } + if (region.isWrite()) { + return true; + } + Entry ent = + memoryManager.getMostRecentStateEntry(snapshot.getKey(), traceAddress); + if (ent == null) { + return true; + } + if (ent.getValue() == TraceMemoryState.KNOWN) { + return false; + } + return true; + } + + protected CompletableFuture readAlignedConditionally(String name, Address targetAddress, + TargetMemoryRegion limit) { + if (targetAddress == null) { + return AsyncUtils.NIL; + } + Address traceAddress = memMapper.targetToTrace(targetAddress); + if (traceAddress == null) { + return AsyncUtils.NIL; + } + if (!checkReadCondition(traceAddress)) { + return AsyncUtils.NIL; + } + AddressRange targetRange = threadMemory.alignWithOptionalLimit(targetAddress, 1, limit); + if (targetRange == null) { + return AsyncUtils.NIL; + } + TimedMsg.info(this, + " Reading memory at " + name + " (" + targetAddress + " -> " + targetRange + ")"); + // NOTE: Recorder takes data via memoryUpdated callback + // TODO: In that callback, sort out process memory from thread memory? + return threadMemory + .readMemory(targetRange.getMinAddress(), (int) targetRange.getLength()) + .exceptionally(ex -> { + Msg.error(this, "Could not read memory at " + name, ex); + return null; + }); + } + + Address registerValueToTargetAddress(TargetRegister reg, byte[] value) { + /** + * TODO: This goes around the horn and back just to select a default address space. We + * should really just go directly to target address space. + */ + RegisterValue rv = regMapper.targetToTrace(reg, value); + if (rv == null) { + return null; + } + Address traceAddress = trace.getBaseLanguage() + .getDefaultSpace() + .getAddress(rv.getUnsignedValue().longValue()); + return memMapper.traceToTarget(traceAddress); + } + + protected CompletableFuture updateRegsMem(TargetMemoryRegion limit) { + TargetRegisterBank bank; + TargetRegister pc; + TargetRegister sp; + Set toRead = new LinkedHashSet<>(); + synchronized (DefaultTraceRecorderPre666_740.this) { + if (regMapper == null) { + return AsyncUtils.NIL; + } + bank = regs.get(0); + pc = pcReg; + sp = spReg; + toRead.addAll(extraRegs); + toRead.add(sp); + toRead.add(pc); + } + if (bank == null || pc == null || sp == null) { + return AsyncUtils.NIL; + } + TimedMsg.info(this, "Reading " + toRead + " of " + targetThread); + return bank.readRegisters(toRead).thenCompose(vals -> { + synchronized (DefaultTraceRecorderPre666_740.this) { + if (memMapper == null) { + return AsyncUtils.NIL; + } + } + if (threadMemory == null) { + return AsyncUtils.NIL; + } + AsyncFence fence = new AsyncFence(); + + Address pcTargetAddr = pcFromStack(); + if (pcTargetAddr == null) { + pcTargetAddr = registerValueToTargetAddress(pcReg, vals.get(pcReg.getIndex())); + } + fence.include(readAlignedConditionally("PC", pcTargetAddr, limit)); + + Address spTargetAddr = + registerValueToTargetAddress(spReg, vals.get(spReg.getIndex())); + fence.include(readAlignedConditionally("SP", spTargetAddr, limit)); + + return fence.ready(); + }).exceptionally(ex -> { + if (LOG_STACK_TRACE) { + Msg.error(this, "Could not read registers", ex); + } + else { + Msg.error(this, "Could not read registers"); + } + return null; + }); + } + + public void stateChanged(final TargetExecutionState newState) { + if (newState == TargetExecutionState.STOPPED) { + updateRegsMem(null); + } + state = newState; + } + + public void threadDestroyed() { + String path = PathUtils.toString(targetThread.getPath()); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, path + " destroyed")) { + // TODO: Should it be key - 1 + // Perhaps, since the thread should not exist + // But it could imply earlier destruction than actually observed + traceThread.setDestructionSnap(snapshot.getKey()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be shrinking + } + } + + public void recordRegisterValues(TargetRegisterBank bank, Map updates) { + synchronized (DefaultTraceRecorderPre666_740.this) { + if (regMapper == null) { + return; + } + } + int frameLevel = getSuccessorFrameLevel(bank); + TimedMsg.info(this, "Reg values changed: " + updates.keySet()); + try (PermanentTransaction tid = PermanentTransaction.start(trace, + "Registers changed in " + PathUtils.toString(bank.getPath()))) { + TraceMemoryRegisterSpace regSpace = + memoryManager.getMemoryRegisterSpace(traceThread, frameLevel, true); + for (Entry ent : updates.entrySet()) { + RegisterValue rv = regMapper.targetToTrace(ent.getKey(), ent.getValue()); + if (rv == null) { + continue; // mapper does not know this register.... + } + regSpace.setValue(snapshot.getKey(), rv); + if (rv.getRegister() == trace.getBaseLanguage().getProgramCounter() && + pcFromStack() == null) { + Address pcTargetAddr = registerValueToTargetAddress(pcReg, ent.getValue()); + readAlignedConditionally("PC", pcTargetAddr, null); // NB: Reports errors + } + if (rv.getRegister() == trace.getBaseCompilerSpec().getStackPointer()) { + Address spTargetAddr = registerValueToTargetAddress(spReg, ent.getValue()); + readAlignedConditionally("SP", spTargetAddr, null); // NB: Reports errors + } + } + } + } + + public void recordFrame(TargetStackFrame frame) { + recordFrame(frame, frame.getProgramCounter()); + } + + public void doRecordFrame(TraceStack traceStack, int frameLevel, Address pc) { + TraceStackFrame traceFrame = traceStack.getFrame(frameLevel, true); + traceFrame.setProgramCounter(pc); + } + + public void recordFrame(TargetStackFrame frame, Address pc) { + synchronized (DefaultTraceRecorderPre666_740.this) { + if (memMapper == null) { + return; + } + Address tracePc = pc == null ? null : memMapper.targetToTrace(pc); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Stack frame added")) { + TraceStack traceStack = + stackManager.getStack(traceThread, snapshot.getKey(), true); + doRecordFrame(traceStack, getFrameLevel(frame), tracePc); + } + } + } + + protected int stackDepth() { + return stack.isEmpty() ? 0 : stack.lastKey() + 1; + } + + public void recordStack() { + synchronized (DefaultTraceRecorderPre666_740.this) { + if (memMapper == null) { + return; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Stack changed")) { + TraceStack traceStack = + stackManager.getStack(traceThread, snapshot.getKey(), true); + traceStack.setDepth(stackDepth(), false); + for (Map.Entry ent : stack.entrySet()) { + Address tracePc = + memMapper.targetToTrace(ent.getValue().getProgramCounter()); + doRecordFrame(traceStack, ent.getKey(), tracePc); + } + } + } + } + + public void popStack() { + synchronized (DefaultTraceRecorderPre666_740.this) { + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Stack popped")) { + TraceStack traceStack = + stackManager.getStack(traceThread, snapshot.getKey(), true); + traceStack.setDepth(stackDepth(), false); + } + } + } + + public void onThreadBreakpointContainers( + Consumer action) { + if (threadBreakpointContainer == null) { + return; + } + action.accept(threadBreakpointContainer); + } + } + + protected class EffectiveBreakpointResolver { + private final TargetBreakpointLocation bpt; + private TargetBreakpointSpec spec; + private boolean affectsProcess = false; + private final Set threadsAffected = new LinkedHashSet<>(); + + public EffectiveBreakpointResolver(TargetBreakpointLocation bpt) { + this.bpt = bpt; + } + + public CompletableFuture resolve() { + AsyncFence fence = new AsyncFence(); + this.spec = bpt.getSpecification(); + + for (TargetObject ref : bpt.getAffects()) { + if (ref.equals(target)) { + affectsProcess = true; + } + else { + fence.include(resolveThread(ref)); + } + } + return fence.ready(); + } + + // TODO: If affects is empty/null, also try to default to the containing process + private CompletableFuture resolveThread(TargetObject ref) { + return DebugModelConventions.findThread(ref).thenAccept(thread -> { + if (thread == null) { + Msg.error(this, + "Could not find process or thread from breakpoint-affected object: " + ref); + return; + } + if (!ref.equals(thread)) { + Msg.warn(this, "Effective breakpoint should apply to process or threads. Got " + + ref + ". Resolved to " + thread); + return; + } + if (!PathUtils.isAncestor(target.getPath(), thread.getPath())) { + /** + * Perfectly normal if the breakpoint container is outside the process + * container. Don't record such in this trace, though. + */ + return; + } + ThreadRecorder rec = listenerForRecord.getOrCreateThreadRecorder(thread); + synchronized (threadsAffected) { + threadsAffected.add(rec.traceThread); + } + }).exceptionally(ex -> { + Msg.error(this, "Error resolving thread from breakpoint-affected object: " + ref); + return null; + }); + } + + public void applyChecksAndConventions() { + if (affectsProcess && !threadsAffected.isEmpty()) { + Msg.warn(this, "Breakpoint affects process and individual threads?: " + bpt); + threadsAffected.clear(); + } + // Check ancestry for "affects" + if (!affectsProcess && threadsAffected.isEmpty()) { + if (PathUtils.isAncestor(target.getPath(), bpt.getPath())) { + for (ThreadRecorder rec : threadMap.byTargetThread.values()) { + if (PathUtils.isAncestor(rec.targetThread.getPath(), bpt.getPath())) { + threadsAffected.add(rec.traceThread); + break; // Only one thread could be its ancestor + } + } + if (threadsAffected.isEmpty()) { + affectsProcess = true; + } + } + } + } + } + + public class ListenerForRecord extends SubTreeListenerAdapter implements + TargetBreakpointSpecListener, TargetEventScopeListener, TargetExecutionStateListener, + TargetFocusScopeListener, TargetRegisterBankListener, TargetMemoryListener { + + //protected final Map modulesByName = new HashMap<>(); + protected final Set breakpoints = new HashSet<>(); + + @Override + protected boolean checkDescend(TargetObject ref) { + // NOTE, cannot return false on match, since it could be a prefix of another + if (HARDCODED_MATCHER.successorCouldMatch(ref.getPath())) { + return true; + } + return false; + } + + // TODO: Move this into conventions? + protected CompletableFuture findThreadOrProcess(TargetObject successor) { + return new DebugModelConventions.AncestorTraversal(successor) { + @Override + protected Result check(TargetObject obj) { + if (obj.isRoot()) { + return Result.FOUND; + } + if (obj instanceof TargetThread) { + return Result.FOUND; + } + if (obj instanceof TargetProcess) { + return Result.FOUND; + } + return Result.CONTINUE; + } + + @Override + protected TargetObject finish(TargetObject obj) { + return obj; + } + }.start(); + } + + @Override + protected void objectAdded(TargetObject added) { + if (!valid) { + return; + } + if (added instanceof TargetThread) { + getOrCreateThreadRecorder((TargetThread) added); + } + if (added instanceof TargetStack) { + // Actually, this may not matter + } + // Do stack frame first, since bank would be it or child. + // Need frames indexed first to determine level of bank + if (added instanceof TargetStackFrame) { + ThreadRecorder rec = threadMap.getForSuccessor(added); + if (rec == null) { + Msg.error(this, "Frame without thread?: " + added); + } + else { + rec.offerStackFrame((TargetStackFrame) added); + } + } + if (added instanceof TargetRegisterBank) { + ThreadRecorder rec = threadMap.getForSuccessor(added); + if (rec == null) { + Msg.error(this, "Bank without thread?: " + added); + } + else { + rec.offerRegisters((TargetRegisterBank) added); + } + } + if (added instanceof TargetRegisterContainer) { + // These are picked up when a bank is added with these descriptions + } + if (added instanceof TargetRegister) { + TargetRegister reg = (TargetRegister) added; + regMappers.get(reg.getContainer()).thenAccept(rm -> { + rm.targetRegisterAdded(reg); + for (ThreadRecorder rec : threadMap.byTargetThread.values()) { + rec.regMapperAmended(rm, reg, false); + } + }); + } + if (added instanceof TargetMemory) { + initMemMapper((TargetMemory) added); + } + if (added instanceof TargetMemoryRegion) { + TargetMemoryRegion region = (TargetMemoryRegion) added; + findThreadOrProcess(added).thenAccept(obj -> { + if (obj == target) { + offerProcessRegion(region); + return; + } + if (obj instanceof TargetThread) { + ThreadRecorder rec = getOrCreateThreadRecorder((TargetThread) obj); + rec.offerThreadRegion(region); + } + }).exceptionally(ex -> { + Msg.error(this, "Error recording memory region", ex); + return null; + }); + } + if (added instanceof TargetModule) { + TargetModule module = (TargetModule) added; + offerProcessModule(module); + } + if (added instanceof TargetSection) { + TargetSection section = (TargetSection) added; + offerProcessModuleSection(section.getModule(), section); + } + if (added instanceof TargetBreakpointContainer) { + TargetBreakpointContainer breaks = (TargetBreakpointContainer) added; + findThreadOrProcess(added).thenAccept(obj -> { + if (obj == target) { + offerProcessBreakpointContainer(breaks); + return; + } + if (obj.isRoot()) { + return; + } + ThreadRecorder rec = getOrCreateThreadRecorder((TargetThread) obj); + rec.offerThreadBreakpointContainer(breaks); + }).exceptionally(ex -> { + Msg.error(this, "Error recording breakpoint container", ex); + return null; + }); + } + if (added instanceof TargetBreakpointSpec) { + // I don't think this matters. UI for live recording only. + } + if (added instanceof TargetBreakpointLocation) { + TargetBreakpointLocation bpt = (TargetBreakpointLocation) added; + breakpoints.add(bpt); + offerEffectiveBreakpoint(bpt); + } + } + + @Override + protected void objectRemoved(TargetObject removed) { + if (!valid) { + return; + } + if (target == removed) { + stopRecording(); + return; + } + if (removed instanceof TargetRegisterContainer) { + regMappers.remove((TargetRegisterContainer) removed); + } + if (removed instanceof TargetRegister) { + TargetRegister reg = (TargetRegister) removed; + DebuggerRegisterMapper rm = regMappers.getCompletedMap().get(reg.getContainer()); + if (rm == null) { + return; + } + rm.targetRegisterRemoved(reg); + for (ThreadRecorder rec : threadMap.byTargetThread.values()) { + rec.regMapperAmended(rm, reg, true); + } + } + if (removed instanceof TargetMemoryRegion) { + TargetMemoryRegion region = (TargetMemoryRegion) removed; + if (processMemory.removeRegion(region)) { + removeProcessRegion(region); + return; + } + // Allow removal notice to fall through to thread recorders + } + if (removed instanceof TargetModule) { + TargetModule module = (TargetModule) removed; + removeProcessModule(module); + return; + } + if (removed instanceof TargetBreakpointLocation) { + TargetBreakpointLocation bpt = (TargetBreakpointLocation) removed; + breakpoints.remove(bpt); + removeEffectiveBreakpoint(bpt); + return; + } + synchronized (threadMap) { + for (Iterator it = threadMap.recorders().iterator(); it + .hasNext();) { + ThreadRecorder rec = it.next(); + if (rec.objectRemoved(removed)) { + it.remove(); + } + } + } + } + + protected boolean successor(TargetObject ref) { + return PathUtils.isAncestor(target.getPath(), ref.getPath()); + } + + protected boolean anyRef(Collection parameters) { + for (Object p : parameters) { + if (!(p instanceof TargetObject)) { + continue; + } + return true; + } + return false; + } + + protected boolean anySuccessor(Collection parameters) { + for (Object p : parameters) { + if (!(p instanceof TargetObject)) { + continue; + } + TargetObject ref = (TargetObject) p; + if (!successor(ref)) { + continue; + } + return true; + } + return false; + } + + protected boolean eventApplies(TargetObject eventThread, TargetEventType type, + List parameters) { + if (type == TargetEventType.RUNNING) { + return false; + /** + * TODO: Perhaps some configuration for this later. It's kind of interesting to + * record the RUNNING event time, but it gets pedantic when these exist between + * steps. + */ + } + if (eventThread != null) { + return successor(eventThread); + } + if (anyRef(parameters)) { + return anySuccessor(parameters); + } + return true; // Some session-wide event, I suppose + } + + @Override + public void event(TargetEventScope object, TargetThread eventThread, TargetEventType type, + String description, List parameters) { + if (!valid) { + return; + } + TimedMsg.info(this, "Event: " + type + " thread=" + eventThread + " description=" + + description + " params=" + parameters); + // Just use this to step the snaps. Creation/destruction still handled in add/remove + if (!eventApplies(eventThread, type, parameters)) { + return; + } + ThreadRecorder rec = threadMap.get(eventThread); + createSnapshot(description, rec == null ? null : rec.traceThread, null); + + if (type == TargetEventType.THREAD_CREATED) { + if (rec == null) { + return; + } + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Adjust thread creation", true)) { + rec.traceThread.setCreationSnap(snapshot.getKey()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be shrinking + } + } + else if (type == TargetEventType.MODULE_LOADED) { + Object p0 = parameters.get(0); + if (!(p0 instanceof TargetObject)) { + return; + } + TargetObject obj = (TargetObject) p0; + if (!(obj instanceof TargetModule)) { + return; + } + TargetModule mod = (TargetModule) obj; + TraceModule traceModule = getTraceModule(mod); + if (traceModule == null) { + return; + } + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Adjust module load", true)) { + traceModule.setLoadedSnap(snapshot.getKey()); + } + catch (DuplicateNameException e) { + Msg.error(this, "Could not set module loaded snap", e); + } + } + } + + @Override + public void attributesChanged(TargetObject parent, Collection removed, + Map added) { + super.attributesChanged(parent, removed, added); + if (!valid) { + return; + } + // Dispatch attribute changes which don't have "built-in" events. + if (parent instanceof TargetBreakpointLocation) { + if (added.containsKey(TargetBreakpointLocation.LENGTH_ATTRIBUTE_NAME)) { + breakpointLengthChanged((TargetBreakpointLocation) parent, + (Integer) added.get(TargetBreakpointLocation.LENGTH_ATTRIBUTE_NAME)); + } + } + if (parent instanceof TargetStackFrame) { + if (added.containsKey(TargetStackFrame.PC_ATTRIBUTE_NAME)) { + framePcUpdated((TargetStackFrame) parent); + } + } + if (parent instanceof TargetRegisterBank) { + if (added.containsKey(TargetRegisterBank.DESCRIPTIONS_ATTRIBUTE_NAME)) { + ThreadRecorder rec = threadMap.getForSuccessor(parent); + if (rec != null) { + rec.doFetchAndInitRegMapper((TargetRegisterBank) parent); + } + } + } + // This should be fixed at construction. + /*if (parent instanceof TargetModule) { + if (added.containsKey(TargetModule.BASE_ATTRIBUTE_NAME)) { + moduleBaseUpdated((TargetModule) parent, + (Address) added.get(TargetModule.BASE_ATTRIBUTE_NAME)); + } + }*/ + } + + @Override + public void executionStateChanged(TargetExecutionStateful stateful, + TargetExecutionState state) { + if (!valid) { + return; + } + TimedMsg.info(this, "State " + state + " for " + stateful); + findThreadOrProcess(stateful).thenAccept(threadOrProcess -> { + if (threadOrProcess == target && state == TargetExecutionState.TERMINATED) { + stopRecording(); + return; + } + ThreadRecorder rec = null; + synchronized (threadMap) { + if (threadOrProcess instanceof TargetThread) { + rec = threadMap.get((TargetThread) threadOrProcess); + } + } + if (rec != null) { + rec.stateChanged(state); + } + // Else we'll discover it and sync state later + }); + } + + protected ThreadRecorder getOrCreateThreadRecorder(TargetThread thread) { + synchronized (threadMap) { + ThreadRecorder rec = threadMap.get(thread); + if (rec != null) { + return rec; + } + TraceThread traceThread; + String path = PathUtils.toString(thread.getPath()); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, path + " created")) { + // Note, if THREAD_CREATED is emitted, it will adjust the creation snap + traceThread = threadManager.createThread(path, thread.getShortDisplay(), + snapshot.getKey()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be a new thread in model + } + rec = new ThreadRecorder(thread, traceThread); + threadMap.put(rec); + return rec; + } + } + + @Override + public void registersUpdated(TargetRegisterBank bank, Map updates) { + if (!valid) { + return; + } + ThreadRecorder rec = threadMap.getForSuccessor(bank); + if (rec == null) { + return; + } + rec.recordRegisterValues(bank, updates); + } + + @Override + public void memoryUpdated(TargetMemory memory, Address address, byte[] data) { + if (!valid) { + return; + } + synchronized (DefaultTraceRecorderPre666_740.this) { + if (memMapper == null) { + Msg.warn(this, "Received memory write before a region has been added"); + return; + } + } + Address traceAddr = memMapper.targetToTrace(address); + long snap = snapshot.getKey(); + TimedMsg.info(this, "Memory updated: " + address + " (" + data.length + ")"); + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Memory observed")) { + ByteBuffer newBytes = ByteBuffer.wrap(data); + memoryManager.putBytes(snap, traceAddr, newBytes); + } + } + + @Override + public void memoryReadError(TargetMemory memory, AddressRange range, + DebuggerMemoryAccessException e) { + if (!valid) { + return; + } + Msg.error(this, "Error reading range " + range, e); + Address traceMin = memMapper.targetToTrace(range.getMinAddress()); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Memory read error")) { + memoryManager.setState(snapshot.getKey(), traceMin, TraceMemoryState.ERROR); + // TODO: Bookmark to describe error? + } + } + + @Override + public void breakpointToggled(TargetBreakpointSpec spec, boolean enabled) { + if (!valid) { + return; + } + spec.getLocations().thenAccept(bpts -> { + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Breakpoint toggled")) { + for (TargetBreakpointLocation eb : bpts) { + TraceBreakpoint traceBpt = getTraceBreakpoint(eb); + if (traceBpt == null) { + String path = PathUtils.toString(eb.getPath()); + Msg.warn(this, "Cannot find toggled trace breakpoint for " + path); + continue; + } + // Verify attributes match? Eh. If they don't, someone has fiddled with it. + traceBpt.splitWithEnabled(snapshot.getKey(), enabled); + } + } + }).exceptionally(ex -> { + Msg.error(this, "Error recording toggled breakpoint spec: " + spec, ex); + return null; + }); + } + + protected void breakpointLengthChanged(TargetBreakpointLocation bpt, int length) { + Address traceAddr = memMapper.targetToTrace(bpt.getAddress()); + String path = PathUtils.toString(bpt.getPath()); + for (TraceBreakpoint traceBpt : breakpointManager.getBreakpointsByPath(path)) { + if (traceBpt.getLength() == length) { + continue; // Nothing to change + } + // TODO: Verify all other attributes match? + // TODO: Should this be allowed to happen? + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Breakpoint length changed")) { + long snap = snapshot.getKey(); + if (traceBpt.getPlacedSnap() == snap) { + traceBpt.delete(); + } + else { + traceBpt.setClearedSnap(snap - 1); + } + breakpointManager.placeBreakpoint(path, snap, range(traceAddr, length), + traceBpt.getThreads(), traceBpt.getKinds(), traceBpt.isEnabled(), + traceBpt.getComment()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Split, and length matters not + } + } + } + + protected void framePcUpdated(TargetStackFrame frame) { + ThreadRecorder rec = threadMap.getForSuccessor(frame); + // Yes, entire stack, otherwise, the stack seems to be just one deep. + rec.recordStack(); + } + + protected void stackUpdated(TargetStack stack) { + ThreadRecorder rec = threadMap.getForSuccessor(stack); + rec.recordStack(); + } + + @Override + public void focusChanged(TargetFocusScope object, TargetObject focused) { + if (!valid) { + return; + } + if (PathUtils.isAncestor(target.getPath(), focused.getPath())) { + curFocus = focused; + } + } + + protected void retroOfferRegMapperDependents() { + List copy; + synchronized (objects) { + copy = List.copyOf(threadMap.byTargetThread.values()); + } + for (ThreadRecorder rec : copy) { + TargetRegisterBank bank = rec.regs.get(0); + if (bank != null) { + rec.recordRegisterValues(bank, bank.getCachedRegisters()); + rec.updateRegsMem(null); + } + } + } + + protected void retroOfferMemMapperDependents() { + List copy; + synchronized (objects) { + copy = List.copyOf(objects.values()); + } + synchronized (DefaultTraceRecorderPre666_740.this) { + for (TargetObject obj : copy) { + if (obj instanceof TargetModule) { + offerProcessModule((TargetModule) obj); + } + if (obj instanceof TargetSection) { + TargetSection section = (TargetSection) obj; + offerProcessModuleSection(section.getModule(), section); + } + if (obj instanceof TargetBreakpointLocation) { + offerEffectiveBreakpoint((TargetBreakpointLocation) obj); + } + if (obj instanceof TargetStack) { + stackUpdated((TargetStack) obj); + } + } + } + } + + public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { + synchronized (objects) { + return (TargetMemoryRegion) objects.get(PathUtils.parse(region.getPath())); + } + } + + public TargetModule getTargetModule(TraceModule module) { + synchronized (objects) { + return (TargetModule) objects.get(PathUtils.parse(module.getPath())); + } + } + + public TargetSection getTargetSection(TraceSection section) { + synchronized (objects) { + return (TargetSection) objects.get(PathUtils.parse(section.getPath())); + } + } + + public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { + synchronized (objects) { + return (TargetBreakpointLocation) objects.get(PathUtils.parse(bpt.getPath())); + } + } + + public List collectBreakpoints(TargetThread thread) { + synchronized (objects) { + return breakpoints.stream().filter(bpt -> { + TargetObjectList affects = bpt.getAffects(); + // N.B. in case thread is null (process), affects.contains(thread) is always false + return affects.isEmpty() || affects.contains(thread) || + affects.contains(target); + }).collect(Collectors.toList()); + } + } + + protected void onProcessBreakpointContainers( + Consumer action) { + synchronized (objects) { + if (processBreakpointContainer == null) { + for (TargetThread thread : threadsView) { + onThreadBreakpointContainers(thread, action); + } + } + else { + action.accept(processBreakpointContainer); + } + } + } + + protected void onThreadBreakpointContainers(TargetThread thread, + Consumer action) { + synchronized (objects) { + getOrCreateThreadRecorder(thread).onThreadBreakpointContainers(action); + } + } + + protected void onBreakpointContainers(TargetThread thread, + Consumer action) { + if (thread == null) { + onProcessBreakpointContainers(action); + } + else { + onThreadBreakpointContainers(thread, action); + } + } + } + + protected final DebuggerModelServicePlugin plugin; + protected final PluginTool tool; + protected final Trace trace; + protected final TargetObject target; + protected final ComposedMemory processMemory = new ComposedMemory(); + protected TargetBreakpointContainer processBreakpointContainer; + + protected final TraceBreakpointManager breakpointManager; + protected final TraceCodeManager codeManager; + protected final TraceBasedDataTypeManager dataTypeManager; + protected final TraceEquateManager equateManager; + protected final TraceMemoryManager memoryManager; + protected final TraceModuleManager moduleManager; + protected final TraceStackManager stackManager; + protected final TraceSymbolManager symbolManager; + protected final TraceThreadManager threadManager; + protected final TraceTimeManager timeManager; + + protected final AbstractDebuggerTargetTraceMapper mapper; + protected DebuggerMemoryMapper memMapper; + protected AsyncLazyMap regMappers; + protected final TargetDataTypeConverter typeConverter; + protected Collection extraRegs; + // TODO: Support automatic recording of user-specified extra registers... + // NOTE: Probably via watches, once we have those + // TODO: Probably move all the auto-reads into watches + + protected final ListenerSet listeners = + new ListenerSet<>(TraceRecorderListener.class); + protected final TriConsumer listenerRegAccChanged = + this::registerAccessibilityChanged; + protected final TriConsumer listenerProcMemAccChanged = + this::processMemoryAccessibilityChanged; + + private final ListenerForRecord listenerForRecord; + + protected final ThreadMap threadMap = new ThreadMap(); + protected final Set threadsView = + Collections.unmodifiableSet(threadMap.byTargetThread.keySet()); + protected final BiMap processBreakpointsMap = + HashBiMap.create(); + + protected final AsyncLazyValue lazyInit = new AsyncLazyValue<>(this::doInit); + + protected TraceSnapshot snapshot = null; + private boolean valid = true; + + protected TargetFocusScope focusScope; + protected TargetObject curFocus; + + public DefaultTraceRecorderPre666_740(DebuggerModelServicePlugin plugin, Trace trace, + TargetObject target, + AbstractDebuggerTargetTraceMapper mapper) { + this.plugin = plugin; + this.tool = plugin.getTool(); + this.trace = trace; + this.target = target; + + this.breakpointManager = trace.getBreakpointManager(); + this.codeManager = trace.getCodeManager(); + this.dataTypeManager = trace.getDataTypeManager(); + this.equateManager = trace.getEquateManager(); + this.memoryManager = trace.getMemoryManager(); + this.moduleManager = trace.getModuleManager(); + this.stackManager = trace.getStackManager(); + this.symbolManager = trace.getSymbolManager(); + this.threadManager = trace.getThreadManager(); + this.timeManager = trace.getTimeManager(); + + this.mapper = mapper; + this.regMappers = + new AsyncLazyMap<>(new HashMap<>(), descs -> mapper.offerRegisters(descs)); + this.typeConverter = new TargetDataTypeConverter(trace.getDataTypeManager()); + + this.listenerForRecord = new ListenerForRecord(); + + processMemory.memAccListeners.add(listenerProcMemAccChanged); + + trace.addConsumer(this); + } + + protected void registerAccessibilityChanged(boolean old, boolean acc, Void __) { + listeners.fire.registerAccessibilityChanged(this); + } + + protected void processMemoryAccessibilityChanged(boolean old, boolean acc, Void __) { + listeners.fire.processMemoryAccessibilityChanged(this); + } + + @Override + public CompletableFuture init() { + return lazyInit.request(); + } + + protected CompletableFuture doInit() { + createSnapshot("Started recording " + PathUtils.toString(target.getPath()) + " in " + + target.getModel(), null, null); + AsyncFence fence = new AsyncFence(); + CompletableFuture futureBreaks = + DebugModelConventions.findSuitable(TargetBreakpointContainer.class, target); + fence.include(futureBreaks.thenAccept(breaks -> { + if (breaks != null && !PathUtils.isAncestor(target.getPath(), breaks.getPath())) { + offerProcessBreakpointContainer(breaks); // instead of objectAdded + listenerForRecord.addListenerAndConsiderSuccessors(breaks); + } + }).exceptionally(e -> { + Msg.error(this, "Could not search for breakpoint container", e); + return null; + })); + + CompletableFuture futureEvents = + DebugModelConventions.findSuitable(TargetEventScope.class, target); + fence.include(futureEvents.thenAccept(events -> { + if (events != null && !PathUtils.isAncestor(target.getPath(), events.getPath())) { + // Don't descend. Scope may be the entire session. + listenerForRecord.addListener(events); + } + }).exceptionally(e -> { + Msg.warn(this, "Could not search for event scope", e); + return null; + })); + + CompletableFuture futureFocus = + DebugModelConventions.findSuitable(TargetFocusScope.class, target); + fence.include(futureFocus.thenAccept(focus -> { + if (focus != null && !PathUtils.isAncestor(target.getPath(), focus.getPath())) { + // Don't descend. Scope may be the entire session. + offerFocusScope(focus); + listenerForRecord.addListener(focus); + } + }).exceptionally(e -> { + Msg.error(this, "Could not search for focus scope", e); + return null; + })); + return fence.ready().thenAccept(__ -> { + listenerForRecord.objectAdded(target); // TODO: This seems wrong + listenerForRecord.addListenerAndConsiderSuccessors(target); + }); + } + + protected synchronized void doAdvanceSnap(String description, TraceThread eventThread) { + snapshot = timeManager.createSnapshot(description); + snapshot.setEventThread(eventThread); + } + + @Override + public TraceSnapshot forceSnapshot() { + createSnapshot("User-forced snapshot", null, null); + return snapshot; + } + + protected void createSnapshot(String description, TraceThread eventThread, + PermanentTransaction tid) { + if (tid != null) { + doAdvanceSnap(description, eventThread); + listeners.fire.snapAdvanced(this, snapshot.getKey()); + return; + } + try (PermanentTransaction tid2 = PermanentTransaction.start(trace, description)) { + doAdvanceSnap(description, eventThread); + } + listeners.fire.snapAdvanced(this, snapshot.getKey()); + } + + // TODO: This could probably be discovered by the offer and passed in at construction + protected synchronized CompletableFuture initMemMapper(TargetMemory memory) { + /** + * TODO: At the moment, there's no real dependency on the memory. When there is, see that + * additional memories can be incorporated into the mapper, and stale ones removed. + * Alternatively, formalize that there is no possible dependency on memory. + */ + if (memMapper != null) { + return AsyncUtils.NIL; + } + return mapper.offerMemory(memory).thenAccept(mm -> { + synchronized (this) { + memMapper = mm; + } + listenerForRecord.retroOfferMemMapperDependents(); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize memory mapper", ex); + return null; + }); + } + + protected Collection getTraceFlags(TargetMemoryRegion region) { + Collection flags = new HashSet<>(); + if (region.isReadable()) { + flags.add(TraceMemoryFlag.READ); + } + if (region.isWritable()) { + flags.add(TraceMemoryFlag.WRITE); + } + if (region.isExecutable()) { + flags.add(TraceMemoryFlag.EXECUTE); + } + // TODO: Volatile? Can any debugger report that? + return flags; + } + + protected void offerProcessRegion(TargetMemoryRegion region) { + TargetMemory mem = region.getMemory(); + processMemory.addRegion(region, mem); + initMemMapper(mem); + synchronized (this) { + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Memory region added")) { + String path = PathUtils.toString(region.getPath()); + TraceMemoryRegion traceRegion = + memoryManager.getLiveRegionByPath(snapshot.getKey(), path); + if (traceRegion != null) { + Msg.warn(this, "Region " + path + " already recorded"); + return; + } + traceRegion = memoryManager.addRegion(path, Range.atLeast(snapshot.getKey()), + memMapper.targetToTrace(region.getRange()), getTraceFlags(region)); + traceRegion.setName(region.getName()); + } + catch (TraceOverlappedRegionException e) { + Msg.error(this, "Failed to create region due to overlap", e); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Just checked for existing + } + } + updateAllThreadsRegsMem(region).exceptionally(ex -> { + Msg.error(this, "Could not add process memory region", ex); + return null; + }); + } + + protected synchronized void removeProcessRegion(TargetMemoryRegion region) { + // Already removed from processMemory. That's how we knew to go here. + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Memory region removed")) { + String path = PathUtils.toString(region.getPath()); + long snap = snapshot.getKey(); + TraceMemoryRegion traceRegion = memoryManager.getLiveRegionByPath(snap, path); + if (traceRegion == null) { + Msg.warn(this, "Could not find region " + path + " in trace to remove"); + return; + } + traceRegion.setDestructionSnap(snap - 1); + } + catch (DuplicateNameException | TraceOverlappedRegionException e) { + throw new AssertionError(e); // Region is shrinking in time + } + } + + protected void recordBreakpoint(TargetBreakpointSpec spec, TargetBreakpointLocation bpt, + Set traceThreads) { + synchronized (this) { + if (memMapper == null) { + throw new IllegalStateException( + "No memory mapper! Have not recorded a region, yet."); + } + } + String path = PathUtils.toString(bpt.getPath()); + String name = nameBreakpoint(bpt); + Address traceAddr = memMapper.targetToTrace(bpt.getAddress()); + AddressRange traceRange = range(traceAddr, bpt.getLength()); + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Breakpoint placed")) { + boolean enabled = spec.isEnabled(); + Set traceKinds = + TraceRecorder.targetToTraceBreakpointKinds(spec.getKinds()); + TraceBreakpoint traceBpt = breakpointManager.placeBreakpoint(path, snapshot.getKey(), + traceRange, traceThreads, traceKinds, enabled, spec.getExpression()); + traceBpt.setName(name); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be new to model, or already cleared + } + } + + protected void offerProcessBreakpointContainer(TargetBreakpointContainer bc) { + if (processBreakpointContainer != null) { + Msg.warn(this, "Already have a breakpoint container for this process"); + } + processBreakpointContainer = bc; + } + + protected void offerFocusScope(TargetFocusScope scope) { + if (this.focusScope != null) { + Msg.warn(this, "Already have a focus scope: " + this.focusScope); + } + this.focusScope = scope; + } + + protected synchronized TraceModule offerProcessModule(TargetModule module) { + if (memMapper == null) { + return null; + } + + String path = PathUtils.toString(module.getPath()); + TraceModule traceModule = moduleManager.getLoadedModuleByPath(snapshot.getKey(), path); + if (traceModule != null) { + return traceModule; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Module " + path + " loaded")) { + AddressRange targetRange = module.getRange(); + AddressRange traceRange = + targetRange == null ? null : memMapper.targetToTrace(targetRange); + traceModule = moduleManager.addLoadedModule(path, module.getModuleName(), traceRange, + snapshot.getKey()); + return traceModule; + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // We checked for existing by path + } + } + + protected synchronized TraceSection offerProcessModuleSection(TargetModule module, + TargetSection section) { + if (memMapper == null) { + return null; + } + String path = PathUtils.toString(section.getPath()); + TraceModule traceModule = offerProcessModule(module); + TraceSection traceSection = moduleManager.getLoadedSectionByPath(snapshot.getKey(), path); + if (traceSection != null) { + Msg.warn(this, path + " already recorded"); + return traceSection; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Section " + path + " added")) { + AddressRange targetRange = section.getRange(); + AddressRange traceRange = memMapper.targetToTrace(targetRange); + traceSection = traceModule.addSection(path, section.getIndex(), traceRange); + return traceSection; + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // We checked for existing by name + } + } + + protected synchronized void removeProcessModule(TargetModule module) { + String path = PathUtils.toString(module.getPath()); + long snap = snapshot.getKey(); + TraceThread eventThread = snapshot.getEventThread(); + TraceModule traceModule = moduleManager.getLoadedModuleByPath(snap, path); + if (traceModule == null) { + Msg.warn(this, "unloaded " + path + " is not in the trace"); + return; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Module " + path + " unloaded")) { + if (traceModule.getLoadedSnap() == snap) { + Msg.warn(this, "Observed module unload in the same snap as its load"); + createSnapshot("WARN: Module removed", eventThread, tid); + snap = snapshot.getKey(); + } + traceModule.setUnloadedSnap(snap - 1); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Module lifespan should be shrinking + } + } + + // NB: No removeProcessModuleSection, because sections should be immutable + // They are removed when the module is removed + + protected void offerEffectiveBreakpoint(TargetBreakpointLocation bpt) { + synchronized (this) { + if (memMapper == null) { + return; + } + } + EffectiveBreakpointResolver resolver = new EffectiveBreakpointResolver(bpt); + resolver.resolve().thenAccept(__ -> { + if (resolver.affectsProcess || !resolver.threadsAffected.isEmpty()) { + recordBreakpoint(resolver.spec, bpt, resolver.threadsAffected); + } + }).exceptionally(ex -> { + Msg.error(this, "Could record target breakpoint: " + bpt, ex); + return null; + }); + } + + protected void removeEffectiveBreakpoint(TargetBreakpointLocation bpt) { + String path = PathUtils.toString(bpt.getPath()); + long snap = snapshot.getKey(); + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Breakpoint deleted")) { + for (TraceBreakpoint traceBpt : breakpointManager.getBreakpointsByPath(path)) { + if (traceBpt.getPlacedSnap() > snap) { + Msg.error(this, + "Tracked, now removed breakpoint was placed in the future? " + bpt); + } + else if (traceBpt.getPlacedSnap() == snap) { + // TODO: I forget if this is allowed for DBTrace iteration + traceBpt.delete(); + } + else { + traceBpt.setClearedSnap(snap - 1); + } + } + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Lifespan in shrinking + } + } + + protected CompletableFuture updateAllThreadsRegsMem(TargetMemoryRegion limit) { + AsyncFence fence = new AsyncFence(); + for (ThreadRecorder rec : threadMap.recorders()) { + fence.include(rec.updateRegsMem(limit)); + } + return fence.ready(); + } + + @Override + public TargetObject getTarget() { + return target; + } + + @Override + public Trace getTrace() { + return trace; + } + + @Override + public long getSnap() { + return snapshot.getKey(); + } + + @Override + public boolean isRecording() { + return valid; + } + + @Override + public void stopRecording() { + invalidate(); + listeners.fire.recordingStopped(this); + } + + @Override + public void addListener(TraceRecorderListener l) { + listeners.add(l); + } + + @Override + public void removeListener(TraceRecorderListener l) { + listeners.remove(l); + } + + @Override + public boolean isViewAtPresent(TraceProgramView view) { + if (!valid) { + return false; + } + if (!Objects.equals(trace, view.getTrace())) { + return false; + } + if (snapshot.getKey() != view.getSnap()) { + return false; + } + return true; + } + + @Override + public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { + return listenerForRecord.getTargetBreakpoint(bpt); + } + + @Override + public TraceBreakpoint getTraceBreakpoint(TargetBreakpointLocation bpt) { + String path = PathUtils.toString(bpt.getPath()); + return breakpointManager.getPlacedBreakpointByPath(snapshot.getKey(), path); + } + + @Override + public List collectBreakpointContainers(TargetThread thread) { + List result = new ArrayList<>(); + listenerForRecord.onBreakpointContainers(thread, result::add); + return result; + } + + @Override + public List collectBreakpoints(TargetThread thread) { + return listenerForRecord.collectBreakpoints(thread); + } + + @Override + public Set getSupportedBreakpointKinds() { + Set tKinds = new HashSet<>(); + listenerForRecord.onBreakpointContainers(null, cont -> { + tKinds.addAll(cont.getSupportedBreakpointKinds()); + }); + return TraceRecorder.targetToTraceBreakpointKinds(tKinds); + } + + @Override + public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { + return listenerForRecord.getTargetMemoryRegion(region); + } + + @Override + public TraceMemoryRegion getTraceMemoryRegion(TargetMemoryRegion region) { + String path = PathUtils.toString(region.getPath()); + return memoryManager.getLiveRegionByPath(snapshot.getKey(), path); + } + + @Override + public TargetModule getTargetModule(TraceModule module) { + return listenerForRecord.getTargetModule(module); + } + + @Override + public TraceModule getTraceModule(TargetModule module) { + String path = PathUtils.toString(module.getPath()); + return moduleManager.getLoadedModuleByPath(snapshot.getKey(), path); + } + + @Override + public TargetSection getTargetSection(TraceSection section) { + return listenerForRecord.getTargetSection(section); + } + + @Override + public TraceSection getTraceSection(TargetSection section) { + String path = PathUtils.toString(section.getPath()); + return moduleManager.getLoadedSectionByPath(snapshot.getKey(), path); + } + + @Override + public TargetThread getTargetThread(TraceThread thread) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.targetThread; + } + + @Override + public TargetExecutionState getTargetThreadState(TargetThread thread) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.state; + } + + @Override + public TargetExecutionState getTargetThreadState(TraceThread thread) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.state; + } + + @Override + public boolean isRegisterBankAccessible(TargetRegisterBank bank) { + if (bank == null) { + return false; + } + synchronized (accessibilityByRegBank) { + KeyedFuture future = accessibilityByRegBank.get(bank); + if (future == null) { + return false; + } + AllRequiredAccess acc = future.getNow(null); + if (acc == null) { + return false; + } + return acc.get(); + } + } + + @Override + public boolean isRegisterBankAccessible(TraceThread thread, int frameLevel) { + return isRegisterBankAccessible(getTargetRegisterBank(thread, frameLevel)); + } + + @Override + public TargetRegisterBank getTargetRegisterBank(TraceThread thread, int frameLevel) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.regs.get(frameLevel); + } + + @Override + public Set getLiveTargetThreads() { + return threadsView; + } + + @Override + public TraceThread getTraceThread(TargetThread thread) { + ThreadRecorder rec = threadMap.byTargetThread.get(thread); + return rec == null ? null : rec.traceThread; + } + + @Override + public TraceThread getTraceThreadForSuccessor(TargetObject successor) { + ThreadRecorder rec = threadMap.getForSuccessor(successor); + return rec == null ? null : rec.traceThread; + } + + protected TraceStackFrame getTraceStackFrame(TraceThread thread, int level) { + TraceStack stack = trace.getStackManager().getLatestStack(thread, snapshot.getKey()); + if (stack == null) { + return null; + } + return stack.getFrame(level, false); + } + + @Override + public TraceStackFrame getTraceStackFrame(TargetStackFrame frame) { + ThreadRecorder rec = threadMap.getForSuccessor(frame); + if (rec == null) { + return null; + } + int level = getFrameLevel(frame); + if (rec.stack.get(level) != frame) { + return null; + } + return getTraceStackFrame(rec.traceThread, level); + } + + @Override + public TraceStackFrame getTraceStackFrameForSuccessor(TargetObject successor) { + ThreadRecorder rec = threadMap.getForSuccessor(successor); + if (rec == null) { + return null; + } + int level = rec.getSuccessorFrameLevel(successor); + return getTraceStackFrame(rec.traceThread, level); + } + + @Override + public TargetStackFrame getTargetStackFrame(TraceThread thread, int frameLevel) { + ThreadRecorder rec = threadMap.get(thread); + if (rec == null) { + return null; + } + return rec.stack.get(frameLevel); + } + + @Override + public DebuggerMemoryMapper getMemoryMapper() { + return memMapper; + } + + @Override + public DebuggerRegisterMapper getRegisterMapper(TraceThread thread) { + ThreadRecorder rec = threadMap.get(thread); + if (rec == null) { + return null; + } + return rec.regMapper; + } + + @Override + public AddressSetView getAccessibleProcessMemory() { + // TODO: Efficiently distinguish which memory is process vs. thread + return getAccessibleMemory(mem -> true); + } + + protected void invalidate() { + valid = false; + listenerForRecord.dispose(); + trace.release(this); + } + + protected TraceThread findLiveThreadByName(String name) { + for (TraceThread traceThread : threadManager.getThreadsByPath(name)) { + if (traceThread != null && traceThread.isAlive()) { + return traceThread; + } + } + return null; + } + + @Override + public CompletableFuture captureThreadRegisters(TraceThread thread, int frameLevel, + Set registers) { + DebuggerRegisterMapper regMapper = getRegisterMapper(thread); + if (regMapper == null) { + throw new IllegalStateException("Have not found register descriptions for " + thread); + } + if (!regMapper.getRegistersOnTarget().containsAll(registers)) { + throw new IllegalArgumentException( + "All given registers must be recognized by the target"); + } + if (registers.isEmpty()) { + return AsyncUtils.NIL; + } + List tRegs = + registers.stream().map(regMapper::traceToTarget).collect(Collectors.toList()); + + TargetRegisterBank bank = getTargetRegisterBank(thread, frameLevel); + if (bank == null) { + throw new IllegalArgumentException( + "Given thread and frame level does not have a live register bank"); + } + // NOTE: Cache update, if applicable, will cause recorder to write values to trace + return bank.readRegisters(tRegs).thenApply(__ -> null); + } + + @Override + public CompletableFuture writeThreadRegisters(TraceThread thread, int frameLevel, + Map values) { + DebuggerRegisterMapper regMapper = getRegisterMapper(thread); + if (regMapper == null) { + throw new IllegalStateException("Have not found register descriptions for " + thread); + } + if (!regMapper.getRegistersOnTarget().containsAll(values.keySet())) { + throw new IllegalArgumentException( + "All given registers must be recognized by the target"); + } + if (values.isEmpty()) { + return AsyncUtils.NIL; + } + Map tVals = values.entrySet().stream().map(ent -> { + if (ent.getKey() != ent.getValue().getRegister()) { + throw new IllegalArgumentException("register name mismatch in value"); + } + return regMapper.traceToTarget(ent.getValue()); + }).collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + TargetRegisterBank bank = getTargetRegisterBank(thread, frameLevel); + if (bank == null) { + throw new IllegalArgumentException( + "Given thread and frame level does not have a live register bank"); + } + // NOTE: Model + recorder will cause applicable trace updates + return bank.writeRegistersNamed(tVals).thenApply(__ -> null); + } + + @Override + public CompletableFuture readProcessMemory(Address start, int length) { + Address tStart = memMapper.traceToTarget(start); + return processMemory.readMemory(tStart, length); + } + + @Override + public CompletableFuture writeProcessMemory(Address start, byte[] data) { + Address tStart = memMapper.traceToTarget(start); + return processMemory.writeMemory(tStart, data); + } + + @Override + public CompletableFuture captureProcessMemory(AddressSetView set, TaskMonitor monitor) { + if (set.isEmpty()) { + return AsyncUtils.NIL; + } + // TODO: Figure out how to display/select per-thread memory. + // Probably need a thread parameter passed in then? + // NOTE: That thread memory will already be chained to process memory. Good. + + int total = 0; + AddressSetView expSet = + expandToBlocks(set).intersect(memoryManager.getRegionsAddressSet(snapshot.getKey())); + for (AddressRange r : expSet) { + total += Long.divideUnsigned(r.getLength() + BLOCK_SIZE - 1, BLOCK_SIZE); + } + monitor.initialize(total); + monitor.setMessage("Capturing memory"); + // TODO: Read blocks in parallel? Probably NO. Tends to overload the agent. + return AsyncUtils.each(TypeSpec.VOID, expSet.iterator(), (r, loop) -> { + AddressRangeChunker it = new AddressRangeChunker(r, BLOCK_SIZE); + AsyncUtils.each(TypeSpec.VOID, it.iterator(), (vRng, inner) -> { + // The listener in the recorder will copy to the Trace. + monitor.incrementProgress(1); + AddressRange tRng = memMapper.traceToTarget(vRng); + processMemory.readMemory(tRng.getMinAddress(), (int) tRng.getLength()) + .thenApply(b -> !monitor.isCancelled()) + .handle(inner::repeatWhile); + }).exceptionally(e -> { + Msg.error(this, "Error reading range " + r + ": " + e); + // NOTE: Above may double log, since recorder listens for errors, too + return null; // Continue looping on errors + }).thenApply(v -> !monitor.isCancelled()).handle(loop::repeatWhile); + }); + } + + @Override + public CompletableFuture captureDataTypes(TargetDataTypeNamespace namespace, + TaskMonitor monitor) { + if (!valid) { + return AsyncUtils.NIL; + } + String path = PathUtils.toString(namespace.getPath()); + monitor.setMessage("Capturing data types for " + path); + return namespace.getTypes().thenCompose(types -> { + monitor.initialize(types.size()); + AsyncFence fence = new AsyncFence(); + List converted = new ArrayList<>(); + for (TargetNamedDataType type : types) { + if (monitor.isCancelled()) { + fence.ready().cancel(false); + return AsyncUtils.nil(); + } + monitor.incrementProgress(1); + fence.include(typeConverter.convertTargetDataType(type).thenAccept(converted::add)); + } + return fence.ready().thenApply(__ -> converted); + }).thenAccept(converted -> { + if (converted == null) { + return; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Capture data types for " + path)) { + // NOTE: createCategory is actually getOrCreate + Category category = dataTypeManager.createCategory(new CategoryPath("/" + path)); + for (DataType dataType : converted) { + category.addDataType(dataType, DataTypeConflictHandler.DEFAULT_HANDLER); + } + } + }); + } + + @Override + public CompletableFuture captureDataTypes(TraceModule module, TaskMonitor monitor) { + TargetModule targetModule = getTargetModule(module); + if (targetModule == null) { + Msg.error(this, "Module " + module + " is not loaded"); + return AsyncUtils.NIL; + } + CompletableFuture> future = + targetModule.fetchChildrenSupporting(TargetDataTypeNamespace.class); + // NOTE: I should expect exactly one namespace... + return future.thenCompose(namespaces -> { + AsyncFence fence = new AsyncFence(); + for (TargetDataTypeNamespace ns : namespaces.values()) { + fence.include(captureDataTypes(ns, monitor)); + } + return fence.ready(); + }); + } + + private TraceNamespaceSymbol createNamespaceIfAbsent(String path) { + try { + return symbolManager.namespaces() + .add(path, symbolManager.getGlobalNamespace(), SourceType.IMPORTED); + } + catch (DuplicateNameException e) { + Msg.info(this, "Namespace for module " + path + + " already exists or another exists with a conflicting name. Using the existing one: " + + e); + TraceNamespaceSymbol ns = symbolManager.namespaces().getGlobalNamed(path); + if (ns != null) { + return ns; + } + Msg.error(this, "Existing namespace for " + path + + " is not a plain namespace. Using global namespace."); + return symbolManager.getGlobalNamespace(); + } + catch (InvalidInputException | IllegalArgumentException e) { + Msg.error(this, + "Could not create namespace for new module: " + path + ". Using global namespace.", + e); + return symbolManager.getGlobalNamespace(); + } + } + + @Override + public CompletableFuture captureSymbols(TargetSymbolNamespace namespace, + TaskMonitor monitor) { + if (!valid) { + return AsyncUtils.NIL; + } + String path = PathUtils.toString(namespace.getPath()); + monitor.setMessage("Capturing symbols for " + path); + return namespace.getSymbols().thenAccept(symbols -> { + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Capture types and symbols for " + path)) { + TraceNamespaceSymbol ns = createNamespaceIfAbsent(path); + monitor.setMessage("Capturing symbols for " + path); + monitor.initialize(symbols.size()); + for (TargetSymbol sym : symbols) { + if (monitor.isCancelled()) { + return; + } + monitor.incrementProgress(1); + String symName = sym.getIndex(); + if (sym.isConstant()) { + // TODO: Equate namespaces? + TraceEquate equate = equateManager.getByName(symName); + long symVal = sym.getValue().getOffset(); + if (equate != null && equate.getValue() == symVal) { + continue; + } + try { + equateManager.create(symName, symVal); + } + catch (DuplicateNameException | IllegalArgumentException e) { + Msg.error(this, "Could not create equate: " + symName, e); + } + continue; + } + Address addr = memMapper.targetToTrace(sym.getValue()); + try { + symbolManager.labels() + .create(snapshot.getKey(), null, addr, symName, ns, + SourceType.IMPORTED); + } + catch (InvalidInputException e) { + Msg.error(this, "Could not add module symbol " + sym + ": " + e); + } + /** + * TODO: Lay down data type, if present + * + * TODO: Interpret "address" type correctly. A symbol with this type is itself + * the pointer. In other words, it is not specifying the type to lay down in + * memory. + */ + } + } + }); + } + + @Override + public CompletableFuture captureSymbols(TraceModule module, TaskMonitor monitor) { + TargetModule targetModule = getTargetModule(module); + if (targetModule == null) { + Msg.error(this, "Module " + module + " is not loaded"); + return AsyncUtils.NIL; + } + CompletableFuture> future = + targetModule.fetchChildrenSupporting(TargetSymbolNamespace.class); + // NOTE: I should expect exactly one namespace... + return future.thenCompose(namespaces -> { + AsyncFence fence = new AsyncFence(); + for (TargetSymbolNamespace ns : namespaces.values()) { + fence.include(captureSymbols(ns, monitor)); + } + return fence.ready(); + }); + } + + @Override + public boolean isSupportsFocus() { + return focusScope != null; + } + + @Override + public TargetObject getFocus() { + if (curFocus == null) { + if (focusScope == null) { + return null; + } + TargetObject focus = focusScope.getFocus(); + if (focus == null || !PathUtils.isAncestor(target.getPath(), focus.getPath())) { + return null; + } + curFocus = focus; + } + return curFocus; + } + + @Override + public CompletableFuture requestFocus(TargetObject focus) { + if (!isSupportsFocus()) { + return CompletableFuture + .failedFuture(new IllegalArgumentException("Target does not support focus")); + } + if (!PathUtils.isAncestor(target.getPath(), focus.getPath())) { + return CompletableFuture.failedFuture(new IllegalArgumentException( + "Requested focus path is not a successor of the target")); + } + if (!PathUtils.isAncestor(focusScope.getPath(), focus.getPath())) { + return CompletableFuture.failedFuture(new IllegalArgumentException( + "Requested focus path is not a successor of the focus scope")); + } + return focusScope.requestFocus(focus).thenApply(__ -> true).exceptionally(ex -> { + ex = AsyncUtils.unwrapThrowable(ex); + if (ex instanceof DebuggerModelAccessException) { + String msg = "Could not focus " + focus + ": " + ex.getMessage(); + Msg.info(this, msg); + plugin.getTool().setStatusInfo(msg); + } + Msg.showError(this, null, "Focus Sync", "Could not focus " + focus, ex); + return false; + }); + } + + @Override + public ListenerForRecord getListenerForRecord() { + return listenerForRecord; + } +} diff --git a/Ghidra/Debug/Debugger/garbage/DefaultTraceRecorderSaved.java b/Ghidra/Debug/Debugger/garbage/DefaultTraceRecorderSaved.java new file mode 100644 index 0000000000..53c38877c7 --- /dev/null +++ b/Ghidra/Debug/Debugger/garbage/DefaultTraceRecorderSaved.java @@ -0,0 +1,2641 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.nio.ByteBuffer; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import com.google.common.collect.*; + +import ghidra.app.plugin.core.debug.mapping.*; +import ghidra.app.services.TraceRecorder; +import ghidra.app.services.TraceRecorderListener; +import ghidra.async.*; +import ghidra.async.AsyncLazyMap.KeyedFuture; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AllRequiredAccess; +import ghidra.dbg.DebugModelConventions.SubTreeListenerAdapter; +import ghidra.dbg.attributes.*; +import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.dbg.error.DebuggerModelAccessException; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibility; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointSpecListener; +import ghidra.dbg.target.TargetEventScope.TargetEventScopeListener; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionStateListener; +import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener; +import ghidra.dbg.target.TargetMemory.TargetMemoryListener; +import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener; +import ghidra.dbg.util.*; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.*; +import ghidra.program.model.data.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.symbol.SourceType; +import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.breakpoint.*; +import ghidra.trace.model.data.TraceBasedDataTypeManager; +import ghidra.trace.model.listing.TraceCodeManager; +import ghidra.trace.model.memory.*; +import ghidra.trace.model.modules.*; +import ghidra.trace.model.program.TraceProgramView; +import ghidra.trace.model.stack.*; +import ghidra.trace.model.symbol.*; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.thread.TraceThreadManager; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.trace.model.time.TraceTimeManager; +import ghidra.util.*; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.datastruct.ListenerSet; +import ghidra.util.exception.DuplicateNameException; +import ghidra.util.exception.InvalidInputException; +import ghidra.util.task.TaskMonitor; + +public class DefaultTraceRecorderSaved implements TraceRecorder { + + private static final boolean LOG_STACK_TRACE = false; + // For large memory captures + private static final int BLOCK_SIZE = 4096; + private static final long BLOCK_MASK = -1L << 12; + + static final PathMatcher HARDCODED_MATCHER = new PathMatcher() { + { + // Paths for GDB + addPattern(PathUtils.parse("Breakpoints[].")); + addPattern(PathUtils.parse("Inferiors[].Memory[]")); + addPattern(PathUtils.parse("Inferiors[].Modules[].Sections[]")); + addPattern(PathUtils.parse("Inferiors[].Registers[]")); + addPattern(PathUtils.parse("Inferiors[].Threads[]")); + addPattern(PathUtils.parse("Inferiors[].Threads[].Stack[]")); + + // Paths for dbgeng + addPattern(PathUtils.parse("Sessions[].Processes[].Memory[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Modules[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Registers[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Stack[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Debug.Breakpoints[]")); + + // (Additional) paths for dbgmodel + addPattern(PathUtils.parse("Sessions[].Attributes")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Stack.Frames[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].TTD.Position")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Registers.User.")); + + // Paths for JDI + addPattern(PathUtils.parse("VirtualMachines[]")); + addPattern(PathUtils.parse("VirtualMachines[].Breakpoints")); + addPattern(PathUtils.parse("VirtualMachines[].Classes[]")); + addPattern(PathUtils.parse("VirtualMachines[].Classes[].Sections[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[].Registers[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[].Stack[]")); + + } + }; + + protected static class PermanentTransaction implements AutoCloseable { + static PermanentTransaction start(Trace trace, String description) { + UndoableTransaction tid = null; + try { + tid = UndoableTransaction.start(trace, description, true); + } + catch (Throwable t) { + tid.close(); + return ExceptionUtils.rethrow(t); + } + return new PermanentTransaction(trace, tid); + } + + private final Trace trace; + private final UndoableTransaction tid; + + public PermanentTransaction(Trace trace, UndoableTransaction tid) { + this.trace = trace; + this.tid = tid; + } + + @Override + public void close() { + tid.close(); + trace.clearUndo(); + } + } + + protected final AsyncLazyMap, AllRequiredAccess> accessibilityByRegBank = + new AsyncLazyMap<>(new HashMap<>(), this::fetchRegAccessibility) { + public AllRequiredAccess remove(TargetRegisterBank key) { + AllRequiredAccess acc = super.remove(key); + if (acc != null) { + acc.removeChangeListener(listenerRegAccChanged); + } + return acc; + } + }; + protected final Map, TargetMemory> byRegion = new HashMap<>(); + protected final AsyncLazyMap, AllRequiredAccess> accessibilityByMemory = + new AsyncLazyMap<>(new HashMap<>(), this::fetchMemAccessibility) { + public AllRequiredAccess remove(TargetMemory key) { + AllRequiredAccess acc = super.remove(key); + if (acc != null) { + acc.removeChangeListener(processMemory.memAccListeners.fire); + } + return acc; + } + }; + + protected CompletableFuture fetchRegAccessibility( + TargetRegisterBank bank) { + return DebugModelConventions.trackAccessibility(bank).thenApply(acc -> { + acc.addChangeListener(listenerRegAccChanged); + return acc; + }); + } + + protected CompletableFuture fetchMemAccessibility(TargetMemory mem) { + return DebugModelConventions.trackAccessibility(mem).thenApply(acc -> { + acc.addChangeListener(processMemory.memAccListeners.fire); + return acc; + }); + } + + /** + * Get accessible memory, as viewed in the trace + * + * @param pred an additional predicate applied via "AND" with accessibility + * @return the computed set + */ + protected AddressSet getAccessibleMemory(Predicate> pred) { + synchronized (accessibilityByMemory) { + // TODO: Might accomplish by using listeners and tracking the accessible set + AddressSet accessible = new AddressSet(); + for (Entry, TargetMemory> ent : byRegion.entrySet()) { + TargetMemory mem = ent.getValue(); + if (!pred.test(mem)) { + continue; + } + AllRequiredAccess acc = accessibilityByMemory.getCompletedMap().get(mem); + if (acc == null || acc.getAllAccessibility() != TargetAccessibility.ACCESSIBLE) { + continue; + } + accessible.add(memMapper.targetToTrace(ent.getKey().getRange())); + } + return accessible; + } + } + + protected class ComposedMemory { + protected final ComposedMemory chain; + + protected final NavigableMap> byMin = new TreeMap<>(); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected final ListenerSet> memAccListeners = + new ListenerSet(TriConsumer.class); + + public ComposedMemory() { + this.chain = null; + } + + public ComposedMemory(ComposedMemory chain) { + this.chain = chain; + } + + protected void addRegion(TargetMemoryRegion region, TargetMemory memory) { + synchronized (accessibilityByMemory) { + TargetMemory old = byRegion.put(region, memory); + assert old == null; + byMin.put(region.getRange().getMinAddress(), region); + accessibilityByMemory.get(memory).exceptionally(e -> { + e = AsyncUtils.unwrapThrowable(e); + Msg.error(this, "Could not track memory accessibility: " + e.getMessage()); + return null; + }); + } + } + + protected boolean removeRegion(TargetObject invalid) { + if (!(invalid instanceof TargetMemoryRegion)) { + return false; + } + synchronized (accessibilityByMemory) { + TargetMemoryRegion invRegion = (TargetMemoryRegion) invalid; + TargetMemory old = byRegion.remove(invRegion); + assert old != null; + byMin.remove(invRegion.getRange().getMinAddress()); + if (!old.isValid() || !byRegion.containsValue(old)) { + accessibilityByMemory.remove(old); + } + return true; + } + } + + protected AllRequiredAccess findChainedMemoryAccess(TargetMemoryRegion region) { + synchronized (accessibilityByMemory) { + TargetMemory mem = byRegion.get(region); + if (mem != null) { + return accessibilityByMemory.getCompletedMap().get(mem); + } + return chain == null ? null : chain.findChainedMemoryAccess(region); + } + } + + protected Entry> findChainedFloor(Address address) { + synchronized (accessibilityByMemory) { + Entry> myFloor = byMin.floorEntry(address); + Entry> byChain = + chain == null ? null : chain.findChainedFloor(address); + if (byChain == null) { + return myFloor; + } + if (myFloor == null) { + return byChain; + } + int c = myFloor.getKey().compareTo(byChain.getKey()); + if (c < 0) { + return byChain; + } + return myFloor; + } + } + + protected AddressRange align(Address address, int length) { + AddressSpace space = address.getAddressSpace(); + long offset = address.getOffset(); + Address start = space.getAddress(offset & BLOCK_MASK); + Address end = space.getAddress(((offset + length - 1) & BLOCK_MASK) + BLOCK_SIZE - 1); + return new AddressRangeImpl(start, end); + } + + protected AddressRange alignWithLimit(Address address, int length, + TargetMemoryRegion limit) { + return align(address, length).intersect(limit.getRange()); + } + + protected AddressRange alignAndLimitToFloor(Address address, int length) { + Entry> floor = findChainedFloor(address); + if (floor == null) { + return null; + } + return alignWithLimit(address, length, floor.getValue()); + } + + protected AddressRange alignWithOptionalLimit(Address address, int length, + TargetMemoryRegion limit) { + if (limit == null) { + return alignAndLimitToFloor(address, length); + } + return alignWithLimit(address, length, limit); + } + + protected CompletableFuture readMemory(Address address, int length) { + synchronized (accessibilityByMemory) { + Entry> floor = findChainedFloor(address); + if (floor == null) { + throw new IllegalArgumentException( + "address " + address + " is not in any known region"); + } + Address max; + try { + max = address.addNoWrap(length - 1); + } + catch (AddressOverflowException e) { + throw new IllegalArgumentException("read extends beyond the address space"); + } + if (!floor.getValue().getRange().contains(max)) { + throw new IllegalArgumentException("read extends beyond a single region"); + } + TargetMemory mem = byRegion.get(floor.getValue()); + if (mem != null) { + return mem.readMemory(address, length); + } + return CompletableFuture.completedFuture(new byte[0]); + } + } + + protected CompletableFuture writeMemory(Address address, byte[] data) { + synchronized (accessibilityByMemory) { + Entry> floor = findChainedFloor(address); + if (floor == null) { + throw new IllegalArgumentException( + "address " + address + " is not in any known region"); + } + Address max; + try { + max = address.addNoWrap(data.length - 1); + } + catch (AddressOverflowException e) { + throw new IllegalArgumentException("read extends beyond the address space"); + } + if (!floor.getValue().getRange().contains(max)) { + throw new IllegalArgumentException("read extends beyond a single region"); + } + TargetMemory mem = byRegion.get(floor.getValue()); + if (mem != null) { + return mem.writeMemory(address, data); + } + throw new IllegalArgumentException("read starts outside any address space"); + } + } + } + + protected static class ThreadMap { + protected final NavigableSet observedThreadPathLengths = new TreeSet<>(); + protected final Map, ThreadRecorder> byTargetThread = new HashMap<>(); + protected final Map byTraceThread = new HashMap<>(); + + public void put(ThreadRecorder rec) { + observedThreadPathLengths.add(rec.targetThread.getPath().size()); + byTargetThread.put(rec.targetThread, rec); + byTraceThread.put(rec.traceThread, rec); + } + + public ThreadRecorder getForSuccessor(TargetObjectRef successor) { + List path = successor.getPath(); + for (int l : observedThreadPathLengths.descendingSet()) { + if (l > path.size()) { + continue; + } + path = List.copyOf(path.subList(0, l)); + TargetObjectRef maybeThread = successor.getModel().createRef(path); + ThreadRecorder rec = byTargetThread.get(maybeThread); + if (rec != null) { + return rec; + } + } + return null; + } + + public ThreadRecorder get(TargetThread thread) { + return byTargetThread.get(thread); + } + + public ThreadRecorder get(TargetObjectRef maybeThread) { + return byTargetThread.get(maybeThread); + } + + public ThreadRecorder get(TraceThread thread) { + return byTraceThread.get(thread); + } + + public void remove(ThreadRecorder rec) { + ThreadRecorder rByTarget = byTargetThread.remove(rec.targetThread); + ThreadRecorder rByTrace = byTraceThread.remove(rec.traceThread); + assert rec == rByTarget; + assert rec == rByTrace; + } + + public Collection recorders() { + return byTargetThread.values(); + } + } + + protected static AddressSetView expandToBlocks(AddressSetView asv) { + AddressSet result = new AddressSet(); + // Not terribly efficient, but this is one range most of the time + for (AddressRange range : asv) { + AddressSpace space = range.getAddressSpace(); + Address min = space.getAddress(range.getMinAddress().getOffset() & BLOCK_MASK); + Address max = space.getAddress(range.getMaxAddress().getOffset() | ~BLOCK_MASK); + result.add(new AddressRangeImpl(min, max)); + } + return result; + } + + protected static AddressRange range(Address min, Integer length) { + if (length == null) { + length = 1; + } + try { + return new AddressRangeImpl(min, length); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + } + + protected static String nameBreakpoint(TargetBreakpointLocation bpt) { + if (bpt instanceof TargetBreakpointSpec) { + return bpt.getIndex(); + } + return bpt.getSpecification().getIndex() + "." + bpt.getIndex(); + } + + protected static int getFrameLevel(TargetStackFrame frame) { + // TODO: A fair assumption? frames are elements with numeric base-10 indices + return Integer.decode(frame.getIndex()); + } + + protected class ThreadRecorder { + protected final TargetThread targetThread; + protected final TraceThread traceThread; + protected DebuggerRegisterMapper regMapper; + protected TargetRegister pcReg; + protected TargetRegister spReg; + protected Map> regs = new HashMap<>(); + protected NavigableMap> stack = + Collections.synchronizedNavigableMap(new TreeMap<>()); + protected final ComposedMemory threadMemory = new ComposedMemory(processMemory); + protected TargetBreakpointContainer threadBreakpointContainer; + protected TargetExecutionState state = TargetExecutionState.ALIVE; + + protected ThreadRecorder(TargetThread targetThread, TraceThread traceThread) { + this.targetThread = targetThread; + this.traceThread = traceThread; + + if (targetThread instanceof TargetExecutionStateful) { + TargetExecutionStateful stateful = (TargetExecutionStateful) targetThread; + state = stateful.getExecutionState(); + } + } + + protected synchronized CompletableFuture initRegMapper( + TargetRegisterContainer registers) { + /** + * TODO: At the moment, this assumes the recorded thread has one register container, or + * at least that all register banks in the thread use the same register container + * (descriptors). If this becomes a problem, then we'll need to keep a separate register + * mapper per register container. This would likely also require some notion of multiple + * languages in the mapper (seems an unlikely design choice). NOTE: In cases where a + * single process may (at least appear to) execute multiple languages, the model should + * strive to present the registers of the physical machine, as they are most likely + * uniform across the process, not those being emulated in the moment. In cases where an + * abstract machine is involved, it is probably more fitting to present separate + * containers (likely provided by separate models) than to present both the physical and + * abstract machine in the same target. + * + *

+ * TODO: Should I formalize that only one register container is present in a recorded + * thread? This seems counter to the model's flexibility. Traces allow polyglot + * disassembly, but not polyglot register spaces. + */ + /*if (regMapper != null) { + return AsyncUtils.NIL; + }*/ + return regMappers.get(registers).thenAccept(rm -> { + synchronized (this) { + regMapper = rm; + Language language = trace.getBaseLanguage(); + pcReg = regMapper.traceToTarget(language.getProgramCounter()); + spReg = regMapper.traceToTarget(trace.getBaseCompilerSpec().getStackPointer()); + extraRegs = new LinkedHashSet<>(); + for (String rn : mapper.getExtraRegNames()) { + Register traceReg = language.getRegister(rn); + if (traceReg == null) { + Msg.error(this, + "Mapper's extra register '" + rn + "' is not in the language!"); + continue; + } + TargetRegister targetReg = regMapper.traceToTarget(traceReg); + if (targetReg == null) { + Msg.error(this, + "Mapper's extra register '" + traceReg + "' is not mappable!"); + continue; + } + extraRegs.add(targetReg); + } + } + listenerForRecord.retroOfferRegMapperDependents(); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize register mapper", ex); + return null; + }); + } + + protected void regMapperAmended(DebuggerRegisterMapper rm, TargetRegister reg, + boolean removed) { + boolean doUpdateRegs = false; + String name = reg.getIndex(); + synchronized (this) { + if (regMapper != rm) { + return; + } + TargetRegister newPcReg = + regMapper.traceToTarget(trace.getBaseLanguage().getProgramCounter()); + if (pcReg != newPcReg) { + pcReg = newPcReg; + doUpdateRegs |= pcReg != null; + } + TargetRegister newSpReg = + regMapper.traceToTarget(trace.getBaseCompilerSpec().getStackPointer()); + if (spReg != newSpReg) { + spReg = newSpReg; + doUpdateRegs |= spReg != null; + } + if (mapper.getExtraRegNames().contains(name)) { + if (removed) { + extraRegs.remove(reg); + } + else { + extraRegs.add(reg); + } + doUpdateRegs = true; + } + } + if (removed) { + return; + } + TargetRegisterBank bank = regs.get(0); + if (bank != null) { + byte[] cachedVal = bank.getCachedRegisters().get(name); + if (cachedVal != null) { + recordRegisterValues(bank, Map.of(name, cachedVal)); + } + if (doUpdateRegs) { + updateRegsMem(null); + } + } + // TODO: This may be too heavy-handed + // listenerForRecord.retroOfferRegMapperDependents(); + } + + protected int getSuccessorFrameLevel(TargetObjectRef successor) { + NavigableSet observedPathLengths = new TreeSet<>(); + for (TargetStackFrame frame : stack.values()) { + observedPathLengths.add(frame.getPath().size()); + } + List path = successor.getPath(); + for (int l : observedPathLengths.descendingSet()) { + if (l > path.size()) { + continue; + } + List sub = path.subList(0, l); + if (!PathUtils.isIndex(sub)) { + continue; + } + int index = Integer.decode(PathUtils.getIndex(sub)); + TargetStackFrame frame = stack.get(index); + if (frame == null || !Objects.equals(sub, frame.getPath())) { + continue; + } + return index; + } + return 0; + } + + CompletableFuture doFetchAndInitRegMapper(TargetRegisterBank bank) { + int frameLevel = getSuccessorFrameLevel(bank); + TypedTargetObjectRef> descsRef = + bank.getDescriptions(); + if (descsRef == null) { + Msg.error(this, "Cannot create mapper, yet: Descriptions is null."); + return AsyncUtils.NIL; + } + return descsRef.fetch().thenCompose(descs -> { + return initRegMapper(descs); + }).thenAccept(__ -> { + if (frameLevel == 0) { + recordRegisterValues(bank, bank.getCachedRegisters()); + updateRegsMem(null); + } + listeners.fire.registerBankMapped(DefaultTraceRecorderSaved.this); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize register mapper", ex); + return null; + }); + } + + protected void offerRegisters(TargetRegisterBank newRegs) { + int frameLevel = getSuccessorFrameLevel(newRegs); + if (regs.isEmpty()) { + // TODO: Technically, each frame may need its own mapper.... + doFetchAndInitRegMapper(newRegs); + } + + TargetRegisterBank oldRegs = regs.put(frameLevel, newRegs); + if (oldRegs == newRegs) { + return; + } + + synchronized (accessibilityByRegBank) { + if (oldRegs != null) { + accessibilityByRegBank.remove(oldRegs); + } + accessibilityByRegBank.get(newRegs).exceptionally(e -> { + e = AsyncUtils.unwrapThrowable(e); + Msg.error(this, "Could not track register accessibility: " + e.getMessage()); + return null; + }); + } + } + + protected void offerStackFrame(TargetStackFrame frame) { + stack.put(getFrameLevel(frame), frame); + recordFrame(frame); + } + + protected void offerThreadRegion(TargetMemoryRegion region) { + region.getMemory().fetch().thenCompose(mem -> { + threadMemory.addRegion(region, mem); + initMemMapper(mem); + // TODO: Add region to trace memory manager (when allowed for threads) + return updateRegsMem(region); + }).exceptionally(ex -> { + Msg.error(this, "Could not add thread memory region", ex); + return null; + }); + } + + protected void offerThreadBreakpointContainer(TargetBreakpointContainer bc) { + if (threadBreakpointContainer != null) { + Msg.warn(this, "Thread already has a breakpoint container"); + } + threadBreakpointContainer = bc; + } + + /** + * Inform the recorder the given object is no longer valid + * + * @param invalid the invalidated object + * @return true if this recorder should be invalidated, too + */ + protected synchronized boolean objectRemoved(TargetObject invalid) { + if (checkThreadRemoved(invalid)) { + return true; + } + if (checkRegistersRemoved(invalid)) { + //return false; + // Regs could also be a stack frame + } + if (checkStackFrameRemoved(invalid)) { + return false; + } + if (threadMemory.removeRegion(invalid)) { + return false; + } + Msg.trace(this, "Ignored removed object: " + invalid); + return false; + } + + protected boolean checkThreadRemoved(TargetObject invalid) { + if (targetThread == invalid) { + threadDestroyed(); + return true; + } + return false; + } + + protected boolean checkRegistersRemoved(TargetObject invalid) { + synchronized (accessibilityByRegBank) { + if (regs.values().remove(invalid)) { + accessibilityByRegBank.remove((TargetRegisterBank) invalid); + return true; + } + return false; + } + } + + protected boolean checkStackFrameRemoved(TargetObject invalid) { + if (stack.values().remove(invalid)) { + popStack(); + return true; + } + return false; + } + + protected Address pcFromStack() { + TargetStackFrame frame = stack.get(0); + if (frame == null) { + return null; + } + return frame.getProgramCounter(); + } + + protected boolean checkReadCondition(Address traceAddress) { + /** + * TODO: This heuristic doesn't really belong here, but I have to implement it here so + * that it doesn't "override" the listing's implementation. Once watches are + * implemented, we should be able to drop this garbage. + */ + TraceMemoryRegion region = + memoryManager.getRegionContaining(snapshot.getKey(), traceAddress); + if (region == null) { + return false; + } + if (region.isWrite()) { + return true; + } + Entry ent = + memoryManager.getMostRecentStateEntry(snapshot.getKey(), traceAddress); + if (ent == null) { + return true; + } + if (ent.getValue() == TraceMemoryState.KNOWN) { + return false; + } + return true; + } + + protected CompletableFuture readAlignedConditionally(String name, Address targetAddress, + TargetMemoryRegion limit) { + if (targetAddress == null) { + return AsyncUtils.NIL; + } + Address traceAddress = memMapper.targetToTrace(targetAddress); + if (traceAddress == null) { + return AsyncUtils.NIL; + } + if (!checkReadCondition(traceAddress)) { + return AsyncUtils.NIL; + } + AddressRange targetRange = threadMemory.alignWithOptionalLimit(targetAddress, 1, limit); + if (targetRange == null) { + return AsyncUtils.NIL; + } + TimedMsg.info(this, + " Reading memory at " + name + " (" + targetAddress + " -> " + targetRange + ")"); + // NOTE: Recorder takes data via memoryUpdated callback + // TODO: In that callback, sort out process memory from thread memory? + return threadMemory + .readMemory(targetRange.getMinAddress(), (int) targetRange.getLength()) + .exceptionally(ex -> { + Msg.error(this, "Could not read memory at " + name, ex); + return null; + }); + } + + Address registerValueToTargetAddress(TargetRegister reg, byte[] value) { + /** + * TODO: This goes around the horn and back just to select a default address space. We + * should really just go directly to target address space. + */ + RegisterValue rv = regMapper.targetToTrace(reg, value); + if (rv == null) { + return null; + } + Address traceAddress = trace.getBaseLanguage() + .getDefaultSpace() + .getAddress(rv.getUnsignedValue().longValue()); + return memMapper.traceToTarget(traceAddress); + } + + protected CompletableFuture updateRegsMem(TargetMemoryRegion limit) { + TargetRegisterBank bank; + TargetRegister pc; + TargetRegister sp; + Set> toRead = new LinkedHashSet<>(); + synchronized (DefaultTraceRecorderSaved.this) { + if (regMapper == null) { + return AsyncUtils.NIL; + } + bank = regs.get(0); + pc = pcReg; + sp = spReg; + toRead.addAll(extraRegs); + toRead.add(sp); + toRead.add(pc); + } + if (bank == null || pc == null || sp == null) { + return AsyncUtils.NIL; + } + TimedMsg.info(this, "Reading " + toRead + " of " + targetThread); + return bank.readRegisters(toRead).thenCompose(vals -> { + synchronized (DefaultTraceRecorderSaved.this) { + if (memMapper == null) { + return AsyncUtils.NIL; + } + } + if (threadMemory == null) { + return AsyncUtils.NIL; + } + AsyncFence fence = new AsyncFence(); + + Address pcTargetAddr = pcFromStack(); + if (pcTargetAddr == null) { + pcTargetAddr = registerValueToTargetAddress(pcReg, vals.get(pcReg.getIndex())); + } + fence.include(readAlignedConditionally("PC", pcTargetAddr, limit)); + + Address spTargetAddr = + registerValueToTargetAddress(spReg, vals.get(spReg.getIndex())); + fence.include(readAlignedConditionally("SP", spTargetAddr, limit)); + + return fence.ready(); + }).exceptionally(ex -> { + if (LOG_STACK_TRACE) { + Msg.error(this, "Could not read registers", ex); + } + else { + Msg.error(this, "Could not read registers"); + } + return null; + }); + } + + public void stateChanged(final TargetExecutionState newState) { + if (newState == TargetExecutionState.STOPPED) { + updateRegsMem(null); + } + state = newState; + } + + public void threadDestroyed() { + String path = PathUtils.toString(targetThread.getPath()); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, path + " destroyed")) { + // TODO: Should it be key - 1 + // Perhaps, since the thread should not exist + // But it could imply earlier destruction than actually observed + traceThread.setDestructionSnap(snapshot.getKey()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be shrinking + } + } + + public void recordRegisterValues(TargetRegisterBank bank, Map updates) { + synchronized (DefaultTraceRecorderSaved.this) { + if (regMapper == null) { + return; + } + } + int frameLevel = getSuccessorFrameLevel(bank); + TimedMsg.info(this, "Reg values changed: " + updates.keySet()); + try (PermanentTransaction tid = PermanentTransaction.start(trace, + "Registers changed in " + PathUtils.toString(bank.getPath()))) { + TraceMemoryRegisterSpace regSpace = + memoryManager.getMemoryRegisterSpace(traceThread, frameLevel, true); + for (Entry ent : updates.entrySet()) { + RegisterValue rv = regMapper.targetToTrace(ent.getKey(), ent.getValue()); + if (rv == null) { + continue; // mapper does not know this register.... + } + regSpace.setValue(snapshot.getKey(), rv); + if (rv.getRegister() == trace.getBaseLanguage().getProgramCounter() && + pcFromStack() == null) { + Address pcTargetAddr = registerValueToTargetAddress(pcReg, ent.getValue()); + readAlignedConditionally("PC", pcTargetAddr, null); // NB: Reports errors + } + if (rv.getRegister() == trace.getBaseCompilerSpec().getStackPointer()) { + Address spTargetAddr = registerValueToTargetAddress(spReg, ent.getValue()); + readAlignedConditionally("SP", spTargetAddr, null); // NB: Reports errors + } + } + } + } + + public void recordFrame(TargetStackFrame frame) { + recordFrame(frame, frame.getProgramCounter()); + } + + public void doRecordFrame(TraceStack traceStack, int frameLevel, Address pc) { + TraceStackFrame traceFrame = traceStack.getFrame(frameLevel, true); + traceFrame.setProgramCounter(pc); + } + + public void recordFrame(TargetStackFrame frame, Address pc) { + synchronized (DefaultTraceRecorderSaved.this) { + if (memMapper == null) { + return; + } + Address tracePc = pc == null ? null : memMapper.targetToTrace(pc); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Stack frame added")) { + TraceStack traceStack = + stackManager.getStack(traceThread, snapshot.getKey(), true); + doRecordFrame(traceStack, getFrameLevel(frame), tracePc); + } + } + } + + protected int stackDepth() { + return stack.isEmpty() ? 0 : stack.lastKey() + 1; + } + + public void recordStack() { + synchronized (DefaultTraceRecorderSaved.this) { + if (memMapper == null) { + return; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Stack changed")) { + TraceStack traceStack = + stackManager.getStack(traceThread, snapshot.getKey(), true); + traceStack.setDepth(stackDepth(), false); + for (Map.Entry> ent : stack.entrySet()) { + Address tracePc = + memMapper.targetToTrace(ent.getValue().getProgramCounter()); + doRecordFrame(traceStack, ent.getKey(), tracePc); + } + } + } + } + + public void popStack() { + synchronized (DefaultTraceRecorderSaved.this) { + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Stack popped")) { + TraceStack traceStack = + stackManager.getStack(traceThread, snapshot.getKey(), true); + traceStack.setDepth(stackDepth(), false); + } + } + } + + public void onThreadBreakpointContainers( + Consumer> action) { + if (threadBreakpointContainer == null) { + return; + } + action.accept(threadBreakpointContainer); + } + } + + protected class EffectiveBreakpointResolver { + private final TargetBreakpointLocation bpt; + private TargetBreakpointSpec spec; + private boolean affectsProcess = false; + private final Set threadsAffected = new LinkedHashSet<>(); + + public EffectiveBreakpointResolver(TargetBreakpointLocation bpt) { + this.bpt = bpt; + } + + public CompletableFuture resolve() { + AsyncFence fence = new AsyncFence(); + fence.include(bpt.getSpecification().fetch().thenAccept(s -> this.spec = s)); + + for (TargetObjectRef ref : bpt.getAffects()) { + if (ref.equals(target)) { + affectsProcess = true; + } + else { + fence.include(resolveThread(ref)); + } + } + return fence.ready(); + } + + // TODO: If affects is empty/null, also try to default to the containing process + private CompletableFuture resolveThread(TargetObjectRef ref) { + return DebugModelConventions.findThread(ref).thenAccept(thread -> { + if (thread == null) { + Msg.error(this, + "Could not find process or thread from breakpoint-affected object: " + ref); + return; + } + if (!ref.equals(thread)) { + Msg.warn(this, "Effective breakpoint should apply to process or threads. Got " + + ref + ". Resolved to " + thread); + return; + } + if (!PathUtils.isAncestor(target.getPath(), thread.getPath())) { + /** + * Perfectly normal if the breakpoint container is outside the process + * container. Don't record such in this trace, though. + */ + return; + } + ThreadRecorder rec = listenerForRecord.getOrCreateThreadRecorder(thread); + synchronized (threadsAffected) { + threadsAffected.add(rec.traceThread); + } + }).exceptionally(ex -> { + Msg.error(this, "Error resolving thread from breakpoint-affected object: " + ref); + return null; + }); + } + + public void applyChecksAndConventions() { + if (affectsProcess && !threadsAffected.isEmpty()) { + Msg.warn(this, "Breakpoint affects process and individual threads?: " + bpt); + threadsAffected.clear(); + } + // Check ancestry for "affects" + if (!affectsProcess && threadsAffected.isEmpty()) { + if (PathUtils.isAncestor(target.getPath(), bpt.getPath())) { + for (ThreadRecorder rec : threadMap.byTargetThread.values()) { + if (PathUtils.isAncestor(rec.targetThread.getPath(), bpt.getPath())) { + threadsAffected.add(rec.traceThread); + break; // Only one thread could be its ancestor + } + } + if (threadsAffected.isEmpty()) { + affectsProcess = true; + } + } + } + } + } + + public class ListenerForRecord extends SubTreeListenerAdapter implements + TargetBreakpointSpecListener, TargetEventScopeListener, TargetExecutionStateListener, + TargetFocusScopeListener, TargetRegisterBankListener, TargetMemoryListener { + + //protected final Map> modulesByName = new HashMap<>(); + protected final Set> breakpoints = new HashSet<>(); + + @Override + protected boolean checkDescend(TargetObjectRef ref) { + // NOTE, cannot return false on match, since it could be a prefix of another + if (HARDCODED_MATCHER.successorCouldMatch(ref.getPath())) { + return true; + } + return false; + } + + // TODO: Move this into conventions? + protected CompletableFuture findThreadOrProcess(TargetObject successor) { + return new DebugModelConventions.AncestorTraversal(successor) { + @Override + protected Result check(TargetObject obj) { + if (obj.isRoot()) { + return Result.FOUND; + } + if (obj instanceof TargetThread) { + return Result.FOUND; + } + if (obj instanceof TargetProcess) { + return Result.FOUND; + } + return Result.CONTINUE; + } + + @Override + protected TargetObject finish(TargetObject obj) { + return obj; + } + }.start(); + } + + @Override + protected void objectAdded(TargetObject added) { + if (!valid) { + return; + } + if (added instanceof TargetThread) { + getOrCreateThreadRecorder((TargetThread) added); + } + if (added instanceof TargetStack) { + // Actually, this may not matter + } + // Do stack frame first, since bank would be it or child. + // Need frames indexed first to determine level of bank + if (added instanceof TargetStackFrame) { + ThreadRecorder rec = threadMap.getForSuccessor(added); + if (rec == null) { + Msg.error(this, "Frame without thread?: " + added); + } + else { + rec.offerStackFrame((TargetStackFrame) added); + } + } + if (added instanceof TargetRegisterBank) { + ThreadRecorder rec = threadMap.getForSuccessor(added); + if (rec == null) { + Msg.error(this, "Bank without thread?: " + added); + } + else { + rec.offerRegisters((TargetRegisterBank) added); + } + } + if (added instanceof TargetRegisterContainer) { + // These are picked up when a bank is added with these descriptions + } + if (added instanceof TargetRegister) { + TargetRegister reg = (TargetRegister) added; + regMappers.get(reg.getContainer()).thenAccept(rm -> { + rm.targetRegisterAdded(reg); + for (ThreadRecorder rec : threadMap.byTargetThread.values()) { + rec.regMapperAmended(rm, reg, false); + } + }); + } + if (added instanceof TargetMemory) { + initMemMapper((TargetMemory) added); + } + if (added instanceof TargetMemoryRegion) { + TargetMemoryRegion region = (TargetMemoryRegion) added; + findThreadOrProcess(added).thenAccept(obj -> { + if (obj == target) { + offerProcessRegion(region); + return; + } + if (obj instanceof TargetThread) { + ThreadRecorder rec = getOrCreateThreadRecorder((TargetThread) obj); + rec.offerThreadRegion(region); + } + }).exceptionally(ex -> { + Msg.error(this, "Error recording memory region", ex); + return null; + }); + } + if (added instanceof TargetModule) { + TargetModule module = (TargetModule) added; + offerProcessModule(module); + } + if (added instanceof TargetSection) { + TargetSection section = (TargetSection) added; + section.getModule().fetch().thenAccept(module -> { + offerProcessModuleSection(module, section); + // I hope this should never be a per-thread thing + }).exceptionally(ex -> { + Msg.error(this, "Error recording module section", ex); + return null; + }); + } + if (added instanceof TargetBreakpointContainer) { + TargetBreakpointContainer breaks = (TargetBreakpointContainer) added; + findThreadOrProcess(added).thenAccept(obj -> { + if (obj == target) { + offerProcessBreakpointContainer(breaks); + return; + } + if (obj.isRoot()) { + return; + } + ThreadRecorder rec = getOrCreateThreadRecorder((TargetThread) obj); + rec.offerThreadBreakpointContainer(breaks); + }).exceptionally(ex -> { + Msg.error(this, "Error recording breakpoint container", ex); + return null; + }); + } + if (added instanceof TargetBreakpointSpec) { + // I don't think this matters. UI for live recording only. + } + if (added instanceof TargetBreakpointLocation) { + TargetBreakpointLocation bpt = (TargetBreakpointLocation) added; + breakpoints.add(bpt); + offerEffectiveBreakpoint(bpt); + } + } + + @Override + protected void objectRemoved(TargetObject removed) { + if (!valid) { + return; + } + if (target == removed) { + stopRecording(); + return; + } + if (removed instanceof TargetRegisterContainer) { + regMappers.remove((TargetRegisterContainer) removed); + } + if (removed instanceof TargetRegister) { + TargetRegister reg = (TargetRegister) removed; + reg.getContainer().fetch().thenAccept(cont -> { + DebuggerRegisterMapper rm = regMappers.getCompletedMap().get(cont); + if (rm == null) { + return; + } + rm.targetRegisterRemoved(reg); + for (ThreadRecorder rec : threadMap.byTargetThread.values()) { + rec.regMapperAmended(rm, reg, true); + } + }); + } + if (removed instanceof TargetMemoryRegion) { + TargetMemoryRegion region = (TargetMemoryRegion) removed; + if (processMemory.removeRegion(region)) { + removeProcessRegion(region); + return; + } + // Allow removal notice to fall through to thread recorders + } + if (removed instanceof TargetModule) { + TargetModule module = (TargetModule) removed; + removeProcessModule(module); + return; + } + if (removed instanceof TargetBreakpointLocation) { + TargetBreakpointLocation bpt = (TargetBreakpointLocation) removed; + breakpoints.remove(bpt); + removeEffectiveBreakpoint(bpt); + return; + } + synchronized (threadMap) { + for (Iterator it = threadMap.recorders().iterator(); it + .hasNext();) { + ThreadRecorder rec = it.next(); + if (rec.objectRemoved(removed)) { + it.remove(); + } + } + } + } + + protected boolean successor(TargetObjectRef ref) { + return PathUtils.isAncestor(target.getPath(), ref.getPath()); + } + + protected boolean anyRef(Collection parameters) { + for (Object p : parameters) { + if (!(p instanceof TargetObjectRef)) { + continue; + } + return true; + } + return false; + } + + protected boolean anySuccessor(Collection parameters) { + for (Object p : parameters) { + if (!(p instanceof TargetObjectRef)) { + continue; + } + TargetObjectRef ref = (TargetObjectRef) p; + if (!successor(ref)) { + continue; + } + return true; + } + return false; + } + + protected boolean eventApplies(TargetObjectRef eventThread, TargetEventType type, + List parameters) { + if (type == TargetEventType.RUNNING) { + return false; + /** + * TODO: Perhaps some configuration for this later. It's kind of interesting to + * record the RUNNING event time, but it gets pedantic when these exist between + * steps. + */ + } + if (eventThread != null) { + return successor(eventThread); + } + if (anyRef(parameters)) { + return anySuccessor(parameters); + } + return true; // Some session-wide event, I suppose + } + + @Override + public void event(TargetEventScope object, + TypedTargetObjectRef> eventThread, TargetEventType type, + String description, List parameters) { + if (!valid) { + return; + } + TimedMsg.info(this, "Event: " + type + " thread=" + eventThread + " description=" + + description + " params=" + parameters); + // Just use this to step the snaps. Creation/destruction still handled in add/remove + if (!eventApplies(eventThread, type, parameters)) { + return; + } + ThreadRecorder rec = threadMap.get(eventThread); + createSnapshot(description, rec == null ? null : rec.traceThread, null); + + if (type == TargetEventType.THREAD_CREATED) { + if (rec == null) { + return; + } + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Adjust thread creation", true)) { + rec.traceThread.setCreationSnap(snapshot.getKey()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be shrinking + } + } + else if (type == TargetEventType.MODULE_LOADED) { + Object p0 = parameters.get(0); + if (!(p0 instanceof TargetObjectRef)) { + return; + } + TargetObjectRef ref = (TargetObjectRef) p0; + ref.fetch().thenAccept(obj -> { + if (!(obj instanceof TargetModule)) { + return; + } + TargetModule mod = (TargetModule) obj; + TraceModule traceModule = getTraceModule(mod); + if (traceModule == null) { + return; + } + try (UndoableTransaction tid = + UndoableTransaction.start(trace, "Adjust module load", true)) { + traceModule.setLoadedSnap(snapshot.getKey()); + } + catch (DuplicateNameException e) { + Msg.error(this, "Could not set module loaded snap", e); + } + }); + } + } + + @Override + public void attributesChanged(TargetObject parent, Collection removed, + Map added) { + super.attributesChanged(parent, removed, added); + if (!valid) { + return; + } + // Dispatch attribute changes which don't have "built-in" events. + if (parent instanceof TargetBreakpointLocation) { + if (added.containsKey(TargetBreakpointLocation.LENGTH_ATTRIBUTE_NAME)) { + breakpointLengthChanged((TargetBreakpointLocation) parent, + (Integer) added.get(TargetBreakpointLocation.LENGTH_ATTRIBUTE_NAME)); + } + } + if (parent instanceof TargetStackFrame) { + if (added.containsKey(TargetStackFrame.PC_ATTRIBUTE_NAME)) { + framePcUpdated((TargetStackFrame) parent); + } + } + if (parent instanceof TargetRegisterBank) { + if (added.containsKey(TargetRegisterBank.DESCRIPTIONS_ATTRIBUTE_NAME)) { + ThreadRecorder rec = threadMap.getForSuccessor(parent); + if (rec != null) { + rec.doFetchAndInitRegMapper((TargetRegisterBank) parent); + } + } + } + // This should be fixed at construction. + /*if (parent instanceof TargetModule) { + if (added.containsKey(TargetModule.BASE_ATTRIBUTE_NAME)) { + moduleBaseUpdated((TargetModule) parent, + (Address) added.get(TargetModule.BASE_ATTRIBUTE_NAME)); + } + }*/ + } + + @Override + public void executionStateChanged(TargetExecutionStateful stateful, + TargetExecutionState state) { + if (!valid) { + return; + } + TimedMsg.info(this, "State " + state + " for " + stateful); + findThreadOrProcess(stateful).thenAccept(threadOrProcess -> { + if (threadOrProcess == target && state == TargetExecutionState.TERMINATED) { + stopRecording(); + return; + } + ThreadRecorder rec = null; + synchronized (threadMap) { + if (threadOrProcess instanceof TargetThread) { + rec = threadMap.get((TargetThread) threadOrProcess); + } + } + if (rec != null) { + rec.stateChanged(state); + } + // Else we'll discover it and sync state later + }); + } + + protected ThreadRecorder getOrCreateThreadRecorder(TargetThread thread) { + synchronized (threadMap) { + ThreadRecorder rec = threadMap.get(thread); + if (rec != null) { + return rec; + } + TraceThread traceThread; + String path = PathUtils.toString(thread.getPath()); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, path + " created")) { + // Note, if THREAD_CREATED is emitted, it will adjust the creation snap + traceThread = threadManager.createThread(path, thread.getShortDisplay(), + snapshot.getKey()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be a new thread in model + } + rec = new ThreadRecorder(thread, traceThread); + threadMap.put(rec); + return rec; + } + } + + @Override + public void registersUpdated(TargetRegisterBank bank, Map updates) { + if (!valid) { + return; + } + ThreadRecorder rec = threadMap.getForSuccessor(bank); + if (rec == null) { + return; + } + rec.recordRegisterValues(bank, updates); + } + + @Override + public void memoryUpdated(TargetMemory memory, Address address, byte[] data) { + if (!valid) { + return; + } + synchronized (DefaultTraceRecorderSaved.this) { + if (memMapper == null) { + Msg.warn(this, "Received memory write before a region has been added"); + return; + } + } + Address traceAddr = memMapper.targetToTrace(address); + long snap = snapshot.getKey(); + TimedMsg.info(this, "Memory updated: " + address + " (" + data.length + ")"); + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Memory observed")) { + ByteBuffer newBytes = ByteBuffer.wrap(data); + memoryManager.putBytes(snap, traceAddr, newBytes); + } + } + + @Override + public void memoryReadError(TargetMemory memory, AddressRange range, + DebuggerMemoryAccessException e) { + if (!valid) { + return; + } + Msg.error(this, "Error reading range " + range, e); + Address traceMin = memMapper.targetToTrace(range.getMinAddress()); + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Memory read error")) { + memoryManager.setState(snapshot.getKey(), traceMin, TraceMemoryState.ERROR); + // TODO: Bookmark to describe error? + } + } + + @Override + public void breakpointToggled(TargetBreakpointSpec spec, boolean enabled) { + if (!valid) { + return; + } + spec.getLocations().thenAccept(bpts -> { + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Breakpoint toggled")) { + for (TargetBreakpointLocation eb : bpts) { + TraceBreakpoint traceBpt = getTraceBreakpoint(eb); + if (traceBpt == null) { + String path = PathUtils.toString(eb.getPath()); + Msg.warn(this, "Cannot find toggled trace breakpoint for " + path); + continue; + } + // Verify attributes match? Eh. If they don't, someone has fiddled with it. + traceBpt.splitWithEnabled(snapshot.getKey(), enabled); + } + } + }).exceptionally(ex -> { + Msg.error(this, "Error recording toggled breakpoint spec: " + spec, ex); + return null; + }); + } + + protected void breakpointLengthChanged(TargetBreakpointLocation bpt, int length) { + Address traceAddr = memMapper.targetToTrace(bpt.getAddress()); + String path = PathUtils.toString(bpt.getPath()); + for (TraceBreakpoint traceBpt : breakpointManager.getBreakpointsByPath(path)) { + if (traceBpt.getLength() == length) { + continue; // Nothing to change + } + // TODO: Verify all other attributes match? + // TODO: Should this be allowed to happen? + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Breakpoint length changed")) { + long snap = snapshot.getKey(); + if (traceBpt.getPlacedSnap() == snap) { + traceBpt.delete(); + } + else { + traceBpt.setClearedSnap(snap - 1); + } + breakpointManager.placeBreakpoint(path, snap, range(traceAddr, length), + traceBpt.getThreads(), traceBpt.getKinds(), traceBpt.isEnabled(), + traceBpt.getComment()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Split, and length matters not + } + } + } + + protected void framePcUpdated(TargetStackFrame frame) { + ThreadRecorder rec = threadMap.getForSuccessor(frame); + // Yes, entire stack, otherwise, the stack seems to be just one deep. + rec.recordStack(); + } + + protected void stackUpdated(TargetStack stack) { + ThreadRecorder rec = threadMap.getForSuccessor(stack); + rec.recordStack(); + } + + @Override + public void focusChanged(TargetFocusScope object, TargetObjectRef focused) { + if (!valid) { + return; + } + if (PathUtils.isAncestor(target.getPath(), focused.getPath())) { + curFocus = focused; + } + } + + protected void retroOfferRegMapperDependents() { + List copy; + synchronized (objects) { + copy = List.copyOf(threadMap.byTargetThread.values()); + } + for (ThreadRecorder rec : copy) { + TargetRegisterBank bank = rec.regs.get(0); + if (bank != null) { + rec.recordRegisterValues(bank, bank.getCachedRegisters()); + rec.updateRegsMem(null); + } + } + } + + protected void retroOfferMemMapperDependents() { + List copy; + synchronized (objects) { + copy = List.copyOf(objects.values()); + } + synchronized (DefaultTraceRecorderSaved.this) { + for (TargetObject obj : copy) { + if (obj instanceof TargetModule) { + offerProcessModule((TargetModule) obj); + } + if (obj instanceof TargetSection) { + TargetSection section = (TargetSection) obj; + section.getModule().fetch().thenAccept(module -> { + offerProcessModuleSection(module, section); + }); + } + if (obj instanceof TargetBreakpointLocation) { + offerEffectiveBreakpoint((TargetBreakpointLocation) obj); + } + if (obj instanceof TargetStack) { + stackUpdated((TargetStack) obj); + } + } + } + } + + public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { + synchronized (objects) { + return (TargetMemoryRegion) objects.get(PathUtils.parse(region.getPath())); + } + } + + public TargetModule getTargetModule(TraceModule module) { + synchronized (objects) { + return (TargetModule) objects.get(PathUtils.parse(module.getPath())); + } + } + + public TargetSection getTargetSection(TraceSection section) { + synchronized (objects) { + return (TargetSection) objects.get(PathUtils.parse(section.getPath())); + } + } + + public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { + synchronized (objects) { + return (TargetBreakpointLocation) objects.get(PathUtils.parse(bpt.getPath())); + } + } + + public List> collectBreakpoints(TargetThread thread) { + synchronized (objects) { + return breakpoints.stream().filter(bpt -> { + TargetObjectRefList affects = bpt.getAffects(); + // N.B. in case thread is null (process), affects.contains(thread) is always false + return affects.isEmpty() || affects.contains(thread) || + affects.contains(target); + }).collect(Collectors.toList()); + } + } + + protected void onProcessBreakpointContainers( + Consumer> action) { + synchronized (objects) { + if (processBreakpointContainer == null) { + for (TargetThread thread : threadsView) { + onThreadBreakpointContainers(thread, action); + } + } + else { + action.accept(processBreakpointContainer); + } + } + } + + protected void onThreadBreakpointContainers(TargetThread thread, + Consumer> action) { + synchronized (objects) { + getOrCreateThreadRecorder(thread).onThreadBreakpointContainers(action); + } + } + + protected void onBreakpointContainers(TargetThread thread, + Consumer> action) { + if (thread == null) { + onProcessBreakpointContainers(action); + } + else { + onThreadBreakpointContainers(thread, action); + } + } + } + + protected final DebuggerModelServicePlugin plugin; + protected final PluginTool tool; + protected final Trace trace; + protected final TargetObject target; + protected final ComposedMemory processMemory = new ComposedMemory(); + protected TargetBreakpointContainer processBreakpointContainer; + + protected final TraceBreakpointManager breakpointManager; + protected final TraceCodeManager codeManager; + protected final TraceBasedDataTypeManager dataTypeManager; + protected final TraceEquateManager equateManager; + protected final TraceMemoryManager memoryManager; + protected final TraceModuleManager moduleManager; + protected final TraceStackManager stackManager; + protected final TraceSymbolManager symbolManager; + protected final TraceThreadManager threadManager; + protected final TraceTimeManager timeManager; + + protected final AbstractDebuggerTargetTraceMapper mapper; + protected DebuggerMemoryMapper memMapper; + protected AsyncLazyMap>, DebuggerRegisterMapper> regMappers; + protected final TargetDataTypeConverter typeConverter; + protected Collection> extraRegs; + // TODO: Support automatic recording of user-specified extra registers... + // NOTE: Probably via watches, once we have those + // TODO: Probably move all the auto-reads into watches + + protected final ListenerSet listeners = + new ListenerSet<>(TraceRecorderListener.class); + protected final TriConsumer listenerRegAccChanged = + this::registerAccessibilityChanged; + protected final TriConsumer listenerProcMemAccChanged = + this::processMemoryAccessibilityChanged; + + private final ListenerForRecord listenerForRecord; + + protected final ThreadMap threadMap = new ThreadMap(); + protected final Set> threadsView = + Collections.unmodifiableSet(threadMap.byTargetThread.keySet()); + protected final BiMap, TraceBreakpoint> processBreakpointsMap = + HashBiMap.create(); + + protected final AsyncLazyValue lazyInit = new AsyncLazyValue<>(this::doInit); + + protected TraceSnapshot snapshot = null; + private boolean valid = true; + + protected TargetFocusScope focusScope; + protected TargetObjectRef curFocus; + + public DefaultTraceRecorderSaved(DebuggerModelServicePlugin plugin, Trace trace, + TargetObject target, AbstractDebuggerTargetTraceMapper mapper) { + this.plugin = plugin; + this.tool = plugin.getTool(); + this.trace = trace; + this.target = target; + + this.breakpointManager = trace.getBreakpointManager(); + this.codeManager = trace.getCodeManager(); + this.dataTypeManager = trace.getDataTypeManager(); + this.equateManager = trace.getEquateManager(); + this.memoryManager = trace.getMemoryManager(); + this.moduleManager = trace.getModuleManager(); + this.stackManager = trace.getStackManager(); + this.symbolManager = trace.getSymbolManager(); + this.threadManager = trace.getThreadManager(); + this.timeManager = trace.getTimeManager(); + + this.mapper = mapper; + this.regMappers = new AsyncLazyMap<>(new HashMap<>(), + ref -> ref.fetch().thenCompose(mapper::offerRegisters)); + this.typeConverter = new TargetDataTypeConverter(trace.getDataTypeManager()); + + this.listenerForRecord = new ListenerForRecord(); + + processMemory.memAccListeners.add(listenerProcMemAccChanged); + + trace.addConsumer(this); + } + + protected void registerAccessibilityChanged(TargetAccessibility old, TargetAccessibility acc, + Void __) { + listeners.fire.registerAccessibilityChanged(this); + } + + protected void processMemoryAccessibilityChanged(TargetAccessibility old, + TargetAccessibility acc, Void __) { + listeners.fire.processMemoryAccessibilityChanged(this); + } + + @Override + public CompletableFuture init() { + return lazyInit.request(); + } + + protected CompletableFuture doInit() { + createSnapshot("Started recording " + PathUtils.toString(target.getPath()) + " in " + + target.getModel(), null, null); + AsyncFence fence = new AsyncFence(); + CompletableFuture> futureBreaks = + DebugModelConventions.findSuitable(TargetBreakpointContainer.tclass, target); + fence.include(futureBreaks.thenAccept(breaks -> { + if (breaks != null && !PathUtils.isAncestor(target.getPath(), breaks.getPath())) { + offerProcessBreakpointContainer(breaks); // instead of objectAdded + listenerForRecord.addListenerAndConsiderSuccessors(breaks); + } + }).exceptionally(e -> { + Msg.error(this, "Could not search for breakpoint container", e); + return null; + })); + + CompletableFuture> futureEvents = + DebugModelConventions.findSuitable(TargetEventScope.tclass, target); + fence.include(futureEvents.thenAccept(events -> { + if (events != null && !PathUtils.isAncestor(target.getPath(), events.getPath())) { + // Don't descend. Scope may be the entire session. + listenerForRecord.addListener(events); + } + }).exceptionally(e -> { + Msg.warn(this, "Could not search for event scope", e); + return null; + })); + + CompletableFuture> futureFocus = + DebugModelConventions.findSuitable(TargetFocusScope.tclass, target); + fence.include(futureFocus.thenAccept(focus -> { + if (focus != null && !PathUtils.isAncestor(target.getPath(), focus.getPath())) { + // Don't descend. Scope may be the entire session. + offerFocusScope(focus); + listenerForRecord.addListener(focus); + } + }).exceptionally(e -> { + Msg.error(this, "Could not search for focus scope", e); + return null; + })); + return fence.ready().thenAccept(__ -> { + listenerForRecord.objectAdded(target); // TODO: This seems wrong + listenerForRecord.addListenerAndConsiderSuccessors(target); + }); + } + + protected synchronized void doAdvanceSnap(String description, TraceThread eventThread) { + snapshot = timeManager.createSnapshot(description); + snapshot.setEventThread(eventThread); + } + + @Override + public TraceSnapshot forceSnapshot() { + createSnapshot("User-forced snapshot", null, null); + return snapshot; + } + + protected void createSnapshot(String description, TraceThread eventThread, + PermanentTransaction tid) { + if (tid != null) { + doAdvanceSnap(description, eventThread); + listeners.fire.snapAdvanced(this, snapshot.getKey()); + return; + } + try (PermanentTransaction tid2 = PermanentTransaction.start(trace, description)) { + doAdvanceSnap(description, eventThread); + } + listeners.fire.snapAdvanced(this, snapshot.getKey()); + } + + // TODO: This could probably be discovered by the offer and passed in at construction + protected synchronized CompletableFuture initMemMapper(TargetMemory memory) { + /** + * TODO: At the moment, there's no real dependency on the memory. When there is, see that + * additional memories can be incorporated into the mapper, and stale ones removed. + * Alternatively, formalize that there is no possible dependency on memory. + */ + if (memMapper != null) { + return AsyncUtils.NIL; + } + return mapper.offerMemory(memory).thenAccept(mm -> { + synchronized (this) { + memMapper = mm; + } + listenerForRecord.retroOfferMemMapperDependents(); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize memory mapper", ex); + return null; + }); + } + + protected Collection getTraceFlags(TargetMemoryRegion region) { + Collection flags = new HashSet<>(); + if (region.isReadable()) { + flags.add(TraceMemoryFlag.READ); + } + if (region.isWritable()) { + flags.add(TraceMemoryFlag.WRITE); + } + if (region.isExecutable()) { + flags.add(TraceMemoryFlag.EXECUTE); + } + // TODO: Volatile? Can any debugger report that? + return flags; + } + + protected void offerProcessRegion(TargetMemoryRegion region) { + region.getMemory().fetch().thenCompose(mem -> { + processMemory.addRegion(region, mem); + initMemMapper(mem); + synchronized (this) { + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Memory region added")) { + String path = PathUtils.toString(region.getPath()); + TraceMemoryRegion traceRegion = + memoryManager.getLiveRegionByPath(snapshot.getKey(), path); + if (traceRegion != null) { + Msg.warn(this, "Region " + path + " already recorded"); + return AsyncUtils.NIL; + } + traceRegion = memoryManager.addRegion(path, Range.atLeast(snapshot.getKey()), + memMapper.targetToTrace(region.getRange()), getTraceFlags(region)); + traceRegion.setName(region.getName()); + } + catch (TraceOverlappedRegionException e) { + Msg.error(this, "Failed to create region due to overlap", e); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Just checked for existing + } + } + return updateAllThreadsRegsMem(region); + }).exceptionally(ex -> { + Msg.error(this, "Could not add process memory region", ex); + return null; + }); + } + + protected synchronized void removeProcessRegion(TargetMemoryRegion region) { + // Already removed from processMemory. That's how we knew to go here. + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Memory region removed")) { + String path = PathUtils.toString(region.getPath()); + long snap = snapshot.getKey(); + TraceMemoryRegion traceRegion = memoryManager.getLiveRegionByPath(snap, path); + if (traceRegion == null) { + Msg.warn(this, "Could not find region " + path + " in trace to remove"); + return; + } + traceRegion.setDestructionSnap(snap - 1); + } + catch (DuplicateNameException | TraceOverlappedRegionException e) { + throw new AssertionError(e); // Region is shrinking in time + } + } + + protected void recordBreakpoint(TargetBreakpointSpec spec, TargetBreakpointLocation bpt, + Set traceThreads) { + synchronized (this) { + if (memMapper == null) { + throw new IllegalStateException( + "No memory mapper! Have not recorded a region, yet."); + } + } + String path = PathUtils.toString(bpt.getPath()); + String name = nameBreakpoint(bpt); + Address traceAddr = memMapper.targetToTrace(bpt.getAddress()); + AddressRange traceRange = range(traceAddr, bpt.getLength()); + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Breakpoint placed")) { + boolean enabled = spec.isEnabled(); + Set traceKinds = + TraceRecorder.targetToTraceBreakpointKinds(spec.getKinds()); + TraceBreakpoint traceBpt = breakpointManager.placeBreakpoint(path, snapshot.getKey(), + traceRange, traceThreads, traceKinds, enabled, spec.getExpression()); + traceBpt.setName(name); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be new to model, or already cleared + } + } + + protected void offerProcessBreakpointContainer(TargetBreakpointContainer bc) { + if (processBreakpointContainer != null) { + Msg.warn(this, "Already have a breakpoint container for this process"); + } + processBreakpointContainer = bc; + } + + protected void offerFocusScope(TargetFocusScope scope) { + if (this.focusScope != null) { + Msg.warn(this, "Already have a focus scope: " + this.focusScope); + } + this.focusScope = scope; + } + + protected synchronized TraceModule offerProcessModule(TargetModule module) { + if (memMapper == null) { + return null; + } + + String path = PathUtils.toString(module.getPath()); + TraceModule traceModule = moduleManager.getLoadedModuleByPath(snapshot.getKey(), path); + if (traceModule != null) { + return traceModule; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Module " + path + " loaded")) { + AddressRange targetRange = module.getRange(); + AddressRange traceRange = + targetRange == null ? null : memMapper.targetToTrace(targetRange); + traceModule = moduleManager.addLoadedModule(path, module.getModuleName(), traceRange, + snapshot.getKey()); + return traceModule; + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // We checked for existing by path + } + } + + protected synchronized TraceSection offerProcessModuleSection(TargetModule module, + TargetSection section) { + if (memMapper == null) { + return null; + } + String path = PathUtils.toString(section.getPath()); + TraceModule traceModule = offerProcessModule(module); + TraceSection traceSection = moduleManager.getLoadedSectionByPath(snapshot.getKey(), path); + if (traceSection != null) { + Msg.warn(this, path + " already recorded"); + return traceSection; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Section " + path + " added")) { + AddressRange targetRange = section.getRange(); + AddressRange traceRange = memMapper.targetToTrace(targetRange); + traceSection = traceModule.addSection(path, section.getIndex(), traceRange); + return traceSection; + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // We checked for existing by name + } + } + + protected synchronized void removeProcessModule(TargetModule module) { + String path = PathUtils.toString(module.getPath()); + long snap = snapshot.getKey(); + TraceThread eventThread = snapshot.getEventThread(); + TraceModule traceModule = moduleManager.getLoadedModuleByPath(snap, path); + if (traceModule == null) { + Msg.warn(this, "unloaded " + path + " is not in the trace"); + return; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Module " + path + " unloaded")) { + if (traceModule.getLoadedSnap() == snap) { + Msg.warn(this, "Observed module unload in the same snap as its load"); + createSnapshot("WARN: Module removed", eventThread, tid); + snap = snapshot.getKey(); + } + traceModule.setUnloadedSnap(snap - 1); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Module lifespan should be shrinking + } + } + + // NB: No removeProcessModuleSection, because sections should be immutable + // They are removed when the module is removed + + protected void offerEffectiveBreakpoint(TargetBreakpointLocation bpt) { + synchronized (this) { + if (memMapper == null) { + return; + } + } + EffectiveBreakpointResolver resolver = new EffectiveBreakpointResolver(bpt); + resolver.resolve().thenAccept(__ -> { + if (resolver.affectsProcess || !resolver.threadsAffected.isEmpty()) { + recordBreakpoint(resolver.spec, bpt, resolver.threadsAffected); + } + }).exceptionally(ex -> { + Msg.error(this, "Could record target breakpoint: " + bpt, ex); + return null; + }); + } + + protected void removeEffectiveBreakpoint(TargetBreakpointLocation bpt) { + String path = PathUtils.toString(bpt.getPath()); + long snap = snapshot.getKey(); + try (PermanentTransaction tid = PermanentTransaction.start(trace, "Breakpoint deleted")) { + for (TraceBreakpoint traceBpt : breakpointManager.getBreakpointsByPath(path)) { + if (traceBpt.getPlacedSnap() > snap) { + Msg.error(this, + "Tracked, now removed breakpoint was placed in the future? " + bpt); + } + else if (traceBpt.getPlacedSnap() == snap) { + // TODO: I forget if this is allowed for DBTrace iteration + traceBpt.delete(); + } + else { + traceBpt.setClearedSnap(snap - 1); + } + } + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Lifespan in shrinking + } + } + + protected CompletableFuture updateAllThreadsRegsMem(TargetMemoryRegion limit) { + AsyncFence fence = new AsyncFence(); + for (ThreadRecorder rec : threadMap.recorders()) { + fence.include(rec.updateRegsMem(limit)); + } + return fence.ready(); + } + + @Override + public TargetObject getTarget() { + return target; + } + + @Override + public Trace getTrace() { + return trace; + } + + @Override + public long getSnap() { + return snapshot.getKey(); + } + + @Override + public boolean isRecording() { + return valid; + } + + @Override + public void stopRecording() { + invalidate(); + listeners.fire.recordingStopped(this); + } + + @Override + public void addListener(TraceRecorderListener l) { + listeners.add(l); + } + + @Override + public void removeListener(TraceRecorderListener l) { + listeners.remove(l); + } + + @Override + public boolean isViewAtPresent(TraceProgramView view) { + if (!valid) { + return false; + } + if (!Objects.equals(trace, view.getTrace())) { + return false; + } + if (snapshot.getKey() != view.getSnap()) { + return false; + } + return true; + } + + @Override + public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { + return listenerForRecord.getTargetBreakpoint(bpt); + } + + @Override + public TraceBreakpoint getTraceBreakpoint(TargetBreakpointLocation bpt) { + String path = PathUtils.toString(bpt.getPath()); + return breakpointManager.getPlacedBreakpointByPath(snapshot.getKey(), path); + } + + @Override + public List> collectBreakpointContainers(TargetThread thread) { + List> result = new ArrayList<>(); + listenerForRecord.onBreakpointContainers(thread, result::add); + return result; + } + + @Override + public List> collectBreakpoints(TargetThread thread) { + return listenerForRecord.collectBreakpoints(thread); + } + + @Override + public Set getSupportedBreakpointKinds() { + Set tKinds = new HashSet<>(); + listenerForRecord.onBreakpointContainers(null, cont -> { + tKinds.addAll(cont.getSupportedBreakpointKinds()); + }); + return TraceRecorder.targetToTraceBreakpointKinds(tKinds); + } + + @Override + public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { + return listenerForRecord.getTargetMemoryRegion(region); + } + + @Override + public TraceMemoryRegion getTraceMemoryRegion(TargetMemoryRegion region) { + String path = PathUtils.toString(region.getPath()); + return memoryManager.getLiveRegionByPath(snapshot.getKey(), path); + } + + @Override + public TargetModule getTargetModule(TraceModule module) { + return listenerForRecord.getTargetModule(module); + } + + @Override + public TraceModule getTraceModule(TargetModule module) { + String path = PathUtils.toString(module.getPath()); + return moduleManager.getLoadedModuleByPath(snapshot.getKey(), path); + } + + @Override + public TargetSection getTargetSection(TraceSection section) { + return listenerForRecord.getTargetSection(section); + } + + @Override + public TraceSection getTraceSection(TargetSection section) { + String path = PathUtils.toString(section.getPath()); + return moduleManager.getLoadedSectionByPath(snapshot.getKey(), path); + } + + @Override + public TargetThread getTargetThread(TraceThread thread) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.targetThread; + } + + @Override + public TargetExecutionState getTargetThreadState(TargetThread thread) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.state; + } + + @Override + public TargetExecutionState getTargetThreadState(TraceThread thread) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.state; + } + + @Override + public boolean isRegisterBankAccessible(TargetRegisterBank bank) { + if (bank == null) { + return false; + } + synchronized (accessibilityByRegBank) { + KeyedFuture future = accessibilityByRegBank.get(bank); + if (future == null) { + return false; + } + AllRequiredAccess acc = future.getNow(null); + if (acc == null) { + return false; + } + return acc.get() == TargetAccessibility.ACCESSIBLE; + } + } + + @Override + public boolean isRegisterBankAccessible(TraceThread thread, int frameLevel) { + return isRegisterBankAccessible(getTargetRegisterBank(thread, frameLevel)); + } + + @Override + public TargetRegisterBank getTargetRegisterBank(TraceThread thread, int frameLevel) { + ThreadRecorder rec = threadMap.get(thread); + return rec == null ? null : rec.regs.get(frameLevel); + } + + @Override + public Set> getLiveTargetThreads() { + return threadsView; + } + + @Override + public TraceThread getTraceThread(TargetThread thread) { + ThreadRecorder rec = threadMap.byTargetThread.get(thread); + return rec == null ? null : rec.traceThread; + } + + @Override + public TraceThread getTraceThreadForSuccessor(TargetObjectRef successor) { + ThreadRecorder rec = threadMap.getForSuccessor(successor); + return rec == null ? null : rec.traceThread; + } + + protected TraceStackFrame getTraceStackFrame(TraceThread thread, int level) { + TraceStack stack = trace.getStackManager().getLatestStack(thread, snapshot.getKey()); + if (stack == null) { + return null; + } + return stack.getFrame(level, false); + } + + @Override + public TraceStackFrame getTraceStackFrame(TargetStackFrame frame) { + ThreadRecorder rec = threadMap.getForSuccessor(frame); + if (rec == null) { + return null; + } + int level = getFrameLevel(frame); + if (rec.stack.get(level) != frame) { + return null; + } + return getTraceStackFrame(rec.traceThread, level); + } + + @Override + public TraceStackFrame getTraceStackFrameForSuccessor(TargetObjectRef successor) { + ThreadRecorder rec = threadMap.getForSuccessor(successor); + if (rec == null) { + return null; + } + int level = rec.getSuccessorFrameLevel(successor); + return getTraceStackFrame(rec.traceThread, level); + } + + @Override + public TargetStackFrame getTargetStackFrame(TraceThread thread, int frameLevel) { + ThreadRecorder rec = threadMap.get(thread); + if (rec == null) { + return null; + } + return rec.stack.get(frameLevel); + } + + @Override + public DebuggerMemoryMapper getMemoryMapper() { + return memMapper; + } + + @Override + public DebuggerRegisterMapper getRegisterMapper(TraceThread thread) { + ThreadRecorder rec = threadMap.get(thread); + if (rec == null) { + return null; + } + return rec.regMapper; + } + + @Override + public AddressSetView getAccessibleProcessMemory() { + // TODO: Efficiently distinguish which memory is process vs. thread + return getAccessibleMemory(mem -> true); + } + + protected void invalidate() { + valid = false; + listenerForRecord.dispose(); + trace.release(this); + } + + protected TraceThread findLiveThreadByName(String name) { + for (TraceThread traceThread : threadManager.getThreadsByPath(name)) { + if (traceThread != null && traceThread.isAlive()) { + return traceThread; + } + } + return null; + } + + @Override + public CompletableFuture captureThreadRegisters(TraceThread thread, int frameLevel, + Set registers) { + DebuggerRegisterMapper regMapper = getRegisterMapper(thread); + if (regMapper == null) { + throw new IllegalStateException("Have not found register descriptions for " + thread); + } + if (!regMapper.getRegistersOnTarget().containsAll(registers)) { + throw new IllegalArgumentException( + "All given registers must be recognized by the target"); + } + if (registers.isEmpty()) { + return AsyncUtils.NIL; + } + List> tRegs = + registers.stream().map(regMapper::traceToTarget).collect(Collectors.toList()); + + TargetRegisterBank bank = getTargetRegisterBank(thread, frameLevel); + if (bank == null) { + throw new IllegalArgumentException( + "Given thread and frame level does not have a live register bank"); + } + // NOTE: Cache update, if applicable, will cause recorder to write values to trace + return bank.readRegisters(tRegs).thenApply(__ -> null); + } + + @Override + public CompletableFuture writeThreadRegisters(TraceThread thread, int frameLevel, + Map values) { + DebuggerRegisterMapper regMapper = getRegisterMapper(thread); + if (regMapper == null) { + throw new IllegalStateException("Have not found register descriptions for " + thread); + } + if (!regMapper.getRegistersOnTarget().containsAll(values.keySet())) { + throw new IllegalArgumentException( + "All given registers must be recognized by the target"); + } + if (values.isEmpty()) { + return AsyncUtils.NIL; + } + Map tVals = values.entrySet().stream().map(ent -> { + if (ent.getKey() != ent.getValue().getRegister()) { + throw new IllegalArgumentException("register name mismatch in value"); + } + return regMapper.traceToTarget(ent.getValue()); + }).collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + TargetRegisterBank bank = getTargetRegisterBank(thread, frameLevel); + if (bank == null) { + throw new IllegalArgumentException( + "Given thread and frame level does not have a live register bank"); + } + // NOTE: Model + recorder will cause applicable trace updates + return bank.writeRegistersNamed(tVals).thenApply(__ -> null); + } + + @Override + public CompletableFuture readProcessMemory(Address start, int length) { + Address tStart = memMapper.traceToTarget(start); + return processMemory.readMemory(tStart, length); + } + + @Override + public CompletableFuture writeProcessMemory(Address start, byte[] data) { + Address tStart = memMapper.traceToTarget(start); + return processMemory.writeMemory(tStart, data); + } + + @Override + public CompletableFuture captureProcessMemory(AddressSetView set, TaskMonitor monitor) { + if (set.isEmpty()) { + return AsyncUtils.NIL; + } + // TODO: Figure out how to display/select per-thread memory. + // Probably need a thread parameter passed in then? + // NOTE: That thread memory will already be chained to process memory. Good. + + int total = 0; + AddressSetView expSet = + expandToBlocks(set).intersect(memoryManager.getRegionsAddressSet(snapshot.getKey())); + for (AddressRange r : expSet) { + total += Long.divideUnsigned(r.getLength() + BLOCK_SIZE - 1, BLOCK_SIZE); + } + monitor.initialize(total); + monitor.setMessage("Capturing memory"); + // TODO: Read blocks in parallel? Probably NO. Tends to overload the agent. + return AsyncUtils.each(TypeSpec.VOID, expSet.iterator(), (r, loop) -> { + AddressRangeChunker it = new AddressRangeChunker(r, BLOCK_SIZE); + AsyncUtils.each(TypeSpec.VOID, it.iterator(), (vRng, inner) -> { + // The listener in the recorder will copy to the Trace. + monitor.incrementProgress(1); + AddressRange tRng = memMapper.traceToTarget(vRng); + processMemory.readMemory(tRng.getMinAddress(), (int) tRng.getLength()) + .thenApply(b -> !monitor.isCancelled()) + .handle(inner::repeatWhile); + }).exceptionally(e -> { + Msg.error(this, "Error reading range " + r + ": " + e); + // NOTE: Above may double log, since recorder listens for errors, too + return null; // Continue looping on errors + }).thenApply(v -> !monitor.isCancelled()).handle(loop::repeatWhile); + }); + } + + @Override + public CompletableFuture captureDataTypes(TargetDataTypeNamespace namespace, + TaskMonitor monitor) { + if (!valid) { + return AsyncUtils.NIL; + } + String path = PathUtils.toString(namespace.getPath()); + monitor.setMessage("Capturing data types for " + path); + return namespace.getTypes().thenCompose(types -> { + monitor.initialize(types.size()); + AsyncFence fence = new AsyncFence(); + List converted = new ArrayList<>(); + for (TargetNamedDataType type : types) { + if (monitor.isCancelled()) { + fence.ready().cancel(false); + return AsyncUtils.nil(); + } + monitor.incrementProgress(1); + fence.include(typeConverter.convertTargetDataType(type).thenAccept(converted::add)); + } + return fence.ready().thenApply(__ -> converted); + }).thenAccept(converted -> { + if (converted == null) { + return; + } + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Capture data types for " + path)) { + // NOTE: createCategory is actually getOrCreate + Category category = dataTypeManager.createCategory(new CategoryPath("/" + path)); + for (DataType dataType : converted) { + category.addDataType(dataType, DataTypeConflictHandler.DEFAULT_HANDLER); + } + } + }); + } + + @Override + public CompletableFuture captureDataTypes(TraceModule module, TaskMonitor monitor) { + TargetModule targetModule = getTargetModule(module); + if (targetModule == null) { + Msg.error(this, "Module " + module + " is not loaded"); + return AsyncUtils.NIL; + } + CompletableFuture>> future = + targetModule.fetchChildrenSupporting(TargetDataTypeNamespace.tclass); + // NOTE: I should expect exactly one namespace... + return future.thenCompose(namespaces -> { + AsyncFence fence = new AsyncFence(); + for (TargetDataTypeNamespace ns : namespaces.values()) { + fence.include(captureDataTypes(ns, monitor)); + } + return fence.ready(); + }); + } + + private TraceNamespaceSymbol createNamespaceIfAbsent(String path) { + try { + return symbolManager.namespaces() + .add(path, symbolManager.getGlobalNamespace(), SourceType.IMPORTED); + } + catch (DuplicateNameException e) { + Msg.info(this, "Namespace for module " + path + + " already exists or another exists with a conflicting name. Using the existing one: " + + e); + TraceNamespaceSymbol ns = symbolManager.namespaces().getGlobalNamed(path); + if (ns != null) { + return ns; + } + Msg.error(this, "Existing namespace for " + path + + " is not a plain namespace. Using global namespace."); + return symbolManager.getGlobalNamespace(); + } + catch (InvalidInputException | IllegalArgumentException e) { + Msg.error(this, + "Could not create namespace for new module: " + path + ". Using global namespace.", + e); + return symbolManager.getGlobalNamespace(); + } + } + + @Override + public CompletableFuture captureSymbols(TargetSymbolNamespace namespace, + TaskMonitor monitor) { + if (!valid) { + return AsyncUtils.NIL; + } + String path = PathUtils.toString(namespace.getPath()); + monitor.setMessage("Capturing symbols for " + path); + return namespace.getSymbols().thenAccept(symbols -> { + try (PermanentTransaction tid = + PermanentTransaction.start(trace, "Capture types and symbols for " + path)) { + TraceNamespaceSymbol ns = createNamespaceIfAbsent(path); + monitor.setMessage("Capturing symbols for " + path); + monitor.initialize(symbols.size()); + for (TargetSymbol sym : symbols) { + if (monitor.isCancelled()) { + return; + } + monitor.incrementProgress(1); + String symName = sym.getIndex(); + if (sym.isConstant()) { + // TODO: Equate namespaces? + TraceEquate equate = equateManager.getByName(symName); + long symVal = sym.getValue().getOffset(); + if (equate != null && equate.getValue() == symVal) { + continue; + } + try { + equateManager.create(symName, symVal); + } + catch (DuplicateNameException | IllegalArgumentException e) { + Msg.error(this, "Could not create equate: " + symName, e); + } + continue; + } + Address addr = memMapper.targetToTrace(sym.getValue()); + try { + symbolManager.labels() + .create(snapshot.getKey(), null, addr, symName, ns, + SourceType.IMPORTED); + } + catch (InvalidInputException e) { + Msg.error(this, "Could not add module symbol " + sym + ": " + e); + } + /** + * TODO: Lay down data type, if present + * + * TODO: Interpret "address" type correctly. A symbol with this type is itself + * the pointer. In other words, it is not specifying the type to lay down in + * memory. + */ + } + } + }); + } + + @Override + public CompletableFuture captureSymbols(TraceModule module, TaskMonitor monitor) { + TargetModule targetModule = getTargetModule(module); + if (targetModule == null) { + Msg.error(this, "Module " + module + " is not loaded"); + return AsyncUtils.NIL; + } + CompletableFuture>> future = + targetModule.fetchChildrenSupporting(TargetSymbolNamespace.tclass); + // NOTE: I should expect exactly one namespace... + return future.thenCompose(namespaces -> { + AsyncFence fence = new AsyncFence(); + for (TargetSymbolNamespace ns : namespaces.values()) { + fence.include(captureSymbols(ns, monitor)); + } + return fence.ready(); + }); + } + + @Override + public boolean isSupportsFocus() { + return focusScope != null; + } + + @Override + public TargetObjectRef getFocus() { + if (curFocus == null) { + if (focusScope == null) { + return null; + } + TargetObjectRef focus = focusScope.getFocus(); + if (focus == null || !PathUtils.isAncestor(target.getPath(), focus.getPath())) { + return null; + } + curFocus = focus; + } + return curFocus; + } + + @Override + public CompletableFuture requestFocus(TargetObjectRef focus) { + if (!isSupportsFocus()) { + return CompletableFuture + .failedFuture(new IllegalArgumentException("Target does not support focus")); + } + if (!PathUtils.isAncestor(target.getPath(), focus.getPath())) { + return CompletableFuture.failedFuture(new IllegalArgumentException( + "Requested focus path is not a successor of the target")); + } + if (!PathUtils.isAncestor(focusScope.getPath(), focus.getPath())) { + return CompletableFuture.failedFuture(new IllegalArgumentException( + "Requested focus path is not a successor of the focus scope")); + } + return focusScope.requestFocus(focus).thenApply(__ -> true).exceptionally(ex -> { + ex = AsyncUtils.unwrapThrowable(ex); + if (ex instanceof DebuggerModelAccessException) { + String msg = "Could not focus " + focus + ": " + ex.getMessage(); + Msg.info(this, msg); + plugin.getTool().setStatusInfo(msg); + } + Msg.showError(this, null, "Focus Sync", "Could not focus " + focus, ex); + return false; + }); + } + + @Override + public TraceEventListener getListenerForRecord() { + return null; + } + + @Override + public ListenerSet getListeners() { + return null; + } + + /* + @Override + public ListenerForRecord getListenerForRecord() { + return listenerForRecord; + } + */ +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java index c7a230a70e..964075033e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProvider.java @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.debug.gui.breakpoint; -import static ghidra.lifecycle.Unfinished.*; +import static ghidra.lifecycle.Unfinished.TODO; import java.awt.BorderLayout; import java.awt.event.*; @@ -51,6 +51,7 @@ import ghidra.trace.model.Trace.TraceBreakpointChangeType; import ghidra.trace.model.TraceDomainObjectListener; import ghidra.trace.model.breakpoint.TraceBreakpoint; import ghidra.util.*; +import ghidra.util.database.ObjectKey; import ghidra.util.datastruct.CollectionChangeListener; import ghidra.util.table.GhidraTable; import ghidra.util.table.GhidraTableFilterPanel; @@ -121,10 +122,10 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter } protected static class LogicalBreakpointTableModel extends RowWrappedEnumeratedColumnTableModel< // - LogicalBreakpointTableColumns, LogicalBreakpointRow, LogicalBreakpoint> { + LogicalBreakpointTableColumns, LogicalBreakpoint, LogicalBreakpointRow, LogicalBreakpoint> { public LogicalBreakpointTableModel(DebuggerBreakpointsProvider provider) { - super("Breakpoints", LogicalBreakpointTableColumns.class, + super("Breakpoints", LogicalBreakpointTableColumns.class, lb -> lb, lb -> new LogicalBreakpointRow(provider, lb)); } @@ -198,10 +199,11 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter protected static class BreakpointLocationTableModel extends RowWrappedEnumeratedColumnTableModel< // - BreakpointLocationTableColumns, BreakpointLocationRow, TraceBreakpoint> { + BreakpointLocationTableColumns, ObjectKey, BreakpointLocationRow, TraceBreakpoint> { public BreakpointLocationTableModel() { - super("Locations", BreakpointLocationTableColumns.class, BreakpointLocationRow::new); + super("Locations", BreakpointLocationTableColumns.class, TraceBreakpoint::getObjectKey, + BreakpointLocationRow::new); } @Override @@ -965,7 +967,9 @@ public class DebuggerBreakpointsProvider extends ComponentProviderAdapter } public void setSelectedLocations(Set sel) { - DebuggerResources.setSelectedRows(sel, locationTableModel.getMap(), locationTable, + DebuggerResources.setSelectedRows( + sel.stream().map(b -> b.getObjectKey()).collect(Collectors.toSet()), + locationTableModel.getMap(), locationTable, locationTableModel, locationFilterPanel); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java index 5c7e5c513d..a2a8d52ff2 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/interpreters/AbstractDebuggerWrappedConsoleConnection.java @@ -16,6 +16,7 @@ package ghidra.app.plugin.core.debug.gui.interpreters; import java.io.*; +import java.lang.invoke.MethodHandles; import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -30,10 +31,9 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.PinInterpreterAction; import ghidra.app.plugin.core.interpreter.InterpreterComponentProvider; import ghidra.app.plugin.core.interpreter.InterpreterConsole; +import ghidra.dbg.AnnotatedDebuggerAttributeListener; import ghidra.dbg.target.TargetConsole.Channel; -import ghidra.dbg.target.TargetConsole.TargetConsoleListener; import ghidra.dbg.target.TargetInterpreter; -import ghidra.dbg.target.TargetInterpreter.TargetInterpreterListener; import ghidra.dbg.target.TargetObject; import ghidra.util.Msg; import ghidra.util.Swing; @@ -45,7 +45,11 @@ public abstract class AbstractDebuggerWrappedConsoleConnection provider.setSubTitle(display)); } - @Override - public void promptChanged(TargetInterpreter i, String prompt) { + @AttributeCallback(TargetInterpreter.PROMPT_ATTRIBUTE_NAME) + public void promptChanged(TargetObject interpreter, String prompt) { Swing.runLater(() -> guiConsole.setPrompt(prompt)); } @@ -109,7 +113,7 @@ public abstract class AbstractDebuggerWrappedConsoleConnection { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java index 7c3e91d7e7..241a47e64e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPlugin.java @@ -27,7 +27,8 @@ import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.*; import ghidra.app.services.*; import ghidra.dbg.DebuggerObjectModel; -import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetInterpreter; +import ghidra.dbg.target.TargetObject; import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; @@ -238,7 +239,7 @@ public class DebuggerObjectsPlugin extends AbstractDebuggerPlugin System.err.println("modelModified " + model); } - public void setFocus(TargetFocusScope object, TargetObject focused) { + public void setFocus(TargetObject object, TargetObject focused) { for (DebuggerObjectsProvider p : providers) { p.setFocus(object, focused); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java index a5bc05d8fc..d2b8826094 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsProvider.java @@ -52,8 +52,8 @@ import ghidra.dbg.target.*; import ghidra.dbg.target.TargetConsole.Channel; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; -import ghidra.dbg.target.TargetObject.TargetObjectListener; import ghidra.dbg.target.TargetSteppable.TargetStepKind; +import ghidra.dbg.util.DebuggerCallbackReorderer; import ghidra.dbg.util.PathUtils; import ghidra.framework.options.AutoOptions; import ghidra.framework.options.SaveState; @@ -70,10 +70,8 @@ import ghidra.util.*; import ghidra.util.table.GhidraTable; import resources.ResourceManager; -public class DebuggerObjectsProvider extends ComponentProviderAdapter implements //AllTargetObjectListenerAdapter, - TargetObjectListener, // - DebuggerModelListener, // - ObjectContainerListener { +public class DebuggerObjectsProvider extends ComponentProviderAdapter + implements ObjectContainerListener { public static final String PATH_JOIN_CHAR = "."; //private static final String AUTOUPDATE_ATTRIBUTE_NAME = "autoupdate"; @@ -170,7 +168,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements description = "The foreground color for links to items in the objects tree", // help = @HelpInfo(anchor = "colors") // ) - Color linkForegroundColor = Color.GREEN; + Color linkForegroundColor = Color.GREEN.darker(); @AutoOptionDefined( // name = "Default Extended Step", // @@ -202,6 +200,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements }; private boolean asTree = true; + private MyObjectListener listener = new MyObjectListener(); public DebuggerMethodInvocationDialog launchDialog; public DebuggerAttachDialog attachDialog; @@ -301,7 +300,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements public void setModel(DebuggerObjectModel model) { currentModel = model; - currentModel.addModelListener(this, true); + currentModel.addModelListener(getListener(), true); refresh(); } @@ -463,8 +462,10 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements List containers = new ArrayList<>(); for (String path : targetMap.keySet()) { if (path.endsWith(key)) { - ObjectContainer container = targetMap.get(path); - containers.add(container); + synchronized (targetMap) { + ObjectContainer container = targetMap.get(path); + containers.add(container); + } } } for (ObjectContainer container : containers) { @@ -659,8 +660,10 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements if (targetObject != null && !container.isLink()) { String key = targetObject.getJoinedPath(PATH_JOIN_CHAR); container.subscribe(); - targetMap.put(key, container); - refSet.add(targetObject); + synchronized (targetMap) { + targetMap.put(key, container); + refSet.add(targetObject); + } if (targetObject instanceof TargetInterpreter) { TargetInterpreter interpreter = (TargetInterpreter) targetObject; getPlugin().showConsole(interpreter); @@ -675,8 +678,10 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements public void deleteFromMap(ObjectContainer container) { TargetObject targetObject = container.getTargetObject(); if (targetObject != null) { - targetMap.remove(targetObject.getJoinedPath(PATH_JOIN_CHAR)); - refSet.remove(targetObject); + synchronized (targetMap) { + targetMap.remove(targetObject.getJoinedPath(PATH_JOIN_CHAR)); + refSet.remove(targetObject); + } } } @@ -741,7 +746,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements public void closeComponent() { TargetObject targetObject = getRoot().getTargetObject(); if (targetObject != null) { - targetObject.removeListener(this); + targetObject.removeListener(getListener()); } super.closeComponent(); } @@ -1153,9 +1158,9 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements .helpLocation(AbstractSetBreakpointAction.help(plugin)) //.withContext(ObjectActionContext.class) .enabledWhen(ctx -> - isInstance(ctx, TargetBreakpointContainer.class) && isStopped(ctx)) + isInstance(ctx, TargetBreakpointSpecContainer.class) && isStopped(ctx)) .popupWhen(ctx -> - isInstance(ctx, TargetBreakpointContainer.class) && isStopped(ctx)) + isInstance(ctx, TargetBreakpointSpecContainer.class) && isStopped(ctx)) .onAction(ctx -> performSetBreakpoint(ctx)) .enabled(false) .buildAndInstallLocal(this); @@ -1348,23 +1353,21 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements }).finish(); } - public CompletableFuture startRecording(TargetProcess targetObject, boolean prompt) { - CompletableFuture future; + public void startRecording(TargetProcess targetObject, boolean prompt) { + TraceRecorder rec; if (prompt) { - future = modelService.recordTargetPromptOffers(targetObject); + rec = modelService.recordTargetPromptOffers(targetObject); } else { - future = modelService.recordTargetBestOffer(targetObject); + rec = modelService.recordTargetBestOffer(targetObject); } - return future.thenAccept(rec -> { - if (rec == null) { - return; // Cancelled - } - //this.recorder = rec; - Trace trace = rec.getTrace(); - traceManager.openTrace(trace); - traceManager.activateTrace(trace); - }); + if (rec == null) { + return; // Cancelled + } + //this.recorder = rec; + Trace trace = rec.getTrace(); + traceManager.openTrace(trace); + traceManager.activateTrace(trace); } public void stopRecording(TargetObject targetObject) { @@ -1409,22 +1412,14 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements DebugModelConventions.findSuitable(TargetProcess.class, obj).thenAccept(process -> { TargetProcess valid = DebugModelConventions.liveProcessOrNull(process); if (valid != null) { - startRecording(valid, true).exceptionally(ex -> { - Msg.showError(this, null, "Record", - "Could not record and/or open target: " + valid, ex); - return null; - }); + startRecording(valid, true); } }).exceptionally(DebuggerResources.showError(getComponent(), "Couldn't record")); } else { TargetProcess valid = DebugModelConventions.liveProcessOrNull(obj); if (valid != null) { - startRecording(valid, true).exceptionally(ex -> { - Msg.showError(this, null, "Record", - "Could not record and/or open target: " + valid, ex); - return null; - }); + startRecording(valid, true); } } } @@ -1520,7 +1515,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements public void performSetBreakpoint(ActionContext context) { TargetObject obj = getObjectFromContext(context); if (!isLocalOnly()) { - DebugModelConventions.findSuitable(TargetBreakpointContainer.class, obj) + DebugModelConventions.findSuitable(TargetBreakpointSpecContainer.class, obj) .thenAccept(suitable -> { breakpointDialog.setContainer(suitable); tool.showDialog(breakpointDialog); @@ -1529,7 +1524,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements DebuggerResources.showError(getComponent(), "Couldn't set breakpoint")); } else { - TargetBreakpointContainer container = (TargetBreakpointContainer) obj; + TargetBreakpointSpecContainer container = (TargetBreakpointSpecContainer) obj; breakpointDialog.setContainer(container); tool.showDialog(breakpointDialog); } @@ -1584,114 +1579,145 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements return false; } - @Override - public void accessibilityChanged(TargetAccessConditioned object, boolean accessible) { - //this.access = accessibility.equals(TargetAccessibility.ACCESSIBLE); - plugin.getTool().contextChanged(this); - } + class MyObjectListener extends AnnotatedDebuggerAttributeListener { + protected final DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this); - @Override - public void consoleOutput(TargetObject console, Channel channel, String out) { - //getPlugin().showConsole((TargetInterpreter) console); - System.err.println("consoleOutput: " + out); - } + public MyObjectListener() { + super(MethodHandles.lookup()); + } - @Override - public void displayChanged(TargetObject object, String display) { - //System.err.println("displayChanged: " + display); - if (ObjectContainer.visibleByDefault(object.getName())) { - pane.signalDataChanged(getContainerByPath(object.getPath())); + @AttributeCallback(TargetAccessConditioned.ACCESSIBLE_ATTRIBUTE_NAME) + public void accessibilityChanged(TargetObject object, boolean accessible) { + //this.access = accessibility.equals(TargetAccessibility.ACCESSIBLE); + plugin.getTool().contextChanged(DebuggerObjectsProvider.this); + } + + @Override + public void consoleOutput(TargetObject console, Channel channel, String out) { + //getPlugin().showConsole((TargetInterpreter) console); + System.err.println("consoleOutput: " + out); + } + + @AttributeCallback(TargetObject.DISPLAY_ATTRIBUTE_NAME) + public void displayChanged(TargetObject object, String display) { + //System.err.println("displayChanged: " + display); + if (ObjectContainer.visibleByDefault(object.getName())) { + pane.signalDataChanged(getContainerByPath(object.getPath())); + } + } + + @AttributeCallback(TargetObject.MODIFIED_ATTRIBUTE_NAME) + public void modifiedChanged(TargetObject object, boolean modified) { + //System.err.println("modifiedChanged: " + display); + if (ObjectContainer.visibleByDefault(object.getName())) { + pane.signalDataChanged(getContainerByPath(object.getPath())); + } + } + + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + public void executionStateChanged(TargetObject object, TargetExecutionState state) { + //this.state = state; + plugin.getTool().contextChanged(DebuggerObjectsProvider.this); + } + + @AttributeCallback(TargetFocusScope.FOCUS_ATTRIBUTE_NAME) + public void focusChanged(TargetObject object, TargetObject focused) { + plugin.setFocus(object, focused); + plugin.getTool().contextChanged(DebuggerObjectsProvider.this); + } + + @Override + public void memoryUpdated(TargetObject memory, Address address, byte[] data) { + //System.err.println("memoryUpdated"); + } + + @Override + public void memoryReadError(TargetObject memory, AddressRange range, + DebuggerMemoryAccessException e) { + System.err.println("memoryReadError"); + } + + @AttributeCallback(TargetInterpreter.PROMPT_ATTRIBUTE_NAME) + public void promptChanged(TargetObject interpreter, String prompt) { + //System.err.println("promptChanged: " + prompt); + } + + @Override + public void registersUpdated(TargetObject bank, Map updates) { + Map cachedElements = bank.getCachedElements(); + for (String key : cachedElements.keySet()) { + TargetObject ref = cachedElements.get(key); + displayChanged(ref, "registersUpdated"); + } + Map cachedAttributes = bank.getCachedAttributes(); + for (String key : cachedAttributes.keySet()) { + Object obj = cachedAttributes.get(key); + if (obj instanceof TargetObject) { + displayChanged((TargetObject) obj, "registersUpdated"); + } + } + } + + @Override + public void elementsChanged(TargetObject parent, Collection removed, + Map added) { + //System.err.println("local EC: " + parent); + ObjectContainer container = + parent == null ? null : getContainerByPath(parent.getPath()); + if (container != null) { + container.augmentElements(removed, added); + boolean visibleChange = false; + for (String key : removed) { + visibleChange |= ObjectContainer.visibleByDefault(key); + } + for (String key : added.keySet()) { + visibleChange |= ObjectContainer.visibleByDefault(key); + } + if (visibleChange) { + container.propagateProvider(DebuggerObjectsProvider.this); + update(container); + } + } + } + + @Override + public void attributesChanged(TargetObject parent, Collection removed, + Map added) { + super.attributesChanged(parent, removed, added); + //System.err.println("local AC: " + parent + ":" + removed + ":" + added); + ObjectContainer container = + parent == null ? null : getContainerByPath(parent.getPath()); + if (container != null) { + container.augmentAttributes(removed, added); + boolean visibleChange = false; + for (String key : removed) { + visibleChange |= ObjectContainer.visibleByDefault(key); + } + for (String key : added.keySet()) { + visibleChange |= ObjectContainer.visibleByDefault(key); + } + if (visibleChange) { + container.propagateProvider(DebuggerObjectsProvider.this); + update(container); + } + } + if (parent != null && isAutorecord() && + parent.getCachedAttribute(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) != null) { + TargetProcess proc = DebugModelConventions.liveProcessOrNull(parent); + if (proc != null) { + startRecording(proc, false); + } + } } } - @Override - public void executionStateChanged(TargetExecutionStateful object, TargetExecutionState state) { - //this.state = state; - plugin.getTool().contextChanged(this); - } - - @Override - public void focusChanged(TargetFocusScope object, TargetObject focused) { - plugin.setFocus(object, focused); - plugin.getTool().contextChanged(this); - } - - public void setFocus(TargetFocusScope object, TargetObject focused) { + public void setFocus(TargetObject object, TargetObject focused) { if (focused.getModel() != currentModel) { return; } pane.setFocus(object, focused); } - @Override - public void memoryUpdated(TargetMemory memory, Address address, byte[] data) { - //System.err.println("memoryUpdated"); - } - - @Override - public void memoryReadError(TargetMemory memory, AddressRange range, - DebuggerMemoryAccessException e) { - System.err.println("memoryReadError"); - } - - @Override - public void promptChanged(TargetInterpreter interpreter, String prompt) { - System.err.println("promptChanged: " + prompt); - } - - @Override - public void registersUpdated(TargetRegisterBank bank, Map updates) { - Map cachedElements = bank.getCachedElements(); - for (String key : cachedElements.keySet()) { - TargetObject ref = cachedElements.get(key); - if (ref instanceof TargetObject) { - displayChanged(ref, "registersUpdated"); - } - } - } - - @Override - public void elementsChanged(TargetObject parent, Collection removed, - Map added) { - //System.err.println("local EC: " + parent); - ObjectContainer container = parent == null ? null : getContainerByPath(parent.getPath()); - if (container != null) { - container.augmentElements(removed, added); - boolean visibleChange = false; - for (String key : removed) { - visibleChange |= ObjectContainer.visibleByDefault(key); - } - for (String key : added.keySet()) { - visibleChange |= ObjectContainer.visibleByDefault(key); - } - if (visibleChange) { - container.propagateProvider(this); - update(container); - } - } - } - - @Override - public void attributesChanged(TargetObject parent, Collection removed, - Map added) { - //System.err.println("local AC: " + parent + ":" + removed + ":" + added); - ObjectContainer container = parent == null ? null : getContainerByPath(parent.getPath()); - if (container != null) { - container.augmentAttributes(removed, added); - boolean visibleChange = false; - for (String key : removed) { - visibleChange |= ObjectContainer.visibleByDefault(key); - } - for (String key : added.keySet()) { - visibleChange |= ObjectContainer.visibleByDefault(key); - } - if (visibleChange) { - container.propagateProvider(this); - update(container); - } - } - } - public DebuggerTraceManagerService getTraceManager() { return traceManager; } @@ -1797,4 +1823,8 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter implements return listingService; } + public DebuggerModelListener getListener() { + return listener.reorderer; + } + } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/ObjectContainer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/ObjectContainer.java index b3651b13c3..9179d0fc8d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/ObjectContainer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/ObjectContainer.java @@ -20,11 +20,8 @@ import java.util.concurrent.CompletableFuture; import org.jdom.Element; -import ghidra.dbg.DebugModelConventions; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.TargetProcess; import ghidra.dbg.util.PathUtils; -import ghidra.util.Msg; import ghidra.util.xml.XmlUtilities; public class ObjectContainer implements Comparable { @@ -157,18 +154,16 @@ public class ObjectContainer implements Comparable { }); } + /* protected void checkAutoRecord() { if (targetObject != null && provider.isAutorecord()) { TargetProcess proc = DebugModelConventions.liveProcessOrNull(targetObject); if (proc != null) { - provider.startRecording(proc, false).exceptionally(ex -> { - Msg.error("Could not record and/or open target: " + targetObject, ex); - return null; - }); + provider.startRecording(proc, false); } - // Note that the recorder seeds its own listener with its target } } + */ public void augmentElements(Collection elementsRemoved, Map elementsAdded) { @@ -340,7 +335,7 @@ public class ObjectContainer implements Comparable { c.propagateProvider(provider); } provider.fireObjectUpdated(this); - checkAutoRecord(); + //checkAutoRecord(); } // This should only be called once when the connection is activated diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerBreakpointDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerBreakpointDialog.java index 806a0cbe6f..43fcfff72e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerBreakpointDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerBreakpointDialog.java @@ -26,7 +26,7 @@ import javax.swing.*; import docking.DialogComponentProvider; import ghidra.app.plugin.core.debug.gui.DebuggerResources.AbstractSetBreakpointAction; import ghidra.app.plugin.core.debug.gui.objects.DebuggerObjectsProvider; -import ghidra.dbg.target.TargetBreakpointContainer; +import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.util.MessageType; import ghidra.util.Msg; @@ -34,7 +34,7 @@ import ghidra.util.layout.PairLayout; public class DebuggerBreakpointDialog extends DialogComponentProvider { protected DebuggerObjectsProvider provider; - protected TargetBreakpointContainer container; + protected TargetBreakpointSpecContainer container; protected JTextField expressionField; @@ -78,7 +78,7 @@ public class DebuggerBreakpointDialog extends DialogComponentProvider { setStatusText("Adding"); Set kinds = new HashSet<>(); - kinds.add(TargetBreakpointKind.SOFTWARE); + kinds.add(TargetBreakpointKind.SW_EXECUTE); container.placeBreakpoint(expression, kinds).exceptionally(e -> { Msg.showError(this, getComponent(), "Could not set breakpoint", e); setStatusText("Could not set breakpoint: " + e.getMessage(), MessageType.ERROR); @@ -87,7 +87,7 @@ public class DebuggerBreakpointDialog extends DialogComponentProvider { close(); } - public void setContainer(TargetBreakpointContainer container) { + public void setContainer(TargetBreakpointSpecContainer container) { this.container = container; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DummyTargetObject.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DummyTargetObject.java index 33ec97c9f7..8e018d4482 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DummyTargetObject.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DummyTargetObject.java @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import org.apache.commons.lang3.StringUtils; import ghidra.async.AsyncUtils; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.target.TargetObject; @@ -172,6 +173,11 @@ public class DummyTargetObject implements TargetObject { return elements; } + @Override + public Map getCallbackElements() { + return elements; + } + @Override public CompletableFuture> fetchAttributes() { if (!key.equals(TargetObject.DISPLAY_ATTRIBUTE_NAME)) { @@ -191,7 +197,6 @@ public class DummyTargetObject implements TargetObject { if (type != null) { addAttribute(TargetObject.TYPE_ATTRIBUTE_NAME, type); } - addAttribute(TargetObject.UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED); } // Why not completedFuture(attributes)? return CompletableFuture.supplyAsync(() -> attributes); @@ -203,11 +208,16 @@ public class DummyTargetObject implements TargetObject { } @Override - public void addListener(TargetObjectListener l) { + public Map getCallbackAttributes() { + return attributes; } @Override - public void removeListener(TargetObjectListener l) { + public void addListener(DebuggerModelListener l) { + } + + @Override + public void removeListener(DebuggerModelListener l) { } public String getJoinedPath() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectNode.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectNode.java index 5e66054698..761a952f3f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectNode.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectNode.java @@ -84,14 +84,16 @@ public class ObjectNode extends GTreeSlowLoadingNode { //extends GTreeNode if (cf != null) { // NB: We're allowed to do this because we're guaranteed to be // in our own thread by the GTreeSlowLoadingNode - ObjectContainer oc = cf.get(5, TimeUnit.SECONDS); + ObjectContainer oc = cf.get(60, TimeUnit.SECONDS); return tree.update(oc); } } - catch (InterruptedException | ExecutionException | TimeoutException e) { - // Ignore + catch (InterruptedException | ExecutionException e) { Msg.warn(this, e); - //e.printStackTrace(); + } + catch (TimeoutException e) { + Msg.showWarn(this, container.getProvider().getComponent(), "Timeout Exception", + "Request for children timed - out - try refreshing the node"); } } List list = new ArrayList<>(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectPane.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectPane.java index 42d641700c..396ed1c51b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectPane.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectPane.java @@ -20,7 +20,6 @@ import java.util.List; import javax.swing.JComponent; import ghidra.app.plugin.core.debug.gui.objects.ObjectContainer; -import ghidra.dbg.target.TargetFocusScope; import ghidra.dbg.target.TargetObject; public interface ObjectPane { @@ -45,7 +44,7 @@ public interface ObjectPane { public String getName(); - public void setFocus(TargetFocusScope object, TargetObject focused); + public void setFocus(TargetObject object, TargetObject focused); public void setRoot(ObjectContainer root, TargetObject targetObject); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectTable.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectTable.java index 56ab9df549..d47cb32cd6 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectTable.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectTable.java @@ -29,7 +29,6 @@ import ghidra.app.plugin.core.debug.gui.objects.DebuggerObjectsProvider; import ghidra.app.plugin.core.debug.gui.objects.ObjectContainer; import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper; import ghidra.app.services.*; -import ghidra.dbg.target.TargetFocusScope; import ghidra.dbg.target.TargetObject; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRangeImpl; @@ -269,7 +268,7 @@ public class ObjectTable implements ObjectPane { } @Override - public void setFocus(TargetFocusScope object, TargetObject focused) { + public void setFocus(TargetObject object, TargetObject focused) { Swing.runIfSwingOrRunLater(() -> { setSelectedObject(focused); }); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectTree.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectTree.java index 5079f56834..96300e76cb 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectTree.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/ObjectTree.java @@ -40,7 +40,8 @@ import ghidra.app.services.*; import ghidra.async.AsyncUtils; import ghidra.async.TypeSpec; import ghidra.dbg.DebugModelConventions; -import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetAccessConditioned; +import ghidra.dbg.target.TargetObject; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRangeImpl; import ghidra.util.*; @@ -352,7 +353,7 @@ public class ObjectTree implements ObjectPane { } @Override - public void setFocus(TargetFocusScope object, TargetObject focused) { + public void setFocus(TargetObject object, TargetObject focused) { Swing.runIfSwingOrRunLater(() -> { List path = focused.getPath(); tree.setSelectedNodeByNamePath(addRootNameToPath(path)); @@ -395,7 +396,7 @@ public class ObjectTree implements ObjectPane { DebuggerObjectsProvider provider = getProvider(); ObjectContainer oc = node.getContainer(); provider.deleteFromMap(oc); - oc.getTargetObject().removeListener(provider); + oc.getTargetObject().removeListener(provider.getListener()); nodeMap.remove(path(node.getContainer())); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java index 831c095318..58d1a70dfb 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersPlugin.java @@ -41,8 +41,7 @@ import ghidra.util.Msg; category = PluginCategoryNames.DEBUGGER, // packageName = DebuggerPluginPackage.NAME, // status = PluginStatus.RELEASED, // - eventsConsumed = { - TraceActivatedPluginEvent.class, // + eventsConsumed = { TraceActivatedPluginEvent.class, // TraceClosedPluginEvent.class, // }, // servicesRequired = { // diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java index a48af6bf43..f522f60684 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProvider.java @@ -29,6 +29,8 @@ import javax.swing.*; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; +import org.apache.commons.lang3.exception.ExceptionUtils; + import com.google.common.collect.Range; import docking.*; @@ -44,6 +46,7 @@ import ghidra.app.plugin.core.debug.gui.DebuggerProvider; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.mapping.DebuggerRegisterMapper; import ghidra.app.services.*; +import ghidra.async.AsyncLazyValue; import ghidra.async.AsyncUtils; import ghidra.base.widgets.table.DataTypeTableCellEditor; import ghidra.dbg.error.DebuggerModelAccessException; @@ -285,17 +288,11 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter class RegAccessListener implements TraceRecorderListener { @Override public void registerBankMapped(TraceRecorder recorder) { - if (readTheseCoords) { - return; - } Swing.runIfSwingOrRunLater(() -> loadValues()); } @Override public void registerAccessibilityChanged(TraceRecorder recorder) { - if (readTheseCoords) { - return; - } Swing.runIfSwingOrRunLater(() -> loadValues()); } } @@ -363,7 +360,8 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter DebuggerCoordinates previous = DebuggerCoordinates.NOWHERE; DebuggerCoordinates current = DebuggerCoordinates.NOWHERE; - private boolean readTheseCoords = false; /* "read" past tense */ + private AsyncLazyValue readTheseCoords = + new AsyncLazyValue<>(this::readRegistersIfLiveAndAccessible); /* "read" past tense */ private Trace currentTrace; // Copy for transition private TraceRecorder currentRecorder; // Copy of transition @@ -707,7 +705,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter previous = current; current = coordinates; - readTheseCoords = false; + readTheseCoords = new AsyncLazyValue<>(this::readRegistersIfLiveAndAccessible); doSetTrace(current.getTrace()); doSetRecorder(current.getRecorder()); updateSubTitle(); @@ -1098,7 +1096,9 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter return AsyncUtils.NIL; } regsTableModel.fireTableDataChanged(); - return readRegistersIfLiveAndAccessible(); + //return AsyncUtils.NIL; + // In case we need to read a non-zero frame + return readTheseCoords.request(); } private Set baseRegisters(Set regs) { @@ -1113,10 +1113,13 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter if (recorder.getSnap() != current.getSnap()) { return AsyncUtils.NIL; } + if (current.getFrame() == 0) { + // Should have been pushed by model. non-zero frames are poll-only + return AsyncUtils.NIL; + } TraceThread traceThread = current.getThread(); TargetThread targetThread = recorder.getTargetThread(traceThread); - if (targetThread == null || - !recorder.isRegisterBankAccessible(traceThread, current.getFrame())) { + if (targetThread == null) { return AsyncUtils.NIL; } Set toRead = new HashSet<>(baseRegisters(getSelectionFor(traceThread))); @@ -1126,16 +1129,14 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter return AsyncUtils.NIL; } toRead.retainAll(regMapper.getRegistersOnTarget()); - TargetRegisterBank bank = - recorder.getTargetRegisterBank(traceThread, current.getFrame()); - if (!bank.isValid()) { + TargetRegisterBank bank = recorder.getTargetRegisterBank(traceThread, current.getFrame()); + if (bank == null || !bank.isValid()) { + Msg.error(this, "Current frame's bank does not exist"); return AsyncUtils.NIL; } CompletableFuture future = recorder.captureThreadRegisters(traceThread, current.getFrame(), toRead); - return future.thenAccept(__ -> { - readTheseCoords = true; - }).exceptionally(ex -> { + return future.exceptionally(ex -> { ex = AsyncUtils.unwrapThrowable(ex); if (ex instanceof DebuggerModelAccessException) { String msg = @@ -1147,7 +1148,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter Msg.showError(this, getComponent(), "Read Target Registers", "Could not read target registers for selected thread", ex); } - return null; + return ExceptionUtils.rethrow(ex); }); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java index e0e55642d7..0b9ea7f1d3 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java @@ -221,6 +221,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider } private void connect(ActionEvent evt) { + connectButton.setEnabled(false); for (Map.Entry, PropertyEditor> ent : propertyEditors.entrySet()) { Property prop = ent.getKey(); @SuppressWarnings("unchecked") @@ -232,11 +233,13 @@ public class DebuggerConnectDialog extends DialogComponentProvider modelService.addModel(model); setStatusText(""); close(); + connectButton.setEnabled(true); return CompletableFuture.runAsync(() -> modelService.activateModel(model), SwingExecutorService.INSTANCE); }).exceptionally(e -> { Msg.showError(this, getComponent(), "Could not connect", e); setStatusText("Could not connect: " + e.getMessage(), MessageType.ERROR); + connectButton.setEnabled(true); return null; }); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProvider.java index 052756ef9a..2ddd4f59d7 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProvider.java @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.debug.gui.target; -import static ghidra.app.plugin.core.debug.gui.DebuggerResources.showError; +import static ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import java.awt.BorderLayout; import java.awt.event.MouseEvent; @@ -290,7 +290,7 @@ public class DebuggerTargetsProvider extends ComponentProviderAdapter { // TODO: Ensure when tree is populated, correct model is selected } // Note, setSelectedNode does not take EventOrigin - tree.setSelectionPaths(new TreePath[] { node.getTreePath() }, EventOrigin.USER_GENERATED); + tree.setSelectionPaths(new TreePath[] { node.getTreePath() }, EventOrigin.API_GENERATED); } protected void clearServiceCaches(DebuggerModelService service) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java index 4005360df7..ee16aecd66 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPlugin.java @@ -29,8 +29,7 @@ import ghidra.framework.plugintool.util.PluginStatus; category = PluginCategoryNames.DEBUGGER, // packageName = DebuggerPluginPackage.NAME, // status = PluginStatus.RELEASED, // - eventsConsumed = { - TraceOpenedPluginEvent.class, // + eventsConsumed = { TraceOpenedPluginEvent.class, // TraceClosedPluginEvent.class, // TraceActivatedPluginEvent.class, // }, // diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMappingOpinion.java index 39a4fe9b66..3b642aa007 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DebuggerMappingOpinion.java @@ -16,10 +16,9 @@ package ghidra.app.plugin.core.debug.mapping; import java.util.*; -import java.util.concurrent.CompletableFuture; -import ghidra.async.AsyncFence; -import ghidra.dbg.target.TargetObject; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.*; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.classfinder.ExtensionPoint; @@ -35,22 +34,16 @@ public interface DebuggerMappingOpinion extends ExtensionPoint { * @param target the target to be recorded, usually a process * @return a future which completes with the set of offers */ - public static CompletableFuture> queryOpinions( - TargetObject target) { + public static List queryOpinions(TargetObject target) { List result = new ArrayList<>(); - AsyncFence fence = new AsyncFence(); for (DebuggerMappingOpinion opinion : ClassSearcher .getInstances(DebuggerMappingOpinion.class)) { - fence.include(opinion.getOffers(target).thenAccept(offers -> { - synchronized (result) { - result.addAll(offers); - } - })); + synchronized (result) { + result.addAll(opinion.getOffers(target)); + } } - return fence.ready().thenApply(__ -> { - result.sort(HIGHEST_CONFIDENCE_FIRST); - return result; - }); + result.sort(HIGHEST_CONFIDENCE_FIRST); + return result; } /** @@ -59,5 +52,18 @@ public interface DebuggerMappingOpinion extends ExtensionPoint { * @param target the target, usually a process * @return a future which completes with true if it knows, false if not */ - CompletableFuture> getOffers(TargetObject target); + public default Set getOffers(TargetObject target) { + if (!(target instanceof TargetProcess)) { + return Set.of(); + } + TargetProcess process = (TargetProcess) target; + DebuggerObjectModel model = process.getModel(); + List pathToEnv = + model.getRootSchema().searchForSuitable(TargetEnvironment.class, process.getPath()); + TargetEnvironment env = (TargetEnvironment) model.getModelObject(pathToEnv); + return offersForEnv(env, process); + } + + Set offersForEnv(TargetEnvironment env, TargetProcess process); + } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerRegisterMapper.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerRegisterMapper.java index 0ea973b08d..954fbab642 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerRegisterMapper.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/DefaultDebuggerRegisterMapper.java @@ -20,14 +20,13 @@ import java.util.*; import ghidra.app.plugin.core.debug.register.RegisterTypeInfo; import ghidra.dbg.target.TargetRegister; import ghidra.dbg.target.TargetRegisterContainer; -import ghidra.dbg.util.PathUtils; import ghidra.program.model.data.PointerDataType; import ghidra.program.model.lang.*; public class DefaultDebuggerRegisterMapper implements DebuggerRegisterMapper { protected final Language language; protected final CompilerSpec cspec; - protected final TargetRegisterContainer targetRegContainer; + //protected final TargetRegisterContainer targetRegContainer; protected final boolean caseSensitive; protected final Map languageRegs = new LinkedHashMap<>(); @@ -41,7 +40,7 @@ public class DefaultDebuggerRegisterMapper implements DebuggerRegisterMapper { TargetRegisterContainer targetRegContainer, boolean caseSensitive) { this.language = cSpec.getLanguage(); this.cspec = cSpec; - this.targetRegContainer = targetRegContainer; + //this.targetRegContainer = targetRegContainer; this.caseSensitive = caseSensitive; this.instrCtrTypeInfo = new RegisterTypeInfo(PointerDataType.dataType, @@ -65,6 +64,16 @@ public class DefaultDebuggerRegisterMapper implements DebuggerRegisterMapper { } } + protected synchronized Register considerRegister(String index) { + String name = normalizeName(index); + Register lReg = filtLanguageRegs.get(name); + if (lReg == null) { + return null; + } + languageRegs.put(name, lReg); + return lReg; + } + protected synchronized Register considerRegister(TargetRegister tReg) { String name = normalizeName(tReg.getIndex()); Register lReg = filtLanguageRegs.get(name); @@ -132,17 +141,17 @@ public class DefaultDebuggerRegisterMapper implements DebuggerRegisterMapper { @Override public synchronized void targetRegisterAdded(TargetRegister register) { - if (!PathUtils.isAncestor(targetRegContainer.getPath(), register.getPath())) { - return; - } + //if (!PathUtils.isAncestor(targetRegContainer.getPath(), register.getPath())) { + // return; + //} considerRegister(register); } @Override public synchronized void targetRegisterRemoved(TargetRegister register) { - if (!PathUtils.isAncestor(targetRegContainer.getPath(), register.getPath())) { - return; - } + //if (!PathUtils.isAncestor(targetRegContainer.getPath(), register.getPath())) { + // return; + //} removeRegister(register); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/LargestSubDebuggerRegisterMapper.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/LargestSubDebuggerRegisterMapper.java index 42a9b51799..cf142bcff8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/LargestSubDebuggerRegisterMapper.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/mapping/LargestSubDebuggerRegisterMapper.java @@ -44,6 +44,19 @@ public class LargestSubDebuggerRegisterMapper extends DefaultDebuggerRegisterMap return true; } + @Override + protected synchronized Register considerRegister(String index) { + Register lReg = super.considerRegister(index); + if (lReg == null) { + return null; + } + //synchronized (present) { + present.computeIfAbsent(lReg.getBaseRegister(), r -> new TreeSet<>(LENGTH_COMPARATOR)) + .add(lReg); + //} + return lReg; + } + @Override protected synchronized Register considerRegister(TargetRegister tReg) { Register lReg = super.considerRegister(tReg); @@ -120,7 +133,10 @@ public class LargestSubDebuggerRegisterMapper extends DefaultDebuggerRegisterMap } Register lReg = languageRegs.get(normalizeName(tRegName)); if (lReg == null) { - return null; + lReg = considerRegister(tRegName); + if (lReg == null) { + return null; + } } Register lbReg = lReg.getBaseRegister(); TreeSet subs = present.get(lbReg); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgengX64DebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgengX64DebuggerMappingOpinion.java index 89cf7b31bf..cdd2b4446c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgengX64DebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/DbgengX64DebuggerMappingOpinion.java @@ -17,10 +17,8 @@ package ghidra.app.plugin.core.debug.platform; import java.util.Collection; import java.util.Set; -import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.DebugModelConventions; import ghidra.dbg.target.*; import ghidra.program.model.lang.*; @@ -57,8 +55,7 @@ public class DbgengX64DebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - protected DebuggerRegisterMapper createRegisterMapper( - TargetRegisterContainer registers) { + protected DebuggerRegisterMapper createRegisterMapper(TargetRegisterContainer registers) { return new DefaultDebuggerRegisterMapper(cSpec, registers, false); } } @@ -85,19 +82,7 @@ public class DbgengX64DebuggerMappingOpinion implements DebuggerMappingOpinion { } } - @Override - public CompletableFuture> getOffers(TargetObject target) { - if (!(target instanceof TargetProcess)) { - return CompletableFuture.completedFuture(Set.of()); - } - TargetProcess process = (TargetProcess) target; - CompletableFuture futureEnv = - DebugModelConventions.findSuitable(TargetEnvironment.class, target); - return futureEnv.thenApply(env -> offersForEnv(env, process)); - } - - protected Set offersForEnv(TargetEnvironment env, - TargetProcess process) { + public Set offersForEnv(TargetEnvironment env, TargetProcess process) { if (env == null || !env.getDebugger().toLowerCase().contains("dbg")) { return Set.of(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbArmDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbArmDebuggerMappingOpinion.java index 79616370b7..0d771e55d5 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbArmDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbArmDebuggerMappingOpinion.java @@ -16,22 +16,19 @@ package ghidra.app.plugin.core.debug.platform; import java.util.Set; -import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.mapping.DebuggerMappingOffer; import ghidra.app.plugin.core.debug.mapping.DebuggerMappingOpinion; -import ghidra.dbg.DebugModelConventions; -import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEnvironment; +import ghidra.dbg.target.TargetProcess; import ghidra.program.model.lang.CompilerSpecID; import ghidra.program.model.lang.LanguageID; public class GdbArmDebuggerMappingOpinion implements DebuggerMappingOpinion { protected static final LanguageID LANG_ID_ARM_LE_V8 = new LanguageID("ARM:LE:32:v8"); protected static final LanguageID LANG_ID_ARM_BE_V8 = new LanguageID("ARM:BE:32:v8"); - protected static final LanguageID LANG_ID_AARCH64_LE_V8A = - new LanguageID("AARCH64:LE:64:v8A"); - protected static final LanguageID LANG_ID_AARCH64_BE_V8A = - new LanguageID("AARCH64:BE:64:v8A"); + protected static final LanguageID LANG_ID_AARCH64_LE_V8A = new LanguageID("AARCH64:LE:64:v8A"); + protected static final LanguageID LANG_ID_AARCH64_BE_V8A = new LanguageID("AARCH64:BE:64:v8A"); protected static final CompilerSpecID COMP_ID_DEFAULT = new CompilerSpecID("default"); protected static class GdbArmLELinuxOffer extends AbstractGdbDebuggerMappingOffer { @@ -48,19 +45,7 @@ public class GdbArmDebuggerMappingOpinion implements DebuggerMappingOpinion { } } - @Override - public CompletableFuture> getOffers(TargetObject target) { - if (!(target instanceof TargetProcess)) { - return CompletableFuture.completedFuture(Set.of()); - } - TargetProcess process = (TargetProcess) target; - CompletableFuture futureEnv = - DebugModelConventions.findSuitable(TargetEnvironment.class, target); - return futureEnv.thenApply(env -> offersForEnv(env, process)); - } - - protected Set offersForEnv(TargetEnvironment env, - TargetProcess process) { + public Set offersForEnv(TargetEnvironment env, TargetProcess process) { if (!env.getDebugger().toLowerCase().contains("gdb")) { return Set.of(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbMipsDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbMipsDebuggerMappingOpinion.java index 779d111883..7328fa44cc 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbMipsDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbMipsDebuggerMappingOpinion.java @@ -16,12 +16,11 @@ package ghidra.app.plugin.core.debug.platform; import java.util.Set; -import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.mapping.DebuggerMappingOffer; import ghidra.app.plugin.core.debug.mapping.DebuggerMappingOpinion; -import ghidra.dbg.DebugModelConventions; -import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEnvironment; +import ghidra.dbg.target.TargetProcess; import ghidra.program.model.lang.CompilerSpecID; import ghidra.program.model.lang.LanguageID; @@ -77,16 +76,6 @@ public class GdbMipsDebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - public CompletableFuture> getOffers(TargetObject target) { - if (!(target instanceof TargetProcess)) { - return CompletableFuture.completedFuture(Set.of()); - } - TargetProcess process = (TargetProcess) target; - CompletableFuture futureEnv = - DebugModelConventions.findSuitable(TargetEnvironment.class, target); - return futureEnv.thenApply(env -> offersForEnv(env, process)); - } - public Set offersForEnv(TargetEnvironment env, TargetProcess process) { if (!env.getDebugger().toLowerCase().contains("gdb")) { return Set.of(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbPowerPCDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbPowerPCDebuggerMappingOpinion.java index c37e91f748..31f7a0d947 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbPowerPCDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbPowerPCDebuggerMappingOpinion.java @@ -16,12 +16,11 @@ package ghidra.app.plugin.core.debug.platform; import java.util.Set; -import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.mapping.DebuggerMappingOffer; import ghidra.app.plugin.core.debug.mapping.DebuggerMappingOpinion; -import ghidra.dbg.DebugModelConventions; -import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEnvironment; +import ghidra.dbg.target.TargetProcess; import ghidra.program.model.lang.CompilerSpecID; import ghidra.program.model.lang.LanguageID; @@ -63,16 +62,6 @@ public class GdbPowerPCDebuggerMappingOpinion implements DebuggerMappingOpinion } @Override - public CompletableFuture> getOffers(TargetObject target) { - if (!(target instanceof TargetProcess)) { - return CompletableFuture.completedFuture(Set.of()); - } - TargetProcess process = (TargetProcess) target; - CompletableFuture futureEnv = - DebugModelConventions.findSuitable(TargetEnvironment.class, target); - return futureEnv.thenApply(env -> offersForEnv(env, process)); - } - public Set offersForEnv(TargetEnvironment env, TargetProcess process) { if (!env.getDebugger().toLowerCase().contains("gdb")) { return Set.of(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbX86DebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbX86DebuggerMappingOpinion.java index 170f604caf..b5337c2fca 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbX86DebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbX86DebuggerMappingOpinion.java @@ -16,10 +16,8 @@ package ghidra.app.plugin.core.debug.platform; import java.util.Set; -import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.DebugModelConventions; import ghidra.dbg.target.*; import ghidra.program.model.lang.*; import ghidra.util.Msg; @@ -114,19 +112,7 @@ public class GdbX86DebuggerMappingOpinion implements DebuggerMappingOpinion { } } - @Override - public CompletableFuture> getOffers(TargetObject target) { - if (!(target instanceof TargetProcess)) { - return CompletableFuture.completedFuture(Set.of()); - } - TargetProcess process = (TargetProcess) target; - CompletableFuture futureEnv = - DebugModelConventions.findSuitable(TargetEnvironment.class, target); - return futureEnv.thenApply(env -> offersForEnv(env, process)); - } - - protected Set offersForEnv(TargetEnvironment env, - TargetProcess process) { + public Set offersForEnv(TargetEnvironment env, TargetProcess process) { if (!env.getDebugger().toLowerCase().contains("gdb")) { return Set.of(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/JdiDalvikDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/JdiDalvikDebuggerMappingOpinion.java index 3a9e9583ca..b244f23af8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/JdiDalvikDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/JdiDalvikDebuggerMappingOpinion.java @@ -17,10 +17,8 @@ package ghidra.app.plugin.core.debug.platform; import java.util.Collection; import java.util.Set; -import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.DebugModelConventions; import ghidra.dbg.target.*; import ghidra.program.model.lang.*; @@ -44,8 +42,7 @@ public class JdiDalvikDebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - protected DebuggerRegisterMapper createRegisterMapper( - TargetRegisterContainer registers) { + protected DebuggerRegisterMapper createRegisterMapper(TargetRegisterContainer registers) { return new DefaultDebuggerRegisterMapper(cSpec, registers, false); } } @@ -66,23 +63,11 @@ public class JdiDalvikDebuggerMappingOpinion implements DebuggerMappingOpinion { } } - @Override - public CompletableFuture> getOffers(TargetObject target) { - if (!(target instanceof TargetProcess)) { - return CompletableFuture.completedFuture(Set.of()); - } - TargetProcess process = (TargetProcess) target; - CompletableFuture futureEnv = - DebugModelConventions.findSuitable(TargetEnvironment.class, target); - return futureEnv.thenApply(env -> offersForEnv(env, process)); - } - protected static boolean containsRecognizedJvmName(String name) { return DALVIK_VM_NAMES.stream().anyMatch(name::contains); } - protected Set offersForEnv(TargetEnvironment env, - TargetProcess process) { + public Set offersForEnv(TargetEnvironment env, TargetProcess process) { if (!env.getDebugger().contains("Java Debug Interface")) { return Set.of(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/JdiJavaDebuggerMappingOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/JdiJavaDebuggerMappingOpinion.java index dcd98a14a9..4f47ec4392 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/JdiJavaDebuggerMappingOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/JdiJavaDebuggerMappingOpinion.java @@ -17,10 +17,8 @@ package ghidra.app.plugin.core.debug.platform; import java.util.Collection; import java.util.Set; -import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.mapping.*; -import ghidra.dbg.DebugModelConventions; import ghidra.dbg.target.*; import ghidra.program.model.lang.*; @@ -44,8 +42,7 @@ public class JdiJavaDebuggerMappingOpinion implements DebuggerMappingOpinion { } @Override - protected DebuggerRegisterMapper createRegisterMapper( - TargetRegisterContainer registers) { + protected DebuggerRegisterMapper createRegisterMapper(TargetRegisterContainer registers) { return new DefaultDebuggerRegisterMapper(cSpec, registers, false); } } @@ -66,23 +63,11 @@ public class JdiJavaDebuggerMappingOpinion implements DebuggerMappingOpinion { } } - @Override - public CompletableFuture> getOffers(TargetObject target) { - if (!(target instanceof TargetProcess)) { - return CompletableFuture.completedFuture(Set.of()); - } - TargetProcess process = (TargetProcess) target; - CompletableFuture futureEnv = - DebugModelConventions.findSuitable(TargetEnvironment.class, target); - return futureEnv.thenApply(env -> offersForEnv(env, process)); - } - protected static boolean containsRecognizedJvmName(String name) { return JVM_NAMES.stream().anyMatch(name::contains); } - protected Set offersForEnv(TargetEnvironment env, - TargetProcess process) { + public Set offersForEnv(TargetEnvironment env, TargetProcess process) { if (!env.getDebugger().contains("Java Debug Interface")) { return Set.of(); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LogicalBreakpointInternal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LogicalBreakpointInternal.java index 911d2cdfc0..b39a73e5e1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LogicalBreakpointInternal.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/LogicalBreakpointInternal.java @@ -369,7 +369,7 @@ interface LogicalBreakpointInternal extends LogicalBreakpoint { Set tKinds = TraceRecorder.traceToTargetBreakpointKinds(kinds); - for (TargetBreakpointContainer cont : recorder + for (TargetBreakpointSpecContainer cont : recorder .collectBreakpointContainers(null)) { LinkedHashSet supKinds = new LinkedHashSet<>(tKinds); supKinds.retainAll(cont.getSupportedBreakpointKinds()); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceBreakpointActionItem.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceBreakpointActionItem.java index b315b0e5a1..76c2c9962b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceBreakpointActionItem.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/breakpoint/PlaceBreakpointActionItem.java @@ -18,17 +18,17 @@ package ghidra.app.plugin.core.debug.service.breakpoint; import java.util.*; import java.util.concurrent.CompletableFuture; -import ghidra.dbg.target.TargetBreakpointContainer; +import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.program.model.address.*; public class PlaceBreakpointActionItem implements BreakpointActionItem { - private final TargetBreakpointContainer container; + private final TargetBreakpointSpecContainer container; private final Address address; private final long length; private final Set kinds; - public PlaceBreakpointActionItem(TargetBreakpointContainer container, Address address, + public PlaceBreakpointActionItem(TargetBreakpointSpecContainer container, Address address, long length, Collection kinds) { this.container = Objects.requireNonNull(container); this.address = Objects.requireNonNull(address); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceInternal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceInternal.java index 75b62c9293..3548cecfcb 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceInternal.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceInternal.java @@ -17,7 +17,6 @@ package ghidra.app.plugin.core.debug.service.model; import java.io.IOException; import java.util.Collection; -import java.util.concurrent.CompletableFuture; import ghidra.app.plugin.core.debug.event.*; import ghidra.app.plugin.core.debug.mapping.DebuggerTargetTraceMapper; @@ -133,5 +132,5 @@ public interface DebuggerModelServiceInternal extends DebuggerModelService { * @param target the target to record * @return a future which completes with the resulting recorder, unless cancelled */ - CompletableFuture doRecordTargetPromptOffers(PluginTool t, TargetObject target); + TraceRecorder doRecordTargetPromptOffers(PluginTool t, TargetObject target); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java index c9e42d1c90..2150f8f613 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java @@ -15,9 +15,10 @@ */ package ghidra.app.plugin.core.debug.service.model; -import static ghidra.app.plugin.core.debug.gui.DebuggerResources.showError; +import static ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.nio.CharBuffer; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -33,15 +34,12 @@ import docking.ActionContext; import docking.action.DockingAction; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; -import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.DisconnectAllAction; import ghidra.app.plugin.core.debug.mapping.*; import ghidra.app.services.*; import ghidra.async.AsyncFence; import ghidra.dbg.*; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener; -import ghidra.dbg.target.TargetObject.TargetObjectListener; import ghidra.dbg.util.PathUtils; import ghidra.framework.main.AppInfo; import ghidra.framework.main.FrontEndOnly; @@ -59,17 +57,8 @@ import ghidra.util.classfinder.ClassSearcher; import ghidra.util.datastruct.CollectionChangeListener; import ghidra.util.datastruct.ListenerSet; -@PluginInfo( - shortDescription = "Debugger models manager service", - description = "Manage debug sessions, connections, and trace recording", - category = PluginCategoryNames.DEBUGGER, - packageName = DebuggerPluginPackage.NAME, - status = PluginStatus.HIDDEN, - servicesRequired = { - }, - servicesProvided = { - DebuggerModelService.class, - }) +@PluginInfo(shortDescription = "Debugger models manager service", description = "Manage debug sessions, connections, and trace recording", category = PluginCategoryNames.DEBUGGER, packageName = DebuggerPluginPackage.NAME, status = PluginStatus.HIDDEN, servicesRequired = {}, servicesProvided = { + DebuggerModelService.class, }) public class DebuggerModelServicePlugin extends Plugin implements DebuggerModelServiceInternal, FrontEndOnly { @@ -83,7 +72,7 @@ public class DebuggerModelServicePlugin extends Plugin protected TargetObject root; protected TargetFocusScope focusScope; - protected TargetObjectListener forRemoval = new TargetObjectListener() { + protected DebuggerModelListener forRemoval = new DebuggerModelListener() { @Override public void invalidated(TargetObject object, TargetObject branch, String reason) { synchronized (listenersByModel) { @@ -99,19 +88,20 @@ public class DebuggerModelServicePlugin extends Plugin } }; - protected TargetFocusScopeListener forFocus = new TargetFocusScopeListener() { - @Override - public void focusChanged(TargetFocusScope object, TargetObject focused) { - fireFocusEvent(focused); - List copy; - synchronized (proxies) { - copy = List.copyOf(proxies); + protected DebuggerModelListener forFocus = + new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + @AttributeCallback(TargetFocusScope.FOCUS_ATTRIBUTE_NAME) + public void focusChanged(TargetObject object, TargetObject focused) { + fireFocusEvent(focused); + List copy; + synchronized (proxies) { + copy = List.copyOf(proxies); + } + for (DebuggerModelServiceProxyPlugin proxy : copy) { + proxy.fireFocusEvent(focused); + } } - for (DebuggerModelServiceProxyPlugin proxy : copy) { - proxy.fireFocusEvent(focused); - } - } - }; + }; protected ListenersForRemovalAndFocus(DebuggerObjectModel model) { this.model = model; @@ -344,6 +334,10 @@ public class DebuggerModelServicePlugin extends Plugin } recorder = doBeginRecording(target, mapper); recorder.addListener(listenerOnRecorders); + recorder.init().exceptionally(e -> { + Msg.showError(this, null, "Record Trace", "Error initializing recorder", e); + return null; + }); recordersByTarget.put(target, recorder); } recorderListeners.fire.elementAdded(recorder); @@ -355,74 +349,64 @@ public class DebuggerModelServicePlugin extends Plugin } @Override - public CompletableFuture recordTargetBestOffer(TargetObject target) { + public TraceRecorder recordTargetBestOffer(TargetObject target) { synchronized (recordersByTarget) { TraceRecorder recorder = recordersByTarget.get(target); if (recorder != null) { Msg.warn(this, "Target is already being recorded: " + target); - return CompletableFuture.completedFuture(recorder); + return recorder; } } - return DebuggerMappingOpinion.queryOpinions(target).thenApply(offers -> { - DebuggerTargetTraceMapper mapper = DebuggerMappingOffer.first(offers); - if (mapper == null) { - throw new NoSuchElementException("No mapper for target: " + target); - } - try { - return recordTarget(target, mapper); - } - catch (IOException e) { - throw new AssertionError("Could not record target: " + target, e); - } - }).exceptionally(ex -> { - Msg.error(this, "Could not query trace-recording opinions", ex); - return null; - }); + DebuggerTargetTraceMapper mapper = + DebuggerMappingOffer.first(DebuggerMappingOpinion.queryOpinions(target)); + if (mapper == null) { + throw new NoSuchElementException("No mapper for target: " + target); + } + try { + return recordTarget(target, mapper); + } + catch (IOException e) { + throw new AssertionError("Could not record target: " + target, e); + } } @Override @Internal - public CompletableFuture doRecordTargetPromptOffers(PluginTool t, - TargetObject target) { + public TraceRecorder doRecordTargetPromptOffers(PluginTool t, TargetObject target) { synchronized (recordersByTarget) { TraceRecorder recorder = recordersByTarget.get(target); if (recorder != null) { Msg.warn(this, "Target is already being recorded: " + target); - return CompletableFuture.completedFuture(recorder); + return recorder; } } - return DebuggerMappingOpinion.queryOpinions(target).thenApply(offers -> { - DebuggerMappingOffer selected; - if (offers.size() == 1) { - selected = offers.get(0); + List offers = DebuggerMappingOpinion.queryOpinions(target); + DebuggerMappingOffer selected; + if (offers.size() == 1) { + selected = offers.get(0); + } + else { + offerDialog.setOffers(offers); + t.showDialog(offerDialog); + // TODO: Is cancelled? + if (offerDialog.isCancelled()) { + return null; } - else { - offerDialog.setOffers(offers); - t.showDialog(offerDialog); - // TODO: Is cancelled? - if (offerDialog.isCancelled()) { - return null; - } - selected = offerDialog.getSelectedOffer(); - } - assert selected != null; - DebuggerTargetTraceMapper mapper = selected.take(); - try { - return recordTarget(target, mapper); - } - catch (IOException e) { - throw new AssertionError("Could not record target: " + target, e); - // TODO: For certain errors, It may not be appropriate to close the dialog. - } - }).exceptionally(ex -> { - Msg.showError(this, null, DebuggerResources.AbstractRecordAction.NAME, - "Could not query trace-recording opinions", ex); - return null; - }); + selected = offerDialog.getSelectedOffer(); + } + assert selected != null; + DebuggerTargetTraceMapper mapper = selected.take(); + try { + return recordTarget(target, mapper); + } + catch (IOException e) { + throw new AssertionError("Could not record target: " + target, e); + // TODO: For certain errors, It may not be appropriate to close the dialog. + } } @Override - public CompletableFuture recordTargetPromptOffers(TargetObject target) { + public TraceRecorder recordTargetPromptOffers(TargetObject target) { return doRecordTargetPromptOffers(tool, target); } @@ -546,10 +530,6 @@ public class DebuggerModelServicePlugin extends Plugin //DefaultTraceRecorder recorder = new DefaultTraceRecorder(this, trace, target, mapper); TraceRecorder recorder = mapper.startRecording(this, trace); trace.release(this); // The recorder now owns it (on behalf of the service) - recorder.init().exceptionally(e -> { - Msg.showError(this, null, "Record Trace", "Error initializing recorder", e); - return null; - }); return recorder; } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java index a587b51861..26e8f222ff 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java @@ -48,21 +48,20 @@ import ghidra.util.datastruct.ListenerSet; import ghidra.util.task.TaskMonitor; @PluginInfo( // - shortDescription = "Debugger models manager service (proxy to front-end)", // - description = "Manage debug sessions, connections, and trace recording", // - category = PluginCategoryNames.DEBUGGER, // - packageName = DebuggerPluginPackage.NAME, // - status = PluginStatus.RELEASED, // - eventsConsumed = { - ProgramActivatedPluginEvent.class, // - ProgramClosedPluginEvent.class, // - }, // - servicesRequired = { // - DebuggerTraceManagerService.class, // - }, // - servicesProvided = { // - DebuggerModelService.class, // - } // + shortDescription = "Debugger models manager service (proxy to front-end)", // + description = "Manage debug sessions, connections, and trace recording", // + category = PluginCategoryNames.DEBUGGER, // + packageName = DebuggerPluginPackage.NAME, // + status = PluginStatus.RELEASED, // + eventsConsumed = { ProgramActivatedPluginEvent.class, // + ProgramClosedPluginEvent.class, // + }, // + servicesRequired = { // + DebuggerTraceManagerService.class, // + }, // + servicesProvided = { // + DebuggerModelService.class, // + } // ) public class DebuggerModelServiceProxyPlugin extends Plugin implements DebuggerModelServiceInternal { @@ -362,18 +361,17 @@ public class DebuggerModelServiceProxyPlugin extends Plugin } @Override - public CompletableFuture recordTargetBestOffer(TargetObject target) { + public TraceRecorder recordTargetBestOffer(TargetObject target) { return delegate.recordTargetBestOffer(target); } @Override - public CompletableFuture doRecordTargetPromptOffers(PluginTool t, - TargetObject target) { + public TraceRecorder doRecordTargetPromptOffers(PluginTool t, TargetObject target) { return delegate.doRecordTargetPromptOffers(t, target); } @Override - public CompletableFuture recordTargetPromptOffers(TargetObject target) { + public TraceRecorder recordTargetPromptOffers(TargetObject target) { return doRecordTargetPromptOffers(tool, target); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultBreakpointRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultBreakpointRecorder.java new file mode 100644 index 0000000000..588145cf95 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultBreakpointRecorder.java @@ -0,0 +1,204 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.Set; +import java.util.concurrent.Executors; + +import ghidra.app.plugin.core.debug.service.model.interfaces.ManagedBreakpointRecorder; +import ghidra.app.services.TraceRecorder; +import ghidra.dbg.target.*; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.*; +import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.*; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.Msg; +import ghidra.util.exception.DuplicateNameException; + +public class DefaultBreakpointRecorder implements ManagedBreakpointRecorder { + + public static AddressRange range(Address min, Integer length) { + if (length == null) { + length = 1; + } + try { + return new AddressRangeImpl(min, length); + } + catch (AddressOverflowException e) { + throw new AssertionError(e); + } + } + + protected static String nameBreakpoint(TargetBreakpointLocation bpt) { + if (bpt instanceof TargetBreakpointSpec) { + return bpt.getIndex(); + } + return bpt.getSpecification().getIndex() + "." + bpt.getIndex(); + } + + private final DefaultTraceRecorder recorder; + private final Trace trace; + private final TraceBreakpointManager breakpointManager; + final PermanentTransactionExecutor tx; + + protected TargetBreakpointSpecContainer breakpointContainer; + + public DefaultBreakpointRecorder(DefaultTraceRecorder recorder) { + this.recorder = recorder; + this.trace = recorder.getTrace(); + this.breakpointManager = trace.getBreakpointManager(); + this.tx = new PermanentTransactionExecutor(trace, + "BreakpointRecorder:" + recorder.target.getJoinedPath("."), + Executors::newCachedThreadPool, 100); + } + + @Override + public void offerBreakpointContainer(TargetBreakpointSpecContainer bc) { + if (breakpointContainer != null) { + Msg.warn(this, "Already have a breakpoint container for this process"); + } + breakpointContainer = bc; + } + + @Override + public void offerBreakpointLocation(TargetObject containerParent, + TargetBreakpointLocation bpt) { + synchronized (this) { + if (recorder.getMemoryMapper() == null) { + return; + } + } + RecorderBreakpointLocationResolver resolver = + new RecorderBreakpointLocationResolver(recorder, bpt); + resolver.updateBreakpoint(containerParent, bpt); + } + + protected void doRecordBreakpoint(long snap, TargetBreakpointLocation loc, + Set traceThreads) { + synchronized (this) { + if (recorder.getMemoryMapper() == null) { + throw new IllegalStateException( + "No memory mapper! Have not recorded a region, yet."); + } + } + String path = PathUtils.toString(loc.getPath()); + String name = nameBreakpoint(loc); + Address traceAddr = recorder.getMemoryMapper().targetToTrace(loc.getAddress()); + AddressRange traceRange = range(traceAddr, loc.getLength()); + try { + TargetBreakpointSpec spec = loc.getSpecification(); + boolean enabled = spec.isEnabled(); + Set traceKinds = + TraceRecorder.targetToTraceBreakpointKinds(spec.getKinds()); + TraceBreakpoint traceBpt = + breakpointManager.placeBreakpoint(path, snap, + traceRange, traceThreads, traceKinds, enabled, spec.getExpression()); + traceBpt.setName(name); + } + catch (DuplicateNameException e) { + Msg.error(this, "Could not record placed breakpoint: " + e); + } + } + + @Override + public void recordBreakpoint(TargetBreakpointLocation loc, + Set traceThreads) { + String path = loc.getJoinedPath("."); + long snap = recorder.getSnap(); + tx.execute("Breakpoint " + path + " placed", () -> { + doRecordBreakpoint(snap, loc, traceThreads); + }); + } + + protected void doRemoveBreakpointLocation(long snap, TargetBreakpointLocation loc) { + String path = loc.getJoinedPath("."); + for (TraceBreakpoint traceBpt : breakpointManager.getBreakpointsByPath(path)) { + try { + if (traceBpt.getPlacedSnap() > snap) { + Msg.error(this, + "Tracked, now removed breakpoint was placed in the future? " + path); + } + else if (traceBpt.getPlacedSnap() == snap) { + // TODO: I forget if this is allowed for DBTrace iteration + traceBpt.delete(); + } + else { + traceBpt.setClearedSnap(snap - 1); + } + } + catch (DuplicateNameException e) { + Msg.error(this, "Could not record breakpoint removal: " + e); + } + } + } + + @Override + public void removeBreakpointLocation(TargetBreakpointLocation loc) { + String path = loc.getJoinedPath("."); + long snap = recorder.getSnap(); + tx.execute("Breakpoint " + path + " deleted", () -> { + doRemoveBreakpointLocation(snap, loc); + }); + } + + protected void doBreakpointLengthChanged(long snap, int length, Address traceAddr, + String path) { + for (TraceBreakpoint traceBpt : breakpointManager.getBreakpointsByPath(path)) { + if (traceBpt.getLength() == length) { + continue; // Nothing to change + } + // TODO: Verify all other attributes match? + // TODO: Should this be allowed to happen? + try { + if (traceBpt.getPlacedSnap() == snap) { + traceBpt.delete(); + } + else { + traceBpt.setClearedSnap(snap - 1); + } + breakpointManager.placeBreakpoint(path, snap, range(traceAddr, length), + traceBpt.getThreads(), traceBpt.getKinds(), traceBpt.isEnabled(), + traceBpt.getComment()); + } + catch (DuplicateNameException e) { + // Split, and length matters not + Msg.error(this, "Could not record breakpoint length change: " + e); + } + } + } + + @Override + public void breakpointLengthChanged(int length, Address traceAddr, String path) + throws AssertionError { + long snap = recorder.getSnap(); + tx.execute("Breakpoint length changed", () -> { + doBreakpointLengthChanged(snap, length, traceAddr, path); + }); + } + + @Override + public TraceBreakpoint getTraceBreakpoint(TargetBreakpointLocation bpt) { + String path = PathUtils.toString(bpt.getPath()); + return breakpointManager.getPlacedBreakpointByPath(recorder.getSnap(), path); + } + + @Override + public TargetBreakpointSpecContainer getBreakpointContainer() { + return breakpointContainer; + } + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultDataTypeRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultDataTypeRecorder.java new file mode 100644 index 0000000000..cc9e6bc2a3 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultDataTypeRecorder.java @@ -0,0 +1,88 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import ghidra.async.AsyncFence; +import ghidra.async.AsyncUtils; +import ghidra.dbg.target.*; +import ghidra.dbg.util.PathUtils; +import ghidra.dbg.util.TargetDataTypeConverter; +import ghidra.program.model.data.*; +import ghidra.trace.model.Trace; +import ghidra.util.task.TaskMonitor; + +public class DefaultDataTypeRecorder { + + //private DefaultTraceRecorder recorder; + private Trace trace; + private final TargetDataTypeConverter typeConverter; + + public DefaultDataTypeRecorder(DefaultTraceRecorder recorder) { + //this.recorder = recorder; + this.trace = recorder.getTrace(); + this.typeConverter = new TargetDataTypeConverter(trace.getDataTypeManager()); + } + + public CompletableFuture captureDataTypes(TargetDataTypeNamespace namespace, + TaskMonitor monitor) { + String path = PathUtils.toString(namespace.getPath()); + monitor.setMessage("Capturing data types for " + path); + return namespace.getTypes().thenCompose(types -> { + monitor.initialize(types.size()); + AsyncFence fence = new AsyncFence(); + List converted = new ArrayList<>(); + for (TargetNamedDataType type : types) { + if (monitor.isCancelled()) { + fence.ready().cancel(false); + return AsyncUtils.nil(); + } + monitor.incrementProgress(1); + fence.include(typeConverter.convertTargetDataType(type).thenAccept(converted::add)); + } + return fence.ready().thenApply(__ -> converted); + }).thenAccept(converted -> { + if (converted == null) { + return; + } + try (RecorderPermanentTransaction tid = + RecorderPermanentTransaction.start(trace, "Capture data types for " + path)) { + // NOTE: createCategory is actually getOrCreate + Category category = + trace.getDataTypeManager().createCategory(new CategoryPath("/" + path)); + for (DataType dataType : converted) { + category.addDataType(dataType, DataTypeConflictHandler.DEFAULT_HANDLER); + } + } + }); + } + + public CompletableFuture captureDataTypes(TargetModule targetModule, + TaskMonitor monitor) { + CompletableFuture> future = + targetModule.fetchChildrenSupporting(TargetDataTypeNamespace.class); + // NOTE: I should expect exactly one namespace... + return future.thenCompose(namespaces -> { + AsyncFence fence = new AsyncFence(); + for (TargetDataTypeNamespace ns : namespaces.values()) { + fence.include(captureDataTypes(ns, monitor)); + } + return fence.ready(); + }); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultMemoryRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultMemoryRecorder.java new file mode 100644 index 0000000000..cc9911ca9d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultMemoryRecorder.java @@ -0,0 +1,174 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.Collection; +import java.util.HashSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; + +import com.google.common.collect.Range; + +import ghidra.app.plugin.core.debug.service.model.interfaces.ManagedMemoryRecorder; +import ghidra.async.AsyncUtils; +import ghidra.async.TypeSpec; +import ghidra.dbg.target.TargetMemory; +import ghidra.dbg.target.TargetMemoryRegion; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.*; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.*; +import ghidra.util.Msg; +import ghidra.util.exception.DuplicateNameException; +import ghidra.util.task.TaskMonitor; + +public class DefaultMemoryRecorder implements ManagedMemoryRecorder { + + // For large memory captures + private static final int BLOCK_SIZE = 4096; + private static final long BLOCK_MASK = -1L << 12; + + protected static AddressSetView expandToBlocks(AddressSetView asv) { + AddressSet result = new AddressSet(); + // Not terribly efficient, but this is one range most of the time + for (AddressRange range : asv) { + AddressSpace space = range.getAddressSpace(); + Address min = space.getAddress(range.getMinAddress().getOffset() & BLOCK_MASK); + Address max = space.getAddress(range.getMaxAddress().getOffset() | ~BLOCK_MASK); + result.add(new AddressRangeImpl(min, max)); + } + return result; + } + + private final DefaultTraceRecorder recorder; + private final Trace trace; + private final TraceMemoryManager memoryManager; + final PermanentTransactionExecutor tx; + + public DefaultMemoryRecorder(DefaultTraceRecorder recorder) { + this.recorder = recorder; + this.trace = recorder.getTrace(); + this.memoryManager = trace.getMemoryManager(); + this.tx = new PermanentTransactionExecutor(trace, + "MemoryRecorder:" + recorder.target.getJoinedPath("."), + Executors::newSingleThreadExecutor, 100); + } + + public CompletableFuture captureProcessMemory(AddressSetView set, TaskMonitor monitor) { + // TODO: Figure out how to display/select per-thread memory. + // Probably need a thread parameter passed in then? + // NOTE: That thread memory will already be chained to process memory. Good. + + int total = 0; + AddressSetView expSet = expandToBlocks(set) + .intersect(trace.getMemoryManager().getRegionsAddressSet(recorder.getSnap())); + for (AddressRange r : expSet) { + total += Long.divideUnsigned(r.getLength() + BLOCK_SIZE - 1, BLOCK_SIZE); + } + monitor.initialize(total); + monitor.setMessage("Capturing memory"); + // TODO: Read blocks in parallel? Probably NO. Tends to overload the agent. + return AsyncUtils.each(TypeSpec.VOID, expSet.iterator(), (r, loop) -> { + AddressRangeChunker it = new AddressRangeChunker(r, BLOCK_SIZE); + AsyncUtils.each(TypeSpec.VOID, it.iterator(), (vRng, inner) -> { + // The listener in the recorder will copy to the Trace. + monitor.incrementProgress(1); + AddressRange tRng = recorder.getMemoryMapper().traceToTarget(vRng); + recorder.getProcessMemory() + .readMemory(tRng.getMinAddress(), (int) tRng.getLength()) + .thenApply(b -> !monitor.isCancelled()) + .handle(inner::repeatWhile); + }).exceptionally(e -> { + Msg.error(this, "Error reading range " + r + ": " + e); + // NOTE: Above may double log, since recorder listens for errors, too + return null; // Continue looping on errors + }).thenApply(v -> !monitor.isCancelled()).handle(loop::repeatWhile); + }); + } + + @Override + public void offerProcessRegion(TargetMemoryRegion region) { + TargetMemory mem = region.getMemory(); + recorder.getProcessMemory().addRegion(region, mem); + //recorder.objectManager.addMemory(mem); + String path = PathUtils.toString(region.getPath()); + long snap = recorder.getSnap(); + tx.execute("Memory region " + path + " added", () -> { + try { + TraceMemoryRegion traceRegion = + memoryManager.getLiveRegionByPath(snap, path); + if (traceRegion != null) { + Msg.warn(this, "Region " + path + " already recorded"); + return; + } + traceRegion = memoryManager.addRegion(path, Range.atLeast(snap), + recorder.getMemoryMapper().targetToTrace(region.getRange()), + getTraceFlags(region)); + traceRegion.setName(region.getName()); + } + catch (TraceOverlappedRegionException e) { + Msg.error(this, "Failed to create region due to overlap: " + e); + } + catch (DuplicateNameException e) { + Msg.error(this, "Failed to create region due to duplicate: " + e); + } + }); + + } + + @Override + public void removeProcessRegion(TargetMemoryRegion region) { + // Already removed from processMemory. That's how we knew to go here. + String path = PathUtils.toString(region.getPath()); + long snap = recorder.getSnap(); + tx.execute("Memory region " + path + " removed", () -> { + try { + TraceMemoryRegion traceRegion = memoryManager.getLiveRegionByPath(snap, path); + if (traceRegion == null) { + Msg.warn(this, "Could not find region " + path + " in trace to remove"); + return; + } + traceRegion.setDestructionSnap(snap - 1); + } + catch (DuplicateNameException | TraceOverlappedRegionException e) { + // Region is shrinking in time + Msg.error(this, "Failed to record region removal: " + e); + } + }); + } + + @Override + public TraceMemoryRegion getTraceMemoryRegion(TargetMemoryRegion region) { + String path = PathUtils.toString(region.getPath()); + return memoryManager.getLiveRegionByPath(recorder.getSnap(), path); + } + + public Collection getTraceFlags(TargetMemoryRegion region) { + Collection flags = new HashSet<>(); + if (region.isReadable()) { + flags.add(TraceMemoryFlag.READ); + } + if (region.isWritable()) { + flags.add(TraceMemoryFlag.WRITE); + } + if (region.isExecutable()) { + flags.add(TraceMemoryFlag.EXECUTE); + } + // TODO: Volatile? Can any debugger report that? + return flags; + } + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultModuleRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultModuleRecorder.java new file mode 100644 index 0000000000..24a9be9e67 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultModuleRecorder.java @@ -0,0 +1,153 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.concurrent.Executors; + +import ghidra.app.plugin.core.debug.service.model.interfaces.ManagedModuleRecorder; +import ghidra.dbg.target.TargetModule; +import ghidra.dbg.target.TargetSection; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.AddressRange; +import ghidra.trace.model.Trace; +import ghidra.trace.model.modules.*; +import ghidra.util.Msg; +import ghidra.util.exception.DuplicateNameException; + +public class DefaultModuleRecorder implements ManagedModuleRecorder { + + private final DefaultTraceRecorder recorder; + private final Trace trace; + private final TraceModuleManager moduleManager; + final PermanentTransactionExecutor tx; + + public DefaultModuleRecorder(DefaultTraceRecorder recorder) { + this.recorder = recorder; + this.trace = recorder.getTrace(); + this.moduleManager = trace.getModuleManager(); + this.tx = new PermanentTransactionExecutor(trace, + "ModuleRecorder:" + recorder.target.getJoinedPath("."), + f -> Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), f), 500); + } + + protected TraceModule doRecordProcessModule(long snap, TargetModule module) { + String path = module.getJoinedPath("."); + if (recorder.getMemoryMapper() == null) { + Msg.error(this, "Got module before memory mapper: " + path); + return null; + } + + // Short-circuit the DuplicateNameException for efficiency? + TraceModule exists = moduleManager.getLoadedModuleByPath(snap, path); + if (exists != null) { + return exists; + } + + try { + AddressRange targetRange = module.getRange(); + if (targetRange == null) { + Msg.error(this, "Range not found for " + module); + return null; + } + AddressRange traceRange = recorder.getMemoryMapper().targetToTrace(targetRange); + return moduleManager.addLoadedModule(path, module.getModuleName(), traceRange, snap); + } + catch (DuplicateNameException e) { + // This resolves the race condition, since DB access is synchronized + return moduleManager.getLoadedModuleByPath(snap, path); + } + } + + @Override + public void offerProcessModule(TargetModule module) { + long snap = recorder.getSnap(); + String path = module.getJoinedPath("."); + tx.execute("Module " + path + " loaded", () -> { + doRecordProcessModule(snap, module); + }); + } + + protected TraceSection doRecordProcessModuleSection(long snap, TargetSection section) { + String path = section.getJoinedPath("."); + if (recorder.getMemoryMapper() == null) { + Msg.error(this, "Got module section before memory mapper: " + path); + return null; + } + TraceModule traceModule = doRecordProcessModule(snap, section.getModule()); + if (traceModule == null) { + return null; // Failure should already be logged + } + try { + AddressRange targetRange = section.getRange(); + AddressRange traceRange = recorder.getMemoryMapper().targetToTrace(targetRange); + return traceModule.addSection(path, section.getIndex(), traceRange); + } + catch (DuplicateNameException e) { + Msg.warn(this, path + " already recorded"); + return moduleManager.getLoadedSectionByPath(snap, path); + } + } + + @Override + public void offerProcessModuleSection(TargetSection section) { + long snap = recorder.getSnap(); + String path = section.getJoinedPath("."); + tx.execute("Section " + path + " added", () -> { + doRecordProcessModuleSection(snap, section); + }); + } + + protected void doRemoveProcessModule(long snap, TargetModule module) { + String path = PathUtils.toString(module.getPath()); + //TraceThread eventThread = recorder.getSnapshot().getEventThread(); + TraceModule traceModule = moduleManager.getLoadedModuleByPath(snap, path); + if (traceModule == null) { + Msg.warn(this, "unloaded " + path + " is not in the trace"); + return; + } + try { + if (traceModule.getLoadedSnap() == snap) { + Msg.warn(this, "Observed module unload in the same snap as its load"); + //recorder.createSnapshot("WARN: Module removed", eventThread, tid); + } + traceModule.setUnloadedSnap(snap); + } + catch (DuplicateNameException e) { + Msg.error(this, "Could not record process module removed: " + e); + } + } + + @Override + public void removeProcessModule(TargetModule module) { + long snap = recorder.getSnap(); + String path = PathUtils.toString(module.getPath()); + tx.execute("Module " + path + " unloaded", () -> { + doRemoveProcessModule(snap, module); + }); + } + + @Override + public TraceModule getTraceModule(TargetModule module) { + String path = PathUtils.toString(module.getPath()); + return moduleManager.getLoadedModuleByPath(recorder.getSnap(), path); + } + + @Override + public TraceSection getTraceSection(TargetSection section) { + String path = PathUtils.toString(section.getPath()); + return moduleManager.getLoadedSectionByPath(recorder.getSnap(), path); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultProcessRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultProcessRecorder.java new file mode 100644 index 0000000000..0177c7ee6e --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultProcessRecorder.java @@ -0,0 +1,93 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.concurrent.CompletableFuture; + +import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper; +import ghidra.app.plugin.core.debug.service.model.interfaces.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.trace.model.Trace; +import ghidra.util.TriConsumer; + +public class DefaultProcessRecorder implements ManagedProcessRecorder { + + private final AbstractRecorderMemory processMemory; + protected final TriConsumer listenerProcMemAccChanged = + this::processMemoryAccessibilityChanged; + + private DefaultBreakpointRecorder breakpointRecorder; + private DefaultTraceRecorder recorder; + + public DefaultProcessRecorder(DefaultTraceRecorder recorder) { + this.recorder = recorder; + this.processMemory = new RecorderSimpleMemory(); + //this.processMemory = new RecorderComposedMemory(this.getProcessMemory()); + + //getProcessMemory().getMemAccListeners().add(listenerProcMemAccChanged); + + this.breakpointRecorder = new DefaultBreakpointRecorder(recorder); + + } + + protected void processMemoryAccessibilityChanged(boolean old, + boolean acc, Void __) { + recorder.getListeners().fire.processMemoryAccessibilityChanged(recorder); + } + + public CompletableFuture readProcessMemory(Address start, int length) { + Address tStart = recorder.getMemoryMapper().traceToTarget(start); + return getProcessMemory().readMemory(tStart, length); + } + + public CompletableFuture writeProcessMemory(Address start, byte[] data) { + Address tStart = recorder.getMemoryMapper().traceToTarget(start); + return getProcessMemory().writeMemory(tStart, data); + } + + public AddressSetView getAccessibleProcessMemory() { + // TODO: Efficiently distinguish which memory is process vs. thread + ///TODO Is this correct? + return getProcessMemory().getAccessibleMemory(mem -> true, recorder.getMemoryMapper()); + } + + @Override + public AbstractRecorderMemory getProcessMemory() { + return processMemory; + } + + @Override + public ManagedBreakpointRecorder getBreakpointRecorder() { + return breakpointRecorder; + } + + @Override + public Trace getTrace() { + return recorder.trace; + } + + @Override + public long getSnap() { + return recorder.getSnap(); + } + + @Override + public DebuggerMemoryMapper getMemoryMapper() { + return recorder.getMemoryMapper(); + } + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultStackRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultStackRecorder.java new file mode 100644 index 0000000000..a855fcb1ff --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultStackRecorder.java @@ -0,0 +1,162 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.*; +import java.util.concurrent.Executors; + +import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper; +import ghidra.app.plugin.core.debug.service.model.interfaces.ManagedStackRecorder; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetStackFrame; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.Address; +import ghidra.trace.model.Trace; +import ghidra.trace.model.stack.*; +import ghidra.trace.model.thread.TraceThread; + +public class DefaultStackRecorder implements ManagedStackRecorder { + + protected static int getFrameLevel(TargetStackFrame frame) { + // TODO: A fair assumption? frames are elements with numeric base-10 indices + return Integer.decode(frame.getIndex()); + } + + private NavigableMap stack = + Collections.synchronizedNavigableMap(new TreeMap<>()); + + private final TraceThread thread; + private final DefaultTraceRecorder recorder; + private final Trace trace; + private final TraceStackManager stackManager; + final PermanentTransactionExecutor tx; + + public DefaultStackRecorder(TraceThread thread, DefaultTraceRecorder recorder) { + this.thread = thread; + this.recorder = recorder; + this.trace = recorder.getTrace(); + this.stackManager = trace.getStackManager(); + this.tx = new PermanentTransactionExecutor(trace, + "ModuleRecorder:" + recorder.target.getJoinedPath("."), + Executors::newSingleThreadExecutor, 100); + } + + @Override + public void offerStackFrame(TargetStackFrame frame) { + recordFrame(frame); + } + + @Override + public void recordStack() { + long snap = recorder.getSnap(); + tx.execute("Stack changed", () -> { + TraceStack traceStack = stackManager.getStack(thread, snap, true); + traceStack.setDepth(stackDepth(), false); + for (Map.Entry ent : stack.entrySet()) { + Address tracePc = recorder.getMemoryMapper() + .targetToTrace(ent.getValue().getProgramCounter()); + doRecordFrame(traceStack, ent.getKey(), tracePc); + } + }); + } + + public void popStack() { + long snap = recorder.getSnap(); + tx.execute("Stack popped", () -> { + TraceStack traceStack = stackManager.getStack(thread, snap, true); + traceStack.setDepth(stackDepth(), false); + }); + } + + public void doRecordFrame(TraceStack traceStack, int frameLevel, Address pc) { + TraceStackFrame traceFrame = traceStack.getFrame(frameLevel, true); + traceFrame.setProgramCounter(pc); + } + + public void recordFrame(TargetStackFrame frame) { + tx.execute("Stack frame added", () -> { + stack.put(getFrameLevel(frame), frame); + DebuggerMemoryMapper memoryMapper = recorder.getMemoryMapper(); + if (memoryMapper == null) { + return; + } + Address pc = frame.getProgramCounter(); + Address tracePc = pc == null ? null : memoryMapper.targetToTrace(pc); + TraceStack traceStack = stackManager.getStack(thread, recorder.getSnap(), true); + doRecordFrame(traceStack, getFrameLevel(frame), tracePc); + }); + } + + protected int stackDepth() { + return stack.isEmpty() ? 0 : stack.lastKey() + 1; + } + + @Override + public int getSuccessorFrameLevel(TargetObject successor) { + NavigableSet observedPathLengths = new TreeSet<>(); + for (TargetStackFrame frame : stack.values()) { + observedPathLengths.add(frame.getPath().size()); + } + List path = successor.getPath(); + for (int l : observedPathLengths.descendingSet()) { + if (l > path.size()) { + continue; + } + List sub = path.subList(0, l); + if (!PathUtils.isIndex(sub)) { + continue; + } + int index = Integer.decode(PathUtils.getIndex(sub)); + TargetStackFrame frame = stack.get(index); + if (frame == null || !Objects.equals(sub, frame.getPath())) { + continue; + } + return index; + } + return 0; + } + + protected boolean checkStackFrameRemoved(TargetObject invalid) { + if (stack.values().remove(invalid)) { + popStack(); + return true; + } + return false; + } + + public Address pcFromStack() { + TargetStackFrame frame = stack.get(0); + if (frame == null) { + return null; + } + return frame.getProgramCounter(); + } + + @Override + public TraceStackFrame getTraceStackFrame(TraceThread thread, int level) { + TraceStack latest = stackManager.getLatestStack(thread, recorder.getSnap()); + if (latest == null) { + return null; + } + return latest.getFrame(level, false); + } + + @Override + public TargetStackFrame getTargetStackFrame(int frameLevel) { + return stack.get(frameLevel); + } + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultSymbolRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultSymbolRecorder.java new file mode 100644 index 0000000000..d67429e4c0 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultSymbolRecorder.java @@ -0,0 +1,137 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import ghidra.async.AsyncFence; +import ghidra.dbg.target.*; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.Address; +import ghidra.program.model.symbol.SourceType; +import ghidra.trace.model.Trace; +import ghidra.trace.model.symbol.*; +import ghidra.util.Msg; +import ghidra.util.exception.DuplicateNameException; +import ghidra.util.exception.InvalidInputException; +import ghidra.util.task.TaskMonitor; + +public class DefaultSymbolRecorder { + + private DefaultTraceRecorder recorder; + private Trace trace; + + public DefaultSymbolRecorder(DefaultTraceRecorder recorder) { + this.recorder = recorder; + this.trace = recorder.getTrace(); + } + + public CompletableFuture captureSymbols(TargetSymbolNamespace namespace, + TaskMonitor monitor) { + String path = PathUtils.toString(namespace.getPath()); + monitor.setMessage("Capturing symbols for " + path); + return namespace.getSymbols().thenAccept(symbols -> { + try (RecorderPermanentTransaction tid = RecorderPermanentTransaction.start(trace, + "Capture types and symbols for " + path)) { + TraceNamespaceSymbol ns = createNamespaceIfAbsent(path); + monitor.setMessage("Capturing symbols for " + path); + monitor.initialize(symbols.size()); + TraceEquateManager equateManager = trace.getEquateManager(); + for (TargetSymbol sym : symbols) { + if (monitor.isCancelled()) { + return; + } + monitor.incrementProgress(1); + String symName = sym.getIndex(); + if (sym.isConstant()) { + // TODO: Equate namespaces? + TraceEquate equate = equateManager.getByName(symName); + long symVal = sym.getValue().getOffset(); + if (equate != null && equate.getValue() == symVal) { + continue; + } + try { + equateManager.create(symName, symVal); + } + catch (DuplicateNameException | IllegalArgumentException e) { + Msg.error(this, "Could not create equate: " + symName, e); + } + continue; + } + Address addr = recorder.getMemoryMapper().targetToTrace(sym.getValue()); + try { + trace.getSymbolManager() + .labels() + .create(recorder.getSnap(), null, addr, symName, ns, + SourceType.IMPORTED); + } + catch (InvalidInputException e) { + Msg.error(this, "Could not add module symbol " + sym + ": " + e); + } + /** + * TODO: Lay down data type, if present + * + * TODO: Interpret "address" type correctly. A symbol with this type is itself + * the pointer. In other words, it is not specifying the type to lay down in + * memory. + */ + } + } + }); + } + + public CompletableFuture captureSymbols(TargetModule targetModule, + TaskMonitor monitor) { + CompletableFuture> future = + targetModule.fetchChildrenSupporting(TargetSymbolNamespace.class); + // NOTE: I should expect exactly one namespace... + return future.thenCompose(namespaces -> { + AsyncFence fence = new AsyncFence(); + for (TargetSymbolNamespace ns : namespaces.values()) { + fence.include(captureSymbols(ns, monitor)); + } + return fence.ready(); + }); + } + + private TraceNamespaceSymbol createNamespaceIfAbsent(String path) { + + TraceSymbolManager symbolManager = trace.getSymbolManager(); + try { + return symbolManager.namespaces() + .add(path, symbolManager.getGlobalNamespace(), SourceType.IMPORTED); + } + catch (DuplicateNameException e) { + Msg.info(this, "Namespace for module " + path + + " already exists or another exists with a conflicting name. Using the existing one: " + + e); + TraceNamespaceSymbol ns = symbolManager.namespaces().getGlobalNamed(path); + if (ns != null) { + return ns; + } + Msg.error(this, "Existing namespace for " + path + + " is not a plain namespace. Using global namespace."); + return symbolManager.getGlobalNamespace(); + } + catch (InvalidInputException | IllegalArgumentException e) { + Msg.error(this, + "Could not create namespace for new module: " + path + ". Using global namespace.", + e); + return symbolManager.getGlobalNamespace(); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultThreadRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultThreadRecorder.java new file mode 100644 index 0000000000..114ac52187 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultThreadRecorder.java @@ -0,0 +1,547 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import ghidra.app.plugin.core.debug.mapping.*; +import ghidra.app.plugin.core.debug.service.model.interfaces.*; +import ghidra.async.AsyncUtils; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.data.Pointer; +import ghidra.program.model.lang.*; +import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.listing.*; +import ghidra.trace.model.memory.*; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.Msg; +import ghidra.util.TimedMsg; +import ghidra.util.exception.DuplicateNameException; + +public class DefaultThreadRecorder implements ManagedThreadRecorder { + + //private static final boolean LOG_STACK_TRACE = false; + + private final TargetThread targetThread; + private final TraceThread traceThread; + + protected final AbstractRecorderMemory threadMemory; + //private AbstractRecorderRegisterSet threadRegisters; + protected TargetBreakpointSpecContainer threadBreakpointContainer; + + protected Map regs = new HashMap<>(); + protected Collection extraRegs; + + protected TargetExecutionState state = TargetExecutionState.ALIVE; + + private final DefaultTraceRecorder recorder; + private final Trace trace; + private final TraceObjectManager objectManager; + + private final TraceMemoryManager memoryManager; + + private DebuggerRegisterMapper regMapper; + private final AbstractDebuggerTargetTraceMapper mapper; + + private final DefaultStackRecorder stackRecorder; + private final DefaultBreakpointRecorder breakpointRecorder; + final PermanentTransactionExecutor tx; + + protected static int getFrameLevel(TargetStackFrame frame) { + // TODO: A fair assumption? frames are elements with numeric base-10 indices + return Integer.decode(frame.getIndex()); + } + + public DefaultThreadRecorder(DefaultTraceRecorder recorder, + AbstractDebuggerTargetTraceMapper mapper, TargetThread targetThread, + TraceThread traceThread) { + this.recorder = recorder; + this.mapper = mapper; + this.trace = recorder.getTrace(); + this.objectManager = recorder.objectManager; + + this.targetThread = targetThread; + this.traceThread = traceThread; + + this.memoryManager = trace.getMemoryManager(); + + this.tx = new PermanentTransactionExecutor(trace, + "ThreadRecorder:" + recorder.target.getJoinedPath("."), + f -> Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), f), 100); + + //this.threadMemory = new RecorderComposedMemory(recorder.getProcessMemory()); + this.threadMemory = recorder.getProcessMemory(); + //this.threadRegisters = recorder.getThreadRegisters(); + + if (targetThread instanceof TargetExecutionStateful) { + TargetExecutionStateful stateful = (TargetExecutionStateful) targetThread; + state = stateful.getExecutionState(); + } + + this.stackRecorder = new DefaultStackRecorder(traceThread, recorder); + this.breakpointRecorder = new DefaultBreakpointRecorder(recorder); + } + + protected synchronized CompletableFuture initRegMapper( + TargetRegisterContainer registers) { + /** + * TODO: At the moment, this assumes the recorded thread has one register container, or at + * least that all register banks in the thread use the same register container + * (descriptors). If this becomes a problem, then we'll need to keep a separate register + * mapper per register container. This would likely also require some notion of multiple + * languages in the mapper (seems an unlikely design choice). NOTE: In cases where a single + * process may (at least appear to) execute multiple languages, the model should strive to + * present the registers of the physical machine, as they are most likely uniform across the + * process, not those being emulated in the moment. In cases where an abstract machine is + * involved, it is probably more fitting to present separate containers (likely provided by + * separate models) than to present both the physical and abstract machine in the same + * target. + * + *

+ * TODO: Should I formalize that only one register container is present in a recorded + * thread? This seems counter to the model's flexibility. Traces allow polyglot disassembly, + * but not polyglot register spaces. + */ + return objectManager.getRegMappers().get(registers).thenAccept(rm -> { + synchronized (this) { + regMapper = rm; + Language language = trace.getBaseLanguage(); + extraRegs = new LinkedHashSet<>(); + for (String rn : mapper.getExtraRegNames()) { + Register traceReg = language.getRegister(rn); + if (traceReg == null) { + Msg.error(this, + "Mapper's extra register '" + rn + "' is not in the language!"); + continue; + } + TargetRegister targetReg = regMapper.traceToTarget(traceReg); + if (targetReg == null) { + Msg.error(this, + "Mapper's extra register '" + traceReg + "' is not mappable!"); + continue; + } + extraRegs.add(targetReg); + } + } + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize register mapper", ex); + return null; + }); + } + + @Override + public CompletableFuture doFetchAndInitRegMapper(TargetRegisterBank bank) { + TargetRegisterContainer descs = bank.getDescriptions(); + if (descs == null) { + Msg.error(this, "Cannot create mapper, yet: Descriptions is null."); + return AsyncUtils.NIL; + } + return initRegMapper(descs).thenAccept(__ -> { + recorder.getListeners().fire.registerBankMapped(recorder); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize register mapper", ex); + return null; + }); + } + + public CompletableFuture captureThreadRegisters(TraceThread thread, int frameLevel, + Set registers) { + if (regMapper == null) { + throw new IllegalStateException("Have not found register descriptions for " + thread); + } + if (!regMapper.getRegistersOnTarget().containsAll(registers)) { + throw new IllegalArgumentException( + "All given registers must be recognized by the target"); + } + if (registers.isEmpty()) { + return AsyncUtils.NIL; + } + List tRegs = + registers.stream().map(regMapper::traceToTarget).collect(Collectors.toList()); + + TargetRegisterBank bank = getTargetRegisterBank(thread, frameLevel); + if (bank == null) { + throw new IllegalArgumentException( + "Given thread and frame level does not have a live register bank"); + } + // NOTE: Cache update, if applicable, will cause recorder to write values to trace + System.err.println("captureThreadRegisters " + thread + ":" + bank); + return bank.readRegisters(tRegs).thenApply(__ -> null); + } + + public TargetRegisterBank getTargetRegisterBank(TraceThread thread, int frameLevel) { + return regs.get(frameLevel); + } + + @Override + public void regMapperAmended(DebuggerRegisterMapper rm, TargetRegister reg, boolean removed) { + String name = reg.getIndex(); + synchronized (this) { + if (regMapper != rm) { + return; + } + if (mapper.getExtraRegNames().contains(name)) { + if (removed) { + extraRegs.remove(reg); + } + else { + extraRegs.add(reg); + } + } + } + } + + @Override + public void offerRegisters(TargetRegisterBank bank) { + if (regMapper == null) { + doFetchAndInitRegMapper(bank); + } + int frameLevel = stackRecorder.getSuccessorFrameLevel(bank); + TargetRegisterBank old = regs.put(frameLevel, bank); + if (null != old) { + Msg.warn(this, "Unexpected register bank replacement"); + } + } + + @Override + public void removeRegisters(TargetRegisterBank bank) { + int frameLevel = stackRecorder.getSuccessorFrameLevel(bank); + TargetRegisterBank old = regs.remove(frameLevel); + if (bank != old) { + Msg.warn(this, "Unexpected register bank upon removal"); + } + } + + @Override + public void offerThreadRegion(TargetMemoryRegion region) { + TargetMemory mem = region.getMemory(); + threadMemory.addRegion(region, mem); + } + + @Override + public void stateChanged(final TargetExecutionState newState) { + state = newState; + } + + public void threadDestroyed() { + String path = getTargetThread().getJoinedPath("."); + long snap = recorder.getSnap(); + tx.execute("Thread " + path + " destroyed", () -> { + // TODO: Should it be key - 1 + // Perhaps, since the thread should not exist + // But it could imply earlier destruction than actually observed + try { + getTraceThread().setDestructionSnap(snap); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be shrinking + } + }); + } + + @Override + public void recordRegisterValues(TargetRegisterBank bank, Map updates) { + synchronized (recorder) { + if (regMapper == null) { + doFetchAndInitRegMapper(bank); + } + } + int frameLevel = stackRecorder.getSuccessorFrameLevel(bank); + long snap = recorder.getSnap(); + String path = bank.getJoinedPath("."); + TimedMsg.info(this, "Reg values changed: " + updates.keySet()); + tx.execute("Registers " + path + " changed", () -> { + TraceCodeManager codeManager = trace.getCodeManager(); + TraceCodeRegisterSpace codeRegisterSpace = + codeManager.getCodeRegisterSpace(traceThread, false); + TraceDefinedDataRegisterView definedData = + codeRegisterSpace == null ? null : codeRegisterSpace.definedData(); + TraceMemoryRegisterSpace regSpace = + memoryManager.getMemoryRegisterSpace(traceThread, frameLevel, true); + for (Entry ent : updates.entrySet()) { + RegisterValue rv = regMapper.targetToTrace(ent.getKey(), ent.getValue()); + if (rv == null) { + continue; // mapper does not know this register.... + } + regSpace.setValue(snap, rv); + Register register = rv.getRegister(); + if (definedData != null) { + TraceData td = definedData.getForRegister(snap, register); + if (td != null && td.getDataType() instanceof Pointer) { + Address addr = registerValueToTargetAddress(rv, ent.getValue()); + readAlignedConditionally(ent.getKey(), addr); // NB: Reports errors + } + } + } + }); + } + + @Override + public void recordRegisterValue(TargetRegister targetRegister, byte[] value) { + TargetRegisterBank bank = (TargetRegisterBank) targetRegister.getParent(); + synchronized (recorder) { + if (regMapper == null) { + doFetchAndInitRegMapper(bank); + } + } + int frameLevel = stackRecorder.getSuccessorFrameLevel(bank); + long snap = recorder.getSnap(); + String path = targetRegister.getJoinedPath("."); + //TimedMsg.info(this, "Register value changed: " + targetRegister); + tx.execute("Register " + path + " changed", () -> { + TraceCodeManager codeManager = trace.getCodeManager(); + TraceCodeRegisterSpace codeRegisterSpace = + codeManager.getCodeRegisterSpace(traceThread, false); + TraceDefinedDataRegisterView definedData = + codeRegisterSpace == null ? null : codeRegisterSpace.definedData(); + TraceMemoryRegisterSpace regSpace = + memoryManager.getMemoryRegisterSpace(traceThread, frameLevel, true); + String key = targetRegister.getName(); + if (PathUtils.isIndex(key)) { + key = key.substring(1, key.length() - 1); + } + RegisterValue rv = regMapper.targetToTrace(key, value); + if (rv == null) { + return; // mapper does not know this register.... + } + regSpace.setValue(snap, rv); + Register register = rv.getRegister(); + if (definedData != null) { + TraceData td = definedData.getForRegister(snap, register); + if (td != null && td.getDataType() instanceof Pointer) { + Address addr = registerValueToTargetAddress(rv, value); + readAlignedConditionally(key, addr); // NB: Reports errors + } + } + }); + } + + public CompletableFuture writeThreadRegisters(int frameLevel, + Map values) { + if (!regMapper.getRegistersOnTarget().containsAll(values.keySet())) { + throw new IllegalArgumentException( + "All given registers must be recognized by the target"); + } + if (values.isEmpty()) { + return AsyncUtils.NIL; + } + Map tVals = values.entrySet().stream().map(ent -> { + if (ent.getKey() != ent.getValue().getRegister()) { + throw new IllegalArgumentException("register name mismatch in value"); + } + return regMapper.traceToTarget(ent.getValue()); + }).collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + TargetRegisterBank bank = getTargetRegisterBank(traceThread, frameLevel); + if (bank == null) { + throw new IllegalArgumentException( + "Given thread and frame level does not have a live register bank"); + } + // NOTE: Model + recorder will cause applicable trace updates + return bank.writeRegistersNamed(tVals).thenApply(__ -> null); + } + + Address registerValueToTargetAddress(RegisterValue rv, byte[] value) { + Address traceAddress = + trace.getBaseLanguage().getDefaultSpace().getAddress(rv.getUnsignedValue().longValue()); + return objectManager.getMemoryMapper().traceToTarget(traceAddress); + } + + protected CompletableFuture readAlignedConditionally(String name, Address targetAddress) { + if (targetAddress == null) { + return AsyncUtils.NIL; + } + Address traceAddress = objectManager.getMemoryMapper().targetToTrace(targetAddress); + if (traceAddress == null) { + return AsyncUtils.NIL; + } + if (!checkReadCondition(traceAddress)) { + return AsyncUtils.NIL; + } + AddressRange targetRange = threadMemory.alignAndLimitToFloor(targetAddress, 1); + if (targetRange == null) { + return AsyncUtils.NIL; + } + TimedMsg.info(this, + " Reading memory at " + name + " (" + targetAddress + " -> " + targetRange + ")"); + // NOTE: Recorder takes data via memoryUpdated callback + // TODO: In that callback, sort out process memory from thread memory? + return threadMemory.readMemory(targetRange.getMinAddress(), (int) targetRange.getLength()) + .exceptionally(ex -> { + Msg.error(this, "Could not read memory at " + name, ex); + return null; + }); + } + + protected boolean checkReadCondition(Address traceAddress) { + /** + * TODO: This heuristic doesn't really belong here, but I have to implement it here so that + * it doesn't "override" the listing's implementation. Once watches are implemented, we + * should be able to drop this garbage. + */ + TraceMemoryRegion region = + memoryManager.getRegionContaining(recorder.getSnap(), traceAddress); + if (region == null) { + return false; + } + if (region.isWrite()) { + return true; + } + Entry ent = + memoryManager.getMostRecentStateEntry(recorder.getSnap(), traceAddress); + if (ent == null) { + return true; + } + if (ent.getValue() == TraceMemoryState.KNOWN) { + return false; + } + return true; + } + + @Override + public TargetThread getTargetThread() { + return targetThread; + } + + @Override + public TraceThread getTraceThread() { + return traceThread; + } + + @Override + public long getSnap() { + return recorder.getSnap(); + } + + @Override + public Trace getTrace() { + return recorder.getTrace(); + } + + @Override + public DebuggerMemoryMapper getMemoryMapper() { + return recorder.objectManager.getMemoryMapper(); + } + + @Override + public ManagedStackRecorder getStackRecorder() { + return stackRecorder; + } + + @Override + public ManagedBreakpointRecorder getBreakpointRecorder() { + return breakpointRecorder; + } + + /** + * Inform the recorder the given object is no longer valid + * + * @param invalid the invalidated object + * @return true if this recorder should be invalidated, too + */ + // UNUSED? + @Override + public synchronized boolean objectRemoved(TargetObject invalid) { + if (checkThreadRemoved(invalid)) { + return true; + } + if (stackRecorder.checkStackFrameRemoved(invalid)) { + return false; + } + if (threadMemory.removeRegion(invalid)) { + return false; + } + Msg.trace(this, "Ignored removed object: " + invalid); + return false; + } + + protected boolean checkThreadRemoved(TargetObject invalid) { + if (getTargetThread() == invalid) { + threadDestroyed(); + return true; + } + return false; + } + + public DebuggerRegisterMapper getRegisterMapper() { + return regMapper; + } + + /* + public CompletableFuture updateRegsMem(TargetMemoryRegion limit) { + TargetRegisterBank bank; + TargetRegister pc; + TargetRegister sp; + Set toRead = new LinkedHashSet<>(); + synchronized (recorder) { + if (regMapper == null) { + return AsyncUtils.NIL; + } + bank = regs.get(0); + pc = pcReg; + sp = spReg; + toRead.addAll(extraRegs); + toRead.add(sp); + toRead.add(pc); + } + if (bank == null || pc == null || sp == null) { + return AsyncUtils.NIL; + } + System.err.println("URM:" + getTargetThread()); + TimedMsg.info(this, "Reading " + toRead + " of " + getTargetThread()); + return bank.readRegisters(toRead).thenCompose(vals -> { + synchronized (recorder) { + if (memoryManager == null) { + return AsyncUtils.NIL; + } + } + if (threadMemory == null) { + return AsyncUtils.NIL; + } + AsyncFence fence = new AsyncFence(); + + Address pcTargetAddr = stackRecorder.pcFromStack(); + if (pcTargetAddr == null) { + pcTargetAddr = registerValueToTargetAddress(pcReg, vals.get(pcReg.getIndex())); + } + fence.include(readAlignedConditionally("PC", pcTargetAddr, limit)); + + Address spTargetAddr = registerValueToTargetAddress(spReg, vals.get(spReg.getIndex())); + fence.include(readAlignedConditionally("SP", spTargetAddr, limit)); + + return fence.ready(); + }).exceptionally(ex -> { + if (LOG_STACK_TRACE) { + Msg.error(this, "Could not read registers", ex); + } + else { + Msg.error(this, "Could not read registers"); + } + return null; + }); + } + */ + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTimeRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTimeRecorder.java new file mode 100644 index 0000000000..92e490b4d0 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTimeRecorder.java @@ -0,0 +1,65 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import ghidra.trace.model.Trace; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSnapshot; + +public class DefaultTimeRecorder { + + private DefaultTraceRecorder recorder; + private Trace trace; + private TraceSnapshot snapshot = null; + + public DefaultTimeRecorder(DefaultTraceRecorder recorder) { + this.recorder = recorder; + this.trace = recorder.getTrace(); + } + + public TraceSnapshot getSnapshot() { + return snapshot; + } + + public long getSnap() { + return snapshot.getKey(); + } + + protected synchronized void doAdvanceSnap(String description, TraceThread eventThread) { + snapshot = trace.getTimeManager().createSnapshot(description); + snapshot.setEventThread(eventThread); + } + + public TraceSnapshot forceSnapshot() { + createSnapshot("User-forced snapshot", null, null); + return snapshot; + } + + public void createSnapshot(String description, TraceThread eventThread, + RecorderPermanentTransaction tid) { + if (tid != null) { + doAdvanceSnap(description, eventThread); + recorder.getListeners().fire.snapAdvanced(recorder, getSnap()); + return; + } + // NB. The also serves as the snap counter, so it must be on the service thread + try (RecorderPermanentTransaction tid2 = + RecorderPermanentTransaction.start(trace, description)) { + doAdvanceSnap(description, eventThread); + } + recorder.getListeners().fire.snapAdvanced(recorder, getSnap()); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java index 8d116b7094..fd9c1804c1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorder.java @@ -15,1668 +15,62 @@ */ package ghidra.app.plugin.core.debug.service.model; -import java.nio.ByteBuffer; import java.util.*; -import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.exception.ExceptionUtils; - -import com.google.common.collect.*; import ghidra.app.plugin.core.debug.mapping.*; +import ghidra.app.plugin.core.debug.service.model.interfaces.*; import ghidra.app.services.TraceRecorder; import ghidra.app.services.TraceRecorderListener; -import ghidra.async.*; -import ghidra.async.AsyncLazyMap.KeyedFuture; -import ghidra.dbg.DebugModelConventions; -import ghidra.dbg.DebugModelConventions.AllRequiredAccess; -import ghidra.dbg.DebugModelConventions.SubTreeListenerAdapter; -import ghidra.dbg.attributes.TargetObjectList; -import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.async.AsyncLazyValue; +import ghidra.async.AsyncUtils; +import ghidra.dbg.agent.AbstractDebuggerObjectModel; import ghidra.dbg.error.DebuggerModelAccessException; import ghidra.dbg.target.*; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; -import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointSpecListener; -import ghidra.dbg.target.TargetEventScope.TargetEventScopeListener; -import ghidra.dbg.target.TargetEventScope.TargetEventType; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; -import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionStateListener; -import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener; -import ghidra.dbg.target.TargetMemory.TargetMemoryListener; -import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener; -import ghidra.dbg.util.*; +import ghidra.dbg.util.PathUtils; import ghidra.framework.plugintool.PluginTool; -import ghidra.program.model.address.*; -import ghidra.program.model.data.*; -import ghidra.program.model.lang.*; -import ghidra.program.model.symbol.SourceType; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; import ghidra.trace.model.Trace; -import ghidra.trace.model.TraceAddressSnapRange; -import ghidra.trace.model.breakpoint.*; -import ghidra.trace.model.data.TraceBasedDataTypeManager; -import ghidra.trace.model.listing.TraceCodeManager; -import ghidra.trace.model.memory.*; -import ghidra.trace.model.modules.*; -import ghidra.trace.model.program.TraceProgramView; -import ghidra.trace.model.stack.*; -import ghidra.trace.model.symbol.*; +import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.modules.TraceSection; +import ghidra.trace.model.stack.TraceStackFrame; import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.model.thread.TraceThreadManager; import ghidra.trace.model.time.TraceSnapshot; -import ghidra.trace.model.time.TraceTimeManager; -import ghidra.util.*; -import ghidra.util.database.UndoableTransaction; +import ghidra.util.Msg; import ghidra.util.datastruct.ListenerSet; import ghidra.util.exception.DuplicateNameException; -import ghidra.util.exception.InvalidInputException; import ghidra.util.task.TaskMonitor; public class DefaultTraceRecorder implements TraceRecorder { - private static final boolean LOG_STACK_TRACE = false; - // For large memory captures - private static final int BLOCK_SIZE = 4096; - private static final long BLOCK_MASK = -1L << 12; - - static final PathMatcher HARDCODED_MATCHER = new PathMatcher() { - { - // Paths for GDB - addPattern(PathUtils.parse("Breakpoints[].")); - addPattern(PathUtils.parse("Inferiors[].Memory[]")); - addPattern(PathUtils.parse("Inferiors[].Modules[].Sections[]")); - addPattern(PathUtils.parse("Inferiors[].Registers[]")); - addPattern(PathUtils.parse("Inferiors[].Threads[]")); - addPattern(PathUtils.parse("Inferiors[].Threads[].Stack[]")); - - // Paths for dbgeng - addPattern(PathUtils.parse("Sessions[].Processes[].Memory[]")); - addPattern(PathUtils.parse("Sessions[].Processes[].Modules[]")); - addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Registers[]")); - addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Stack[]")); - addPattern(PathUtils.parse("Sessions[].Processes[].Debug.Breakpoints[]")); - - // (Additional) paths for dbgmodel - addPattern(PathUtils.parse("Sessions[].Attributes")); - addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Stack.Frames[]")); - addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].TTD.Position")); - addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Registers.User.")); - - // Paths for JDI - addPattern(PathUtils.parse("VirtualMachines[]")); - addPattern(PathUtils.parse("VirtualMachines[].Breakpoints")); - addPattern(PathUtils.parse("VirtualMachines[].Classes[]")); - addPattern(PathUtils.parse("VirtualMachines[].Classes[].Sections[]")); - addPattern(PathUtils.parse("VirtualMachines[].Threads[]")); - addPattern(PathUtils.parse("VirtualMachines[].Threads[].Registers[]")); - addPattern(PathUtils.parse("VirtualMachines[].Threads[].Stack[]")); - - } - }; - - protected static class PermanentTransaction implements AutoCloseable { - static PermanentTransaction start(Trace trace, String description) { - UndoableTransaction tid = null; - try { - tid = UndoableTransaction.start(trace, description, true); - } - catch (Throwable t) { - tid.close(); - return ExceptionUtils.rethrow(t); - } - return new PermanentTransaction(trace, tid); - } - - private final Trace trace; - private final UndoableTransaction tid; - - public PermanentTransaction(Trace trace, UndoableTransaction tid) { - this.trace = trace; - this.tid = tid; - } - - @Override - public void close() { - tid.close(); - trace.clearUndo(); - } - } - - protected final AsyncLazyMap accessibilityByRegBank = - new AsyncLazyMap<>(new HashMap<>(), this::fetchRegAccessibility) { - public AllRequiredAccess remove(TargetRegisterBank key) { - AllRequiredAccess acc = super.remove(key); - if (acc != null) { - acc.removeChangeListener(listenerRegAccChanged); - } - return acc; - } - }; - protected final Map byRegion = new HashMap<>(); - protected final AsyncLazyMap accessibilityByMemory = - new AsyncLazyMap<>(new HashMap<>(), this::fetchMemAccessibility) { - public AllRequiredAccess remove(TargetMemory key) { - AllRequiredAccess acc = super.remove(key); - if (acc != null) { - acc.removeChangeListener(processMemory.memAccListeners.fire); - } - return acc; - } - }; - - protected CompletableFuture fetchRegAccessibility( - TargetRegisterBank bank) { - return DebugModelConventions.trackAccessibility(bank).thenApply(acc -> { - acc.addChangeListener(listenerRegAccChanged); - return acc; - }); - } - - protected CompletableFuture fetchMemAccessibility(TargetMemory mem) { - return DebugModelConventions.trackAccessibility(mem).thenApply(acc -> { - acc.addChangeListener(processMemory.memAccListeners.fire); - return acc; - }); - } - - /** - * Get accessible memory, as viewed in the trace - * - * @param pred an additional predicate applied via "AND" with accessibility - * @return the computed set - */ - protected AddressSet getAccessibleMemory(Predicate pred) { - synchronized (accessibilityByMemory) { - // TODO: Might accomplish by using listeners and tracking the accessible set - AddressSet accessible = new AddressSet(); - for (Entry ent : byRegion.entrySet()) { - TargetMemory mem = ent.getValue(); - if (!pred.test(mem)) { - continue; - } - AllRequiredAccess acc = accessibilityByMemory.getCompletedMap().get(mem); - if (acc == null || !acc.getAllAccessibility()) { - continue; - } - accessible.add(memMapper.targetToTrace(ent.getKey().getRange())); - } - return accessible; - } - } - - protected class ComposedMemory { - protected final ComposedMemory chain; - - protected final NavigableMap byMin = new TreeMap<>(); - - @SuppressWarnings({ "rawtypes", "unchecked" }) - protected final ListenerSet> memAccListeners = - new ListenerSet(TriConsumer.class); - - public ComposedMemory() { - this.chain = null; - } - - public ComposedMemory(ComposedMemory chain) { - this.chain = chain; - } - - protected void addRegion(TargetMemoryRegion region, TargetMemory memory) { - synchronized (accessibilityByMemory) { - TargetMemory old = byRegion.put(region, memory); - assert old == null; - byMin.put(region.getRange().getMinAddress(), region); - accessibilityByMemory.get(memory).exceptionally(e -> { - e = AsyncUtils.unwrapThrowable(e); - Msg.error(this, "Could not track memory accessibility: " + e.getMessage()); - return null; - }); - } - } - - protected boolean removeRegion(TargetObject invalid) { - if (!(invalid instanceof TargetMemoryRegion)) { - return false; - } - synchronized (accessibilityByMemory) { - TargetMemoryRegion invRegion = (TargetMemoryRegion) invalid; - TargetMemory old = byRegion.remove(invRegion); - assert old != null; - byMin.remove(invRegion.getRange().getMinAddress()); - if (!old.isValid() || !byRegion.containsValue(old)) { - accessibilityByMemory.remove(old); - } - return true; - } - } - - protected AllRequiredAccess findChainedMemoryAccess(TargetMemoryRegion region) { - synchronized (accessibilityByMemory) { - TargetMemory mem = byRegion.get(region); - if (mem != null) { - return accessibilityByMemory.getCompletedMap().get(mem); - } - return chain == null ? null : chain.findChainedMemoryAccess(region); - } - } - - protected Entry findChainedFloor(Address address) { - synchronized (accessibilityByMemory) { - Entry myFloor = byMin.floorEntry(address); - Entry byChain = - chain == null ? null : chain.findChainedFloor(address); - if (byChain == null) { - return myFloor; - } - if (myFloor == null) { - return byChain; - } - int c = myFloor.getKey().compareTo(byChain.getKey()); - if (c < 0) { - return byChain; - } - return myFloor; - } - } - - protected AddressRange align(Address address, int length) { - AddressSpace space = address.getAddressSpace(); - long offset = address.getOffset(); - Address start = space.getAddress(offset & BLOCK_MASK); - Address end = space.getAddress(((offset + length - 1) & BLOCK_MASK) + BLOCK_SIZE - 1); - return new AddressRangeImpl(start, end); - } - - protected AddressRange alignWithLimit(Address address, int length, - TargetMemoryRegion limit) { - return align(address, length).intersect(limit.getRange()); - } - - protected AddressRange alignAndLimitToFloor(Address address, int length) { - Entry floor = findChainedFloor(address); - if (floor == null) { - return null; - } - return alignWithLimit(address, length, floor.getValue()); - } - - protected AddressRange alignWithOptionalLimit(Address address, int length, - TargetMemoryRegion limit) { - if (limit == null) { - return alignAndLimitToFloor(address, length); - } - return alignWithLimit(address, length, limit); - } - - protected CompletableFuture readMemory(Address address, int length) { - synchronized (accessibilityByMemory) { - Entry floor = findChainedFloor(address); - if (floor == null) { - throw new IllegalArgumentException( - "address " + address + " is not in any known region"); - } - Address max; - try { - max = address.addNoWrap(length - 1); - } - catch (AddressOverflowException e) { - throw new IllegalArgumentException("read extends beyond the address space"); - } - if (!floor.getValue().getRange().contains(max)) { - throw new IllegalArgumentException("read extends beyond a single region"); - } - TargetMemory mem = byRegion.get(floor.getValue()); - if (mem != null) { - return mem.readMemory(address, length); - } - return CompletableFuture.completedFuture(new byte[0]); - } - } - - protected CompletableFuture writeMemory(Address address, byte[] data) { - synchronized (accessibilityByMemory) { - Entry floor = findChainedFloor(address); - if (floor == null) { - throw new IllegalArgumentException( - "address " + address + " is not in any known region"); - } - Address max; - try { - max = address.addNoWrap(data.length - 1); - } - catch (AddressOverflowException e) { - throw new IllegalArgumentException("read extends beyond the address space"); - } - if (!floor.getValue().getRange().contains(max)) { - throw new IllegalArgumentException("read extends beyond a single region"); - } - TargetMemory mem = byRegion.get(floor.getValue()); - if (mem != null) { - return mem.writeMemory(address, data); - } - throw new IllegalArgumentException("read starts outside any address space"); - } - } - } - - protected static class ThreadMap { - protected final Map byTargetThread = new HashMap<>(); - protected final Map byTraceThread = new HashMap<>(); - - public void put(ThreadRecorder rec) { - byTargetThread.put(rec.targetThread, rec); - byTraceThread.put(rec.traceThread, rec); - } - - public ThreadRecorder getForSuccessor(TargetObject successor) { - while (successor != null) { - ThreadRecorder rec = byTargetThread.get(successor); - if (rec != null) { - return rec; - } - successor = successor.getParent(); - } - return null; - } - - public ThreadRecorder get(TargetThread thread) { - return byTargetThread.get(thread); - } - - public ThreadRecorder get(TargetObject maybeThread) { - return byTargetThread.get(maybeThread); - } - - public ThreadRecorder get(TraceThread thread) { - return byTraceThread.get(thread); - } - - public void remove(ThreadRecorder rec) { - ThreadRecorder rByTarget = byTargetThread.remove(rec.targetThread); - ThreadRecorder rByTrace = byTraceThread.remove(rec.traceThread); - assert rec == rByTarget; - assert rec == rByTrace; - } - - public Collection recorders() { - return byTargetThread.values(); - } - } - - protected static AddressSetView expandToBlocks(AddressSetView asv) { - AddressSet result = new AddressSet(); - // Not terribly efficient, but this is one range most of the time - for (AddressRange range : asv) { - AddressSpace space = range.getAddressSpace(); - Address min = space.getAddress(range.getMinAddress().getOffset() & BLOCK_MASK); - Address max = space.getAddress(range.getMaxAddress().getOffset() | ~BLOCK_MASK); - result.add(new AddressRangeImpl(min, max)); - } - return result; - } - - protected static AddressRange range(Address min, Integer length) { - if (length == null) { - length = 1; - } - try { - return new AddressRangeImpl(min, length); - } - catch (AddressOverflowException e) { - throw new AssertionError(e); - } - } - - protected static String nameBreakpoint(TargetBreakpointLocation bpt) { - if (bpt instanceof TargetBreakpointSpec) { - return bpt.getIndex(); - } - return bpt.getSpecification().getIndex() + "." + bpt.getIndex(); - } - - protected static int getFrameLevel(TargetStackFrame frame) { - // TODO: A fair assumption? frames are elements with numeric base-10 indices - return Integer.decode(frame.getIndex()); - } - - protected class ThreadRecorder { - protected final TargetThread targetThread; - protected final TraceThread traceThread; - protected DebuggerRegisterMapper regMapper; - protected TargetRegister pcReg; - protected TargetRegister spReg; - protected Map regs = new HashMap<>(); - protected NavigableMap stack = - Collections.synchronizedNavigableMap(new TreeMap<>()); - protected final ComposedMemory threadMemory = new ComposedMemory(processMemory); - protected TargetBreakpointContainer threadBreakpointContainer; - protected TargetExecutionState state = TargetExecutionState.ALIVE; - - protected ThreadRecorder(TargetThread targetThread, TraceThread traceThread) { - this.targetThread = targetThread; - this.traceThread = traceThread; - - if (targetThread instanceof TargetExecutionStateful) { - TargetExecutionStateful stateful = (TargetExecutionStateful) targetThread; - state = stateful.getExecutionState(); - } - } - - protected synchronized CompletableFuture initRegMapper( - TargetRegisterContainer registers) { - /** - * TODO: At the moment, this assumes the recorded thread has one register container, or - * at least that all register banks in the thread use the same register container - * (descriptors). If this becomes a problem, then we'll need to keep a separate register - * mapper per register container. This would likely also require some notion of multiple - * languages in the mapper (seems an unlikely design choice). NOTE: In cases where a - * single process may (at least appear to) execute multiple languages, the model should - * strive to present the registers of the physical machine, as they are most likely - * uniform across the process, not those being emulated in the moment. In cases where an - * abstract machine is involved, it is probably more fitting to present separate - * containers (likely provided by separate models) than to present both the physical and - * abstract machine in the same target. - * - *

- * TODO: Should I formalize that only one register container is present in a recorded - * thread? This seems counter to the model's flexibility. Traces allow polyglot - * disassembly, but not polyglot register spaces. - */ - /*if (regMapper != null) { - return AsyncUtils.NIL; - }*/ - return regMappers.get(registers).thenAccept(rm -> { - synchronized (this) { - regMapper = rm; - Language language = trace.getBaseLanguage(); - pcReg = regMapper.traceToTarget(language.getProgramCounter()); - spReg = regMapper.traceToTarget(trace.getBaseCompilerSpec().getStackPointer()); - extraRegs = new LinkedHashSet<>(); - for (String rn : mapper.getExtraRegNames()) { - Register traceReg = language.getRegister(rn); - if (traceReg == null) { - Msg.error(this, - "Mapper's extra register '" + rn + "' is not in the language!"); - continue; - } - TargetRegister targetReg = regMapper.traceToTarget(traceReg); - if (targetReg == null) { - Msg.error(this, - "Mapper's extra register '" + traceReg + "' is not mappable!"); - continue; - } - extraRegs.add(targetReg); - } - } - listenerForRecord.retroOfferRegMapperDependents(); - }).exceptionally(ex -> { - Msg.error(this, "Could not intialize register mapper", ex); - return null; - }); - } - - protected void regMapperAmended(DebuggerRegisterMapper rm, TargetRegister reg, - boolean removed) { - boolean doUpdateRegs = false; - String name = reg.getIndex(); - synchronized (this) { - if (regMapper != rm) { - return; - } - TargetRegister newPcReg = - regMapper.traceToTarget(trace.getBaseLanguage().getProgramCounter()); - if (pcReg != newPcReg) { - pcReg = newPcReg; - doUpdateRegs |= pcReg != null; - } - TargetRegister newSpReg = - regMapper.traceToTarget(trace.getBaseCompilerSpec().getStackPointer()); - if (spReg != newSpReg) { - spReg = newSpReg; - doUpdateRegs |= spReg != null; - } - if (mapper.getExtraRegNames().contains(name)) { - if (removed) { - extraRegs.remove(reg); - } - else { - extraRegs.add(reg); - } - doUpdateRegs = true; - } - } - if (removed) { - return; - } - TargetRegisterBank bank = regs.get(0); - if (bank != null) { - byte[] cachedVal = bank.getCachedRegisters().get(name); - if (cachedVal != null) { - recordRegisterValues(bank, Map.of(name, cachedVal)); - } - if (doUpdateRegs) { - updateRegsMem(null); - } - } - // TODO: This may be too heavy-handed - // listenerForRecord.retroOfferRegMapperDependents(); - } - - protected int getSuccessorFrameLevel(TargetObject successor) { - NavigableSet observedPathLengths = new TreeSet<>(); - for (TargetStackFrame frame : stack.values()) { - observedPathLengths.add(frame.getPath().size()); - } - List path = successor.getPath(); - for (int l : observedPathLengths.descendingSet()) { - if (l > path.size()) { - continue; - } - List sub = path.subList(0, l); - if (!PathUtils.isIndex(sub)) { - continue; - } - int index = Integer.decode(PathUtils.getIndex(sub)); - TargetStackFrame frame = stack.get(index); - if (frame == null || !Objects.equals(sub, frame.getPath())) { - continue; - } - return index; - } - return 0; - } - - CompletableFuture doFetchAndInitRegMapper(TargetRegisterBank bank) { - int frameLevel = getSuccessorFrameLevel(bank); - TargetRegisterContainer descs = bank.getDescriptions(); - if (descs == null) { - Msg.error(this, "Cannot create mapper, yet: Descriptions is null."); - return AsyncUtils.NIL; - } - return initRegMapper(descs).thenAccept(__ -> { - if (frameLevel == 0) { - recordRegisterValues(bank, bank.getCachedRegisters()); - updateRegsMem(null); - } - listeners.fire.registerBankMapped(DefaultTraceRecorder.this); - }).exceptionally(ex -> { - Msg.error(this, "Could not intialize register mapper", ex); - return null; - }); - } - - protected void offerRegisters(TargetRegisterBank newRegs) { - int frameLevel = getSuccessorFrameLevel(newRegs); - if (regs.isEmpty()) { - // TODO: Technically, each frame may need its own mapper.... - doFetchAndInitRegMapper(newRegs); - } - - TargetRegisterBank oldRegs = regs.put(frameLevel, newRegs); - if (oldRegs == newRegs) { - return; - } - - synchronized (accessibilityByRegBank) { - if (oldRegs != null) { - accessibilityByRegBank.remove(oldRegs); - } - accessibilityByRegBank.get(newRegs).exceptionally(e -> { - e = AsyncUtils.unwrapThrowable(e); - Msg.error(this, "Could not track register accessibility: " + e.getMessage()); - return null; - }); - } - } - - protected void offerStackFrame(TargetStackFrame frame) { - stack.put(getFrameLevel(frame), frame); - recordFrame(frame); - } - - protected void offerThreadRegion(TargetMemoryRegion region) { - TargetMemory mem = region.getMemory(); - threadMemory.addRegion(region, mem); - initMemMapper(mem); - // TODO: Add region to trace memory manager (when allowed for threads) - updateRegsMem(region).exceptionally(ex -> { - Msg.error(this, "Could not add thread memory region", ex); - return null; - }); - } - - protected void offerThreadBreakpointContainer(TargetBreakpointContainer bc) { - if (threadBreakpointContainer != null) { - Msg.warn(this, "Thread already has a breakpoint container"); - } - threadBreakpointContainer = bc; - } - - /** - * Inform the recorder the given object is no longer valid - * - * @param invalid the invalidated object - * @return true if this recorder should be invalidated, too - */ - protected synchronized boolean objectRemoved(TargetObject invalid) { - if (checkThreadRemoved(invalid)) { - return true; - } - if (checkRegistersRemoved(invalid)) { - //return false; - // Regs could also be a stack frame - } - if (checkStackFrameRemoved(invalid)) { - return false; - } - if (threadMemory.removeRegion(invalid)) { - return false; - } - Msg.trace(this, "Ignored removed object: " + invalid); - return false; - } - - protected boolean checkThreadRemoved(TargetObject invalid) { - if (targetThread == invalid) { - threadDestroyed(); - return true; - } - return false; - } - - protected boolean checkRegistersRemoved(TargetObject invalid) { - synchronized (accessibilityByRegBank) { - if (regs.values().remove(invalid)) { - accessibilityByRegBank.remove((TargetRegisterBank) invalid); - return true; - } - return false; - } - } - - protected boolean checkStackFrameRemoved(TargetObject invalid) { - if (stack.values().remove(invalid)) { - popStack(); - return true; - } - return false; - } - - protected Address pcFromStack() { - TargetStackFrame frame = stack.get(0); - if (frame == null) { - return null; - } - return frame.getProgramCounter(); - } - - protected boolean checkReadCondition(Address traceAddress) { - /** - * TODO: This heuristic doesn't really belong here, but I have to implement it here so - * that it doesn't "override" the listing's implementation. Once watches are - * implemented, we should be able to drop this garbage. - */ - TraceMemoryRegion region = - memoryManager.getRegionContaining(snapshot.getKey(), traceAddress); - if (region == null) { - return false; - } - if (region.isWrite()) { - return true; - } - Entry ent = - memoryManager.getMostRecentStateEntry(snapshot.getKey(), traceAddress); - if (ent == null) { - return true; - } - if (ent.getValue() == TraceMemoryState.KNOWN) { - return false; - } - return true; - } - - protected CompletableFuture readAlignedConditionally(String name, Address targetAddress, - TargetMemoryRegion limit) { - if (targetAddress == null) { - return AsyncUtils.NIL; - } - Address traceAddress = memMapper.targetToTrace(targetAddress); - if (traceAddress == null) { - return AsyncUtils.NIL; - } - if (!checkReadCondition(traceAddress)) { - return AsyncUtils.NIL; - } - AddressRange targetRange = threadMemory.alignWithOptionalLimit(targetAddress, 1, limit); - if (targetRange == null) { - return AsyncUtils.NIL; - } - TimedMsg.info(this, - " Reading memory at " + name + " (" + targetAddress + " -> " + targetRange + ")"); - // NOTE: Recorder takes data via memoryUpdated callback - // TODO: In that callback, sort out process memory from thread memory? - return threadMemory - .readMemory(targetRange.getMinAddress(), (int) targetRange.getLength()) - .exceptionally(ex -> { - Msg.error(this, "Could not read memory at " + name, ex); - return null; - }); - } - - Address registerValueToTargetAddress(TargetRegister reg, byte[] value) { - /** - * TODO: This goes around the horn and back just to select a default address space. We - * should really just go directly to target address space. - */ - RegisterValue rv = regMapper.targetToTrace(reg, value); - if (rv == null) { - return null; - } - Address traceAddress = trace.getBaseLanguage() - .getDefaultSpace() - .getAddress(rv.getUnsignedValue().longValue()); - return memMapper.traceToTarget(traceAddress); - } - - protected CompletableFuture updateRegsMem(TargetMemoryRegion limit) { - TargetRegisterBank bank; - TargetRegister pc; - TargetRegister sp; - Set toRead = new LinkedHashSet<>(); - synchronized (DefaultTraceRecorder.this) { - if (regMapper == null) { - return AsyncUtils.NIL; - } - bank = regs.get(0); - pc = pcReg; - sp = spReg; - toRead.addAll(extraRegs); - toRead.add(sp); - toRead.add(pc); - } - if (bank == null || pc == null || sp == null) { - return AsyncUtils.NIL; - } - TimedMsg.info(this, "Reading " + toRead + " of " + targetThread); - return bank.readRegisters(toRead).thenCompose(vals -> { - synchronized (DefaultTraceRecorder.this) { - if (memMapper == null) { - return AsyncUtils.NIL; - } - } - if (threadMemory == null) { - return AsyncUtils.NIL; - } - AsyncFence fence = new AsyncFence(); - - Address pcTargetAddr = pcFromStack(); - if (pcTargetAddr == null) { - pcTargetAddr = registerValueToTargetAddress(pcReg, vals.get(pcReg.getIndex())); - } - fence.include(readAlignedConditionally("PC", pcTargetAddr, limit)); - - Address spTargetAddr = - registerValueToTargetAddress(spReg, vals.get(spReg.getIndex())); - fence.include(readAlignedConditionally("SP", spTargetAddr, limit)); - - return fence.ready(); - }).exceptionally(ex -> { - if (LOG_STACK_TRACE) { - Msg.error(this, "Could not read registers", ex); - } - else { - Msg.error(this, "Could not read registers"); - } - return null; - }); - } - - public void stateChanged(final TargetExecutionState newState) { - if (newState == TargetExecutionState.STOPPED) { - updateRegsMem(null); - } - state = newState; - } - - public void threadDestroyed() { - String path = PathUtils.toString(targetThread.getPath()); - try (PermanentTransaction tid = - PermanentTransaction.start(trace, path + " destroyed")) { - // TODO: Should it be key - 1 - // Perhaps, since the thread should not exist - // But it could imply earlier destruction than actually observed - traceThread.setDestructionSnap(snapshot.getKey()); - } - catch (DuplicateNameException e) { - throw new AssertionError(e); // Should be shrinking - } - } - - public void recordRegisterValues(TargetRegisterBank bank, Map updates) { - synchronized (DefaultTraceRecorder.this) { - if (regMapper == null) { - return; - } - } - int frameLevel = getSuccessorFrameLevel(bank); - TimedMsg.info(this, "Reg values changed: " + updates.keySet()); - try (PermanentTransaction tid = PermanentTransaction.start(trace, - "Registers changed in " + PathUtils.toString(bank.getPath()))) { - TraceMemoryRegisterSpace regSpace = - memoryManager.getMemoryRegisterSpace(traceThread, frameLevel, true); - for (Entry ent : updates.entrySet()) { - RegisterValue rv = regMapper.targetToTrace(ent.getKey(), ent.getValue()); - if (rv == null) { - continue; // mapper does not know this register.... - } - regSpace.setValue(snapshot.getKey(), rv); - if (rv.getRegister() == trace.getBaseLanguage().getProgramCounter() && - pcFromStack() == null) { - Address pcTargetAddr = registerValueToTargetAddress(pcReg, ent.getValue()); - readAlignedConditionally("PC", pcTargetAddr, null); // NB: Reports errors - } - if (rv.getRegister() == trace.getBaseCompilerSpec().getStackPointer()) { - Address spTargetAddr = registerValueToTargetAddress(spReg, ent.getValue()); - readAlignedConditionally("SP", spTargetAddr, null); // NB: Reports errors - } - } - } - } - - public void recordFrame(TargetStackFrame frame) { - recordFrame(frame, frame.getProgramCounter()); - } - - public void doRecordFrame(TraceStack traceStack, int frameLevel, Address pc) { - TraceStackFrame traceFrame = traceStack.getFrame(frameLevel, true); - traceFrame.setProgramCounter(pc); - } - - public void recordFrame(TargetStackFrame frame, Address pc) { - synchronized (DefaultTraceRecorder.this) { - if (memMapper == null) { - return; - } - Address tracePc = pc == null ? null : memMapper.targetToTrace(pc); - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Stack frame added")) { - TraceStack traceStack = - stackManager.getStack(traceThread, snapshot.getKey(), true); - doRecordFrame(traceStack, getFrameLevel(frame), tracePc); - } - } - } - - protected int stackDepth() { - return stack.isEmpty() ? 0 : stack.lastKey() + 1; - } - - public void recordStack() { - synchronized (DefaultTraceRecorder.this) { - if (memMapper == null) { - return; - } - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Stack changed")) { - TraceStack traceStack = - stackManager.getStack(traceThread, snapshot.getKey(), true); - traceStack.setDepth(stackDepth(), false); - for (Map.Entry ent : stack.entrySet()) { - Address tracePc = - memMapper.targetToTrace(ent.getValue().getProgramCounter()); - doRecordFrame(traceStack, ent.getKey(), tracePc); - } - } - } - } - - public void popStack() { - synchronized (DefaultTraceRecorder.this) { - try (PermanentTransaction tid = PermanentTransaction.start(trace, "Stack popped")) { - TraceStack traceStack = - stackManager.getStack(traceThread, snapshot.getKey(), true); - traceStack.setDepth(stackDepth(), false); - } - } - } - - public void onThreadBreakpointContainers( - Consumer action) { - if (threadBreakpointContainer == null) { - return; - } - action.accept(threadBreakpointContainer); - } - } - - protected class EffectiveBreakpointResolver { - private final TargetBreakpointLocation bpt; - private TargetBreakpointSpec spec; - private boolean affectsProcess = false; - private final Set threadsAffected = new LinkedHashSet<>(); - - public EffectiveBreakpointResolver(TargetBreakpointLocation bpt) { - this.bpt = bpt; - } - - public CompletableFuture resolve() { - AsyncFence fence = new AsyncFence(); - this.spec = bpt.getSpecification(); - - for (TargetObject ref : bpt.getAffects()) { - if (ref.equals(target)) { - affectsProcess = true; - } - else { - fence.include(resolveThread(ref)); - } - } - return fence.ready(); - } - - // TODO: If affects is empty/null, also try to default to the containing process - private CompletableFuture resolveThread(TargetObject ref) { - return DebugModelConventions.findThread(ref).thenAccept(thread -> { - if (thread == null) { - Msg.error(this, - "Could not find process or thread from breakpoint-affected object: " + ref); - return; - } - if (!ref.equals(thread)) { - Msg.warn(this, "Effective breakpoint should apply to process or threads. Got " + - ref + ". Resolved to " + thread); - return; - } - if (!PathUtils.isAncestor(target.getPath(), thread.getPath())) { - /** - * Perfectly normal if the breakpoint container is outside the process - * container. Don't record such in this trace, though. - */ - return; - } - ThreadRecorder rec = listenerForRecord.getOrCreateThreadRecorder(thread); - synchronized (threadsAffected) { - threadsAffected.add(rec.traceThread); - } - }).exceptionally(ex -> { - Msg.error(this, "Error resolving thread from breakpoint-affected object: " + ref); - return null; - }); - } - - public void applyChecksAndConventions() { - if (affectsProcess && !threadsAffected.isEmpty()) { - Msg.warn(this, "Breakpoint affects process and individual threads?: " + bpt); - threadsAffected.clear(); - } - // Check ancestry for "affects" - if (!affectsProcess && threadsAffected.isEmpty()) { - if (PathUtils.isAncestor(target.getPath(), bpt.getPath())) { - for (ThreadRecorder rec : threadMap.byTargetThread.values()) { - if (PathUtils.isAncestor(rec.targetThread.getPath(), bpt.getPath())) { - threadsAffected.add(rec.traceThread); - break; // Only one thread could be its ancestor - } - } - if (threadsAffected.isEmpty()) { - affectsProcess = true; - } - } - } - } - } - - public class ListenerForRecord extends SubTreeListenerAdapter implements - TargetBreakpointSpecListener, TargetEventScopeListener, TargetExecutionStateListener, - TargetFocusScopeListener, TargetRegisterBankListener, TargetMemoryListener { - - //protected final Map modulesByName = new HashMap<>(); - protected final Set breakpoints = new HashSet<>(); - - @Override - protected boolean checkDescend(TargetObject ref) { - // NOTE, cannot return false on match, since it could be a prefix of another - if (HARDCODED_MATCHER.successorCouldMatch(ref.getPath())) { - return true; - } - return false; - } - - // TODO: Move this into conventions? - protected CompletableFuture findThreadOrProcess(TargetObject successor) { - return new DebugModelConventions.AncestorTraversal(successor) { - @Override - protected Result check(TargetObject obj) { - if (obj.isRoot()) { - return Result.FOUND; - } - if (obj instanceof TargetThread) { - return Result.FOUND; - } - if (obj instanceof TargetProcess) { - return Result.FOUND; - } - return Result.CONTINUE; - } - - @Override - protected TargetObject finish(TargetObject obj) { - return obj; - } - }.start(); - } - - @Override - protected void objectAdded(TargetObject added) { - if (!valid) { - return; - } - if (added instanceof TargetThread) { - getOrCreateThreadRecorder((TargetThread) added); - } - if (added instanceof TargetStack) { - // Actually, this may not matter - } - // Do stack frame first, since bank would be it or child. - // Need frames indexed first to determine level of bank - if (added instanceof TargetStackFrame) { - ThreadRecorder rec = threadMap.getForSuccessor(added); - if (rec == null) { - Msg.error(this, "Frame without thread?: " + added); - } - else { - rec.offerStackFrame((TargetStackFrame) added); - } - } - if (added instanceof TargetRegisterBank) { - ThreadRecorder rec = threadMap.getForSuccessor(added); - if (rec == null) { - Msg.error(this, "Bank without thread?: " + added); - } - else { - rec.offerRegisters((TargetRegisterBank) added); - } - } - if (added instanceof TargetRegisterContainer) { - // These are picked up when a bank is added with these descriptions - } - if (added instanceof TargetRegister) { - TargetRegister reg = (TargetRegister) added; - regMappers.get(reg.getContainer()).thenAccept(rm -> { - rm.targetRegisterAdded(reg); - for (ThreadRecorder rec : threadMap.byTargetThread.values()) { - rec.regMapperAmended(rm, reg, false); - } - }); - } - if (added instanceof TargetMemory) { - initMemMapper((TargetMemory) added); - } - if (added instanceof TargetMemoryRegion) { - TargetMemoryRegion region = (TargetMemoryRegion) added; - findThreadOrProcess(added).thenAccept(obj -> { - if (obj == target) { - offerProcessRegion(region); - return; - } - if (obj instanceof TargetThread) { - ThreadRecorder rec = getOrCreateThreadRecorder((TargetThread) obj); - rec.offerThreadRegion(region); - } - }).exceptionally(ex -> { - Msg.error(this, "Error recording memory region", ex); - return null; - }); - } - if (added instanceof TargetModule) { - TargetModule module = (TargetModule) added; - offerProcessModule(module); - } - if (added instanceof TargetSection) { - TargetSection section = (TargetSection) added; - offerProcessModuleSection(section.getModule(), section); - } - if (added instanceof TargetBreakpointContainer) { - TargetBreakpointContainer breaks = (TargetBreakpointContainer) added; - findThreadOrProcess(added).thenAccept(obj -> { - if (obj == target) { - offerProcessBreakpointContainer(breaks); - return; - } - if (obj.isRoot()) { - return; - } - ThreadRecorder rec = getOrCreateThreadRecorder((TargetThread) obj); - rec.offerThreadBreakpointContainer(breaks); - }).exceptionally(ex -> { - Msg.error(this, "Error recording breakpoint container", ex); - return null; - }); - } - if (added instanceof TargetBreakpointSpec) { - // I don't think this matters. UI for live recording only. - } - if (added instanceof TargetBreakpointLocation) { - TargetBreakpointLocation bpt = (TargetBreakpointLocation) added; - breakpoints.add(bpt); - offerEffectiveBreakpoint(bpt); - } - } - - @Override - protected void objectRemoved(TargetObject removed) { - if (!valid) { - return; - } - if (target == removed) { - stopRecording(); - return; - } - if (removed instanceof TargetRegisterContainer) { - regMappers.remove((TargetRegisterContainer) removed); - } - if (removed instanceof TargetRegister) { - TargetRegister reg = (TargetRegister) removed; - DebuggerRegisterMapper rm = regMappers.getCompletedMap().get(reg.getContainer()); - if (rm == null) { - return; - } - rm.targetRegisterRemoved(reg); - for (ThreadRecorder rec : threadMap.byTargetThread.values()) { - rec.regMapperAmended(rm, reg, true); - } - } - if (removed instanceof TargetMemoryRegion) { - TargetMemoryRegion region = (TargetMemoryRegion) removed; - if (processMemory.removeRegion(region)) { - removeProcessRegion(region); - return; - } - // Allow removal notice to fall through to thread recorders - } - if (removed instanceof TargetModule) { - TargetModule module = (TargetModule) removed; - removeProcessModule(module); - return; - } - if (removed instanceof TargetBreakpointLocation) { - TargetBreakpointLocation bpt = (TargetBreakpointLocation) removed; - breakpoints.remove(bpt); - removeEffectiveBreakpoint(bpt); - return; - } - synchronized (threadMap) { - for (Iterator it = threadMap.recorders().iterator(); it - .hasNext();) { - ThreadRecorder rec = it.next(); - if (rec.objectRemoved(removed)) { - it.remove(); - } - } - } - } - - protected boolean successor(TargetObject ref) { - return PathUtils.isAncestor(target.getPath(), ref.getPath()); - } - - protected boolean anyRef(Collection parameters) { - for (Object p : parameters) { - if (!(p instanceof TargetObject)) { - continue; - } - return true; - } - return false; - } - - protected boolean anySuccessor(Collection parameters) { - for (Object p : parameters) { - if (!(p instanceof TargetObject)) { - continue; - } - TargetObject ref = (TargetObject) p; - if (!successor(ref)) { - continue; - } - return true; - } - return false; - } - - protected boolean eventApplies(TargetObject eventThread, TargetEventType type, - List parameters) { - if (type == TargetEventType.RUNNING) { - return false; - /** - * TODO: Perhaps some configuration for this later. It's kind of interesting to - * record the RUNNING event time, but it gets pedantic when these exist between - * steps. - */ - } - if (eventThread != null) { - return successor(eventThread); - } - if (anyRef(parameters)) { - return anySuccessor(parameters); - } - return true; // Some session-wide event, I suppose - } - - @Override - public void event(TargetEventScope object, TargetThread eventThread, TargetEventType type, - String description, List parameters) { - if (!valid) { - return; - } - TimedMsg.info(this, "Event: " + type + " thread=" + eventThread + " description=" + - description + " params=" + parameters); - // Just use this to step the snaps. Creation/destruction still handled in add/remove - if (!eventApplies(eventThread, type, parameters)) { - return; - } - ThreadRecorder rec = threadMap.get(eventThread); - createSnapshot(description, rec == null ? null : rec.traceThread, null); - - if (type == TargetEventType.THREAD_CREATED) { - if (rec == null) { - return; - } - try (UndoableTransaction tid = - UndoableTransaction.start(trace, "Adjust thread creation", true)) { - rec.traceThread.setCreationSnap(snapshot.getKey()); - } - catch (DuplicateNameException e) { - throw new AssertionError(e); // Should be shrinking - } - } - else if (type == TargetEventType.MODULE_LOADED) { - Object p0 = parameters.get(0); - if (!(p0 instanceof TargetObject)) { - return; - } - TargetObject obj = (TargetObject) p0; - if (!(obj instanceof TargetModule)) { - return; - } - TargetModule mod = (TargetModule) obj; - TraceModule traceModule = getTraceModule(mod); - if (traceModule == null) { - return; - } - try (UndoableTransaction tid = - UndoableTransaction.start(trace, "Adjust module load", true)) { - traceModule.setLoadedSnap(snapshot.getKey()); - } - catch (DuplicateNameException e) { - Msg.error(this, "Could not set module loaded snap", e); - } - } - } - - @Override - public void attributesChanged(TargetObject parent, Collection removed, - Map added) { - super.attributesChanged(parent, removed, added); - if (!valid) { - return; - } - // Dispatch attribute changes which don't have "built-in" events. - if (parent instanceof TargetBreakpointLocation) { - if (added.containsKey(TargetBreakpointLocation.LENGTH_ATTRIBUTE_NAME)) { - breakpointLengthChanged((TargetBreakpointLocation) parent, - (Integer) added.get(TargetBreakpointLocation.LENGTH_ATTRIBUTE_NAME)); - } - } - if (parent instanceof TargetStackFrame) { - if (added.containsKey(TargetStackFrame.PC_ATTRIBUTE_NAME)) { - framePcUpdated((TargetStackFrame) parent); - } - } - if (parent instanceof TargetRegisterBank) { - if (added.containsKey(TargetRegisterBank.DESCRIPTIONS_ATTRIBUTE_NAME)) { - ThreadRecorder rec = threadMap.getForSuccessor(parent); - if (rec != null) { - rec.doFetchAndInitRegMapper((TargetRegisterBank) parent); - } - } - } - // This should be fixed at construction. - /*if (parent instanceof TargetModule) { - if (added.containsKey(TargetModule.BASE_ATTRIBUTE_NAME)) { - moduleBaseUpdated((TargetModule) parent, - (Address) added.get(TargetModule.BASE_ATTRIBUTE_NAME)); - } - }*/ - } - - @Override - public void executionStateChanged(TargetExecutionStateful stateful, - TargetExecutionState state) { - if (!valid) { - return; - } - TimedMsg.info(this, "State " + state + " for " + stateful); - findThreadOrProcess(stateful).thenAccept(threadOrProcess -> { - if (threadOrProcess == target && state == TargetExecutionState.TERMINATED) { - stopRecording(); - return; - } - ThreadRecorder rec = null; - synchronized (threadMap) { - if (threadOrProcess instanceof TargetThread) { - rec = threadMap.get((TargetThread) threadOrProcess); - } - } - if (rec != null) { - rec.stateChanged(state); - } - // Else we'll discover it and sync state later - }); - } - - protected ThreadRecorder getOrCreateThreadRecorder(TargetThread thread) { - synchronized (threadMap) { - ThreadRecorder rec = threadMap.get(thread); - if (rec != null) { - return rec; - } - TraceThread traceThread; - String path = PathUtils.toString(thread.getPath()); - try (PermanentTransaction tid = - PermanentTransaction.start(trace, path + " created")) { - // Note, if THREAD_CREATED is emitted, it will adjust the creation snap - traceThread = threadManager.createThread(path, thread.getShortDisplay(), - snapshot.getKey()); - } - catch (DuplicateNameException e) { - throw new AssertionError(e); // Should be a new thread in model - } - rec = new ThreadRecorder(thread, traceThread); - threadMap.put(rec); - return rec; - } - } - - @Override - public void registersUpdated(TargetRegisterBank bank, Map updates) { - if (!valid) { - return; - } - ThreadRecorder rec = threadMap.getForSuccessor(bank); - if (rec == null) { - return; - } - rec.recordRegisterValues(bank, updates); - } - - @Override - public void memoryUpdated(TargetMemory memory, Address address, byte[] data) { - if (!valid) { - return; - } - synchronized (DefaultTraceRecorder.this) { - if (memMapper == null) { - Msg.warn(this, "Received memory write before a region has been added"); - return; - } - } - Address traceAddr = memMapper.targetToTrace(address); - long snap = snapshot.getKey(); - TimedMsg.info(this, "Memory updated: " + address + " (" + data.length + ")"); - try (PermanentTransaction tid = PermanentTransaction.start(trace, "Memory observed")) { - ByteBuffer newBytes = ByteBuffer.wrap(data); - memoryManager.putBytes(snap, traceAddr, newBytes); - } - } - - @Override - public void memoryReadError(TargetMemory memory, AddressRange range, - DebuggerMemoryAccessException e) { - if (!valid) { - return; - } - Msg.error(this, "Error reading range " + range, e); - Address traceMin = memMapper.targetToTrace(range.getMinAddress()); - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Memory read error")) { - memoryManager.setState(snapshot.getKey(), traceMin, TraceMemoryState.ERROR); - // TODO: Bookmark to describe error? - } - } - - @Override - public void breakpointToggled(TargetBreakpointSpec spec, boolean enabled) { - if (!valid) { - return; - } - spec.getLocations().thenAccept(bpts -> { - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Breakpoint toggled")) { - for (TargetBreakpointLocation eb : bpts) { - TraceBreakpoint traceBpt = getTraceBreakpoint(eb); - if (traceBpt == null) { - String path = PathUtils.toString(eb.getPath()); - Msg.warn(this, "Cannot find toggled trace breakpoint for " + path); - continue; - } - // Verify attributes match? Eh. If they don't, someone has fiddled with it. - traceBpt.splitWithEnabled(snapshot.getKey(), enabled); - } - } - }).exceptionally(ex -> { - Msg.error(this, "Error recording toggled breakpoint spec: " + spec, ex); - return null; - }); - } - - protected void breakpointLengthChanged(TargetBreakpointLocation bpt, int length) { - Address traceAddr = memMapper.targetToTrace(bpt.getAddress()); - String path = PathUtils.toString(bpt.getPath()); - for (TraceBreakpoint traceBpt : breakpointManager.getBreakpointsByPath(path)) { - if (traceBpt.getLength() == length) { - continue; // Nothing to change - } - // TODO: Verify all other attributes match? - // TODO: Should this be allowed to happen? - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Breakpoint length changed")) { - long snap = snapshot.getKey(); - if (traceBpt.getPlacedSnap() == snap) { - traceBpt.delete(); - } - else { - traceBpt.setClearedSnap(snap - 1); - } - breakpointManager.placeBreakpoint(path, snap, range(traceAddr, length), - traceBpt.getThreads(), traceBpt.getKinds(), traceBpt.isEnabled(), - traceBpt.getComment()); - } - catch (DuplicateNameException e) { - throw new AssertionError(e); // Split, and length matters not - } - } - } - - protected void framePcUpdated(TargetStackFrame frame) { - ThreadRecorder rec = threadMap.getForSuccessor(frame); - // Yes, entire stack, otherwise, the stack seems to be just one deep. - rec.recordStack(); - } - - protected void stackUpdated(TargetStack stack) { - ThreadRecorder rec = threadMap.getForSuccessor(stack); - rec.recordStack(); - } - - @Override - public void focusChanged(TargetFocusScope object, TargetObject focused) { - if (!valid) { - return; - } - if (PathUtils.isAncestor(target.getPath(), focused.getPath())) { - curFocus = focused; - } - } - - protected void retroOfferRegMapperDependents() { - List copy; - synchronized (objects) { - copy = List.copyOf(threadMap.byTargetThread.values()); - } - for (ThreadRecorder rec : copy) { - TargetRegisterBank bank = rec.regs.get(0); - if (bank != null) { - rec.recordRegisterValues(bank, bank.getCachedRegisters()); - rec.updateRegsMem(null); - } - } - } - - protected void retroOfferMemMapperDependents() { - List copy; - synchronized (objects) { - copy = List.copyOf(objects.values()); - } - synchronized (DefaultTraceRecorder.this) { - for (TargetObject obj : copy) { - if (obj instanceof TargetModule) { - offerProcessModule((TargetModule) obj); - } - if (obj instanceof TargetSection) { - TargetSection section = (TargetSection) obj; - offerProcessModuleSection(section.getModule(), section); - } - if (obj instanceof TargetBreakpointLocation) { - offerEffectiveBreakpoint((TargetBreakpointLocation) obj); - } - if (obj instanceof TargetStack) { - stackUpdated((TargetStack) obj); - } - } - } - } - - public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { - synchronized (objects) { - return (TargetMemoryRegion) objects.get(PathUtils.parse(region.getPath())); - } - } - - public TargetModule getTargetModule(TraceModule module) { - synchronized (objects) { - return (TargetModule) objects.get(PathUtils.parse(module.getPath())); - } - } - - public TargetSection getTargetSection(TraceSection section) { - synchronized (objects) { - return (TargetSection) objects.get(PathUtils.parse(section.getPath())); - } - } - - public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { - synchronized (objects) { - return (TargetBreakpointLocation) objects.get(PathUtils.parse(bpt.getPath())); - } - } - - public List collectBreakpoints(TargetThread thread) { - synchronized (objects) { - return breakpoints.stream().filter(bpt -> { - TargetObjectList affects = bpt.getAffects(); - // N.B. in case thread is null (process), affects.contains(thread) is always false - return affects.isEmpty() || affects.contains(thread) || - affects.contains(target); - }).collect(Collectors.toList()); - } - } - - protected void onProcessBreakpointContainers( - Consumer action) { - synchronized (objects) { - if (processBreakpointContainer == null) { - for (TargetThread thread : threadsView) { - onThreadBreakpointContainers(thread, action); - } - } - else { - action.accept(processBreakpointContainer); - } - } - } - - protected void onThreadBreakpointContainers(TargetThread thread, - Consumer action) { - synchronized (objects) { - getOrCreateThreadRecorder(thread).onThreadBreakpointContainers(action); - } - } - - protected void onBreakpointContainers(TargetThread thread, - Consumer action) { - if (thread == null) { - onProcessBreakpointContainers(action); - } - else { - onThreadBreakpointContainers(thread, action); - } - } - } - protected final DebuggerModelServicePlugin plugin; protected final PluginTool tool; - protected final Trace trace; protected final TargetObject target; - protected final ComposedMemory processMemory = new ComposedMemory(); - protected TargetBreakpointContainer processBreakpointContainer; + protected final Trace trace; - protected final TraceBreakpointManager breakpointManager; - protected final TraceCodeManager codeManager; - protected final TraceBasedDataTypeManager dataTypeManager; - protected final TraceEquateManager equateManager; - protected final TraceMemoryManager memoryManager; - protected final TraceModuleManager moduleManager; - protected final TraceStackManager stackManager; - protected final TraceSymbolManager symbolManager; - protected final TraceThreadManager threadManager; - protected final TraceTimeManager timeManager; + final RecorderThreadMap threadMap = new RecorderThreadMap(); - protected final AbstractDebuggerTargetTraceMapper mapper; - protected DebuggerMemoryMapper memMapper; - protected AsyncLazyMap regMappers; - protected final TargetDataTypeConverter typeConverter; - protected Collection extraRegs; - // TODO: Support automatic recording of user-specified extra registers... - // NOTE: Probably via watches, once we have those - // TODO: Probably move all the auto-reads into watches + TraceObjectManager objectManager; - protected final ListenerSet listeners = - new ListenerSet<>(TraceRecorderListener.class); - protected final TriConsumer listenerRegAccChanged = - this::registerAccessibilityChanged; - protected final TriConsumer listenerProcMemAccChanged = - this::processMemoryAccessibilityChanged; - - private final ListenerForRecord listenerForRecord; - - protected final ThreadMap threadMap = new ThreadMap(); - protected final Set threadsView = - Collections.unmodifiableSet(threadMap.byTargetThread.keySet()); - protected final BiMap processBreakpointsMap = - HashBiMap.create(); + DefaultBreakpointRecorder breakpointRecorder; + DefaultDataTypeRecorder datatypeRecorder; + DefaultMemoryRecorder memoryRecorder; + DefaultModuleRecorder moduleRecorder; + DefaultProcessRecorder processRecorder; + DefaultSymbolRecorder symbolRecorder; + DefaultTimeRecorder timeRecorder; protected final AsyncLazyValue lazyInit = new AsyncLazyValue<>(this::doInit); - - protected TraceSnapshot snapshot = null; private boolean valid = true; - protected TargetFocusScope focusScope; - protected TargetObject curFocus; - public DefaultTraceRecorder(DebuggerModelServicePlugin plugin, Trace trace, TargetObject target, AbstractDebuggerTargetTraceMapper mapper) { this.plugin = plugin; @@ -1684,371 +78,260 @@ public class DefaultTraceRecorder implements TraceRecorder { this.trace = trace; this.target = target; - this.breakpointManager = trace.getBreakpointManager(); - this.codeManager = trace.getCodeManager(); - this.dataTypeManager = trace.getDataTypeManager(); - this.equateManager = trace.getEquateManager(); - this.memoryManager = trace.getMemoryManager(); - this.moduleManager = trace.getModuleManager(); - this.stackManager = trace.getStackManager(); - this.symbolManager = trace.getSymbolManager(); - this.threadManager = trace.getThreadManager(); - this.timeManager = trace.getTimeManager(); - - this.mapper = mapper; - this.regMappers = - new AsyncLazyMap<>(new HashMap<>(), descs -> mapper.offerRegisters(descs)); - this.typeConverter = new TargetDataTypeConverter(trace.getDataTypeManager()); - - this.listenerForRecord = new ListenerForRecord(); - - processMemory.memAccListeners.add(listenerProcMemAccChanged); + this.processRecorder = new DefaultProcessRecorder(this); + this.breakpointRecorder = new DefaultBreakpointRecorder(this); + this.datatypeRecorder = new DefaultDataTypeRecorder(this); + this.memoryRecorder = new DefaultMemoryRecorder(this); + this.moduleRecorder = new DefaultModuleRecorder(this); + this.symbolRecorder = new DefaultSymbolRecorder(this); + this.timeRecorder = new DefaultTimeRecorder(this); + this.objectManager = new TraceObjectManager(target, mapper, this); trace.addConsumer(this); + System.err.println("constructor for DefaultTraceRecorder complete"); } - protected void registerAccessibilityChanged(boolean old, boolean acc, Void __) { - listeners.fire.registerAccessibilityChanged(this); + /*---------------- OBJECT MANAGER METHODS -------------------*/ + + @Override + public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { + return objectManager.getTargetBreakpoint(bpt); } - protected void processMemoryAccessibilityChanged(boolean old, boolean acc, Void __) { - listeners.fire.processMemoryAccessibilityChanged(this); + @Override + public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { + return objectManager.getTargetMemoryRegion(region); } + @Override + public TargetModule getTargetModule(TraceModule module) { + return objectManager.getTargetModule(module); + } + + @Override + public TargetSection getTargetSection(TraceSection section) { + return objectManager.getTargetSection(section); + } + + @Override + public List collectBreakpointContainers(TargetThread thread) { + List result = new ArrayList<>(); + objectManager.onBreakpointContainers(thread, result::add); + return result; + } + + @Override + public List collectBreakpoints(TargetThread thread) { + return objectManager.collectBreakpoints(thread); + } + + @Override + public Set getSupportedBreakpointKinds() { + Set tKinds = new HashSet<>(); + objectManager.onBreakpointContainers(null, cont -> { + tKinds.addAll(cont.getSupportedBreakpointKinds()); + }); + return TraceRecorder.targetToTraceBreakpointKinds(tKinds); + + } + /*---------------- RECORDER ACCESS METHODS -------------------*/ + + @Override + public TraceBreakpoint getTraceBreakpoint(TargetBreakpointLocation bpt) { + return breakpointRecorder.getTraceBreakpoint(bpt); + } + + @Override + public TraceMemoryRegion getTraceMemoryRegion(TargetMemoryRegion region) { + return memoryRecorder.getTraceMemoryRegion(region); + } + + @Override + public TraceModule getTraceModule(TargetModule module) { + return moduleRecorder.getTraceModule(module); + } + + @Override + public TraceSection getTraceSection(TargetSection section) { + return moduleRecorder.getTraceSection(section); + } + + /*---------------- BY-THREAD OBJECT MANAGER METHODS -------------------*/ + + public ManagedThreadRecorder computeIfAbsent(TargetThread thread) { + AbstractDebuggerObjectModel model = (AbstractDebuggerObjectModel) thread.getModel(); + synchronized (model.lock) { + if (!threadMap.byTargetThread.containsKey(thread)) { + if (objectManager.hasObject(thread)) { + createTraceThread(thread); + } + } + return threadMap.get(thread); + } + } + + public TraceThread createTraceThread(TargetThread thread) { + //System.err.println("createTraceThread " + thread); + String path = PathUtils.toString(thread.getPath()); + // NB. Keep this on service thread, since thread creation must precede any dependent + try (RecorderPermanentTransaction tid = + RecorderPermanentTransaction.start(trace, path + " created")) { + // Note, if THREAD_CREATED is emitted, it will adjust the creation snap + TraceThread tthread = + trace.getThreadManager().createThread(path, thread.getShortDisplay(), getSnap()); + threadMap.put( + new DefaultThreadRecorder(this, objectManager.getMapper(), thread, tthread)); + return tthread; + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be a new thread in model + } + } + + @Override + public TargetThread getTargetThread(TraceThread thread) { + DefaultThreadRecorder rec = getThreadRecorder(thread); + return rec == null ? null : rec.getTargetThread(); + } + + @Override + public TargetExecutionState getTargetThreadState(TargetThread thread) { + DefaultThreadRecorder rec = (DefaultThreadRecorder) getThreadRecorder(thread); + return rec == null ? null : rec.state; + } + + @Override + public TargetExecutionState getTargetThreadState(TraceThread thread) { + DefaultThreadRecorder rec = getThreadRecorder(thread); + return rec == null ? null : rec.state; + } + + @Override + public TargetRegisterBank getTargetRegisterBank(TraceThread thread, int frameLevel) { + DefaultThreadRecorder rec = getThreadRecorder(thread); + return rec.getTargetRegisterBank(thread, frameLevel); + } + + @Override + public TargetStackFrame getTargetStackFrame(TraceThread thread, int frameLevel) { + DefaultThreadRecorder rec = getThreadRecorder(thread); + if (rec == null) { + return null; + } + return rec.getStackRecorder().getTargetStackFrame(frameLevel); + } + + /*---------------- BY-THREAD RECORDER ACCESS METHODS -------------------*/ + + @Override + public TraceThread getTraceThread(TargetThread thread) { + ManagedThreadRecorder rec = getThreadRecorder(thread); + return rec == null ? null : rec.getTraceThread(); + } + + @Override + public TraceThread getTraceThreadForSuccessor(TargetObject successor) { + ManagedThreadRecorder rec = getThreadRecorderForSuccessor(successor); + return rec == null ? null : rec.getTraceThread(); + } + + @Override + public TraceStackFrame getTraceStackFrame(TargetStackFrame frame) { + // THIS IS UNUSED + return null; + } + + @Override + public TraceStackFrame getTraceStackFrameForSuccessor(TargetObject successor) { + ManagedThreadRecorder rec = getThreadRecorderForSuccessor(successor); + if (rec == null) { + return null; + } + ManagedStackRecorder stackRecorder = rec.getStackRecorder(); + int level = stackRecorder.getSuccessorFrameLevel(successor); + return stackRecorder.getTraceStackFrame(rec.getTraceThread(), level); + } + + /*---------------- CAPTURE METHODS -------------------*/ + + @Override + public CompletableFuture captureProcessMemory(AddressSetView set, TaskMonitor monitor) { + if (set.isEmpty()) { + return AsyncUtils.NIL; + } + return memoryRecorder.captureProcessMemory(set, monitor); + } + + @Override + public CompletableFuture captureDataTypes(TargetDataTypeNamespace namespace, + TaskMonitor monitor) { + if (!valid) { + return AsyncUtils.NIL; + } + return datatypeRecorder.captureDataTypes(namespace, monitor); + } + + @Override + public CompletableFuture captureDataTypes(TraceModule module, TaskMonitor monitor) { + TargetModule targetModule = getTargetModule(module); + if (targetModule == null) { + Msg.error(this, "Module " + module + " is not loaded"); + return AsyncUtils.NIL; + } + return datatypeRecorder.captureDataTypes(targetModule, monitor); + } + + @Override + public CompletableFuture captureSymbols(TargetSymbolNamespace namespace, + TaskMonitor monitor) { + if (!valid) { + return AsyncUtils.NIL; + } + return symbolRecorder.captureSymbols(namespace, monitor); + } + + @Override + public CompletableFuture captureSymbols(TraceModule module, TaskMonitor monitor) { + TargetModule targetModule = getTargetModule(module); + if (targetModule == null) { + Msg.error(this, "Module " + module + " is not loaded"); + return AsyncUtils.NIL; + } + return symbolRecorder.captureSymbols(targetModule, monitor); + } + + @Override + public CompletableFuture captureThreadRegisters(TraceThread thread, int frameLevel, + Set registers) { + System.err.println("captureThreadRegisters " + thread); + DefaultThreadRecorder rec = getThreadRecorder(thread); + return rec.captureThreadRegisters(thread, frameLevel, registers); + } + + /*---------------- SNAPSHOT METHODS -------------------*/ + @Override public CompletableFuture init() { return lazyInit.request(); } protected CompletableFuture doInit() { - createSnapshot("Started recording " + PathUtils.toString(target.getPath()) + " in " + - target.getModel(), null, null); - AsyncFence fence = new AsyncFence(); - CompletableFuture futureBreaks = - DebugModelConventions.findSuitable(TargetBreakpointContainer.class, target); - fence.include(futureBreaks.thenAccept(breaks -> { - if (breaks != null && !PathUtils.isAncestor(target.getPath(), breaks.getPath())) { - offerProcessBreakpointContainer(breaks); // instead of objectAdded - listenerForRecord.addListenerAndConsiderSuccessors(breaks); - } - }).exceptionally(e -> { - Msg.error(this, "Could not search for breakpoint container", e); - return null; - })); + timeRecorder.createSnapshot( + "Started recording" + PathUtils.toString(target.getPath()) + " in " + target.getModel(), + null, null); + objectManager.init(); + return AsyncUtils.NIL; - CompletableFuture futureEvents = - DebugModelConventions.findSuitable(TargetEventScope.class, target); - fence.include(futureEvents.thenAccept(events -> { - if (events != null && !PathUtils.isAncestor(target.getPath(), events.getPath())) { - // Don't descend. Scope may be the entire session. - listenerForRecord.addListener(events); - } - }).exceptionally(e -> { - Msg.warn(this, "Could not search for event scope", e); - return null; - })); - - CompletableFuture futureFocus = - DebugModelConventions.findSuitable(TargetFocusScope.class, target); - fence.include(futureFocus.thenAccept(focus -> { - if (focus != null && !PathUtils.isAncestor(target.getPath(), focus.getPath())) { - // Don't descend. Scope may be the entire session. - offerFocusScope(focus); - listenerForRecord.addListener(focus); - } - }).exceptionally(e -> { - Msg.error(this, "Could not search for focus scope", e); - return null; - })); - return fence.ready().thenAccept(__ -> { - listenerForRecord.objectAdded(target); // TODO: This seems wrong - listenerForRecord.addListenerAndConsiderSuccessors(target); - }); - } - - protected synchronized void doAdvanceSnap(String description, TraceThread eventThread) { - snapshot = timeManager.createSnapshot(description); - snapshot.setEventThread(eventThread); - } - - @Override - public TraceSnapshot forceSnapshot() { - createSnapshot("User-forced snapshot", null, null); - return snapshot; - } - - protected void createSnapshot(String description, TraceThread eventThread, - PermanentTransaction tid) { - if (tid != null) { - doAdvanceSnap(description, eventThread); - listeners.fire.snapAdvanced(this, snapshot.getKey()); - return; - } - try (PermanentTransaction tid2 = PermanentTransaction.start(trace, description)) { - doAdvanceSnap(description, eventThread); - } - listeners.fire.snapAdvanced(this, snapshot.getKey()); - } - - // TODO: This could probably be discovered by the offer and passed in at construction - protected synchronized CompletableFuture initMemMapper(TargetMemory memory) { - /** - * TODO: At the moment, there's no real dependency on the memory. When there is, see that - * additional memories can be incorporated into the mapper, and stale ones removed. - * Alternatively, formalize that there is no possible dependency on memory. - */ - if (memMapper != null) { - return AsyncUtils.NIL; - } - return mapper.offerMemory(memory).thenAccept(mm -> { - synchronized (this) { - memMapper = mm; - } - listenerForRecord.retroOfferMemMapperDependents(); - }).exceptionally(ex -> { - Msg.error(this, "Could not intialize memory mapper", ex); - return null; - }); - } - - protected Collection getTraceFlags(TargetMemoryRegion region) { - Collection flags = new HashSet<>(); - if (region.isReadable()) { - flags.add(TraceMemoryFlag.READ); - } - if (region.isWritable()) { - flags.add(TraceMemoryFlag.WRITE); - } - if (region.isExecutable()) { - flags.add(TraceMemoryFlag.EXECUTE); - } - // TODO: Volatile? Can any debugger report that? - return flags; - } - - protected void offerProcessRegion(TargetMemoryRegion region) { - TargetMemory mem = region.getMemory(); - processMemory.addRegion(region, mem); - initMemMapper(mem); - synchronized (this) { - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Memory region added")) { - String path = PathUtils.toString(region.getPath()); - TraceMemoryRegion traceRegion = - memoryManager.getLiveRegionByPath(snapshot.getKey(), path); - if (traceRegion != null) { - Msg.warn(this, "Region " + path + " already recorded"); - return; - } - traceRegion = memoryManager.addRegion(path, Range.atLeast(snapshot.getKey()), - memMapper.targetToTrace(region.getRange()), getTraceFlags(region)); - traceRegion.setName(region.getName()); - } - catch (TraceOverlappedRegionException e) { - Msg.error(this, "Failed to create region due to overlap", e); - } - catch (DuplicateNameException e) { - throw new AssertionError(e); // Just checked for existing - } - } - updateAllThreadsRegsMem(region).exceptionally(ex -> { - Msg.error(this, "Could not add process memory region", ex); - return null; - }); - } - - protected synchronized void removeProcessRegion(TargetMemoryRegion region) { - // Already removed from processMemory. That's how we knew to go here. - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Memory region removed")) { - String path = PathUtils.toString(region.getPath()); - long snap = snapshot.getKey(); - TraceMemoryRegion traceRegion = memoryManager.getLiveRegionByPath(snap, path); - if (traceRegion == null) { - Msg.warn(this, "Could not find region " + path + " in trace to remove"); - return; - } - traceRegion.setDestructionSnap(snap - 1); - } - catch (DuplicateNameException | TraceOverlappedRegionException e) { - throw new AssertionError(e); // Region is shrinking in time - } - } - - protected void recordBreakpoint(TargetBreakpointSpec spec, TargetBreakpointLocation bpt, - Set traceThreads) { - synchronized (this) { - if (memMapper == null) { - throw new IllegalStateException( - "No memory mapper! Have not recorded a region, yet."); - } - } - String path = PathUtils.toString(bpt.getPath()); - String name = nameBreakpoint(bpt); - Address traceAddr = memMapper.targetToTrace(bpt.getAddress()); - AddressRange traceRange = range(traceAddr, bpt.getLength()); - try (PermanentTransaction tid = PermanentTransaction.start(trace, "Breakpoint placed")) { - boolean enabled = spec.isEnabled(); - Set traceKinds = - TraceRecorder.targetToTraceBreakpointKinds(spec.getKinds()); - TraceBreakpoint traceBpt = breakpointManager.placeBreakpoint(path, snapshot.getKey(), - traceRange, traceThreads, traceKinds, enabled, spec.getExpression()); - traceBpt.setName(name); - } - catch (DuplicateNameException e) { - throw new AssertionError(e); // Should be new to model, or already cleared - } - } - - protected void offerProcessBreakpointContainer(TargetBreakpointContainer bc) { - if (processBreakpointContainer != null) { - Msg.warn(this, "Already have a breakpoint container for this process"); - } - processBreakpointContainer = bc; - } - - protected void offerFocusScope(TargetFocusScope scope) { - if (this.focusScope != null) { - Msg.warn(this, "Already have a focus scope: " + this.focusScope); - } - this.focusScope = scope; - } - - protected synchronized TraceModule offerProcessModule(TargetModule module) { - if (memMapper == null) { - return null; - } - - String path = PathUtils.toString(module.getPath()); - TraceModule traceModule = moduleManager.getLoadedModuleByPath(snapshot.getKey(), path); - if (traceModule != null) { - return traceModule; - } - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Module " + path + " loaded")) { - AddressRange targetRange = module.getRange(); - AddressRange traceRange = - targetRange == null ? null : memMapper.targetToTrace(targetRange); - traceModule = moduleManager.addLoadedModule(path, module.getModuleName(), traceRange, - snapshot.getKey()); - return traceModule; - } - catch (DuplicateNameException e) { - throw new AssertionError(e); // We checked for existing by path - } - } - - protected synchronized TraceSection offerProcessModuleSection(TargetModule module, - TargetSection section) { - if (memMapper == null) { - return null; - } - String path = PathUtils.toString(section.getPath()); - TraceModule traceModule = offerProcessModule(module); - TraceSection traceSection = moduleManager.getLoadedSectionByPath(snapshot.getKey(), path); - if (traceSection != null) { - Msg.warn(this, path + " already recorded"); - return traceSection; - } - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Section " + path + " added")) { - AddressRange targetRange = section.getRange(); - AddressRange traceRange = memMapper.targetToTrace(targetRange); - traceSection = traceModule.addSection(path, section.getIndex(), traceRange); - return traceSection; - } - catch (DuplicateNameException e) { - throw new AssertionError(e); // We checked for existing by name - } - } - - protected synchronized void removeProcessModule(TargetModule module) { - String path = PathUtils.toString(module.getPath()); - long snap = snapshot.getKey(); - TraceThread eventThread = snapshot.getEventThread(); - TraceModule traceModule = moduleManager.getLoadedModuleByPath(snap, path); - if (traceModule == null) { - Msg.warn(this, "unloaded " + path + " is not in the trace"); - return; - } - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Module " + path + " unloaded")) { - if (traceModule.getLoadedSnap() == snap) { - Msg.warn(this, "Observed module unload in the same snap as its load"); - createSnapshot("WARN: Module removed", eventThread, tid); - snap = snapshot.getKey(); - } - traceModule.setUnloadedSnap(snap - 1); - } - catch (DuplicateNameException e) { - throw new AssertionError(e); // Module lifespan should be shrinking - } - } - - // NB: No removeProcessModuleSection, because sections should be immutable - // They are removed when the module is removed - - protected void offerEffectiveBreakpoint(TargetBreakpointLocation bpt) { - synchronized (this) { - if (memMapper == null) { - return; - } - } - EffectiveBreakpointResolver resolver = new EffectiveBreakpointResolver(bpt); - resolver.resolve().thenAccept(__ -> { - if (resolver.affectsProcess || !resolver.threadsAffected.isEmpty()) { - recordBreakpoint(resolver.spec, bpt, resolver.threadsAffected); - } - }).exceptionally(ex -> { - Msg.error(this, "Could record target breakpoint: " + bpt, ex); - return null; - }); - } - - protected void removeEffectiveBreakpoint(TargetBreakpointLocation bpt) { - String path = PathUtils.toString(bpt.getPath()); - long snap = snapshot.getKey(); - try (PermanentTransaction tid = PermanentTransaction.start(trace, "Breakpoint deleted")) { - for (TraceBreakpoint traceBpt : breakpointManager.getBreakpointsByPath(path)) { - if (traceBpt.getPlacedSnap() > snap) { - Msg.error(this, - "Tracked, now removed breakpoint was placed in the future? " + bpt); - } - else if (traceBpt.getPlacedSnap() == snap) { - // TODO: I forget if this is allowed for DBTrace iteration - traceBpt.delete(); - } - else { - traceBpt.setClearedSnap(snap - 1); - } - } - } - catch (DuplicateNameException e) { - throw new AssertionError(e); // Lifespan in shrinking - } - } - - protected CompletableFuture updateAllThreadsRegsMem(TargetMemoryRegion limit) { - AsyncFence fence = new AsyncFence(); - for (ThreadRecorder rec : threadMap.recorders()) { - fence.include(rec.updateRegsMem(limit)); - } - return fence.ready(); - } - - @Override - public TargetObject getTarget() { - return target; - } - - @Override - public Trace getTrace() { - return trace; } @Override public long getSnap() { - return snapshot.getKey(); + return timeRecorder.getSnap(); + } + + @Override + public TraceSnapshot forceSnapshot() { + return timeRecorder.forceSnapshot(); } @Override @@ -2059,508 +342,42 @@ public class DefaultTraceRecorder implements TraceRecorder { @Override public void stopRecording() { invalidate(); - listeners.fire.recordingStopped(this); - } - - @Override - public void addListener(TraceRecorderListener l) { - listeners.add(l); - } - - @Override - public void removeListener(TraceRecorderListener l) { - listeners.remove(l); - } - - @Override - public boolean isViewAtPresent(TraceProgramView view) { - if (!valid) { - return false; - } - if (!Objects.equals(trace, view.getTrace())) { - return false; - } - if (snapshot.getKey() != view.getSnap()) { - return false; - } - return true; - } - - @Override - public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { - return listenerForRecord.getTargetBreakpoint(bpt); - } - - @Override - public TraceBreakpoint getTraceBreakpoint(TargetBreakpointLocation bpt) { - String path = PathUtils.toString(bpt.getPath()); - return breakpointManager.getPlacedBreakpointByPath(snapshot.getKey(), path); - } - - @Override - public List collectBreakpointContainers(TargetThread thread) { - List result = new ArrayList<>(); - listenerForRecord.onBreakpointContainers(thread, result::add); - return result; - } - - @Override - public List collectBreakpoints(TargetThread thread) { - return listenerForRecord.collectBreakpoints(thread); - } - - @Override - public Set getSupportedBreakpointKinds() { - Set tKinds = new HashSet<>(); - listenerForRecord.onBreakpointContainers(null, cont -> { - tKinds.addAll(cont.getSupportedBreakpointKinds()); - }); - return TraceRecorder.targetToTraceBreakpointKinds(tKinds); - } - - @Override - public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { - return listenerForRecord.getTargetMemoryRegion(region); - } - - @Override - public TraceMemoryRegion getTraceMemoryRegion(TargetMemoryRegion region) { - String path = PathUtils.toString(region.getPath()); - return memoryManager.getLiveRegionByPath(snapshot.getKey(), path); - } - - @Override - public TargetModule getTargetModule(TraceModule module) { - return listenerForRecord.getTargetModule(module); - } - - @Override - public TraceModule getTraceModule(TargetModule module) { - String path = PathUtils.toString(module.getPath()); - return moduleManager.getLoadedModuleByPath(snapshot.getKey(), path); - } - - @Override - public TargetSection getTargetSection(TraceSection section) { - return listenerForRecord.getTargetSection(section); - } - - @Override - public TraceSection getTraceSection(TargetSection section) { - String path = PathUtils.toString(section.getPath()); - return moduleManager.getLoadedSectionByPath(snapshot.getKey(), path); - } - - @Override - public TargetThread getTargetThread(TraceThread thread) { - ThreadRecorder rec = threadMap.get(thread); - return rec == null ? null : rec.targetThread; - } - - @Override - public TargetExecutionState getTargetThreadState(TargetThread thread) { - ThreadRecorder rec = threadMap.get(thread); - return rec == null ? null : rec.state; - } - - @Override - public TargetExecutionState getTargetThreadState(TraceThread thread) { - ThreadRecorder rec = threadMap.get(thread); - return rec == null ? null : rec.state; - } - - @Override - public boolean isRegisterBankAccessible(TargetRegisterBank bank) { - if (bank == null) { - return false; - } - synchronized (accessibilityByRegBank) { - KeyedFuture future = accessibilityByRegBank.get(bank); - if (future == null) { - return false; - } - AllRequiredAccess acc = future.getNow(null); - if (acc == null) { - return false; - } - return acc.get(); - } - } - - @Override - public boolean isRegisterBankAccessible(TraceThread thread, int frameLevel) { - return isRegisterBankAccessible(getTargetRegisterBank(thread, frameLevel)); - } - - @Override - public TargetRegisterBank getTargetRegisterBank(TraceThread thread, int frameLevel) { - ThreadRecorder rec = threadMap.get(thread); - return rec == null ? null : rec.regs.get(frameLevel); - } - - @Override - public Set getLiveTargetThreads() { - return threadsView; - } - - @Override - public TraceThread getTraceThread(TargetThread thread) { - ThreadRecorder rec = threadMap.byTargetThread.get(thread); - return rec == null ? null : rec.traceThread; - } - - @Override - public TraceThread getTraceThreadForSuccessor(TargetObject successor) { - ThreadRecorder rec = threadMap.getForSuccessor(successor); - return rec == null ? null : rec.traceThread; - } - - protected TraceStackFrame getTraceStackFrame(TraceThread thread, int level) { - TraceStack stack = trace.getStackManager().getLatestStack(thread, snapshot.getKey()); - if (stack == null) { - return null; - } - return stack.getFrame(level, false); - } - - @Override - public TraceStackFrame getTraceStackFrame(TargetStackFrame frame) { - ThreadRecorder rec = threadMap.getForSuccessor(frame); - if (rec == null) { - return null; - } - int level = getFrameLevel(frame); - if (rec.stack.get(level) != frame) { - return null; - } - return getTraceStackFrame(rec.traceThread, level); - } - - @Override - public TraceStackFrame getTraceStackFrameForSuccessor(TargetObject successor) { - ThreadRecorder rec = threadMap.getForSuccessor(successor); - if (rec == null) { - return null; - } - int level = rec.getSuccessorFrameLevel(successor); - return getTraceStackFrame(rec.traceThread, level); - } - - @Override - public TargetStackFrame getTargetStackFrame(TraceThread thread, int frameLevel) { - ThreadRecorder rec = threadMap.get(thread); - if (rec == null) { - return null; - } - return rec.stack.get(frameLevel); - } - - @Override - public DebuggerMemoryMapper getMemoryMapper() { - return memMapper; - } - - @Override - public DebuggerRegisterMapper getRegisterMapper(TraceThread thread) { - ThreadRecorder rec = threadMap.get(thread); - if (rec == null) { - return null; - } - return rec.regMapper; - } - - @Override - public AddressSetView getAccessibleProcessMemory() { - // TODO: Efficiently distinguish which memory is process vs. thread - return getAccessibleMemory(mem -> true); + getListeners().fire.recordingStopped(this); } protected void invalidate() { valid = false; - listenerForRecord.dispose(); + //listenerForRecord.dispose(); trace.release(this); } - protected TraceThread findLiveThreadByName(String name) { - for (TraceThread traceThread : threadManager.getThreadsByPath(name)) { - if (traceThread != null && traceThread.isAlive()) { - return traceThread; - } - } - return null; - } + /*---------------- FOCUS-SUPPORT METHODS -------------------*/ - @Override - public CompletableFuture captureThreadRegisters(TraceThread thread, int frameLevel, - Set registers) { - DebuggerRegisterMapper regMapper = getRegisterMapper(thread); - if (regMapper == null) { - throw new IllegalStateException("Have not found register descriptions for " + thread); - } - if (!regMapper.getRegistersOnTarget().containsAll(registers)) { - throw new IllegalArgumentException( - "All given registers must be recognized by the target"); - } - if (registers.isEmpty()) { - return AsyncUtils.NIL; - } - List tRegs = - registers.stream().map(regMapper::traceToTarget).collect(Collectors.toList()); - - TargetRegisterBank bank = getTargetRegisterBank(thread, frameLevel); - if (bank == null) { - throw new IllegalArgumentException( - "Given thread and frame level does not have a live register bank"); - } - // NOTE: Cache update, if applicable, will cause recorder to write values to trace - return bank.readRegisters(tRegs).thenApply(__ -> null); - } - - @Override - public CompletableFuture writeThreadRegisters(TraceThread thread, int frameLevel, - Map values) { - DebuggerRegisterMapper regMapper = getRegisterMapper(thread); - if (regMapper == null) { - throw new IllegalStateException("Have not found register descriptions for " + thread); - } - if (!regMapper.getRegistersOnTarget().containsAll(values.keySet())) { - throw new IllegalArgumentException( - "All given registers must be recognized by the target"); - } - if (values.isEmpty()) { - return AsyncUtils.NIL; - } - Map tVals = values.entrySet().stream().map(ent -> { - if (ent.getKey() != ent.getValue().getRegister()) { - throw new IllegalArgumentException("register name mismatch in value"); - } - return regMapper.traceToTarget(ent.getValue()); - }).collect(Collectors.toMap(Entry::getKey, Entry::getValue)); - - TargetRegisterBank bank = getTargetRegisterBank(thread, frameLevel); - if (bank == null) { - throw new IllegalArgumentException( - "Given thread and frame level does not have a live register bank"); - } - // NOTE: Model + recorder will cause applicable trace updates - return bank.writeRegistersNamed(tVals).thenApply(__ -> null); - } - - @Override - public CompletableFuture readProcessMemory(Address start, int length) { - Address tStart = memMapper.traceToTarget(start); - return processMemory.readMemory(tStart, length); - } - - @Override - public CompletableFuture writeProcessMemory(Address start, byte[] data) { - Address tStart = memMapper.traceToTarget(start); - return processMemory.writeMemory(tStart, data); - } - - @Override - public CompletableFuture captureProcessMemory(AddressSetView set, TaskMonitor monitor) { - if (set.isEmpty()) { - return AsyncUtils.NIL; - } - // TODO: Figure out how to display/select per-thread memory. - // Probably need a thread parameter passed in then? - // NOTE: That thread memory will already be chained to process memory. Good. - - int total = 0; - AddressSetView expSet = - expandToBlocks(set).intersect(memoryManager.getRegionsAddressSet(snapshot.getKey())); - for (AddressRange r : expSet) { - total += Long.divideUnsigned(r.getLength() + BLOCK_SIZE - 1, BLOCK_SIZE); - } - monitor.initialize(total); - monitor.setMessage("Capturing memory"); - // TODO: Read blocks in parallel? Probably NO. Tends to overload the agent. - return AsyncUtils.each(TypeSpec.VOID, expSet.iterator(), (r, loop) -> { - AddressRangeChunker it = new AddressRangeChunker(r, BLOCK_SIZE); - AsyncUtils.each(TypeSpec.VOID, it.iterator(), (vRng, inner) -> { - // The listener in the recorder will copy to the Trace. - monitor.incrementProgress(1); - AddressRange tRng = memMapper.traceToTarget(vRng); - processMemory.readMemory(tRng.getMinAddress(), (int) tRng.getLength()) - .thenApply(b -> !monitor.isCancelled()) - .handle(inner::repeatWhile); - }).exceptionally(e -> { - Msg.error(this, "Error reading range " + r + ": " + e); - // NOTE: Above may double log, since recorder listens for errors, too - return null; // Continue looping on errors - }).thenApply(v -> !monitor.isCancelled()).handle(loop::repeatWhile); - }); - } - - @Override - public CompletableFuture captureDataTypes(TargetDataTypeNamespace namespace, - TaskMonitor monitor) { - if (!valid) { - return AsyncUtils.NIL; - } - String path = PathUtils.toString(namespace.getPath()); - monitor.setMessage("Capturing data types for " + path); - return namespace.getTypes().thenCompose(types -> { - monitor.initialize(types.size()); - AsyncFence fence = new AsyncFence(); - List converted = new ArrayList<>(); - for (TargetNamedDataType type : types) { - if (monitor.isCancelled()) { - fence.ready().cancel(false); - return AsyncUtils.nil(); - } - monitor.incrementProgress(1); - fence.include(typeConverter.convertTargetDataType(type).thenAccept(converted::add)); - } - return fence.ready().thenApply(__ -> converted); - }).thenAccept(converted -> { - if (converted == null) { - return; - } - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Capture data types for " + path)) { - // NOTE: createCategory is actually getOrCreate - Category category = dataTypeManager.createCategory(new CategoryPath("/" + path)); - for (DataType dataType : converted) { - category.addDataType(dataType, DataTypeConflictHandler.DEFAULT_HANDLER); - } - } - }); - } - - @Override - public CompletableFuture captureDataTypes(TraceModule module, TaskMonitor monitor) { - TargetModule targetModule = getTargetModule(module); - if (targetModule == null) { - Msg.error(this, "Module " + module + " is not loaded"); - return AsyncUtils.NIL; - } - CompletableFuture> future = - targetModule.fetchChildrenSupporting(TargetDataTypeNamespace.class); - // NOTE: I should expect exactly one namespace... - return future.thenCompose(namespaces -> { - AsyncFence fence = new AsyncFence(); - for (TargetDataTypeNamespace ns : namespaces.values()) { - fence.include(captureDataTypes(ns, monitor)); - } - return fence.ready(); - }); - } - - private TraceNamespaceSymbol createNamespaceIfAbsent(String path) { - try { - return symbolManager.namespaces() - .add(path, symbolManager.getGlobalNamespace(), SourceType.IMPORTED); - } - catch (DuplicateNameException e) { - Msg.info(this, "Namespace for module " + path + - " already exists or another exists with a conflicting name. Using the existing one: " + - e); - TraceNamespaceSymbol ns = symbolManager.namespaces().getGlobalNamed(path); - if (ns != null) { - return ns; - } - Msg.error(this, "Existing namespace for " + path + - " is not a plain namespace. Using global namespace."); - return symbolManager.getGlobalNamespace(); - } - catch (InvalidInputException | IllegalArgumentException e) { - Msg.error(this, - "Could not create namespace for new module: " + path + ". Using global namespace.", - e); - return symbolManager.getGlobalNamespace(); - } - } - - @Override - public CompletableFuture captureSymbols(TargetSymbolNamespace namespace, - TaskMonitor monitor) { - if (!valid) { - return AsyncUtils.NIL; - } - String path = PathUtils.toString(namespace.getPath()); - monitor.setMessage("Capturing symbols for " + path); - return namespace.getSymbols().thenAccept(symbols -> { - try (PermanentTransaction tid = - PermanentTransaction.start(trace, "Capture types and symbols for " + path)) { - TraceNamespaceSymbol ns = createNamespaceIfAbsent(path); - monitor.setMessage("Capturing symbols for " + path); - monitor.initialize(symbols.size()); - for (TargetSymbol sym : symbols) { - if (monitor.isCancelled()) { - return; - } - monitor.incrementProgress(1); - String symName = sym.getIndex(); - if (sym.isConstant()) { - // TODO: Equate namespaces? - TraceEquate equate = equateManager.getByName(symName); - long symVal = sym.getValue().getOffset(); - if (equate != null && equate.getValue() == symVal) { - continue; - } - try { - equateManager.create(symName, symVal); - } - catch (DuplicateNameException | IllegalArgumentException e) { - Msg.error(this, "Could not create equate: " + symName, e); - } - continue; - } - Address addr = memMapper.targetToTrace(sym.getValue()); - try { - symbolManager.labels() - .create(snapshot.getKey(), null, addr, symName, ns, - SourceType.IMPORTED); - } - catch (InvalidInputException e) { - Msg.error(this, "Could not add module symbol " + sym + ": " + e); - } - /** - * TODO: Lay down data type, if present - * - * TODO: Interpret "address" type correctly. A symbol with this type is itself - * the pointer. In other words, it is not specifying the type to lay down in - * memory. - */ - } - } - }); - } - - @Override - public CompletableFuture captureSymbols(TraceModule module, TaskMonitor monitor) { - TargetModule targetModule = getTargetModule(module); - if (targetModule == null) { - Msg.error(this, "Module " + module + " is not loaded"); - return AsyncUtils.NIL; - } - CompletableFuture> future = - targetModule.fetchChildrenSupporting(TargetSymbolNamespace.class); - // NOTE: I should expect exactly one namespace... - return future.thenCompose(namespaces -> { - AsyncFence fence = new AsyncFence(); - for (TargetSymbolNamespace ns : namespaces.values()) { - fence.include(captureSymbols(ns, monitor)); - } - return fence.ready(); - }); - } + protected TargetObject curFocus; @Override public boolean isSupportsFocus() { - return focusScope != null; + return findFocusScope() != null; + } + + // NOTE: This may require the scope to be an ancestor of the target + // That should be fine + protected TargetFocusScope findFocusScope() { + List path = target.getModel() + .getRootSchema() + .searchForSuitable(TargetFocusScope.class, target.getPath()); + return (TargetFocusScope) target.getModel().getModelObject(path); } @Override public TargetObject getFocus() { if (curFocus == null) { + TargetFocusScope focusScope = findFocusScope(); if (focusScope == null) { return null; } TargetObject focus = focusScope.getFocus(); - if (focus == null || !PathUtils.isAncestor(target.getPath(), focus.getPath())) { + if (focus == null || !PathUtils.isAncestor(getTarget().getPath(), focus.getPath())) { return null; } curFocus = focus; @@ -2574,11 +391,13 @@ public class DefaultTraceRecorder implements TraceRecorder { return CompletableFuture .failedFuture(new IllegalArgumentException("Target does not support focus")); } - if (!PathUtils.isAncestor(target.getPath(), focus.getPath())) { + if (!PathUtils.isAncestor(getTarget().getPath(), focus.getPath())) { return CompletableFuture.failedFuture(new IllegalArgumentException( "Requested focus path is not a successor of the target")); } + TargetFocusScope focusScope = findFocusScope(); if (!PathUtils.isAncestor(focusScope.getPath(), focus.getPath())) { + // This should be rare, if not forbidden return CompletableFuture.failedFuture(new IllegalArgumentException( "Requested focus path is not a successor of the focus scope")); } @@ -2594,8 +413,137 @@ public class DefaultTraceRecorder implements TraceRecorder { }); } + /*---------------- ACCESSOR METHODS -------------------*/ + @Override - public ListenerForRecord getListenerForRecord() { - return listenerForRecord; + public TargetObject getTarget() { + return target; } + + @Override + public Trace getTrace() { + return trace; + } + + public RecorderThreadMap getThreadMap() { + return threadMap; + } + + public Set getThreadsView() { + return getThreadMap().byTargetThread.keySet(); + } + + // UNUSED? + @Override + public Set getLiveTargetThreads() { + return getThreadsView(); + } + + public DefaultThreadRecorder getThreadRecorder(TraceThread thread) { + return (DefaultThreadRecorder) getThreadMap().get(thread); + } + + public ManagedThreadRecorder getThreadRecorder(TargetThread thread) { + return computeIfAbsent(thread); + } + + public ManagedThreadRecorder getThreadRecorderForSuccessor(TargetObject successor) { + TargetObject obj = successor; + while (obj != null && !(obj instanceof TargetThread)) { + obj = obj.getParent(); + } + if (obj == null) { + return null; + } + return computeIfAbsent((TargetThread) obj); + } + + @Override + public DebuggerMemoryMapper getMemoryMapper() { + return objectManager.getMemoryMapper(); + } + + @Override + public DebuggerRegisterMapper getRegisterMapper(TraceThread thread) { + DefaultThreadRecorder rec = getThreadRecorder(thread); + if (rec == null) { + return null; + } + return rec.getRegisterMapper(); + } + + //public AbstractRecorderRegisterSet getThreadRegisters() { + // return objectManager.getThreadRegisters(); + //} + + /*---------------- LISTENER METHODS -------------------*/ + + // UNUSED? + @Override + public TraceEventListener getListenerForRecord() { + return objectManager.getEventListener(); + } + + @Override + public ListenerSet getListeners() { + return objectManager.getListeners(); + } + + @Override + public void addListener(TraceRecorderListener l) { + getListeners().add(l); + } + + @Override + public void removeListener(TraceRecorderListener l) { + getListeners().remove(l); + } + + /*---------------- DELEGATED METHODS -------------------*/ + + public AbstractRecorderMemory getProcessMemory() { + return processRecorder.getProcessMemory(); + } + + @Override + public AddressSetView getAccessibleProcessMemory() { + return processRecorder.getAccessibleProcessMemory(); + } + + @Override + public CompletableFuture readProcessMemory(Address start, int length) { + return processRecorder.readProcessMemory(start, length); + } + + @Override + public CompletableFuture writeProcessMemory(Address start, byte[] data) { + return processRecorder.writeProcessMemory(start, data); + } + + @Override + public CompletableFuture writeThreadRegisters(TraceThread thread, int frameLevel, + Map values) { + DefaultThreadRecorder rec = getThreadRecorder(thread); + return (rec == null) ? null : rec.writeThreadRegisters(frameLevel, values); + } + + public TraceSnapshot getSnapshot() { + return timeRecorder.getSnapshot(); + } + + public void createSnapshot(String description, TraceThread eventThread, + RecorderPermanentTransaction tid) { + timeRecorder.createSnapshot(description, eventThread, tid); + } + + @Override + public boolean isRegisterBankAccessible(TargetRegisterBank bank) { + return true; + } + + @Override + public boolean isRegisterBankAccessible(TraceThread thread, int frameLevel) { + return true; + } + } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/PermanentTransactionExecutor.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/PermanentTransactionExecutor.java new file mode 100644 index 0000000000..e97565e341 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/PermanentTransactionExecutor.java @@ -0,0 +1,51 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.concurrent.*; +import java.util.function.Function; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; + +import ghidra.app.plugin.core.debug.utils.DefaultTransactionCoalescer; +import ghidra.app.plugin.core.debug.utils.TransactionCoalescer; +import ghidra.app.plugin.core.debug.utils.TransactionCoalescer.CoalescedTx; +import ghidra.framework.model.UndoableDomainObject; +import ghidra.util.Msg; + +public class PermanentTransactionExecutor { + + private final TransactionCoalescer txc; + private final Executor executor; + + public PermanentTransactionExecutor(UndoableDomainObject obj, String name, + Function executorFactory, int delayMs) { + txc = new DefaultTransactionCoalescer<>(obj, RecorderPermanentTransaction::start, delayMs); + this.executor = executorFactory.apply( + new BasicThreadFactory.Builder().namingPattern(name + "-thread-%d").build()); + } + + public void execute(String description, Runnable runnable) { + CompletableFuture.runAsync(() -> { + try (CoalescedTx tx = txc.start(description)) { + runnable.run(); + } + }, executor).exceptionally(e -> { + Msg.error(this, "Trouble recording " + description, e); + return null; + }); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderBreakpointLocationResolver.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderBreakpointLocationResolver.java new file mode 100644 index 0000000000..daf599f887 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderBreakpointLocationResolver.java @@ -0,0 +1,107 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import ghidra.app.plugin.core.debug.service.model.interfaces.ManagedThreadRecorder; +import ghidra.async.AsyncFence; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.target.*; +import ghidra.dbg.util.PathUtils; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.Msg; + +public class RecorderBreakpointLocationResolver { + // TODO: I'm not sure this class really offers anything anymore + + private DefaultTraceRecorder recorder; + private final TargetBreakpointLocation bpt; + private final TargetBreakpointSpec spec; + private boolean affectsProcess = false; + private final Set threadsAffected = new LinkedHashSet<>(); + + public RecorderBreakpointLocationResolver(DefaultTraceRecorder recorder, + TargetBreakpointLocation bpt) { + this.recorder = recorder; + this.bpt = bpt; + this.spec = bpt.getSpecification(); + } + + // TODO: This is a stopgap, since Location.getAffects is removed + // Do we really need to worry about per-thread breakpoints? + static Collection getAffects(TargetBreakpointLocation bpt) { + TargetObject findProc = bpt; + while (!(findProc instanceof TargetProcess)) { + findProc = findProc.getParent(); + } + return List.of(findProc); + } + + private CompletableFuture resolve(TargetObject obj) { + AsyncFence fence = new AsyncFence(); + if (obj.equals(recorder.getTarget())) { + affectsProcess = true; + } + else { + fence.include(resolveThread(obj)); + } + return fence.ready(); + } + + // TODO: If affects is empty/null, also try to default to the containing process + private CompletableFuture resolveThread(TargetObject ref) { + return DebugModelConventions.findThread(ref).thenAccept(thread -> { + if (thread == null) { + Msg.error(this, + "Could not find process or thread from breakpoint-affected object: " + ref); + return; + } + if (!ref.equals(thread)) { + Msg.warn(this, "Effective breakpoint should apply to process or threads. Got " + + ref + ". Resolved to " + thread); + return; + } + if (!PathUtils.isAncestor(recorder.getTarget().getPath(), thread.getPath())) { + /** + * Perfectly normal if the breakpoint container is outside the process container. + * Don't record such in this trace, though. + */ + return; + } + ManagedThreadRecorder rec = recorder.getThreadRecorder(thread); //listenerForRecord.getOrCreateThreadRecorder(thread); + synchronized (threadsAffected) { + threadsAffected.add(rec.getTraceThread()); + } + }).exceptionally(ex -> { + Msg.error(this, "Error resolving thread from breakpoint-affected object: " + ref); + return null; + }); + } + + public void updateBreakpoint(TargetObject containerParent, TargetBreakpointLocation loc) { + resolve(containerParent).thenAccept(__ -> { + if (affectsProcess || !threadsAffected.isEmpty()) { + recorder.breakpointRecorder.recordBreakpoint(loc, threadsAffected); + } + }).exceptionally(ex -> { + Msg.error(this, "Could record target breakpoint: " + loc, ex); + return null; + }); + } + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderComposedMemory.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderComposedMemory.java new file mode 100644 index 0000000000..227eb61138 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderComposedMemory.java @@ -0,0 +1,236 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper; +import ghidra.app.plugin.core.debug.service.model.interfaces.AbstractRecorderMemory; +import ghidra.async.AsyncLazyMap; +import ghidra.async.AsyncUtils; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AllRequiredAccess; +import ghidra.dbg.target.*; +import ghidra.program.model.address.*; +import ghidra.util.Msg; +import ghidra.util.TriConsumer; +import ghidra.util.datastruct.ListenerSet; + +public class RecorderComposedMemory implements AbstractRecorderMemory { + + private static final int BLOCK_SIZE = 4096; + private static final long BLOCK_MASK = -1L << 12; + + protected final RecorderComposedMemory chain; + + protected final NavigableMap byMin = new TreeMap<>(); + + protected final Map byRegion = new HashMap<>(); + protected final AsyncLazyMap accessibilityByMemory = + new AsyncLazyMap<>(new HashMap<>(), this::fetchMemAccessibility) { + public AllRequiredAccess remove(TargetMemory key) { + AllRequiredAccess acc = super.remove(key); + if (acc != null) { + acc.removeChangeListener(getMemAccListeners().fire); + } + return acc; + } + }; + + protected CompletableFuture fetchMemAccessibility(TargetMemory mem) { + return DebugModelConventions.trackAccessibility(mem).thenApply(acc -> { + acc.addChangeListener(getMemAccListeners().fire); + return acc; + }); + } + + /** + * Get accessible memory, as viewed in the trace + * + * @param pred an additional predicate applied via "AND" with accessibility + * @param memMapper target-to-trace mapping utility + * @return the computed set + */ + @Override + public AddressSet getAccessibleMemory(Predicate pred, + DebuggerMemoryMapper memMapper) { + synchronized (accessibilityByMemory) { + // TODO: Might accomplish by using listeners and tracking the accessible set + AddressSet accessible = new AddressSet(); + for (Entry ent : byRegion.entrySet()) { + TargetMemory mem = ent.getValue(); + if (!pred.test(mem)) { + continue; + } + AllRequiredAccess acc = accessibilityByMemory.getCompletedMap().get(mem); + if (acc == null || !acc.getAllAccessibility()) { + continue; + } + accessible.add(memMapper.targetToTrace(ent.getKey().getRange())); + } + return accessible; + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private final ListenerSet> memAccListeners = + new ListenerSet(TriConsumer.class); + + public RecorderComposedMemory(AbstractRecorderMemory memory) { + this.chain = (RecorderComposedMemory) memory; + } + + protected TargetMemory getMemory(Address address, int length) { + Entry floor = findChainedFloor(address); + if (floor == null) { + throw new IllegalArgumentException( + "address " + address + " is not in any known region"); + } + Address max; + try { + max = address.addNoWrap(length - 1); + } + catch (AddressOverflowException e) { + throw new IllegalArgumentException("read extends beyond the address space"); + } + if (!floor.getValue().getRange().contains(max)) { + throw new IllegalArgumentException("read extends beyond a single region"); + } + return byRegion.get(floor.getValue()); + } + + @Override + public void addRegion(TargetMemoryRegion region, TargetMemory memory) { + synchronized (accessibilityByMemory) { + TargetMemory old = byRegion.put(region, memory); + assert old == null; + byMin.put(region.getRange().getMinAddress(), region); + accessibilityByMemory.get(memory).exceptionally(e -> { + e = AsyncUtils.unwrapThrowable(e); + Msg.error(this, "Could not track memory accessibility: " + e.getMessage()); + return null; + }); + } + } + + @Override + public boolean removeRegion(TargetObject invalid) { + if (!(invalid instanceof TargetMemoryRegion)) { + return false; + } + synchronized (accessibilityByMemory) { + TargetMemoryRegion invRegion = (TargetMemoryRegion) invalid; + TargetMemory old = byRegion.remove(invRegion); + assert old != null; + byMin.remove(invRegion.getRange().getMinAddress()); + if (!old.isValid() || !byRegion.containsValue(old)) { + accessibilityByMemory.remove(old); + } + return true; + } + } + + /* + protected AllRequiredAccess findChainedMemoryAccess(TargetMemoryRegion region) { + synchronized (accessibilityByMemory) { + TargetMemory mem = byRegion.get(region); + if (mem != null) { + return accessibilityByMemory.getCompletedMap().get(mem); + } + return chain == null ? null : chain.findChainedMemoryAccess(region); + } + } + */ + + public Entry findChainedFloor(Address address) { + synchronized (accessibilityByMemory) { + Entry myFloor = byMin.floorEntry(address); + Entry byChain = + chain == null ? null : chain.findChainedFloor(address); + if (byChain == null) { + return myFloor; + } + if (myFloor == null) { + return byChain; + } + int c = myFloor.getKey().compareTo(byChain.getKey()); + if (c < 0) { + return byChain; + } + return myFloor; + } + } + + protected AddressRange align(Address address, int length) { + AddressSpace space = address.getAddressSpace(); + long offset = address.getOffset(); + Address start = space.getAddress(offset & BLOCK_MASK); + Address end = space.getAddress(((offset + length - 1) & BLOCK_MASK) + BLOCK_SIZE - 1); + return new AddressRangeImpl(start, end); + } + + protected AddressRange alignWithLimit(Address address, int length, + TargetMemoryRegion limit) { + return align(address, length).intersect(limit.getRange()); + } + + @Override + public AddressRange alignAndLimitToFloor(Address address, int length) { + Entry floor = findChainedFloor(address); + if (floor == null) { + return null; + } + return alignWithLimit(address, length, floor.getValue()); + } + + public AddressRange alignWithOptionalLimit(Address address, int length, + TargetMemoryRegion limit) { + if (limit == null) { + return alignAndLimitToFloor(address, length); + } + return alignWithLimit(address, length, limit); + } + + @Override + public CompletableFuture readMemory(Address address, int length) { + synchronized (accessibilityByMemory) { + TargetMemory mem = getMemory(address, length); + if (mem != null) { + return mem.readMemory(address, length); + } + return CompletableFuture.completedFuture(new byte[0]); + } + } + + @Override + public CompletableFuture writeMemory(Address address, byte[] data) { + synchronized (accessibilityByMemory) { + TargetMemory mem = getMemory(address, data.length); + if (mem != null) { + return mem.writeMemory(address, data); + } + throw new IllegalArgumentException("read starts outside any address space"); + } + } + + public ListenerSet> getMemAccListeners() { + return memAccListeners; + } + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderComposedRegisterSet.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderComposedRegisterSet.java new file mode 100644 index 0000000000..821b8b1269 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderComposedRegisterSet.java @@ -0,0 +1,108 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import ghidra.app.services.TraceRecorder; +import ghidra.async.AsyncLazyMap; +import ghidra.async.AsyncLazyMap.KeyedFuture; +import ghidra.async.AsyncUtils; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AllRequiredAccess; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetRegisterBank; +import ghidra.util.Msg; +import ghidra.util.TriConsumer; + +public class RecorderComposedRegisterSet { + + private TraceRecorder recorder; + + protected final TriConsumer listenerRegAccChanged = + this::registerAccessibilityChanged; + + protected void registerAccessibilityChanged(boolean old, boolean acc, + Void __) { + recorder.getListeners().fire.registerAccessibilityChanged(recorder); + } + + protected final AsyncLazyMap accessibilityByRegBank = + new AsyncLazyMap<>(new HashMap<>(), this::fetchRegAccessibility) { + public AllRequiredAccess remove(TargetRegisterBank key) { + AllRequiredAccess acc = super.remove(key); + if (acc != null) { + acc.removeChangeListener(listenerRegAccChanged); + } + return acc; + } + }; + + protected CompletableFuture fetchRegAccessibility( + TargetRegisterBank bank) { + return DebugModelConventions.trackAccessibility(bank).thenApply(acc -> { + acc.addChangeListener(listenerRegAccChanged); + return acc; + }); + } + + public RecorderComposedRegisterSet(TraceRecorder recorder) { + this.recorder = recorder; + } + + public void updateRegisters(TargetRegisterBank newRegs, TargetRegisterBank oldRegs) { + synchronized (accessibilityByRegBank) { + if (oldRegs != null) { + accessibilityByRegBank.remove(oldRegs); + } + accessibilityByRegBank.get(newRegs).exceptionally(e -> { + e = AsyncUtils.unwrapThrowable(e); + Msg.error(this, "Could not track register accessibility: " + e.getMessage()); + return null; + }); + } + } + + public boolean checkRegistersRemoved(Map regs, + TargetObject invalid) { + synchronized (accessibilityByRegBank) { + if (regs.values().remove(invalid)) { + accessibilityByRegBank.remove((TargetRegisterBank) invalid); + return true; + } + return false; + } + } + + public boolean isRegisterBankAccessible(TargetRegisterBank bank) { + if (bank == null) { + return false; + } + synchronized (accessibilityByRegBank) { + KeyedFuture future = accessibilityByRegBank.get(bank); + if (future == null) { + return false; + } + AllRequiredAccess acc = future.getNow(null); + if (acc == null) { + return false; + } + return acc.get(); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderPermanentTransaction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderPermanentTransaction.java new file mode 100644 index 0000000000..51292926b2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderPermanentTransaction.java @@ -0,0 +1,41 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import ghidra.framework.model.UndoableDomainObject; +import ghidra.util.database.UndoableTransaction; + +public class RecorderPermanentTransaction implements AutoCloseable { + + static RecorderPermanentTransaction start(UndoableDomainObject obj, String description) { + UndoableTransaction tid = UndoableTransaction.start(obj, description, true); + return new RecorderPermanentTransaction(obj, tid); + } + + private final UndoableDomainObject obj; + private final UndoableTransaction tid; + + public RecorderPermanentTransaction(UndoableDomainObject obj, UndoableTransaction tid) { + this.obj = obj; + this.tid = tid; + } + + @Override + public void close() { + tid.close(); + obj.clearUndo(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderSimpleMemory.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderSimpleMemory.java new file mode 100644 index 0000000000..4dd8172115 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderSimpleMemory.java @@ -0,0 +1,126 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.Map.Entry; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper; +import ghidra.app.plugin.core.debug.service.model.interfaces.AbstractRecorderMemory; +import ghidra.dbg.target.*; +import ghidra.program.model.address.*; + +public class RecorderSimpleMemory implements AbstractRecorderMemory { + + private static final int BLOCK_SIZE = 4096; + private static final long BLOCK_MASK = -1L << 12; + + protected final NavigableMap byMin = new TreeMap<>(); + protected TargetMemory memory; + + public RecorderSimpleMemory() { + } + + @Override + public void addRegion(TargetMemoryRegion region, TargetMemory memory) { + synchronized (this) { + if (this.memory == null) { + this.memory = memory; + } + byMin.put(region.getRange().getMinAddress(), region); + } + } + + @Override + public boolean removeRegion(TargetObject invalid) { + if (!(invalid instanceof TargetMemoryRegion)) { + return false; + } + synchronized (this) { + TargetMemoryRegion invRegion = (TargetMemoryRegion) invalid; + byMin.remove(invRegion.getRange().getMinAddress()); + return true; + } + } + + @Override + public CompletableFuture readMemory(Address address, int length) { + synchronized (this) { + if (memory != null) { + return memory.readMemory(address, length); + } + return CompletableFuture.completedFuture(new byte[0]); + } + } + + @Override + public CompletableFuture writeMemory(Address address, byte[] data) { + synchronized (this) { + if (memory != null) { + return memory.writeMemory(address, data); + } + throw new IllegalArgumentException("read starts outside any address space"); + } + } + + /** + * Get accessible memory, as viewed in the trace + * + * @param pred an additional predicate applied via "AND" with accessibility + * @param memMapper target-to-trace mapping utility + * @return the computed set + */ + @Override + public AddressSet getAccessibleMemory(Predicate pred, + DebuggerMemoryMapper memMapper) { + synchronized (this) { + // TODO: Might accomplish by using listeners and tracking the accessible set + AddressSet accessible = new AddressSet(); + if (memMapper != null) { + for (Entry ent : byMin.entrySet()) { + accessible.add(memMapper.targetToTrace(ent.getValue().getRange())); + } + } + return accessible; + } + } + + @Override + public AddressRange alignAndLimitToFloor(Address address, int length) { + Entry floor = findChainedFloor(address); + if (floor == null) { + return null; + } + return align(address, length).intersect(floor.getValue().getRange()); + } + + protected Entry findChainedFloor(Address address) { + synchronized (this) { + return byMin.floorEntry(address); + } + } + + protected AddressRange align(Address address, int length) { + AddressSpace space = address.getAddressSpace(); + long offset = address.getOffset(); + Address start = space.getAddress(offset & BLOCK_MASK); + Address end = space.getAddress(((offset + length - 1) & BLOCK_MASK) + BLOCK_SIZE - 1); + return new AddressRangeImpl(start, end); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderSimpleRegisterSet.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderSimpleRegisterSet.java new file mode 100644 index 0000000000..375191e70d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderSimpleRegisterSet.java @@ -0,0 +1,34 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import ghidra.app.services.TraceRecorder; +import ghidra.dbg.target.TargetRegisterBank; + +public class RecorderSimpleRegisterSet { + + private TraceRecorder recorder; + private TargetRegisterBank bank; + + public RecorderSimpleRegisterSet(TraceRecorder recorder) { + this.recorder = recorder; + } + + public void updateRegisters(TargetRegisterBank newRegs, TargetRegisterBank oldRegs) { + this.bank = newRegs; + } + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderThreadMap.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderThreadMap.java new file mode 100644 index 0000000000..e11ee0ee6c --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/RecorderThreadMap.java @@ -0,0 +1,72 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.*; + +import ghidra.app.plugin.core.debug.service.model.interfaces.ManagedThreadRecorder; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetThread; +import ghidra.trace.model.thread.TraceThread; + +public class RecorderThreadMap { + + protected final NavigableSet observedThreadPathLengths = new TreeSet<>(); + protected final Map byTargetThread = new HashMap<>(); + protected final Map byTraceThread = new HashMap<>(); + + public void put(ManagedThreadRecorder rec) { + observedThreadPathLengths.add(rec.getTargetThread().getPath().size()); + byTargetThread.put(rec.getTargetThread(), rec); + byTraceThread.put(rec.getTraceThread(), rec); + } + + /* + public ManagedThreadRecorder getForSuccessor(TargetObject successor) { + while (successor != null) { + ManagedThreadRecorder rec = byTargetThread.get(successor); + if (rec != null) { + return rec; + } + successor = successor.getParent(); + } + return null; + } + */ + + public ManagedThreadRecorder get(TargetThread thread) { + return byTargetThread.get(thread); + } + + public ManagedThreadRecorder get(TargetObject maybeThread) { + return byTargetThread.get(maybeThread); + } + + public ManagedThreadRecorder get(TraceThread thread) { + return byTraceThread.get(thread); + } + + public void remove(ManagedThreadRecorder rec) { + ManagedThreadRecorder rByTarget = byTargetThread.remove(rec.getTargetThread()); + ManagedThreadRecorder rByTrace = byTraceThread.remove(rec.getTraceThread()); + assert rec == rByTarget; + assert rec == rByTrace; + } + + public Collection recorders() { + return byTargetThread.values(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java new file mode 100644 index 0000000000..60ef9c2d94 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceEventListener.java @@ -0,0 +1,269 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.lang.invoke.MethodHandles; +import java.nio.ByteBuffer; +import java.util.*; + +import ghidra.app.plugin.core.debug.service.model.interfaces.ManagedStackRecorder; +import ghidra.app.plugin.core.debug.service.model.interfaces.ManagedThreadRecorder; +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.util.DebuggerCallbackReorderer; +import ghidra.dbg.util.PathUtils; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.trace.model.memory.TraceMemoryManager; +import ghidra.trace.model.memory.TraceMemoryState; +import ghidra.trace.model.modules.TraceModule; +import ghidra.util.Msg; +import ghidra.util.TimedMsg; +import ghidra.util.exception.DuplicateNameException; + +public class TraceEventListener extends AnnotatedDebuggerAttributeListener { + + private final DefaultTraceRecorder recorder; + private final TargetObject target; + private final Trace trace; + private final TraceMemoryManager memoryManager; + + private boolean valid = true; + protected TargetObject curFocus; + protected final DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this); + + public TraceEventListener(TraceObjectManager collection) { + super(MethodHandles.lookup()); + this.recorder = collection.getRecorder(); + this.target = recorder.getTarget(); + this.trace = recorder.getTrace(); + this.memoryManager = trace.getMemoryManager(); + } + + public void init() { + DebuggerObjectModel model = target.getModel(); + model.addModelListener(reorderer, true); + } + + private boolean successor(TargetObject ref) { + return PathUtils.isAncestor(target.getPath(), ref.getPath()); + } + + private boolean anyRef(Collection parameters) { + for (Object p : parameters) { + if (!(p instanceof TargetObject)) { + continue; + } + return true; + } + return false; + } + + private boolean anySuccessor(Collection parameters) { + for (Object p : parameters) { + if (!(p instanceof TargetObject)) { + continue; + } + TargetObject ref = (TargetObject) p; + if (!successor(ref)) { + continue; + } + return true; + } + return false; + } + + private boolean eventApplies(TargetObject eventThread, TargetEventType type, + List parameters) { + if (type == TargetEventType.RUNNING) { + return false; + /** + * TODO: Perhaps some configuration for this later. It's kind of interesting to record + * the RUNNING event time, but it gets pedantic when these exist between steps. + */ + } + if (eventThread != null) { + return successor(eventThread); + } + if (anyRef(parameters)) { + return anySuccessor(parameters); + } + return true; // Some session-wide event, I suppose + } + + @Override + public void event(TargetObject object, TargetThread eventThread, TargetEventType type, + String description, List parameters) { + if (!valid) { + return; + } + TimedMsg.info(this, "Event: " + type + " thread=" + eventThread + " description=" + + description + " params=" + parameters); + // Just use this to step the snaps. Creation/destruction still handled in add/remove + if (eventThread == null) { + if (!type.equals(TargetEventType.PROCESS_CREATED)) { + Msg.error(this, "Null eventThread for " + type); + } + return; + } + if (!eventApplies(eventThread, type, parameters)) { + return; + } + ManagedThreadRecorder rec = recorder.getThreadRecorder(eventThread); + recorder.createSnapshot(description, rec == null ? null : rec.getTraceThread(), null); + + if (type == TargetEventType.MODULE_LOADED) { + long snap = recorder.getSnap(); + Object p0 = parameters.get(0); + if (!(p0 instanceof TargetModule)) { + return; + } + TargetModule mod = (TargetModule) p0; + recorder.moduleRecorder.tx.execute("Adjust module load", () -> { + TraceModule traceModule = recorder.getTraceModule(mod); + if (traceModule == null) { + return; + } + try { + traceModule.setLoadedSnap(snap); + } + catch (DuplicateNameException e) { + Msg.error(this, "Could not set module loaded snap", e); + } + }); + } + } + + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + public void executionStateChanged(TargetObject stateful, TargetExecutionState state) { + if (!valid) { + return; + } + TimedMsg.info(this, "State " + state + " for " + stateful); + TargetObject x = recorder.objectManager.findThreadOrProcess(stateful); + if (x != null) { + if (x == target && state == TargetExecutionState.TERMINATED) { + recorder.stopRecording(); + return; + } + ManagedThreadRecorder rec = null; + if (x instanceof TargetThread) { + rec = recorder.getThreadRecorder((TargetThread) x); + } + if (rec != null) { + rec.stateChanged(state); + } + // Else we'll discover it and sync state later + } + } + + @Override + public void registersUpdated(TargetObject bank, Map updates) { + if (!valid) { + return; + } + ManagedThreadRecorder rec = recorder.getThreadRecorderForSuccessor(bank); + if (rec != null) { + rec.recordRegisterValues((TargetRegisterBank) bank, updates); + } + } + + @Override + public void memoryUpdated(TargetObject memory, Address address, byte[] data) { + if (!valid) { + return; + } + synchronized (recorder) { + if (recorder.getMemoryMapper() == null) { + Msg.warn(this, "Received memory write before a region has been added"); + return; + } + } + Address traceAddr = recorder.getMemoryMapper().targetToTrace(address); + long snap = recorder.getSnap(); + TimedMsg.info(this, "Memory updated: " + address + " (" + data.length + ")"); + recorder.memoryRecorder.tx.execute("Memory observed", () -> { + memoryManager.putBytes(snap, traceAddr, ByteBuffer.wrap(data)); + }); + } + + @Override + public void memoryReadError(TargetObject memory, AddressRange range, + DebuggerMemoryAccessException e) { + if (!valid) { + return; + } + Msg.error(this, "Error reading range " + range, e); + Address traceMin = recorder.getMemoryMapper().targetToTrace(range.getMinAddress()); + long snap = recorder.getSnap(); + recorder.memoryRecorder.tx.execute("Memory read error", () -> { + memoryManager.setState(snap, traceMin, TraceMemoryState.ERROR); + // TODO: Bookmark to describe error? + }); + } + + @AttributeCallback(TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME) + public void breakpointToggled(TargetObject obj, boolean enabled) { + if (!valid) { + return; + } + TargetBreakpointSpec spec = (TargetBreakpointSpec) obj; + long snap = recorder.getSnap(); + spec.getLocations().thenAccept(bpts -> { + recorder.breakpointRecorder.tx.execute("Breakpoint toggled", () -> { + for (TargetBreakpointLocation eb : bpts) { + TraceBreakpoint traceBpt = recorder.getTraceBreakpoint(eb); + if (traceBpt == null) { + String path = PathUtils.toString(eb.getPath()); + Msg.warn(this, "Cannot find toggled trace breakpoint for " + path); + continue; + } + // Verify attributes match? Eh. If they don't, someone has fiddled with it. + traceBpt.splitWithEnabled(snap, enabled); + } + }); + }).exceptionally(ex -> { + Msg.error(this, "Error recording toggled breakpoint spec: " + spec, ex); + return null; + }); + } + + protected void stackUpdated(TargetStack stack) { + ManagedStackRecorder rec = recorder.getThreadRecorderForSuccessor(stack).getStackRecorder(); + rec.recordStack(); + } + + @AttributeCallback(TargetFocusScope.FOCUS_ATTRIBUTE_NAME) + public void focusChanged(TargetObject scope, TargetObject focused) { + if (!valid) { + return; + } + if (PathUtils.isAncestor(target.getPath(), focused.getPath())) { + curFocus = focused; + } + } + + public RecorderThreadMap getThreadMap() { + return recorder.getThreadMap(); + } + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectListener.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectListener.java new file mode 100644 index 0000000000..03232756f3 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectListener.java @@ -0,0 +1,263 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import ghidra.async.AsyncFence; +import ghidra.dbg.*; +import ghidra.dbg.target.*; +import ghidra.dbg.util.DebuggerCallbackReorderer; +import ghidra.dbg.util.PathUtils.PathComparator; +import ghidra.util.Msg; + +public class TraceObjectListener implements DebuggerModelListener { + + private TraceObjectManager objectManager; + private TargetObject target; + + protected boolean disposed = false; + protected final NavigableMap, TargetObject> initialized = + new TreeMap<>(PathComparator.KEYED); + protected final DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this); + + public TraceObjectListener(TraceObjectManager manager) { + this.objectManager = manager; + this.target = objectManager.getTarget(); + } + + public void init() { + findInitialObjects(target).thenAccept(adds -> { + for (TargetObject added : adds) { + processInit(added); + } + DebuggerObjectModel model = target.getModel(); + model.addModelListener(reorderer, true); + }); + } + + boolean matchesTarget(TargetObject object) { + TargetObject proc = object; + while (proc != null) { + if (proc == target) + return true; + if (proc.getClass().equals(target.getClass())) + return false; + proc = proc.getParent(); + } + return true; + } + + protected void processCreate(TargetObject added) { + if (!objectManager.hasObject(added) && matchesTarget(added)) { + objectManager.addObject(added); + objectManager.createObject(added); + } + /* + else { + Msg.info(this, "processCreate dropped " + added); + } + */ + } + + protected void processInit(TargetObject added) { + if (objectManager.hasObject(added)) { + if (!initialized.containsKey(added.getPath())) { + initialized.put(added.getPath(), added); + objectManager.initObject(added); + } + } + } + + protected void processRemove(TargetObject removed) { + if (objectManager.hasObject(removed)) { + objectManager.removeObject(removed); + objectManager.removeObject(removed.getPath()); + } + } + + protected void processAttributesChanged(TargetObject changed, Map added) { + if (objectManager.hasObject(changed)) { + objectManager.attributesChanged(changed, added); + } + } + + protected void processElementsChanged(TargetObject changed, Map added) { + if (objectManager.hasObject(changed)) { + objectManager.elementsChanged(changed, added); + } + } + + @Override + public void created(TargetObject object) { + //System.err.println("CR:" + object); + processCreate(object); + } + + @Override + public void invalidated(TargetObject object, TargetObject branch, String reason) { + processRemove(object); + } + + @Override + public void attributesChanged(TargetObject parent, Collection removed, + Map added) { + //System.err.println("AC:" + added + ":" + parent); + if (parent.isValid()) { + processInit(parent); + processAttributesChanged(parent, added); + } + } + + @Override + public void elementsChanged(TargetObject parent, Collection removed, + Map added) { + //System.err.println("EC:" + added + ":" + parent); + if (parent.isValid()) { + processElementsChanged(parent, added); + } + } + + public List collectBreakpoints(TargetThread thread) { + synchronized (objectManager.objects) { + return objectManager.collectBreakpoints(thread); + } + } + + protected void onProcessBreakpointContainers( + Consumer action) { + synchronized (objectManager.objects) { + objectManager.onProcessBreakpointContainers(action); + } + } + + protected void onThreadBreakpointContainers(TargetThread thread, + Consumer action) { + synchronized (objectManager.objects) { + objectManager.onThreadBreakpointContainers(thread, action); + } + } + + /* + public boolean addListener(TargetObject obj) { + if (obj == null) { + return false; + } + obj.addListener(this); + synchronized (objects) { + if (objects.put(obj.getPath(), obj) == obj) { + return false; + } + } + return true; + } + + public void dispose() { + synchronized (objects) { + disposed = true; + for (Iterator it = objects.values().iterator(); it.hasNext();) { + TargetObject obj = it.next(); + obj.removeListener(this); + it.remove(); + } + } + } + */ + + private CompletableFuture> findInitialObjects(TargetObject target) { + List result = new ArrayList<>(); + result.add(target); + AsyncFence fence = new AsyncFence(); + CompletableFuture futureEvents = + DebugModelConventions.findSuitable(TargetEventScope.class, target); + fence.include(futureEvents.thenAccept(events -> { + if (events != null) { + result.add(events); + } + }).exceptionally(e -> { + Msg.warn(this, "Could not search for event scope", e); + return null; + })); + CompletableFuture futureFocus = + DebugModelConventions.findSuitable(TargetFocusScope.class, target); + fence.include(futureFocus.thenAccept(focus -> { + if (focus != null) { + // Don't descend. Scope may be the entire session. + result.add(focus); + } + }).exceptionally(e -> { + Msg.error(this, "Could not search for focus scope", e); + return null; + })); + return fence.ready().thenApply(__ -> { + return result; + }); + } + + /* + private CompletableFuture> findDependenciesTop(TargetObject added) { + List result = new ArrayList<>(); + result.add(added); + return findDependencies(added, result); + } + + private CompletableFuture> findDependencies(TargetObject added, + List result) { + //System.err.println("findDependencies " + added); + AsyncFence fence = new AsyncFence(); + fence.include(added.fetchAttributes(false).thenCompose(attrs -> { + AsyncFence af = new AsyncFence(); + for (String key : attrs.keySet()) { //requiredObjKeys) { + Object object = attrs.get(key); + if (!(object instanceof TargetObject)) { + continue; + } + TargetObject ref = (TargetObject) object; + if (PathUtils.isLink(added.getPath(), key, ref.getPath())) { + continue; + } + af.include(ref.fetch().thenCompose(obj -> { + if (!objectManager.isRequired(obj)) { + return CompletableFuture.completedFuture(result); + } + synchronized (result) { + result.add(obj); + } + return findDependencies(obj, result); + })); + } + return af.ready(); + })); + fence.include(added.fetchElements(false).thenCompose(elems -> { + AsyncFence ef = new AsyncFence(); + for (TargetObject ref : elems.values()) { + ef.include(ref.fetch().thenCompose(obj -> { + synchronized (result) { + result.add(obj); + } + return findDependencies(obj, result); + })); + } + return ef.ready(); + })); + return fence.ready().thenApply(__ -> { + return result; + }); + } + */ +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectManager.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectManager.java new file mode 100644 index 0000000000..318751b28c --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceObjectManager.java @@ -0,0 +1,655 @@ +/* ### + * 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.app.plugin.core.debug.service.model; + +import java.math.BigInteger; +import java.util.*; +import java.util.Map.Entry; +import java.util.function.*; +import java.util.stream.Collectors; + +import ghidra.app.plugin.core.debug.mapping.*; +import ghidra.app.plugin.core.debug.service.model.interfaces.*; +import ghidra.app.services.TraceRecorderListener; +import ghidra.async.AsyncLazyMap; +import ghidra.dbg.target.*; +import ghidra.dbg.util.PathUtils; +import ghidra.dbg.util.PathUtils.PathComparator; +import ghidra.program.model.address.Address; +import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.modules.TraceSection; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.Msg; +import ghidra.util.database.UndoableTransaction; +import ghidra.util.datastruct.ListenerSet; +import ghidra.util.exception.DuplicateNameException; + +public class TraceObjectManager { + + private final TargetObject target; + private final TraceEventListener eventListener; + final TraceObjectListener objectListener; + + protected final NavigableMap, TargetObject> objects = + new TreeMap<>(PathComparator.KEYED); + + private DefaultTraceRecorder recorder; + + private AbstractDebuggerTargetTraceMapper mapper; + protected DebuggerMemoryMapper memMapper; + protected AsyncLazyMap regMappers; + //private AbstractRecorderRegisterSet threadRegisters; + + private final ListenerSet listeners = + new ListenerSet<>(TraceRecorderListener.class); + + protected final Set breakpoints = new HashSet<>(); + + // NB: We add the objects in top-down order and initialize them bottom-up + private LinkedHashMap, Function> handlerMapCreate = + new LinkedHashMap<>(); + private LinkedHashMap, Function> handlerMapInit = + new LinkedHashMap<>(); + private LinkedHashMap, Function> handlerMapRemove = + new LinkedHashMap<>(); + private LinkedHashMap, BiFunction, Void>> handlerMapElements = + new LinkedHashMap<>(); + private LinkedHashMap, BiFunction, Void>> handlerMapAttributes = + new LinkedHashMap<>(); + + public TraceObjectManager(TargetObject target, AbstractDebuggerTargetTraceMapper mapper, + DefaultTraceRecorder recorder) { + this.target = target; + this.mapper = mapper; + this.recorder = recorder; + this.regMappers = new AsyncLazyMap<>(new HashMap<>(), ref -> mapper.offerRegisters(ref)); + //this.threadRegisters = new RecorderComposedRegisterSet(recorder); + defaultHandlers(); + this.eventListener = new TraceEventListener(this); + this.objectListener = new TraceObjectListener(this); + //objectListener.addListenerAndConsiderSuccessors(target); + } + + public void init() { + objectListener.init(); + eventListener.init(); + } + + private void defaultHandlers() { + putCreateHandler(TargetThread.class, this::createThread); + putCreateHandler(TargetMemory.class, this::createMemory); + putCreateHandler(TargetRegister.class, this::createRegister); + + putInitHandler(TargetStack.class, this::addStack); + putInitHandler(TargetStackFrame.class, this::addStackFrame); + putInitHandler(TargetRegisterBank.class, this::addRegisterBank); + putInitHandler(TargetRegisterContainer.class, this::addRegisterContainer); + //putInitHandler(TargetMemoryRegion.class, this::addMemoryRegion); + putInitHandler(TargetModule.class, this::addModule); + //putInitHandler(TargetSection.class, this::addSection); // This is brutally expensive + putInitHandler(TargetBreakpointSpecContainer.class, this::addBreakpointContainer); + putInitHandler(TargetBreakpointSpec.class, this::addBreakpointSpec); + putInitHandler(TargetBreakpointLocation.class, this::addBreakpointLocation); + + putElementsHandler(TargetBreakpointLocationContainer.class, + this::elementsChangedBreakpointLocationContainer); + putElementsHandler(TargetMemory.class, this::elementsChangedMemory); + putElementsHandler(TargetSectionContainer.class, this::elementsChangedSectionContainer); + putElementsHandler(TargetStack.class, this::elementsChangedStack); + + putAttributesHandler(TargetBreakpointLocation.class, + this::attributesChangedBreakpointLocation); + putAttributesHandler(TargetRegister.class, this::attributesChangedRegister); + putAttributesHandler(TargetStackFrame.class, this::attributesChangedStackFrame); + + putRemHandler(TargetProcess.class, this::removeProcess); + putRemHandler(TargetThread.class, this::removeThread); + putRemHandler(TargetStack.class, this::removeStack); + putRemHandler(TargetStackFrame.class, this::removeStackFrame); + putRemHandler(TargetStack.class, this::removeRegisterBank); + putRemHandler(TargetRegisterContainer.class, this::removeRegisterContainer); + putRemHandler(TargetRegister.class, this::removeRegister); + putRemHandler(TargetMemory.class, this::removeMemory); + putRemHandler(TargetMemoryRegion.class, this::removeMemoryRegion); + putRemHandler(TargetModule.class, this::removeModule); + putRemHandler(TargetSection.class, this::removeSection); + putRemHandler(TargetBreakpointSpecContainer.class, this::removeBreakpointContainer); + putRemHandler(TargetBreakpointSpec.class, this::removeBreakpointSpec); + putRemHandler(TargetBreakpointLocation.class, this::removeBreakpointLocation); + } + + private Function putHandler(Class key, + Consumer handler, + LinkedHashMap, Function> handlerMap) { + return handlerMap.put(key, (u) -> { + handler.accept(u); + return null; + }); + } + + private BiFunction, Void> putHandler( + Class key, BiConsumer> handler, + LinkedHashMap, BiFunction, Void>> handlerMap) { + return handlerMap.put(key, (u, v) -> { + handler.accept(u, v); + return null; + }); + } + + public Function putCreateHandler(Class key, + Consumer handler) { + return putHandler(key, handler, handlerMapCreate); + } + + public Function putInitHandler(Class key, + Consumer handler) { + return putHandler(key, handler, handlerMapInit); + } + + public Function putRemHandler(Class key, + Consumer handler) { + return putHandler(key, handler, handlerMapRemove); + } + + public BiFunction, Void> putAttributesHandler( + Class key, BiConsumer> handler) { + return putHandler(key, handler, handlerMapAttributes); + } + + public BiFunction, Void> putElementsHandler( + Class key, BiConsumer> handler) { + return putHandler(key, handler, handlerMapElements); + } + + private void processObject(TargetObject targetObject, + LinkedHashMap, Function> handlerMap) { + Set> interfaces = targetObject.getSchema().getInterfaces(); + for (Class ifc : interfaces) { + Function function = handlerMap.get(ifc); + if (function != null) { + function.apply(targetObject); + } + } + } + + private void processObject(TargetObject targetObject, Map map, + LinkedHashMap, BiFunction, Void>> handlerMap) { + Set> interfaces = targetObject.getSchema().getInterfaces(); + for (Class ifc : interfaces) { + BiFunction, ? extends Void> function = handlerMap.get(ifc); + if (function != null) { + function.apply(targetObject, map); + } + } + } + + public void createObject(TargetObject toInit) { + processObject(toInit, handlerMapCreate); + } + + public void initObject(TargetObject added) { + //System.err.println("initObject " + added); + processObject(added, handlerMapInit); + } + + public void removeObject(TargetObject removed) { + processObject(removed, handlerMapRemove); + } + + public void attributesChanged(TargetObject changed, Map added) { + processObject(changed, added, handlerMapAttributes); + } + + public void elementsChanged(TargetObject changed, Map added) { + processObject(changed, added, handlerMapElements); + } + + public boolean isRequired(TargetObject obj) { + if (obj.getName().equals("Debug")) + return true; + if (obj.getName().equals("Stack")) + return true; + + Set> interfaces = obj.getSchema().getInterfaces(); + for (Class ifc : interfaces) { + if (handlerMapInit.keySet().contains(ifc)) { + return true; + } + } + return false; + } + + public void addProcess(TargetObject added) { + // Create a new processRecorder + recorder.init(); + } + + public void removeProcess(TargetObject removed) { + recorder.stopRecording(); + } + + public void createThread(TargetObject added) { + //System.err.println("createThread " + added + ":" + this); + synchronized (recorder.threadMap) { + ManagedThreadRecorder threadRecorder = recorder.getThreadRecorder((TargetThread) added); + TraceThread traceThread = threadRecorder.getTraceThread(); + recorder.createSnapshot(traceThread + " started", traceThread, null); + try (UndoableTransaction tid = + UndoableTransaction.start(recorder.getTrace(), "Adjust thread creation", true)) { + traceThread.setCreationSnap(recorder.getSnap()); + } + catch (DuplicateNameException e) { + throw new AssertionError(e); // Should be shrinking + } + } + } + + public void removeThread(TargetObject removed) { + synchronized (recorder.threadMap) { + ManagedThreadRecorder threadRecorder = + recorder.getThreadRecorder((TargetThread) removed); + threadRecorder.objectRemoved(removed); + } + } + + public void addStack(TargetObject added) { + //addEventListener(added); + } + + public void removeStack(TargetObject removed) { + // Nothing for now + } + + public void addStackFrame(TargetObject added) { + ManagedThreadRecorder rec = recorder.getThreadRecorderForSuccessor(added); + if (rec == null) { + Msg.error(this, "Frame without thread?: " + added); + } + else { + rec.getStackRecorder().offerStackFrame((TargetStackFrame) added); + } + } + + public void removeStackFrame(TargetObject removed) { + synchronized (recorder.threadMap) { + ManagedThreadRecorder threadRecorder = recorder.getThreadRecorderForSuccessor(removed); + threadRecorder.objectRemoved(removed); + } + } + + public void addRegisterBank(TargetObject added) { + ManagedThreadRecorder rec = recorder.getThreadRecorderForSuccessor(added); + rec.offerRegisters((TargetRegisterBank) added); + } + + public void removeRegisterBank(TargetObject removed) { + ManagedThreadRecorder rec = recorder.getThreadRecorderForSuccessor(removed); + rec.removeRegisters((TargetRegisterBank) removed); + } + + public void addRegisterContainer(TargetObject added) { + // These are picked up when a bank is added with these descriptions + } + + public void removeRegisterContainer(TargetObject removed) { + regMappers.remove((TargetRegisterContainer) removed); + } + + public void createRegister(TargetObject added) { + if (added.getCachedAttribute(TargetRegister.CONTAINER_ATTRIBUTE_NAME) != null) { + TargetRegister register = (TargetRegister) added; + regMappers.get(register.getContainer()).thenAccept(rm -> { + if (rm != null) { + rm.targetRegisterAdded(register); + for (ManagedThreadRecorder rec : recorder.threadMap.byTargetThread.values()) { + rec.regMapperAmended(rm, register, false); + } + } + }); + } + } + + public void removeRegister(TargetObject removed) { + TargetRegister register = (TargetRegister) removed; + TargetRegisterContainer cont = register.getContainer(); + DebuggerRegisterMapper rm = regMappers.getCompletedMap().get(cont); + if (rm == null) { + return; + } + rm.targetRegisterRemoved(register); + for (ManagedThreadRecorder rec : recorder.threadMap.byTargetThread.values()) { + rec.regMapperAmended(rm, register, true); + } + } + + public void createMemory(TargetObject added) { + if (memMapper != null) { + return; + } + mapper.offerMemory((TargetMemory) added).thenAccept(mm -> { + synchronized (this) { + memMapper = mm; + //addEventListener(added); + } + //listenerForRecord.retroOfferMemMapperDependents(); + }).exceptionally(ex -> { + Msg.error(this, "Could not intialize memory mapper", ex); + return null; + }); + } + + public void removeMemory(TargetObject removed) { + // Nothing for now + } + + public void addMemoryRegion(TargetObject added) { + /* + TargetMemoryRegion region = (TargetMemoryRegion) added; + findThreadOrProcess(added).thenAccept(obj -> { + if (obj == target) { + recorder.memoryRecorder.offerProcessRegion(region); + return; + } + if (obj instanceof TargetThread) { + ManagedThreadRecorder rec = recorder.getThreadRecorderForSuccessor(added); + rec.offerThreadRegion(region); + } + }).exceptionally(ex -> { + Msg.error(this, "Error recording memory region", ex); + return null; + }); + */ + } + + public void removeMemoryRegion(TargetObject removed) { + recorder.memoryRecorder.removeProcessRegion((TargetMemoryRegion) removed); + } + + public void addModule(TargetObject added) { + recorder.moduleRecorder.offerProcessModule((TargetModule) added); + } + + public void removeModule(TargetObject removed) { + recorder.moduleRecorder.removeProcessModule((TargetModule) removed); + } + + public void addSection(TargetObject added) { + /* + TargetSection section = (TargetSection) added; + TargetModule module = section.getModule(); + recorder.moduleRecorder.offerProcessModuleSection(module, section); + // I hope this should never be a per-thread thing + */ + } + + public void removeSection(TargetObject removed) { + // Nothing for now + } + + public void addBreakpointContainer(TargetObject added) { + TargetObject obj = findThreadOrProcess(added); + if (obj != null) { + ManagedBreakpointRecorder breakpointRecorder = recorder.breakpointRecorder; + if (obj instanceof TargetThread) { + ManagedBreakpointRecorder rec = + recorder.getThreadRecorderForSuccessor(added).getBreakpointRecorder(); + rec.offerBreakpointContainer((TargetBreakpointSpecContainer) added); + return; + } + breakpointRecorder.offerBreakpointContainer((TargetBreakpointSpecContainer) added); + } + else { + Msg.error(this, "Error recording breakpoint container " + added); + } + } + + public void removeBreakpointContainer(TargetObject removed) { + // Nothing for now + } + + public void addBreakpointSpec(TargetObject added) { + // Nothing for now + } + + public void removeBreakpointSpec(TargetObject removed) { + // Nothing for now + } + + public void addBreakpointLocation(TargetObject added) { + // Nothing for now + //breakpoints.add((TargetBreakpointLocation) added); + //recorder.breakpointRecorder.offerEffectiveBreakpoint((TargetBreakpointLocation) added); + } + + public void removeBreakpointLocation(TargetObject removed) { + breakpoints.remove(removed); + recorder.breakpointRecorder.removeBreakpointLocation((TargetBreakpointLocation) removed); + } + + protected TargetObject findThreadOrProcess(TargetObject successor) { + TargetObject object = successor; + while (object != null) { + if (object instanceof TargetProcess) + return object; + if (object instanceof TargetThread) + return object; + object = object.getParent(); + } + return object; + } + + public AbstractDebuggerTargetTraceMapper getMapper() { + return mapper; + } + + public DebuggerMemoryMapper getMemoryMapper() { + return memMapper; + } + + public AsyncLazyMap getRegMappers() { + return regMappers; + } + + public Set getBreakpoints() { + return breakpoints; + } + + public void attributesChangedBreakpointLocation(TargetObject bpt, Map added) { + if (added.containsKey(TargetBreakpointLocation.LENGTH_ATTRIBUTE_NAME)) { + Address traceAddr = recorder.getMemoryMapper() + .targetToTrace(((TargetBreakpointLocation) bpt).getAddress()); + String path = bpt.getJoinedPath("."); + Integer length = (Integer) added.get(TargetBreakpointLocation.LENGTH_ATTRIBUTE_NAME); + recorder.breakpointRecorder.breakpointLengthChanged(length, traceAddr, path); + } + } + + public void attributesChangedRegister(TargetObject parent, Map added) { + if (added.containsKey(TargetRegister.CONTAINER_ATTRIBUTE_NAME)) { + TargetRegister register = (TargetRegister) parent; + regMappers.get(register.getContainer()).thenAccept(rm -> { + rm.targetRegisterAdded(register); + for (ManagedThreadRecorder rec : recorder.threadMap.byTargetThread.values()) { + rec.regMapperAmended(rm, register, false); + } + }); + } + if (added.containsKey(TargetObject.VALUE_ATTRIBUTE_NAME)) { + TargetRegister register = (TargetRegister) parent; + String valstr = (String) added.get(TargetObject.VALUE_ATTRIBUTE_NAME); + byte[] value = new BigInteger(valstr, 16).toByteArray(); + ManagedThreadRecorder rec = recorder.getThreadRecorderForSuccessor(register); + rec.recordRegisterValue(register, value); + } + } + + public void attributesChangedStackFrame(TargetObject frame, Map added) { + if (added.containsKey(TargetStackFrame.PC_ATTRIBUTE_NAME)) { + ManagedThreadRecorder rec = recorder.getThreadRecorderForSuccessor(frame); + if (rec != null) { + rec.getStackRecorder().offerStackFrame((TargetStackFrame) frame); + } + } + } + + public void elementsChangedBreakpointLocationContainer(TargetObject locationContainer, + Map added) { + TargetObject x = findThreadOrProcess(locationContainer); + if (x != null) { + for (Entry entry : added.entrySet()) { + TargetBreakpointLocation loc = (TargetBreakpointLocation) entry.getValue(); + if (loc.isValid()) { + breakpoints.add(loc); + recorder.breakpointRecorder.offerBreakpointLocation(x, loc); + } + } + } + } + + public void elementsChangedMemory(TargetObject memory, Map added) { + // TODO: This should probably only ever be a process + TargetObject threadOrProcess = findThreadOrProcess(memory); + if (threadOrProcess != null) { + for (Object object : added.values()) { + TargetMemoryRegion region = (TargetMemoryRegion) object; + if (!region.isValid()) { + continue; + } + if (threadOrProcess == target) { + recorder.memoryRecorder.offerProcessRegion(region); + } + else if (threadOrProcess instanceof TargetThread) { + ManagedThreadRecorder rec = + recorder.getThreadRecorderForSuccessor(threadOrProcess); + rec.offerThreadRegion(region); + } + } + } + else { + Msg.error(this, "Could not find process/thread for " + memory); + } + } + + public void elementsChangedSectionContainer(TargetObject sectionContainer, + Map added) { + for (Object object : added.values()) { + TargetSection section = (TargetSection) object; + if (!section.isValid()) { + continue; + } + recorder.moduleRecorder.offerProcessModuleSection(section); + } + } + + public void elementsChangedStack(TargetObject stack, Map added) { + ManagedStackRecorder rec = recorder.getThreadRecorderForSuccessor(stack).getStackRecorder(); + rec.recordStack(); + } + + public TargetMemoryRegion getTargetMemoryRegion(TraceMemoryRegion region) { + synchronized (objects) { + return (TargetMemoryRegion) objects.get(PathUtils.parse(region.getPath())); + } + } + + public TargetModule getTargetModule(TraceModule module) { + synchronized (objects) { + return (TargetModule) objects.get(PathUtils.parse(module.getPath())); + } + } + + public TargetSection getTargetSection(TraceSection section) { + synchronized (objects) { + return (TargetSection) objects.get(PathUtils.parse(section.getPath())); + } + } + + public TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt) { + synchronized (objects) { + return (TargetBreakpointLocation) objects.get(PathUtils.parse(bpt.getPath())); + } + } + + public List collectBreakpoints(TargetThread thread) { + return getBreakpoints().stream().collect(Collectors.toList()); + } + + public void onBreakpointContainers(TargetThread thread, + Consumer action) { + if (thread == null) { + objectListener.onProcessBreakpointContainers(action); + } + else { + objectListener.onThreadBreakpointContainers(thread, action); + } + } + + public void onProcessBreakpointContainers( + Consumer action) { + TargetBreakpointSpecContainer breakpointContainer = + recorder.breakpointRecorder.getBreakpointContainer(); + if (breakpointContainer == null) { + for (TargetThread thread : recorder.getThreadsView()) { + objectListener.onThreadBreakpointContainers(thread, action); + } + } + else { + action.accept(breakpointContainer); + } + } + + public void onThreadBreakpointContainers(TargetThread thread, + Consumer action) { + TargetBreakpointSpecContainer breakpointContainer = + recorder.getThreadRecorder(thread).getBreakpointRecorder().getBreakpointContainer(); + if (breakpointContainer == null) { + return; + } + action.accept(breakpointContainer); + } + + // Needed by TraceRecorder + public TraceEventListener getEventListener() { + return eventListener; + } + + public ListenerSet getListeners() { + return listeners; + } + + public TargetObject getTarget() { + return target; + } + + public DefaultTraceRecorder getRecorder() { + return recorder; + } + + public boolean hasObject(TargetObject object) { + return objects.containsKey(object.getPath()); + } + + public void addObject(TargetObject added) { + objects.put(added.getPath(), added); + } + + public void removeObject(List path) { + objects.remove(path); + } + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/AbstractRecorderMemory.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/AbstractRecorderMemory.java new file mode 100644 index 0000000000..9d49785cb2 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/AbstractRecorderMemory.java @@ -0,0 +1,40 @@ +/* ### + * 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.app.plugin.core.debug.service.model.interfaces; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper; +import ghidra.dbg.target.*; +import ghidra.program.model.address.*; + +public interface AbstractRecorderMemory { + + public void addRegion(TargetMemoryRegion region, TargetMemory memory); + + public boolean removeRegion(TargetObject invalid); + + public CompletableFuture readMemory(Address address, int length); + + public CompletableFuture writeMemory(Address address, byte[] data); + + public AddressSet getAccessibleMemory(Predicate pred, + DebuggerMemoryMapper memMapper); + + public AddressRange alignAndLimitToFloor(Address targetAddress, int length); + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/AbstractTraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/AbstractTraceRecorder.java new file mode 100644 index 0000000000..2887617879 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/AbstractTraceRecorder.java @@ -0,0 +1,31 @@ +/* ### + * 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.app.plugin.core.debug.service.model.interfaces; + +import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper; +import ghidra.trace.model.Trace; + +public interface AbstractTraceRecorder { + + public Trace getTrace(); + + public long getSnap(); + + public DebuggerMemoryMapper getMemoryMapper(); + + public ManagedBreakpointRecorder getBreakpointRecorder(); + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedBreakpointRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedBreakpointRecorder.java new file mode 100644 index 0000000000..4de8dd1953 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedBreakpointRecorder.java @@ -0,0 +1,41 @@ +/* ### + * 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.app.plugin.core.debug.service.model.interfaces; + +import java.util.Set; + +import ghidra.dbg.target.*; +import ghidra.program.model.address.Address; +import ghidra.trace.model.breakpoint.TraceBreakpoint; +import ghidra.trace.model.thread.TraceThread; + +public interface ManagedBreakpointRecorder { + + void offerBreakpointContainer(TargetBreakpointSpecContainer added); + + void offerBreakpointLocation(TargetObject target, TargetBreakpointLocation added); + + void recordBreakpoint(TargetBreakpointLocation loc, Set traceThreads); + + void removeBreakpointLocation(TargetBreakpointLocation removed); + + TargetBreakpointSpecContainer getBreakpointContainer(); + + TraceBreakpoint getTraceBreakpoint(TargetBreakpointLocation bpt); + + void breakpointLengthChanged(int length, Address traceAddr, String path); + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedMemoryRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedMemoryRecorder.java new file mode 100644 index 0000000000..3537889458 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedMemoryRecorder.java @@ -0,0 +1,29 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.service.model.interfaces; + +import ghidra.dbg.target.TargetMemoryRegion; +import ghidra.trace.model.memory.TraceMemoryRegion; + +public interface ManagedMemoryRecorder { + + void offerProcessRegion(TargetMemoryRegion region); + + void removeProcessRegion(TargetMemoryRegion region); + + TraceMemoryRegion getTraceMemoryRegion(TargetMemoryRegion region); + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedModuleRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedModuleRecorder.java new file mode 100644 index 0000000000..ad177f6e2e --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedModuleRecorder.java @@ -0,0 +1,35 @@ +/* ### + * 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.app.plugin.core.debug.service.model.interfaces; + +import ghidra.dbg.target.TargetModule; +import ghidra.dbg.target.TargetSection; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.modules.TraceSection; + +public interface ManagedModuleRecorder { + + void offerProcessModule(TargetModule module); + + void offerProcessModuleSection(TargetSection section); + + void removeProcessModule(TargetModule module); + + TraceModule getTraceModule(TargetModule module); + + TraceSection getTraceSection(TargetSection section); + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedProcessRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedProcessRecorder.java new file mode 100644 index 0000000000..337a9f537a --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedProcessRecorder.java @@ -0,0 +1,22 @@ +/* ### + * 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.app.plugin.core.debug.service.model.interfaces; + +public interface ManagedProcessRecorder extends AbstractTraceRecorder { + + public AbstractRecorderMemory getProcessMemory(); + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedStackRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedStackRecorder.java new file mode 100644 index 0000000000..6d495d9b1b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedStackRecorder.java @@ -0,0 +1,35 @@ +/* ### + * 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.app.plugin.core.debug.service.model.interfaces; + +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetStackFrame; +import ghidra.trace.model.stack.TraceStackFrame; +import ghidra.trace.model.thread.TraceThread; + +public interface ManagedStackRecorder { + + void offerStackFrame(TargetStackFrame added); + + void recordStack(); + + int getSuccessorFrameLevel(TargetObject successor); + + TraceStackFrame getTraceStackFrame(TraceThread traceThread, int level); + + TargetStackFrame getTargetStackFrame(int frameLevel); + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedThreadRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedThreadRecorder.java new file mode 100644 index 0000000000..f0a5aad404 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/interfaces/ManagedThreadRecorder.java @@ -0,0 +1,52 @@ +/* ### + * 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.app.plugin.core.debug.service.model.interfaces; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import ghidra.app.plugin.core.debug.mapping.DebuggerRegisterMapper; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.trace.model.thread.TraceThread; + +public interface ManagedThreadRecorder extends AbstractTraceRecorder { + + public TargetThread getTargetThread(); + + public TraceThread getTraceThread(); + + public void offerRegisters(TargetRegisterBank added); + + public void removeRegisters(TargetRegisterBank removed); + + public void offerThreadRegion(TargetMemoryRegion region); + + public void recordRegisterValue(TargetRegister targetRegister, byte[] value); + + public void recordRegisterValues(TargetRegisterBank bank, Map updates); + + public boolean objectRemoved(TargetObject removed); + + public void stateChanged(TargetExecutionState state); + + public void regMapperAmended(DebuggerRegisterMapper rm, TargetRegister reg, boolean b); + + public CompletableFuture doFetchAndInitRegMapper(TargetRegisterBank parent); + + public ManagedStackRecorder getStackRecorder(); + +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java index 64b3b5a2b3..444317cd06 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java @@ -101,15 +101,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } private void threadAdded(TraceThread thread) { + TraceRecorder recorder = current.getRecorder(); + if (supportsFocus(recorder)) { + // TODO: Same for stack frame? I can't imagine it's as common as this.... + if (thread == recorder.getTraceThreadForSuccessor(recorder.getFocus())) { + activate(DebuggerCoordinates.thread(thread)); + } + return; + } if (current.getTrace() != trace) { return; } if (current.getThread() != null) { return; } - if (supportsFocus(current.getRecorder())) { - return; - } activate(DebuggerCoordinates.thread(thread)); } @@ -198,8 +203,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin return t; } - protected CompletableFuture tryHarder(Supplier> action, - int retries, long retryAfterMillis) { + protected CompletableFuture tryHarder(Supplier> action, int retries, + long retryAfterMillis) { Executor exe = CompletableFuture.delayedExecutor(retryAfterMillis, TimeUnit.MILLISECONDS); // NB. thenCompose(f -> f) also ensures exceptions are handled here, not passed through CompletableFuture result = @@ -478,7 +483,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin // Note, not likely we can view non-zero frame with emulated ticks Integer frame = coordinates.getFrame(); if (frame == null) { - if (recorder != null && recorder.isSupportsFocus()) { + if (supportsFocus(recorder)) { TraceStackFrame traceFrame = frameFromTargetFocus(recorder, focus); if (traceFrame == null) { Msg.warn(this, @@ -664,7 +669,9 @@ public class DebuggerTraceManagerServicePlugin extends Plugin if (varView != null) { varView.setSnap(coordinates.getSnap()); } - firePluginEvent(new TraceActivatedPluginEvent(getName(), coordinates)); + Swing.runIfSwingOrRunLater(() -> { + firePluginEvent(new TraceActivatedPluginEvent(getName(), coordinates)); + }); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/DefaultTransactionCoalescer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/DefaultTransactionCoalescer.java new file mode 100644 index 0000000000..cc62ed155d --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/DefaultTransactionCoalescer.java @@ -0,0 +1,99 @@ +/* ### + * 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.app.plugin.core.debug.utils; + +import ghidra.async.AsyncDebouncer; +import ghidra.async.AsyncTimer; +import ghidra.framework.model.UndoableDomainObject; +import ghidra.util.Msg; + +public class DefaultTransactionCoalescer + implements TransactionCoalescer { + + protected class Coalescer { + private final AsyncDebouncer debouncer = + new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, delayMs); + private final U tid; + + private volatile int activeCount = 0; + + public Coalescer(String description) { + this.tid = factory.apply(obj, description); + + debouncer.addListener(this::settled); + } + + private void enter() { + ++activeCount; + } + + private void exit() { + if (--activeCount == 0) { + debouncer.contact(null); + } + } + + private void settled(Void __) { + synchronized (lock) { + if (activeCount == 0) { + try { + tid.close(); + } + catch (Exception e) { + Msg.error(this, "Could not close transaction: ", e); + } + tx = null; + } + } + } + } + + public class DefaultCoalescedTx implements CoalescedTx { + protected DefaultCoalescedTx(String description) { + synchronized (lock) { + if (tx == null) { + tx = new Coalescer(description); + } + tx.enter(); + } + } + + @Override + public void close() { + synchronized (lock) { + tx.exit(); + } + } + } + + protected final Object lock = new Object(); + protected final T obj; + protected final TxFactory factory; + protected final int delayMs; + + protected Coalescer tx; + + public DefaultTransactionCoalescer(T obj, TxFactory factory, int delayMs) { + this.obj = obj; + this.factory = factory; + this.delayMs = delayMs; + } + + @Override + public DefaultCoalescedTx start(String description) { + return new DefaultCoalescedTx(description); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/TransactionCoalescer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/TransactionCoalescer.java index 716cd9f009..1509fd798c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/TransactionCoalescer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/TransactionCoalescer.java @@ -15,51 +15,19 @@ */ package ghidra.app.plugin.core.debug.utils; -import java.util.Deque; -import java.util.LinkedList; +import java.util.function.BiFunction; -import ghidra.async.AsyncDebouncer; -import ghidra.async.AsyncTimer; -import ghidra.program.model.listing.Program; -import ghidra.util.Msg; -import ghidra.util.database.UndoableTransaction; +import ghidra.framework.model.UndoableDomainObject; -public class TransactionCoalescer { - protected final Program program; - protected final AsyncDebouncer debouncer; - - protected final Deque coalesced = new LinkedList<>(); - - public TransactionCoalescer(Program program, int delayWindow) { - this.program = program; - this.debouncer = new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, delayWindow); - - this.debouncer.addListener(v -> processCoalesced()); +public interface TransactionCoalescer { + public interface TxFactory + extends BiFunction { } - protected void processCoalesced() { - try (UndoableTransaction tid = UndoableTransaction.start(program, "Coalesced", false)) { - while (true) { - Runnable next; - synchronized (coalesced) { - next = coalesced.poll(); - } - if (next == null) { - break; - } - next.run(); - } - tid.commit(); - } - catch (Exception e) { - Msg.error(this, "Cancelled coalesced transaction due to exception", e); - } - // TODO: Is this really a good place for this? - program.clearUndo(); + public interface CoalescedTx extends AutoCloseable { + @Override + void close(); } - public synchronized void submit(Runnable runnable) { - coalesced.offer(runnable); - debouncer.contact(null); - } + CoalescedTx start(String description); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java index fedf64ae35..d0d3233653 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java @@ -32,9 +32,7 @@ import ghidra.trace.model.Trace; import ghidra.trace.model.thread.TraceThread; import ghidra.util.datastruct.CollectionChangeListener; -@ServiceInfo( - defaultProvider = DebuggerModelServiceProxyPlugin.class, - description = "Service for managing debug sessions and connections") +@ServiceInfo(defaultProvider = DebuggerModelServiceProxyPlugin.class, description = "Service for managing debug sessions and connections") public interface DebuggerModelService { /** * Get the set of model factories found on the classpath @@ -128,7 +126,7 @@ public interface DebuggerModelService { * @param target the target to record. * @return a future which completes with the recorder, or completes exceptionally */ - CompletableFuture recordTargetBestOffer(TargetObject target); + TraceRecorder recordTargetBestOffer(TargetObject target); /** * Query mapping opinions, prompt the user, and record the given target @@ -143,7 +141,7 @@ public interface DebuggerModelService { * @param target the target to record. * @return a future which completes with the recorder, or completes exceptionally */ - CompletableFuture recordTargetPromptOffers(TargetObject target); + TraceRecorder recordTargetPromptOffers(TargetObject target); /** * Start and open a new trace on the given target diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java index 5a44fbb07a..b16bcd474b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/TraceRecorder.java @@ -21,7 +21,7 @@ import java.util.stream.Collectors; import ghidra.app.plugin.core.debug.mapping.DebuggerMemoryMapper; import ghidra.app.plugin.core.debug.mapping.DebuggerRegisterMapper; -import ghidra.app.plugin.core.debug.service.model.DefaultTraceRecorder.ListenerForRecord; +import ghidra.app.plugin.core.debug.service.model.TraceEventListener; import ghidra.dbg.target.*; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; @@ -36,11 +36,11 @@ import ghidra.trace.model.breakpoint.TraceBreakpointKind; import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.trace.model.modules.TraceModule; import ghidra.trace.model.modules.TraceSection; -import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.stack.TraceStackFrame; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.model.time.TraceTimeManager; +import ghidra.util.datastruct.ListenerSet; import ghidra.util.task.TaskMonitor; /** @@ -75,9 +75,9 @@ public interface TraceRecorder { return TraceBreakpointKind.READ; case WRITE: return TraceBreakpointKind.WRITE; - case EXECUTE: + case HW_EXECUTE: return TraceBreakpointKind.EXECUTE; - case SOFTWARE: + case SW_EXECUTE: return TraceBreakpointKind.SOFTWARE; default: throw new AssertionError(); @@ -110,9 +110,9 @@ public interface TraceRecorder { case WRITE: return TargetBreakpointKind.WRITE; case EXECUTE: - return TargetBreakpointKind.EXECUTE; + return TargetBreakpointKind.HW_EXECUTE; case SOFTWARE: - return TargetBreakpointKind.SOFTWARE; + return TargetBreakpointKind.SW_EXECUTE; default: throw new AssertionError(); } @@ -204,8 +204,6 @@ public interface TraceRecorder { */ void removeListener(TraceRecorderListener listener); - boolean isViewAtPresent(TraceProgramView view); - TargetBreakpointLocation getTargetBreakpoint(TraceBreakpoint bpt); TraceBreakpoint getTraceBreakpoint(TargetBreakpointLocation bpt); @@ -407,7 +405,7 @@ public interface TraceRecorder { * @param thread an optional thread, or {@code null} for the process * @return the list of collected containers, possibly empty */ - List collectBreakpointContainers(TargetThread thread); + List collectBreakpointContainers(TargetThread thread); /** * Collect effective breakpoint pertinent to the target or a given thread @@ -424,8 +422,8 @@ public interface TraceRecorder { /** * Get the kinds of breakpoints supported by any of the recorded breakpoint containers. * - * This is the union of all kinds supported among all {@link TargetBreakpointContainer}s found - * applicable to the target by this recorder. Chances are, there is only one container. + * This is the union of all kinds supported among all {@link TargetBreakpointSpecContainer}s + * found applicable to the target by this recorder. Chances are, there is only one container. * * @return the set of supported kinds */ @@ -478,5 +476,7 @@ public interface TraceRecorder { * @return the listener */ @Internal - ListenerForRecord getListenerForRecord(); + TraceEventListener getListenerForRecord(); + + ListenerSet getListeners(); } diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPluginScreenShots.java index d939613ccd..d5d2d7be11 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsPluginScreenShots.java @@ -34,9 +34,9 @@ import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerService import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin; import ghidra.app.services.*; import ghidra.dbg.model.TestDebuggerModelBuilder; -import ghidra.dbg.target.TargetBreakpointContainer; +import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; -import ghidra.dbg.util.DebuggerModelTestUtils; +import ghidra.dbg.testutil.DebuggerModelTestUtils; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; @@ -131,16 +131,16 @@ public class DebuggerBreakpointsPluginScreenShots extends GhidraScreenShotGenera new ProgramLocation(program, addr(program, 0x00400000)), 0x00010000, false); } - TargetBreakpointContainer bc1 = + TargetBreakpointSpecContainer bc1 = waitFor(() -> Unique.assertAtMostOne(recorder1.collectBreakpointContainers(null)), "No container"); - waitOn(bc1.placeBreakpoint(mb.addr(0x00401234), Set.of(TargetBreakpointKind.SOFTWARE))); + waitOn(bc1.placeBreakpoint(mb.addr(0x00401234), Set.of(TargetBreakpointKind.SW_EXECUTE))); waitOn(bc1.placeBreakpoint(mb.rng(0x00604321, 0x00604324), Set.of(TargetBreakpointKind.WRITE))); - TargetBreakpointContainer bc3 = + TargetBreakpointSpecContainer bc3 = waitFor(() -> Unique.assertAtMostOne(recorder3.collectBreakpointContainers(null)), "No container"); - waitOn(bc3.placeBreakpoint(mb.addr(0x7fac1234), Set.of(TargetBreakpointKind.SOFTWARE))); + waitOn(bc3.placeBreakpoint(mb.addr(0x7fac1234), Set.of(TargetBreakpointKind.SW_EXECUTE))); TraceBreakpoint bpt = waitForValue(() -> Unique.assertAtMostOne( trace3.getBreakpointManager().getBreakpointsAt(0, addr(trace3, 0x7fac1234)))); diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPluginScreenShots.java index 08b778f930..91d39f7c7c 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/objects/DebuggerObjectsPluginScreenShots.java @@ -28,7 +28,7 @@ import ghidra.app.plugin.core.debug.gui.objects.components.*; import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceProxyPlugin; import ghidra.dbg.model.*; import ghidra.dbg.target.*; -import ghidra.dbg.util.DebuggerModelTestUtils; +import ghidra.dbg.testutil.DebuggerModelTestUtils; import ghidra.util.Swing; import help.screenshot.GhidraScreenShotGenerator; diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java index ac8b4c7de2..5d06a5b67e 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerGUITest.java @@ -208,7 +208,7 @@ public abstract class AbstractGhidraHeadedDebuggerGUITest } } - protected static TargetBreakpointContainer getBreakpointContainer(TraceRecorder r) { + protected static TargetBreakpointSpecContainer getBreakpointContainer(TraceRecorder r) { return waitFor(() -> Unique.assertAtMostOne(r.collectBreakpointContainers(null)), "No container"); } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPluginTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPluginTest.java index 86d435e591..b07f81c80a 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPluginTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointMarkerPluginTest.java @@ -45,7 +45,7 @@ import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceTest; import ghidra.app.services.*; import ghidra.app.services.LogicalBreakpoint.Enablement; import ghidra.app.util.viewer.listingpanel.ListingPanel; -import ghidra.dbg.target.TargetBreakpointContainer; +import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.framework.store.LockException; import ghidra.program.disassemble.Disassembler; @@ -100,8 +100,8 @@ public class DebuggerBreakpointMarkerPluginTest extends AbstractGhidraHeadedDebu protected void addLiveMemoryAndBreakpoint(TraceRecorder recorder) throws InterruptedException, ExecutionException, TimeoutException { mb.testProcess1.addRegion("bin:.text", mb.rng(0x55550000, 0x55550fff), "rx"); - TargetBreakpointContainer cont = getBreakpointContainer(recorder); - cont.placeBreakpoint(mb.addr(0x55550123), Set.of(TargetBreakpointKind.SOFTWARE)) + TargetBreakpointSpecContainer cont = getBreakpointContainer(recorder); + cont.placeBreakpoint(mb.addr(0x55550123), Set.of(TargetBreakpointKind.SW_EXECUTE)) .get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderTest.java index 3132bc4189..7c2e8f4743 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/breakpoint/DebuggerBreakpointsProviderTest.java @@ -33,9 +33,10 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import ghidra.app.plugin.core.debug.gui.breakpoint.DebuggerBreakpointsProvider.LogicalBreakpointTableModel; import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceTest; import ghidra.app.services.*; +import ghidra.async.AsyncTestUtils; import ghidra.dbg.model.TestTargetProcess; -import ghidra.dbg.target.TargetBreakpointContainer; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.framework.store.LockException; import ghidra.program.model.address.AddressOverflowException; import ghidra.program.model.listing.Program; @@ -50,7 +51,8 @@ import ghidra.util.exception.CancelledException; import ghidra.util.exception.DuplicateNameException; import ghidra.util.task.TaskMonitor; -public class DebuggerBreakpointsProviderTest extends AbstractGhidraHeadedDebuggerGUITest { +public class DebuggerBreakpointsProviderTest extends AbstractGhidraHeadedDebuggerGUITest + implements AsyncTestUtils { protected static final long TIMEOUT_MILLIS = SystemUtilities.isInTestingBatchMode() ? 5000 : Long.MAX_VALUE; @@ -84,14 +86,13 @@ public class DebuggerBreakpointsProviderTest extends AbstractGhidraHeadedDebugge } protected void addLiveBreakpoint(TraceRecorder recorder, long offset) throws Exception { - TargetBreakpointContainer cont = getBreakpointContainer(recorder); - cont.placeBreakpoint(mb.addr(offset), Set.of(TargetBreakpointKind.SOFTWARE)) + TargetBreakpointSpecContainer cont = getBreakpointContainer(recorder); + cont.placeBreakpoint(mb.addr(offset), Set.of(TargetBreakpointKind.SW_EXECUTE)) .get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } - protected void addStaticMemoryAndBreakpoint() - throws LockException, DuplicateNameException, MemoryConflictException, - AddressOverflowException, CancelledException { + protected void addStaticMemoryAndBreakpoint() throws LockException, DuplicateNameException, + MemoryConflictException, AddressOverflowException, CancelledException { try (UndoableTransaction tid = UndoableTransaction.start(program, "Add bookmark break", true)) { program.getMemory() @@ -114,7 +115,7 @@ public class DebuggerBreakpointsProviderTest extends AbstractGhidraHeadedDebugge } @Test - public void testAddLiveOpenTracePopulatesProvider() throws Exception { + public void testAddLiveOpenTracePopulatesProvider() throws Throwable { createTestModel(); mb.createTestProcessesAndThreads(); TraceRecorder recorder = modelService.recordTarget(mb.testProcess1, @@ -122,6 +123,7 @@ public class DebuggerBreakpointsProviderTest extends AbstractGhidraHeadedDebugge Trace trace = recorder.getTrace(); addLiveMemoryAndBreakpoint(mb.testProcess1, recorder); + waitOn(mb.testModel.flushEvents()); waitForDomainObject(trace); // NB, optionally open trace. Mapping only works if open... @@ -514,8 +516,7 @@ public class DebuggerBreakpointsProviderTest extends AbstractGhidraHeadedDebugge assertEquals(2, lb2.getTraceBreakpoints().size()); }); - List breakData = - bptModel.getModelData(); + List breakData = bptModel.getModelData(); List filtLocs = breakpointsProvider.locationFilterPanel.getTableFilterModel().getModelData(); @@ -574,8 +575,7 @@ public class DebuggerBreakpointsProviderTest extends AbstractGhidraHeadedDebugge } public static final Set POPUP_ACTIONS = Set.of( - AbstractEnableSelectedBreakpointsAction.NAME, - AbstractDisableSelectedBreakpointsAction.NAME, + AbstractEnableSelectedBreakpointsAction.NAME, AbstractDisableSelectedBreakpointsAction.NAME, AbstractClearSelectedBreakpointsAction.NAME); @Test @@ -590,10 +590,10 @@ public class DebuggerBreakpointsProviderTest extends AbstractGhidraHeadedDebugge // NOTE: the row becomes selected by right-click clickTableCellWithButton(breakpointsProvider.breakpointTable, 0, 0, MouseEvent.BUTTON3); waitForSwing(); - assertMenu(POPUP_ACTIONS, Set.of( - AbstractEnableSelectedBreakpointsAction.NAME, - AbstractDisableSelectedBreakpointsAction.NAME, - AbstractClearSelectedBreakpointsAction.NAME)); + assertMenu(POPUP_ACTIONS, + Set.of(AbstractEnableSelectedBreakpointsAction.NAME, + AbstractDisableSelectedBreakpointsAction.NAME, + AbstractClearSelectedBreakpointsAction.NAME)); // NOTE: With no selection, no actions (even table built-in) apply, so no menu } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPluginTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPluginTest.java index d6d7ddb496..329244c7d0 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPluginTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/interpreters/DebuggerInterpreterPluginTest.java @@ -29,7 +29,7 @@ import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.interpreter.InterpreterComponentProvider; import ghidra.dbg.model.TestTargetInterpreter.ExecuteCall; import ghidra.dbg.target.TargetConsole.Channel; -import ghidra.dbg.util.DebuggerModelTestUtils; +import ghidra.dbg.testutil.DebuggerModelTestUtils; public class DebuggerInterpreterPluginTest extends AbstractGhidraHeadedDebuggerGUITest implements DebuggerModelTestUtils { diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProviderTest.java index c02f4aca84..18ceae5375 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/register/DebuggerRegistersProviderTest.java @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.debug.gui.register; -import static ghidra.lifecycle.Unfinished.TODO; +import static ghidra.lifecycle.Unfinished.*; import static org.junit.Assert.*; import java.math.BigInteger; @@ -30,12 +30,11 @@ import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingTrackLocationAction.LocationTrackingSpec; import ghidra.app.plugin.core.debug.gui.register.DebuggerRegistersProvider.RegisterTableColumns; -import ghidra.app.plugin.core.debug.mapping.DebuggerRegisterMapper; import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceTest; import ghidra.app.services.TraceRecorder; +import ghidra.async.AsyncTestUtils; import ghidra.program.model.data.*; import ghidra.program.model.lang.Register; -import ghidra.program.model.lang.RegisterValue; import ghidra.program.model.util.CodeUnitInsertionException; import ghidra.trace.database.ToyDBTraceBuilder; import ghidra.trace.database.listing.DBTraceCodeRegisterSpace; @@ -47,7 +46,8 @@ import ghidra.trace.model.thread.TraceThread; import ghidra.util.database.UndoableTransaction; import ghidra.util.exception.DuplicateNameException; -public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerGUITest { +public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerGUITest + implements AsyncTestUtils { static { DebuggerModelServiceTest.addTestModelPathPatterns(); } @@ -151,6 +151,7 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG if (thread == null) { return false; } + /* DebuggerRegisterMapper mapper = recorder.getRegisterMapper(thread); if (mapper == null) { return false; @@ -158,6 +159,7 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG if (!mapper.getRegistersOnTarget().containsAll(baseRegs)) { return false; } + */ return true; }); return recorder; @@ -320,15 +322,18 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG } @Test - public void testLiveActivateThenAddValuesPopulatesPanel() throws Exception { + public void testLiveActivateThenAddValuesPopulatesPanel() throws Throwable { TraceRecorder recorder = recordAndWaitSync(); traceManager.openTrace(recorder.getTrace()); traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); waitForSwing(); mb.testBank1.writeRegister("pc", new byte[] { 0x00, 0x40, 0x00, 0x00 }); + waitOn(mb.testModel.flushEvents()); waitForDomainObject(recorder.getTrace()); + RegisterRow rowL = findRegisterRow(pc); + waitForPass(() -> assertTrue(rowL.isKnown())); assertPCRowValuePopulated(); } @@ -396,7 +401,7 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG } @Test - public void testLiveModifySubValueAffectsTarget() throws Exception { + public void testLiveModifySubValueAffectsTarget() throws Throwable { TraceRecorder recorder = recordAndWaitSync(); Trace trace = recorder.getTrace(); traceManager.openTrace(trace); @@ -407,14 +412,12 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG assertTrue(registersProvider.actionEnableEdits.isEnabled()); performAction(registersProvider.actionEnableEdits); - try (UndoableTransaction tid = UndoableTransaction.start(trace, "Pretend fetch", true)) { - TraceMemoryRegisterSpace regs = - trace.getMemoryManager().getMemoryRegisterSpace(thread, true); - regs.setValue(0, new RegisterValue(r0, BigInteger.ZERO)); - } + mb.testBank1.writeRegistersNamed(Map.of("r0", new byte[] { 0 })); + waitOn(mb.testModel.flushEvents()); + waitForDomainObject(trace); RegisterRow rowL = findRegisterRow(r0l); - assertTrue(rowL.isValueEditable()); + waitForPass(() -> assertTrue(rowL.isValueEditable())); setRowText(rowL, "05060708"); waitForSwing(); @@ -656,7 +659,8 @@ public class DebuggerRegistersProviderTest extends AbstractGhidraHeadedDebuggerG // TODO: It'd be nice if plugin tracked disconnected providers.... DebuggerRegistersProvider cloned = (DebuggerRegistersProvider) tool.getActiveComponentProvider(); - assertEquals("[Registers: Thread1, 0]", cloned.getTitle()); + assertEquals("[Registers]", cloned.getTitle()); + assertEquals("Thread1", cloned.getSubTitle()); traceManager.activateThread(thread2); waitForSwing(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceTest.java index 7a554ce436..e92690491b 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/breakpoint/DebuggerLogicalBreakpointServiceTest.java @@ -34,7 +34,7 @@ import ghidra.dbg.model.TestTargetMemoryRegion; import ghidra.dbg.model.TestTargetProcess; import ghidra.dbg.target.*; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; -import ghidra.dbg.util.DebuggerModelTestUtils; +import ghidra.dbg.testutil.DebuggerModelTestUtils; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Bookmark; import ghidra.program.model.listing.Program; @@ -290,7 +290,7 @@ public class DebuggerLogicalBreakpointServiceTest extends AbstractGhidraHeadedDe } protected void addTargetAccessBreakpoint(TraceRecorder r) throws Exception { - TargetBreakpointContainer cont = getBreakpointContainer(r); + TargetBreakpointSpecContainer cont = getBreakpointContainer(r); cont.placeBreakpoint(mb.testModel.getAddress("ram", 0x56550123), Set.of(TargetBreakpointKind.READ, TargetBreakpointKind.WRITE)) .get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); @@ -301,13 +301,13 @@ public class DebuggerLogicalBreakpointServiceTest extends AbstractGhidraHeadedDe TraceMemoryRegion textRegion = waitFor(() -> r.getTraceMemoryRegion(region), "Recorder missed region: " + region); long offset = textRegion.getMinAddress().getOffset() + 0x0123; - TargetBreakpointContainer cont = getBreakpointContainer(r); - cont.placeBreakpoint(mb.addr(offset), Set.of(TargetBreakpointKind.SOFTWARE)) + TargetBreakpointSpecContainer cont = getBreakpointContainer(r); + cont.placeBreakpoint(mb.addr(offset), Set.of(TargetBreakpointKind.SW_EXECUTE)) .get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } protected void removeTargetSoftwareBreakpoint(TraceRecorder r) throws Exception { - TargetBreakpointContainer cont = getBreakpointContainer(r); + TargetBreakpointSpecContainer cont = getBreakpointContainer(r); cont.fetchElements().thenAccept(elements -> { for (TargetObject obj : elements.values()) { if (!(obj instanceof TargetBreakpointSpec) || @@ -315,7 +315,7 @@ public class DebuggerLogicalBreakpointServiceTest extends AbstractGhidraHeadedDe continue; } TargetBreakpointSpec spec = (TargetBreakpointSpec) obj; - if (!spec.getKinds().contains(TargetBreakpointKind.SOFTWARE)) { + if (!spec.getKinds().contains(TargetBreakpointKind.SW_EXECUTE)) { continue; } TargetDeletable del = (TargetDeletable) obj; diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java index 187b4a76e6..871d269151 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java @@ -33,7 +33,9 @@ import ghidra.dbg.DebuggerModelFactory; import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.model.TestDebuggerObjectModel; import ghidra.dbg.model.TestLocalDebuggerModelFactory; -import ghidra.dbg.util.*; +import ghidra.dbg.testutil.DebuggerModelTestUtils; +import ghidra.dbg.util.PathMatcher; +import ghidra.dbg.util.PathUtils; import ghidra.trace.model.Trace; import ghidra.trace.model.thread.TraceThread; import ghidra.util.SystemUtilities; @@ -51,8 +53,43 @@ public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITes protected static final long TIMEOUT_MILLIS = SystemUtilities.isInTestingBatchMode() ? 5000 : Long.MAX_VALUE; + public static final PathMatcher HARDCODED_MATCHER = new PathMatcher() { + { + // Paths for GDB + addPattern(PathUtils.parse("Breakpoints[].")); + addPattern(PathUtils.parse("Inferiors[].Memory[]")); + addPattern(PathUtils.parse("Inferiors[].Modules[].Sections[]")); + addPattern(PathUtils.parse("Inferiors[].Registers[]")); + addPattern(PathUtils.parse("Inferiors[].Threads[]")); + addPattern(PathUtils.parse("Inferiors[].Threads[].Stack[]")); + + // Paths for dbgeng + addPattern(PathUtils.parse("Sessions[].Processes[].Memory[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Modules[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Registers[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Stack[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Debug.Breakpoints[]")); + + // (Additional) paths for dbgmodel + addPattern(PathUtils.parse("Sessions[].Attributes")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Stack.Frames[]")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].TTD.Position")); + addPattern(PathUtils.parse("Sessions[].Processes[].Threads[].Registers.User.")); + + // Paths for JDI + addPattern(PathUtils.parse("VirtualMachines[]")); + addPattern(PathUtils.parse("VirtualMachines[].Breakpoints")); + addPattern(PathUtils.parse("VirtualMachines[].Classes[]")); + addPattern(PathUtils.parse("VirtualMachines[].Classes[].Sections[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[].Registers[]")); + addPattern(PathUtils.parse("VirtualMachines[].Threads[].Stack[]")); + + } + }; + public static void addTestModelPathPatterns() { - PathMatcher m = DefaultTraceRecorder.HARDCODED_MATCHER; + PathMatcher m = HARDCODED_MATCHER; m.addPattern(PathUtils.parse("Processes[]")); m.addPattern(PathUtils.parse("Processes[].Breakpoints[]")); m.addPattern(PathUtils.parse("Processes[].Memory[]")); @@ -84,8 +121,7 @@ public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITes void elementRemoved(E element); } - static class CollectionChangeDelegateWrapper - implements CollectionChangeListener { + static class CollectionChangeDelegateWrapper implements CollectionChangeListener { protected final CollectionChangeDelegate delegate; public CollectionChangeDelegateWrapper(CollectionChangeDelegate delegate) { diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorderTest.java index bc979f4e7f..9c0a3450ae 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DefaultTraceRecorderTest.java @@ -22,16 +22,20 @@ import java.util.Map.Entry; import org.junit.Test; +import com.google.common.collect.Range; + import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; import ghidra.app.plugin.core.debug.mapping.DebuggerRegisterMapper; import ghidra.app.services.TraceRecorder; import ghidra.dbg.model.TestTargetMemoryRegion; import ghidra.dbg.model.TestTargetRegisterBankInThread; import ghidra.dbg.util.PathUtils; +import ghidra.program.model.data.PointerDataType; import ghidra.program.model.lang.Language; import ghidra.program.model.lang.Register; import ghidra.trace.model.Trace; import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.listing.TraceCodeRegisterSpace; import ghidra.trace.model.memory.*; import ghidra.trace.model.thread.TraceThread; import ghidra.util.database.UndoableTransaction; @@ -106,13 +110,14 @@ public class DefaultTraceRecorderTest extends AbstractGhidraHeadedDebuggerGUITes Language lang = trace.getBaseLanguage(); Register r0 = lang.getRegister("r0"); Register r1 = lang.getRegister("r1"); - TraceThread thread = waitForValue(() -> recorder.getTraceThread(mb.testThread1)); + //TraceThread thread = waitForValue(() -> recorder.getTraceThread(mb.testThread1)); + TraceThread thread = recorder.getTraceThread(mb.testThread1); TraceMemoryRegisterSpace rs = createRegSpace(thread); mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), Register::isBaseRegister); TestTargetRegisterBankInThread regs = mb.testThread1.addRegisterBank(); - waitForCondition(() -> registerMapped(recorder, thread, r0)); + //waitForCondition(() -> registerMapped(recorder, thread, r0)); regs.writeRegister("r0", tb.arr(1)).get(); waitForPass(() -> { @@ -145,12 +150,16 @@ public class DefaultTraceRecorderTest extends AbstractGhidraHeadedDebuggerGUITes mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), r -> r.isBaseRegister() && r != pc && r != sp); TestTargetRegisterBankInThread regs = mb.testThread1.addRegisterBank(); + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Add PC type", true)) { + TraceCodeRegisterSpace code = trace.getCodeManager().getCodeRegisterSpace(thread, true); + code.definedData().create(Range.atLeast(0L), pc, PointerDataType.dataType); + } assertNull(rs.getMostRecentStateEntry(recorder.getSnap(), pc.getAddress())); assertNull(rs.getMostRecentStateEntry(recorder.getSnap(), sp.getAddress())); mb.testProcess1.regs.addRegister(pc); - waitForCondition(() -> registerMapped(recorder, thread, pc)); + //waitForCondition(() -> registerMapped(recorder, thread, pc)); regs.writeRegister("pc", tb.arr(0x55, 0x55, 0x01, 0x23)); waitForPass(() -> { @@ -190,12 +199,16 @@ public class DefaultTraceRecorderTest extends AbstractGhidraHeadedDebuggerGUITes mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), r -> r.isBaseRegister() && r != pc && r != sp); TestTargetRegisterBankInThread regs = mb.testThread1.addRegisterBank(); + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Add SP type", true)) { + TraceCodeRegisterSpace code = trace.getCodeManager().getCodeRegisterSpace(thread, true); + code.definedData().create(Range.atLeast(0L), sp, PointerDataType.dataType); + } assertNull(rs.getMostRecentStateEntry(recorder.getSnap(), pc.getAddress())); assertNull(rs.getMostRecentStateEntry(recorder.getSnap(), sp.getAddress())); mb.testProcess1.regs.addRegister(sp); - waitForCondition(() -> registerMapped(recorder, thread, sp)); + //waitForCondition(() -> registerMapped(recorder, thread, sp)); regs.writeRegister("sp", tb.arr(0x22, 0x22, 0x03, 0x21)); waitForPass(() -> { @@ -228,13 +241,17 @@ public class DefaultTraceRecorderTest extends AbstractGhidraHeadedDebuggerGUITes Trace trace = recorder.getTrace(); Language lang = trace.getBaseLanguage(); Register pc = lang.getRegister("pc"); - TraceThread thread = waitForValue(() -> recorder.getTraceThread(mb.testThread1)); mb.testProcess1.addRegion("bin:.text", mb.rng(0x55550123, 0x55550321), "rx"); mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), Register::isBaseRegister); TestTargetRegisterBankInThread regs = mb.testThread1.addRegisterBank(); - waitForCondition(() -> registerMapped(recorder, thread, pc)); + //waitForCondition(() -> registerMapped(recorder, thread, pc)); + TraceThread thread = waitForValue(() -> recorder.getTraceThread(mb.testThread1)); + try (UndoableTransaction tid = UndoableTransaction.start(trace, "Add PC type", true)) { + TraceCodeRegisterSpace code = trace.getCodeManager().getCodeRegisterSpace(thread, true); + code.definedData().create(Range.atLeast(0L), pc, PointerDataType.dataType); + } regs.writeRegister("pc", tb.arr(0x55, 0x55, 0x02, 0x22)); TraceMemoryManager mm = trace.getMemoryManager(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecTest.java index f747db9ce0..fd8b060d59 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/pcode/exec/TraceRecorderAsyncPcodeExecTest.java @@ -28,7 +28,7 @@ import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceTest; import ghidra.app.plugin.processors.sleigh.SleighLanguage; import ghidra.app.services.TraceRecorder; import ghidra.dbg.model.TestTargetRegisterBankInThread; -import ghidra.dbg.util.DebuggerModelTestUtils; +import ghidra.dbg.testutil.DebuggerModelTestUtils; import ghidra.pcode.utils.Utils; import ghidra.program.model.lang.Language; import ghidra.program.model.lang.Register; diff --git a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncReference.java b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncReference.java index 4a80c805a2..ad3b32b02b 100644 --- a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncReference.java +++ b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncReference.java @@ -18,8 +18,7 @@ package ghidra.async; import java.lang.ref.Cleaner.Cleanable; import java.lang.ref.WeakReference; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.*; import java.util.function.Function; import java.util.function.Predicate; @@ -168,7 +167,7 @@ public class AsyncReference { listener.accept(oldVal, newVal, cause); } catch (RejectedExecutionException exc) { - Msg.trace(this, "Ignoring rejection", exc); + Msg.trace(this, "Ignoring rejection: " + exc); } catch (Throwable exc) { Msg.error(this, "Ignoring exception on async reference listener: ", exc); @@ -376,7 +375,7 @@ public class AsyncReference { } } - IllegalStateException ex = new IllegalStateException(reason); + ExecutionException ex = new ExecutionException("Disposed", reason); for (CompletableFuture future : toExcept) { future.completeExceptionally(ex); } diff --git a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncUtils.java b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncUtils.java index 5b23270b9e..5f96b91962 100644 --- a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncUtils.java +++ b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncUtils.java @@ -830,7 +830,11 @@ public interface AsyncUtils { } public interface TemperamentalRunnable { - public void run() throws Exception; + public void run() throws Throwable; + } + + public interface TemperamentalSupplier { + public T get() throws Throwable; } /** diff --git a/Ghidra/Debug/Framework-AsyncComm/src/test/java/ghidra/async/AsyncTestUtils.java b/Ghidra/Debug/Framework-AsyncComm/src/test/java/ghidra/async/AsyncTestUtils.java index 2af3b1d6dd..d59f8e5925 100644 --- a/Ghidra/Debug/Framework-AsyncComm/src/test/java/ghidra/async/AsyncTestUtils.java +++ b/Ghidra/Debug/Framework-AsyncComm/src/test/java/ghidra/async/AsyncTestUtils.java @@ -15,24 +15,107 @@ */ package ghidra.async; +import java.util.Collection; import java.util.concurrent.*; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import ghidra.async.AsyncUtils.TemperamentalRunnable; +import ghidra.async.AsyncUtils.TemperamentalSupplier; +import ghidra.util.Msg; import ghidra.util.SystemUtilities; public interface AsyncTestUtils { - static final long TIMEOUT_MILLISECONDS = + static final long TIMEOUT_MS = SystemUtilities.isInTestingBatchMode() ? 5000 : Long.MAX_VALUE; + static final long RETRY_INTERVAL_MS = 100; + + default T waitOnNoValidate(CompletableFuture future) { + // Do this instead of plain ol' .get(time), to ease debugging + // When suspended in .get(time), you can't introspect much, otherwise + long started = System.currentTimeMillis(); + while (true) { + try { + return future.get(100, TimeUnit.MILLISECONDS); + } + catch (TimeoutException e) { + if (Long.compareUnsigned(System.currentTimeMillis() - started, TIMEOUT_MS) >= 0) { + throw new RuntimeException(AsyncUtils.unwrapThrowable(e)); + } + } + catch (Exception e) { + Throwable unwrapped = AsyncUtils.unwrapThrowable(e); + if (unwrapped instanceof RuntimeException) { + throw (RuntimeException) unwrapped; + } + return ExceptionUtils.rethrow(e); + } + } + } + + default void validateCompletionThread() { + } default T waitOn(CompletableFuture future) throws Throwable { - try { - return future.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS); - } - catch (Exception e) { - throw AsyncUtils.unwrapThrowable(e); - } + /** + * NB. CF's may issue dependent callbacks either on the thread completing the dependency, or + * on the thread chaining the dependent. If the CF completes before the chain, then the + * callback comes to me, and so currentThread will not be the model's callback thread. Thus, + * I should not validate the currentThread at callback if it is the currentThread now. + */ + Thread waitingThread = Thread.currentThread(); + CompletableFuture validated = future.whenComplete((t, ex) -> { + if (Thread.currentThread() != waitingThread) { + validateCompletionThread(); + } + }); + return waitOnNoValidate(validated); } default void waitOn(Executor executor) throws Throwable { waitOn(CompletableFuture.supplyAsync(() -> null, executor)); } + + default void retryVoid(TemperamentalRunnable runnable, + Collection> retriable) throws Throwable { + retry(() -> { + runnable.run(); + return null; + }, retriable); + } + + default T retry(TemperamentalSupplier supplier, + Collection> retriable) throws Throwable { + return retry(TIMEOUT_MS, supplier, retriable); + } + + default T retry(long timeoutMs, TemperamentalSupplier supplier, + Collection> retriable) throws Throwable { + long retryAttempts = timeoutMs / RETRY_INTERVAL_MS; + Throwable lastExc = null; + for (int i = 0; i < retryAttempts; i++) { + if (i != 0) { + Thread.sleep(RETRY_INTERVAL_MS); + } + try { + return supplier.get(); + } + catch (Throwable e) { + if (i < 10) { + Msg.debug(this, "Retrying after " + e); + } + lastExc = e; + for (Class et : retriable) { + if (et.isAssignableFrom(e.getClass())) { + e = null; + break; + } + } + if (e != null) { + throw e; + } + } + } + throw lastExc; + } } diff --git a/Ghidra/Debug/Framework-Debugging/build.gradle b/Ghidra/Debug/Framework-Debugging/build.gradle index 445866655b..3ee07fc6ae 100644 --- a/Ghidra/Debug/Framework-Debugging/build.gradle +++ b/Ghidra/Debug/Framework-Debugging/build.gradle @@ -19,18 +19,22 @@ dependencies { task testSpecimenWin64 { dependsOn 'expCreateProcessWin64Executable' dependsOn 'expCreateThreadExitWin64Executable' - dependsOn 'expCreateThreadSpinWin64Executable' + //dependsOn 'expCreateThreadSpinWin64Executable' dependsOn 'expPrintWin64Executable' - dependsOn 'expSpinWin64Executable' + //dependsOn 'expSpinWin64Executable' + dependsOn 'expRegistersWin64Executable' + dependsOn 'expStackWin64Executable' } task testSpecimenLinux64 { dependsOn 'expCloneExecExecutable'//Linux64Executable' dependsOn 'expCloneExitExecutable'//Linux64Executable' - dependsOn 'expCloneSpinExecutable'//Linux64Executable' + //dependsOn 'expCloneSpinExecutable'//Linux64Executable' dependsOn 'expForkExecutable'//Linux64Executable' dependsOn 'expPrintLinux64Executable' - dependsOn 'expTypesExecutable'//Linux64Executable' + //dependsOn 'expTypesExecutable'//Linux64Executable' + dependsOn 'expRegistersLinux64Executable' + dependsOn 'expStackLinux64Executable' } model { @@ -83,25 +87,54 @@ model { expTypes(NativeExecutableSpec) { targetPlatform "linux64" //targetPlatform "linux32" // TODO: Test on these + binaries { + withType(NativeExecutableBinarySpec) { + if (toolChain in Gcc) { + cCompiler.args("-gdwarf-2") + } + } + } + } + expRegisters(NativeExecutableSpec) { + targetPlatform "linux64" + //targetPlatform "linux32" // TODO: Test on these + targetPlatform "win64" + targetPlatform "win32" // TODO: Test on these + } + expStack(NativeExecutableSpec) { + targetPlatform "linux64" + //targetPlatform "linux32" // TODO: Test on these + targetPlatform "win64" + targetPlatform "win32" // TODO: Test on these } } binaries { withType(NativeExecutableBinarySpec) { if (toolChain in Gcc) { - cCompiler.args("-gdwarf-2", "-std=c99") + cCompiler.args("-std=c99") linker.args("-lpthread") linker.args("-lutil") } if (toolChain in VisualCpp) { cppCompiler.define("VS_PROJECT") - linker.args("/SUBSYSTEM:CONSOLE", "/DYNAMICBASE", "/NXCOMPAT") + // NB. No /SUBSYSTEM:CONSOLE + // that creates a subprocess + linker.args("/SUBSYSTEM:windows", "/DYNAMICBASE", "/NXCOMPAT") + linker.args("shell32.lib"); } if (targetPlatform.name.startsWith("win")) { cppCompiler.define("WIN32") + cCompiler.define("WIN32") + cppCompiler.define("_WINDOWS") + cCompiler.define("_WINDOWS") + cppCompiler.define("UNICODE") + cCompiler.define("_UNICODE") + cppCompiler.define("_UNICODE") + cCompiler.define("UNICODE") } } } diff --git a/Ghidra/Debug/Framework-Debugging/certification.manifest b/Ghidra/Debug/Framework-Debugging/certification.manifest index fa66778128..bda191789e 100644 --- a/Ghidra/Debug/Framework-Debugging/certification.manifest +++ b/Ghidra/Debug/Framework-Debugging/certification.manifest @@ -4,3 +4,4 @@ Module.manifest||GHIDRA||||END| build.gradle||GHIDRA||||END| data/ExtensionPoint.manifest||GHIDRA||||END| +src/test/resources/ghidra/dbg/model/test_schema.xml||GHIDRA||||END| diff --git a/Ghidra/Debug/Framework-Debugging/src/expCreateProcess/c/expCreateProcess.c b/Ghidra/Debug/Framework-Debugging/src/expCreateProcess/c/expCreateProcess.c index 358e9a7469..cdfc77a36a 100644 --- a/Ghidra/Debug/Framework-Debugging/src/expCreateProcess/c/expCreateProcess.c +++ b/Ghidra/Debug/Framework-Debugging/src/expCreateProcess/c/expCreateProcess.c @@ -15,25 +15,38 @@ */ #include #include +#include +#include int __declspec(dllexport) func(char* msg) { printf("%s\n", msg); } -int main(int argc, char** argv) { +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { + OutputDebugStringW(L"Starting\n"); + LPWSTR *argvW; + int argc = 0; + argvW = CommandLineToArgvW(GetCommandLine(), &argc); + if (argvW != NULL) { + LocalFree(argvW); + } else { + return 0; + } + wprintf(L"argc: %d\n", argc); if (argc != 1) { func("I'm the child"); return 1; } STARTUPINFO sStartupInfo = {sizeof(sStartupInfo)}; PROCESS_INFORMATION sProcessInformation = {0}; - BOOL result = CreateProcess(argv[0], "expCreateProcess child", NULL, NULL, FALSE, 0, NULL, NULL, &sStartupInfo, &sProcessInformation); + wprintf(L"Me: %s\n", argvW[0]); + BOOL result = CreateProcessW(argvW[0], L"expCreateProcess child", NULL, NULL, FALSE, 0, NULL, NULL, &sStartupInfo, &sProcessInformation); if (result == FALSE) { DWORD le = GetLastError(); fprintf(stderr, "Could not create child process: %d\n", le); - char err[1024]; + wchar_t err[1024]; FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, le, 0, err, sizeof(err), NULL); - fprintf(stderr, " Message: '%s'\n", err); + fwprintf(stderr, L" Message: '%s'\n", err); DebugBreak(); return -1; } diff --git a/Ghidra/Debug/Framework-Debugging/src/expCreateThreadExit/c/expCreateThreadExit.c b/Ghidra/Debug/Framework-Debugging/src/expCreateThreadExit/c/expCreateThreadExit.c index 89c95daf2f..6ae62de7fd 100644 --- a/Ghidra/Debug/Framework-Debugging/src/expCreateThreadExit/c/expCreateThreadExit.c +++ b/Ghidra/Debug/Framework-Debugging/src/expCreateThreadExit/c/expCreateThreadExit.c @@ -26,7 +26,7 @@ __declspec(dllexport) unsigned int WINAPI work(DWORD* param) { } } -int main(int argc, char** argv) { +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { DWORD zero = 0; DWORD one = 1; HANDLE thread = _beginthreadex(NULL, 0, work, &one, 0, NULL); diff --git a/Ghidra/Debug/Framework-Debugging/src/expCreateThreadSpin/c/expCreateThreadSpin.c b/Ghidra/Debug/Framework-Debugging/src/expCreateThreadSpin/c/expCreateThreadSpin.c index b600a639d9..1a1e88c071 100644 --- a/Ghidra/Debug/Framework-Debugging/src/expCreateThreadSpin/c/expCreateThreadSpin.c +++ b/Ghidra/Debug/Framework-Debugging/src/expCreateThreadSpin/c/expCreateThreadSpin.c @@ -24,7 +24,7 @@ __declspec(dllexport) unsigned int WINAPI work(DWORD* param) { } } -int main(int argc, char** argv) { +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { DWORD zero = 0; DWORD one = 1; HANDLE thread = _beginthreadex(NULL, 0, work, &one, 0, NULL); diff --git a/Ghidra/Debug/Framework-Debugging/src/expPrint/c/expPrint.c b/Ghidra/Debug/Framework-Debugging/src/expPrint/c/expPrint.c index 3e780bf6bb..4e40f3e54d 100644 --- a/Ghidra/Debug/Framework-Debugging/src/expPrint/c/expPrint.c +++ b/Ghidra/Debug/Framework-Debugging/src/expPrint/c/expPrint.c @@ -16,14 +16,21 @@ #include #ifdef WIN32 +#include +#include #define DLLEXPORT __declspec(dllexport) #else #define DLLEXPORT +#define OutputDebugString(out) printf("%s\n", out) #endif DLLEXPORT volatile char overwrite[] = "Hello, World!"; +#ifdef WIN32 +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { +#else int main(int argc, char** argv) { - printf("%s\n", overwrite); +#endif + OutputDebugString(overwrite); return overwrite[0]; } diff --git a/Ghidra/Debug/Framework-Debugging/src/expRegisters/c/expRegisters.c b/Ghidra/Debug/Framework-Debugging/src/expRegisters/c/expRegisters.c new file mode 100644 index 0000000000..4396fe3f33 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/expRegisters/c/expRegisters.c @@ -0,0 +1,33 @@ +/* ### + * 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. + */ +#ifdef WIN32 +#include +#define DLLEXPORT __declspec(dllexport) +#else +#define DLLEXPORT +#endif + +int DLLEXPORT break_here(int val) { + return val; +} + +#ifdef WIN32 +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { +#else +int main(int argc, char** argv) { +#endif + return break_here(0); +} diff --git a/Ghidra/Debug/Framework-Debugging/src/expSpin/c/expSpin.c b/Ghidra/Debug/Framework-Debugging/src/expSpin/c/expSpin.c index a447555355..1ac8f0ee19 100644 --- a/Ghidra/Debug/Framework-Debugging/src/expSpin/c/expSpin.c +++ b/Ghidra/Debug/Framework-Debugging/src/expSpin/c/expSpin.c @@ -15,7 +15,7 @@ */ #include -__declspec(dllexport) int main(int argc, char** argv) { +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { for (int i = 0; i < 10; i++) { Sleep(1000); } diff --git a/Ghidra/Debug/Framework-Debugging/src/expStack/c/expStack.c b/Ghidra/Debug/Framework-Debugging/src/expStack/c/expStack.c new file mode 100644 index 0000000000..07e2e20129 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/expStack/c/expStack.c @@ -0,0 +1,45 @@ +/* ### + * 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. + */ +#ifdef WIN32 +#include +#define DLLEXPORT __declspec(dllexport) +#else +#define DLLEXPORT +#endif + +int DLLEXPORT break_here() { + return 0; +} + +int DLLEXPORT funcC() { + return break_here(); +} + +int DLLEXPORT funcB() { + return funcC(); +} + +int DLLEXPORT funcA() { + return funcB(); +} + +#ifdef WIN32 +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { +#else +int main(int argc, char** argv) { +#endif + return funcA(); +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/AnnotatedDebuggerAttributeListener.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/AnnotatedDebuggerAttributeListener.java new file mode 100644 index 0000000000..7bbeb5facc --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/AnnotatedDebuggerAttributeListener.java @@ -0,0 +1,126 @@ +/* ### + * 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; + +import java.lang.annotation.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; + +import ghidra.dbg.target.TargetObject; +import ghidra.util.Msg; + +public abstract class AnnotatedDebuggerAttributeListener implements DebuggerModelListener { + private static final String ATTR_METHODS = + "@" + AttributeCallback.class.getSimpleName() + "-annotated methods"; + private static final String PARAMS_ERR = + ATTR_METHODS + " must accept 2 parameters: (TargetObject, T)"; + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + protected @interface AttributeCallback { + String value(); + } + + private static class Wiring { + private final Map> handles = new HashMap<>(); + + private Wiring(Class cls, Lookup lookup) { + try { + collect(cls, lookup); + } + catch (IllegalAccessException e) { + throw new IllegalArgumentException("Lookup must have access " + ATTR_METHODS, e); + } + } + + private void collectFromClass(Class cls, Lookup lookup) throws IllegalAccessException { + for (Method m : cls.getDeclaredMethods()) { + AttributeCallback annot = m.getAnnotation(AttributeCallback.class); + if (annot == null) { + continue; + } + Parameter[] parameters = m.getParameters(); + if (parameters.length != 2) { + throw new IllegalArgumentException(PARAMS_ERR); + } + if (!parameters[0].getType().isAssignableFrom(TargetObject.class)) { + throw new IllegalArgumentException(PARAMS_ERR); + } + MethodHandle handle = lookup.unreflect(m); + handles.computeIfAbsent(annot.value(), __ -> new HashSet<>()).add(handle); + } + } + + private void collect(Class cls, Lookup lookup) throws IllegalAccessException { + collectFromClass(cls, lookup); + + Class s = cls.getSuperclass(); + if (s != null) { + collect(s, lookup); + } + + for (Class i : cls.getInterfaces()) { + collect(i, lookup); + } + } + + private void fireChange(AnnotatedDebuggerAttributeListener l, TargetObject object, + String name, Object value) { + Set set = handles.get(name); + if (set == null) { + return; + } + for (MethodHandle h : set) { + try { + h.invoke(l, object, value); + } + catch (Throwable e) { + Msg.error(this, "Error invoking " + h + ": " + e); + } + } + } + } + + private static final Map, Wiring> WIRINGS_BY_CLASS = + new HashMap<>(); + + private final Wiring wiring; + + public AnnotatedDebuggerAttributeListener(Lookup lookup) { + wiring = WIRINGS_BY_CLASS.computeIfAbsent(getClass(), cls -> new Wiring(cls, lookup)); + } + + protected boolean checkFire(TargetObject object) { + return true; + } + + @Override + public void attributesChanged(TargetObject object, Collection removed, + Map added) { + if (!checkFire(object)) { + return; + } + for (String name : removed) { + wiring.fireChange(this, object, name, null); + } + for (Map.Entry ent : added.entrySet()) { + wiring.fireChange(this, object, ent.getKey(), ent.getValue()); + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java index 697651b9ea..5ad3ff8fab 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java @@ -15,6 +15,7 @@ */ package ghidra.dbg; +import java.lang.invoke.MethodHandles; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; @@ -22,9 +23,9 @@ import java.util.stream.Collectors; import ghidra.async.*; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; -import ghidra.dbg.target.TargetObject.TargetObjectListener; +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.util.PathPredicates; import ghidra.dbg.util.PathUtils; import ghidra.dbg.util.PathUtils.PathComparator; import ghidra.util.Msg; @@ -80,7 +81,9 @@ public enum DebugModelConventions { * @param seed the starting object * @return a future which completes with the discovered object or completes with null, if not * found. + * @deprecated use {@link #suitable(Class, TargetObject)} instead */ + @Deprecated(forRemoval = true) public static CompletableFuture findSuitable(Class iface, TargetObject seed) { if (iface.isAssignableFrom(seed.getClass())) { @@ -97,6 +100,56 @@ public enum DebugModelConventions { return findParentSuitable(iface, seed); } + /** + * Search for a suitable object implementing the given interface, starting at a given seed. + * + *

+ * This performs an n-up-m-down search starting at the given seed, seeking an object which + * implements the given interface. The m-down part is only applied from objects implementing + * {@link TargetAggregate}. See {@link TargetObject} for the specifics of expected model + * conventions. + * + *

+ * Note that many a debugger target object interface type require a self-referential {@code T} + * parameter referring to the implementing class type. To avoid referring to a particular + * implementation, it becomes necessary to leave {@code T} as {@code ?}, but that can never + * satisfy the constraints of this method. To work around this, such interfaces must provide a + * static {@code tclass} field, which can properly satisfy the type constraints of this method + * for such self-referential type variables. The returned value must be ascribed to the + * wild-carded type, because the work-around involves a hidden class. Perhaps a little verbose + * (hey, it's Java!), the following is the recommended pattern, e.g., to discover the + * environment of a given process: + * + *

+	 * CompletableFuture> futureEnv =
+	 * 	DebugModelConventions.suitable(TargetEnvironment.tclass, aProcess);
+	 * 
+ * + * @param the desired interface type. + * @param iface the (probably {@code tclass}) of the desired interface type + * @param seed the starting object + * @return a future which completes with the discovered object or completes with null, if not + * found. + */ + public static CompletableFuture suitable(Class iface, + TargetObject seed) { + List path = + seed.getModel().getRootSchema().searchForSuitable(iface, seed.getPath()); + if (path == null) { + return null; + } + return seed.getModel().fetchModelObject(path).thenApply(obj -> iface.cast(obj)); + } + + public static T ancestor(Class iface, TargetObject seed) { + List path = + seed.getModel().getRootSchema().searchForAncestor(iface, seed.getPath()); + if (path == null) { + return null; + } + return iface.cast(seed.getModel().getModelObject(path)); + } + private static CompletableFuture findParentSuitable(Class iface, TargetObject obj) { TargetObject parent = obj.getParent(); @@ -285,8 +338,11 @@ public enum DebugModelConventions { * @param seed the starting point (root of subtree to inspect) * @param iface the class of the interface * @return the collection of successor elements supporting the interface + * @deprecated use {@link TargetObjectSchema#searchFor(Class, boolean)} and + * {@link PathPredicates#collectSuccessorRefs(TargetObject)} instead. */ // TODO: Test this method + @Deprecated(forRemoval = true) public static CompletableFuture> collectSuccessors( TargetObject seed, Class iface) { Collection result = @@ -366,7 +422,29 @@ public enum DebugModelConventions { } /** - * Check if a target is a live process + * Check if the given process is alive + * + * @param process the process + * @return true if alive + */ + public static boolean isProcessAlive(TargetProcess process) { + if (!process.isValid()) { + return false; + } + if (!(process instanceof TargetExecutionStateful)) { + return true; + } + TargetExecutionStateful exe = (TargetExecutionStateful) process; + TargetExecutionState state = exe.getExecutionState(); + if (state == null) { + Msg.error(null, "null state for " + exe); + return false; + } + return state.isAlive(); + } + + /** + * Check if a target is a live process, and cast if so * * @param target the potential process * @return the process if live, or null @@ -375,27 +453,22 @@ public enum DebugModelConventions { if (!(target instanceof TargetProcess)) { return null; } - // TODO: When schemas are introduced, we'll better handle "associated" - // For now, require "implements" - if (!(target instanceof TargetExecutionStateful)) { - return (TargetProcess) target; - } - TargetExecutionStateful exe = (TargetExecutionStateful) target; - TargetExecutionState state = exe.getExecutionState(); - if (!state.isAlive()) { - return null; - } - return (TargetProcess) target; + TargetProcess process = (TargetProcess) target; + return isProcessAlive(process) ? process : null; } /** * A convenience for listening to selected portions (possible all) of a sub-tree of a model */ - public abstract static class SubTreeListenerAdapter implements TargetObjectListener { + public abstract static class SubTreeListenerAdapter extends AnnotatedDebuggerAttributeListener { protected boolean disposed = false; protected final NavigableMap, TargetObject> objects = new TreeMap<>(PathComparator.KEYED); + public SubTreeListenerAdapter() { + super(MethodHandles.lookup()); + } + /** * An object has been removed from the sub-tree * @@ -494,7 +567,7 @@ public enum DebugModelConventions { } } - private void considerAttributes(TargetObject obj, Map attributes) { + protected void considerAttributes(TargetObject obj, Map attributes) { synchronized (objects) { if (disposed) { return; @@ -612,20 +685,28 @@ public enum DebugModelConventions { } } + /** + * A variable that is updated whenever access changes according to the (now deprecated) + * "every-ancestor" convention. + * + * @deprecated The "every-ancestor" thing doesn't add any flexibility to model implementations. + * It might even restrict it. Not to mention it's obtuse to implement. + */ + @Deprecated(forRemoval = true) public static class AllRequiredAccess extends AsyncReference { - protected class ListenerForAccess implements TargetAccessibilityListener { + protected class ListenerForAccess extends AnnotatedDebuggerAttributeListener { protected final TargetAccessConditioned access; private boolean accessible; public ListenerForAccess(TargetAccessConditioned access) { + super(MethodHandles.lookup()); this.access = access; this.access.addListener(this); this.accessible = access.isAccessible(); } - @Override - public void accessibilityChanged(TargetAccessConditioned object, - boolean accessibility) { + @AttributeCallback(TargetAccessConditioned.ACCESSIBLE_ATTRIBUTE_NAME) + public void accessibilityChanged(TargetObject object, boolean accessibility) { //Msg.debug(this, "Obj " + object + " has become " + accessibility); synchronized (AllRequiredAccess.this) { this.accessible = accessibility; @@ -651,6 +732,59 @@ public enum DebugModelConventions { } } + public static class AsyncAttribute extends AsyncReference + implements DebuggerModelListener { + private final TargetObject obj; + private final String name; + + @SuppressWarnings("unchecked") + public AsyncAttribute(TargetObject obj, String name) { + this.name = name; + this.obj = obj; + obj.addListener(this); + obj.fetchAttribute(name).thenAccept(t -> { + set((T) t, null); + }).exceptionally(ex -> { + Msg.error(this, "Could not get initial value of " + name + " for " + obj, ex); + return null; + }); + } + + @Override + @SuppressWarnings("unchecked") + public void attributesChanged(TargetObject parent, Collection removed, + Map added) { + if (added.containsKey(name)) { + set((T) added.get(name), null); + } + else if (removed.contains(name)) { + set(null, null); + } + } + + public void dispose() { + this.dispose(new AssertionError("disposed")); + } + + @Override + public void dispose(Throwable reason) { + super.dispose(reason); + obj.removeListener(this); + } + } + + public static class AsyncState extends AsyncAttribute { + public AsyncState(TargetExecutionStateful stateful) { + super(stateful, TargetExecutionStateful.STATE_ATTRIBUTE_NAME); + } + } + + public static class AsyncAccess extends AsyncAttribute { + public AsyncAccess(TargetAccessConditioned ac) { + super(ac, TargetAccessConditioned.ACCESSIBLE_ATTRIBUTE_NAME); + } + } + /** * Obtain an object which tracks accessibility for a given target object. * @@ -669,7 +803,10 @@ public enum DebugModelConventions { * @param obj the object whose accessibility to track * @return a future which completes with an {@link AsyncReference} of the objects effective * accessibility. + * @deprecated Just listen on the nearest {@link TargetAccessConditioned} ancestor instead. The + * "every-ancestor" convention is deprecated. */ + @Deprecated public static CompletableFuture trackAccessibility(TargetObject obj) { CompletableFuture> collectAncestors = collectAncestors(obj, TargetAccessConditioned.class); diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerModelListener.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerModelListener.java index 42eca3afad..46b11407b9 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerModelListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerModelListener.java @@ -15,29 +15,21 @@ */ 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 java.util.*; + +import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetConsole.Channel; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; import ghidra.util.Msg; /** - * A listener for events related to the debugger model, usually a connection - * - *

- * TODO: Most (non-client) models do not implement this. Even the client ones do not implement - * {@link #modelStateChanged()} + * A listener for events related to the debugger model, including its connection and objects */ -public interface DebuggerModelListener - extends TargetObjectListener, TargetAccessibilityListener, TargetBreakpointListener, - TargetInterpreterListener, TargetEventScopeListener, TargetExecutionStateListener, - TargetFocusScopeListener, TargetMemoryListener, TargetRegisterBankListener { +public interface DebuggerModelListener { /** * An error occurred such that this listener will no longer receive events @@ -58,14 +50,9 @@ public interface DebuggerModelListener } /** - * The root object has been added to the model - * - *

- * This indicates the root is ready, not just {@link #created(TargetObject)}. - * - * @param root the root object + * The model's state has changed, prompting an update to its description */ - default public void rootAdded(TargetObject root) { + default public void modelStateChanged() { } /** @@ -81,8 +68,253 @@ public interface DebuggerModelListener } /** - * The model's state has changed, prompting an update to its description + * An object was created + * + *

+ * 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 + * the model. 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 public void modelStateChanged() { + default void created(TargetObject object) { + } + + /** + * An object is no longer valid + * + *

+ * This should be the final callback ever issued for this object. Invalidation of an object + * implies invalidation of all its successors; nevertheless, the implementation MUST explicitly + * invoke this callback for those successors in preorder. Users need only listen for + * invalidation by installing a listener on the object of interest. However, a user must be able + * to ignore invalidation events on an object it has already removed and/or invalidated. The + * {@code branch} parameter will identify the branch node of the sub-tree being removed. For + * models that are managed by a client connection, disconnecting or otherwise terminating the + * session should invalidate the root, and thus every object must receive this callback. + * + *

+ * If an invalidated object is replaced (i.e., a new object with the same path is added to the + * model), the implementation must be careful to issue all invalidations related to the removed + * object before the replacement is added, so that delayed invalidations are not mistakenly + * applied to the replacement or its successors. + * + * @param object the now-invalid object + * @param branch the root of the sub-tree being invalidated + * @param reason an informational, human-consumable reason, if applicable + */ + default void invalidated(TargetObject object, TargetObject branch, String reason) { + } + + /** + * The root object has been added to the model + * + *

+ * This indicates the root is ready, not just {@link #created(TargetObject)}. Note this callback + * indicates the root being "added to the model." + * + * @param root the root object + */ + default public void rootAdded(TargetObject root) { + } + + /** + * The object's elements changed + * + *

+ * The listener must have received a prior {@link #created(TargetObject)} callback for the + * parent and all (object-valued) elements being added. Assuming {@code object} has already been + * "added the model," this callback indicates all objects in the {@code added} parameter being + * "added to the model" along with their successors. + * + * @param object the object whose children changed + * @param removed the list of removed children + * @param added a map of indices to new children references + */ + default void elementsChanged(TargetObject object, Collection removed, + Map added) { + } + + /** + * The object's attributes changed + * + *

+ * In the case of an object-valued attribute, changes to that object do not constitute a changed + * attribute. The attribute is considered changed only when that attribute is assigned to a + * completely different object. + * + * @param object the object whose attributes changed + * @param removed the list of removed attributes + * @param added a map of names to new/changed attributes + */ + default void attributesChanged(TargetObject object, Collection removed, + Map added) { + } + + /** + * The model has requested the client invalidate (non-tree) caches associated with an object + * + *

+ * For objects with methods exposing contents other than elements and attributes (e.g., memory + * and register contents), this callback requests that any caches associated with that content + * be invalidated. Most notably, this usually occurs when an object (e.g., thread) enters the + * {@link TargetExecutionState#RUNNING} state, to inform proxies that they should invalidate + * their memory and register caches. In most cases, clients need not worry about this callback. + * Protocol implementations that use the model, however, should forward this request to the + * client-side peer. + * + *

+ * Note caches of elements and attributes are not affected by this callback. See + * {@link TargetObject#invalidateCaches()}. + * + * @param object the object whose caches must be invalidated + */ + default void invalidateCacheRequested(TargetObject object) { + } + + /** + * A breakpoint trapped execution + * + *

+ * The program counter can be obtained in a few ways. The most reliable is to get the address of + * the breakpoint location. If available, the frame will also contain the program counter. + * Finally, the trapped object or one of its relatives may offer the program counter. + * + * @param container the container whose breakpoint trapped execution + * @param trapped the object whose execution was trapped, usually a {@link TargetThread} + * @param frame the innermost stack frame, if available, of the trapped object + * @param spec the breakpoint specification + * @param breakpoint the breakpoint location that actually trapped execution + */ + default void breakpointHit(TargetObject container, TargetObject trapped, + TargetStackFrame frame, TargetBreakpointSpec spec, + TargetBreakpointLocation breakpoint) { + } + + /** + * A console has produced output (given as bytes) + * + *

+ * Note that "captured" outputs will not be reported in this callback. See + * {@link TargetInterpreter#executeCapture(String)}. + * + * @param console the console producing the output + * @param channel identifies the "output stream", stdout or stderr + * @param data the output data + */ + default void consoleOutput(TargetObject console, Channel channel, byte[] data) { + } + + /** + * A console has produced output (given as a string) + * + * @implNote Overriding this method is not a substitute for overriding + * {@link #consoleOutput(TargetObject, Channel, byte[])}. Some models may invoke this + * {@code String} variant as a convenience, which by default, invokes the + * {@code byte[]} variant, but models are only expected to invoke the {@code byte[]} + * variant. A client may override this method simply to avoid back-and-forth + * conversions between {@code String}s and {@code byte[]}s. + * + * @param console the console producing the output + * @param channel identifies the "output stream", stdout or stderr + * @param text the output text + */ + default void consoleOutput(TargetObject console, Channel channel, String text) { + consoleOutput(console, channel, text.getBytes(TargetConsole.CHARSET)); + } + + /** + * A "special" event has occurred + * + *

+ * When present, this callback must be invoked before any other callback which results from this + * event, except creation events. E.g., for PROCESS_EXITED, this must be called before the + * affected process is invalidated. + * + *

+ * Whenever possible, event thread must be given. This is often the thread given focus by the + * debugger immediately upon stopping for the event. Parameters are not (yet) strictly + * specified, but it should include the stopped target, if that target is not already given by + * the event thread. It may optionally contain other useful information, such as an exit code, + * but no client should depend on that information being given. + * + *

+ * The best way to communicate to users what has happened is via the description. Almost every + * other result of an event is communicated by other means in the model, e.g., state changes, + * object creation, invalidation. The description should contain as much information as possible + * to cue users as to why the other changes have occurred, and point them to relevant objects. + * For example, if trapped on a breakpoint, the description might contain the breakpoint's + * identifier. If the debugger prints a message for this event, that message is probably a + * sufficient description. + * + * @param object the event scope + * @param eventThread if applicable, the thread causing the event + * @param type the type of event + * @param description a human-readable description of the event + * @param parameters extra parameters for the event. TODO: Specify these for each type, or break + * this into other callbacks. + */ + default void event(TargetObject object, TargetThread eventThread, TargetEventType type, + String description, List parameters) { + } + + /** + * Memory was successfully read or written + * + *

+ * This implies memory caches should be updated. If the implementation employs a cache, then it + * need only report reads or writes which updated that cache. However, that cache must be + * invalidated whenever any other event occurs which could change memory, e.g., the target + * stepping or running. See {@link #invalidateCacheRequested(TargetObject)}. If the + * implementation does not employ a cache, then it must report every successful + * client-driven read or write. If the implementation can detect debugger-driven memory + * reads and writes, then it is recommended to call this method for those events. However, this + * method must not be called for target-driven memory changes. In other words, + * this method should only be called for reads or writes requested by the user. + * + * @param memory this memory object + * @param address the starting address of the affected range + * @param data the new data for the affected range + */ + default void memoryUpdated(TargetObject memory, Address address, byte[] data) { + } + + /** + * An attempt to read memory failed + * + *

+ * Like {@link #memoryUpdated(TargetMemory, Address, byte[])}, this should only be invoked for + * user-driven requests. Failure of the target to read its own memory would + * likely be reported via an exception, not this callback. + * + * @param memory the memory object + * @param range the range for the read which generated the error + * @param e the error + */ + default void memoryReadError(TargetObject memory, AddressRange range, + DebuggerMemoryAccessException e) { + } + + /** + * Registers were successfully read or written + * + *

+ * This implies register caches should be updated. If the implementation employs a cache, then + * it need only report reads or writes which updated that cache. However, that cache must be + * invalidated whenever any other event occurs which could change register values, e.g., the + * target stepping or running. See {@link #invalidateCacheRequested(TargetObject)}. If the + * implementation doe not employ a cache, then it must report every successful + * client-driven read or write. If the implementation can detect debugger-driven + * register reads and writes, then it recommended to call this method for those events. However, + * this method must not be called for target-driven register changes, except + * perhaps when the target becomes suspended. Note that some models may additionally provide a + * {@code value} attribute on each register -- when the register bank is its own description + * container -- however, updating those attributes is not a substitute for this callback. + * + * @param bank this register bank object + * @param updates a name-value map of updated registers + */ + default void registersUpdated(TargetObject bank, Map updates) { } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java index 8cc528e159..a287c57227 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModel.java @@ -15,10 +15,10 @@ */ package ghidra.dbg; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.RejectedExecutionException; +import java.util.function.Predicate; import ghidra.async.AsyncUtils; import ghidra.async.TypeSpec; @@ -27,6 +27,7 @@ import ghidra.dbg.target.TargetMemory; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.EnumerableTargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.*; import ghidra.util.Msg; @@ -223,16 +224,16 @@ public interface DebuggerObjectModel { * * @param the required implementation-specific type * @param cls the class for the required type - * @param ref the reference (or object) to check + * @param obj the object to check * @return the object, cast to the desired typed - * @throws IllegalArgumentException if -ref- does not belong to this model + * @throws DebuggerIllegalArgumentException if {@code obj} does not belong to this model */ - default T assertMine(Class cls, TargetObject ref) { - if (ref.getModel() != this) { - throw new IllegalArgumentException( - "TargetObject (or ref)" + ref + " does not belong to this model"); + default T assertMine(Class cls, TargetObject obj) { + if (obj.getModel() != this) { + throw new DebuggerIllegalArgumentException( + "TargetObject " + obj + " does not belong to this model"); } - return cls.cast(ref); + return cls.cast(obj); } /** @@ -304,18 +305,19 @@ public interface DebuggerObjectModel { * *

* The root is a virtual object to contain all the top-level objects of the model tree. This - * object represents the debugger itself. + * object represents the debugger itself. Note in most cases {@link #getModelRoot()} is + * sufficient; however, if you've just created the model, it is prudent to wait for it to create + * its root. For asynchronous cases, just listen for the root-creation and -added events. This + * method returns a future which completes after the root-added event. * - * @return the root - * @deprecated use {@link #getModelRoot()} instead + * @return a future which completes with the root */ - @Deprecated(forRemoval = true) public CompletableFuture fetchModelRoot(); /** * Get the root object of the model * - * @return the root + * @return the root or {@code null} if it hasn't been created, yet */ public TargetObject getModelRoot(); @@ -426,9 +428,15 @@ public interface DebuggerObjectModel { } /** - * @see #fetchModelObject(List) - * @deprecated Use {@link #getModelObject(List)} instead, or {@link #fetchModelObject(List)} if - * a refresh is needed + * Get an object from the model, resyncing according to the schema + * + *

+ * This is necessary when an object in the path has a resync mode other than + * {@link ResyncMode#NEVER} for the child being retrieved. Please note that some synchronization + * may still be required on the client side, since accessing the object before it is created + * will cause a {@code null} completion. + * + * @return a future that completes with the object or with {@code null} if it doesn't exist */ @Deprecated public default CompletableFuture fetchModelObject(List path) { @@ -441,13 +449,25 @@ public interface DebuggerObjectModel { *

* 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. + * return an object after it has been added. This method also never follows links. * * @param path the path of the object - * @return the object + * @return the object or {@code null} if it doesn't exist */ public TargetObject getModelObject(List path); + /** + * Get all created objects matching a given predicate + * + *

+ * Note the predicate is executed while holding an internal model-wide lock. Be careful and keep + * it simple. + * + * @param predicate the predicate + * @return the set of matching objects + */ + public Set getModelObjects(Predicate predicate); + /** * @see #fetchModelObject(List) */ @@ -455,6 +475,9 @@ public interface DebuggerObjectModel { return fetchModelObject(List.of(path)); } + /** + * @see #getModelObject(List) + */ public default TargetObject getModelObject(String... path) { return getModelObject(List.of(path)); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModelWithMemory.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModelWithMemory.java new file mode 100644 index 0000000000..44367ce237 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebuggerObjectModelWithMemory.java @@ -0,0 +1,26 @@ +/* ### + * 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; + +import ghidra.dbg.target.TargetMemory; +import ghidra.dbg.target.TargetObject; +import ghidra.program.model.address.Address; + +public interface DebuggerObjectModelWithMemory { + + TargetMemory getMemory(TargetObject target, Address address, int length); + +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java index ef30c9dc7e..99720da2b0 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractDebuggerObjectModel.java @@ -17,16 +17,24 @@ package ghidra.dbg.agent; import java.util.*; import java.util.concurrent.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; import ghidra.async.AsyncUtils; import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.target.TargetObject; import ghidra.dbg.util.PathUtils; +import ghidra.util.Msg; import ghidra.util.datastruct.ListenerSet; public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectModel { - protected final Object lock = new Object(); - protected final ExecutorService clientExecutor = Executors.newSingleThreadExecutor(); + public final Object lock = new Object(); + protected final ExecutorService clientExecutor = + Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() + .namingPattern(getClass().getSimpleName() + "-thread-%d") + .build()); protected final ListenerSet listeners = new ListenerSet<>(DebuggerModelListener.class, clientExecutor); @@ -50,19 +58,18 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo } protected void objectInvalidated(TargetObject object) { - synchronized (lock) { - creationLog.remove(object); - } + creationLog.remove(object.getPath()); } 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); } - root.getSchema().validateTypeAndInterfaces(root, null, null, root.enforcesStrictSchema()); - this.completedRoot.completeAsync(() -> root, clientExecutor); - listeners.fire.rootAdded(root); } @Override @@ -77,20 +84,27 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo } } + protected void onClientExecutor(DebuggerModelListener listener, Runnable r) { + CompletableFuture.runAsync(r, clientExecutor).exceptionally(t -> { + Msg.error(this, "Listener " + listener + " caused unexpected exception", t); + return null; + }); + } + protected void replayTreeEvents(DebuggerModelListener listener) { if (root == null) { assert creationLog.isEmpty(); return; } for (SpiTargetObject object : creationLog.values()) { - listener.created(object); + onClientExecutor(listener, () -> listener.created(object)); } Set visited = new HashSet<>(); for (SpiTargetObject object : creationLog.values()) { replayAddEvents(listener, object, visited); } if (rootAdded) { - listener.rootAdded(root); + onClientExecutor(listener, () -> listener.rootAdded(root)); } } @@ -99,34 +113,42 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo if (!visited.add(object)) { return; } - for (Object val : object.getCachedAttributes().values()) { + Map cachedAttributes = object.getCachedAttributes(); + for (Object val : cachedAttributes.values()) { if (!(val instanceof TargetObject)) { continue; } assert val instanceof SpiTargetObject; replayAddEvents(listener, (SpiTargetObject) val, visited); } - listener.attributesChanged(object, List.of(), object.getCachedAttributes()); - for (TargetObject elem : object.getCachedElements().values()) { + if (!cachedAttributes.isEmpty()) { + onClientExecutor(listener, + () -> listener.attributesChanged(object, List.of(), cachedAttributes)); + } + Map cachedElements = object.getCachedElements(); + for (TargetObject elem : cachedElements.values()) { assert elem instanceof SpiTargetObject; replayAddEvents(listener, (SpiTargetObject) elem, visited); } - listener.elementsChanged(object, List.of(), object.getCachedElements()); + if (!cachedElements.isEmpty()) { + onClientExecutor(listener, + () -> listener.elementsChanged(object, List.of(), cachedElements)); + } } @Override public void addModelListener(DebuggerModelListener listener, boolean replay) { - CompletableFuture.runAsync(() -> { + try { synchronized (lock) { if (replay) { replayTreeEvents(listener); } listeners.add(listener); } - }, clientExecutor).exceptionally(ex -> { + } + catch (Throwable ex) { listener.catastrophic(ex); - return null; - }); + } } @Override @@ -137,30 +159,27 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo /** * Ensure that dependent computations occur on the client executor * - *

- * Use as a method reference in a final call to - * {@link CompletableFuture#thenCompose(java.util.function.Function)} to ensure the final stage - * completes on the client executor. - * * @param the type of the future value - * @param v the value - * @return a future while completes with the given value on the client executor + * @param v the future + * @return a future which completes after the given one on the client executor */ - public CompletableFuture gateFuture(T v) { - //Msg.debug(this, "Gate requested @" + System.identityHashCode(clientExecutor)); - //Msg.debug(this, " rvalue: " + v); - return CompletableFuture.supplyAsync(() -> { - //Msg.debug(this, "Gate completing @" + System.identityHashCode(clientExecutor)); - //Msg.debug(this, " cvalue: " + v); - return v; + public CompletableFuture gateFuture(CompletableFuture future) { + return future.whenCompleteAsync((t, ex) -> { }, clientExecutor); } + @Override + public CompletableFuture flushEvents() { + return CompletableFuture.supplyAsync(() -> null, clientExecutor); + } + + /* @Override public CompletableFuture flushEvents() { return gateFuture(null); //return CompletableFuture.supplyAsync(() -> gateFuture((Void) null)).thenCompose(f -> f); } + */ @Override public CompletableFuture close() { @@ -197,7 +216,17 @@ public abstract class AbstractDebuggerObjectModel implements SpiDebuggerObjectMo @Override public TargetObject getModelObject(List path) { synchronized (lock) { + if (path.isEmpty()) { + return root; + } return creationLog.get(path); } } + + @Override + public Set getModelObjects(Predicate predicate) { + synchronized (lock) { + return creationLog.values().stream().filter(predicate).collect(Collectors.toSet()); + } + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractTargetObject.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractTargetObject.java index f14a2dbe75..14e6bd3fba 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractTargetObject.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/AbstractTargetObject.java @@ -19,13 +19,13 @@ import java.lang.reflect.Proxy; import java.util.*; import java.util.concurrent.CompletableFuture; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.DebuggerObjectModel; -import ghidra.dbg.agent.AbstractTargetObject.ProxyFactory; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.TargetObject.TargetObjectListener; import ghidra.dbg.target.schema.EnumerableTargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.PathUtils; +import ghidra.lifecycle.Internal; import ghidra.util.datastruct.ListenerSet; /** @@ -40,8 +40,7 @@ import ghidra.util.datastruct.ListenerSet; * * @param

the type of the parent */ -public abstract class AbstractTargetObject

- implements SpiTargetObject { +public abstract class AbstractTargetObject

implements SpiTargetObject { public static interface ProxyFactory { SpiTargetObject createProxy(AbstractTargetObject delegate, I info); } @@ -64,12 +63,12 @@ public abstract class AbstractTargetObject

protected boolean valid = true; // TODO: Remove these, and just do invocations on model's listeners? - protected final ListenerSet listeners; + protected final ListenerSet listeners; public AbstractTargetObject(ProxyFactory proxyFactory, I proxyInfo, AbstractDebuggerObjectModel model, P parent, String key, String typeHint, TargetObjectSchema schema) { - this.listeners = new ListenerSet<>(TargetObjectListener.class, model.clientExecutor); + this.listeners = new ListenerSet<>(DebuggerModelListener.class, model.clientExecutor); this.model = model; listeners.addChained(model.listeners); this.parent = parent; @@ -129,6 +128,11 @@ public abstract class AbstractTargetObject

return DebuggerObjectModel.requireIface(iface, getProxy(), path); } + @Override + public Collection getInterfaceNames() { + return Protected.getInterfaceNamesOf(getProxy().getClass()); + } + /** * Check if this object strictly conforms to the schema * @@ -162,11 +166,11 @@ public abstract class AbstractTargetObject

@Override public String toString() { if (schema == null) { - return String.format("<%s: path=%s model=%s schema=NULL>", getClass().getSimpleName(), - path, getModel()); + return String.format("<%s: path=%s model=%s schema=>", getClass().getSimpleName(), + getJoinedPath("."), getModel()); } - return String.format("<%s: path=%s model=%s schema=%s>", getClass().getSimpleName(), path, - getModel(), schema.getName()); + return String.format("<%s: path=%s model=%s schema=%s>", getClass().getSimpleName(), + getJoinedPath("."), getModel(), schema.getName()); } @Override @@ -185,7 +189,7 @@ public abstract class AbstractTargetObject

} @Override - public void addListener(TargetObjectListener l) { + public void addListener(DebuggerModelListener l) { if (!valid) { throw new IllegalStateException("Object is no longer valid: " + getProxy()); } @@ -193,7 +197,7 @@ public abstract class AbstractTargetObject

} @Override - public void removeListener(TargetObjectListener l) { + public void removeListener(DebuggerModelListener l) { listeners.remove(l); } @@ -242,35 +246,43 @@ public abstract class AbstractTargetObject

model.objectInvalidated(getProxy()); listeners.fire.invalidated(getProxy(), branch, reason); listeners.clear(); + listeners.clearChained(); } - protected void doInvalidateElements(Collection elems, String reason) { - for (Object e : elems) { + protected void doInvalidateElements(Map elems, String reason) { + for (Map.Entry ent : elems.entrySet()) { + String name = ent.getKey(); + Object e = ent.getValue(); if (e instanceof InvalidatableTargetObjectIf && e instanceof TargetObject) { InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) e; - obj.invalidateSubtree((TargetObject) e, reason); + if (!PathUtils.isElementLink(getPath(), name, obj.getPath())) { + obj.doInvalidateSubtree((TargetObject) e, reason); + } } } } - protected void doInvalidateElements(TargetObject branch, Collection elems, String reason) { - for (Object e : elems) { + protected void doInvalidateElements(TargetObject branch, Map elems, String reason) { + for (Map.Entry ent : elems.entrySet()) { + String name = ent.getKey(); + Object e = ent.getValue(); if (e instanceof InvalidatableTargetObjectIf) { InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) e; - obj.invalidateSubtree(branch, reason); + if (!PathUtils.isElementLink(getPath(), name, obj.getPath())) { + obj.doInvalidateSubtree(branch, reason); + } } } } - protected void doInvalidateAttributes(Map attrs, - String reason) { + protected void doInvalidateAttributes(Map attrs, String reason) { for (Map.Entry ent : attrs.entrySet()) { String name = ent.getKey(); Object a = ent.getValue(); if (a instanceof InvalidatableTargetObjectIf && a instanceof TargetObject) { InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) a; if (!PathUtils.isLink(getPath(), name, obj.getPath())) { - obj.invalidateSubtree((TargetObject) a, reason); + obj.doInvalidateSubtree((TargetObject) a, reason); } } } @@ -284,21 +296,40 @@ public abstract class AbstractTargetObject

if (a instanceof InvalidatableTargetObjectIf) { InvalidatableTargetObjectIf obj = (InvalidatableTargetObjectIf) a; if (!PathUtils.isLink(getPath(), name, obj.getPath())) { - obj.invalidateSubtree(branch, reason); + obj.doInvalidateSubtree(branch, reason); } } } } @Override - public void invalidateSubtree(TargetObject branch, String reason) { + public void doInvalidateSubtree(TargetObject branch, String reason) { // Pre-ordered traversal doInvalidate(branch, reason); - doInvalidateElements(branch, getCachedElements().values(), reason); + doInvalidateElements(branch, getCachedElements(), reason); doInvalidateAttributes(branch, getCachedAttributes(), reason); } - public ListenerSet getListeners() { + @Override + public void invalidateSubtree(TargetObject branch, String reason) { + synchronized (model.lock) { + doInvalidateSubtree(branch, reason); + } + } + + /** + * Get the listener set + * + *

+ * TODO: This method should only be used by the internal implementation. It's not exposed on the + * {@link TargetObject} interface, but it could be dangerous to have it here, since clients + * could cast to {@link AbstractTargetObject} and get at it, even if the implementation's jar is + * excluded from the compile-time classpath. + * + * @return the listener set + */ + @Internal + public ListenerSet getListeners() { return listeners; } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetObject.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetObject.java index c60763959d..b1f654f967 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetObject.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/DefaultTargetObject.java @@ -17,15 +17,18 @@ package ghidra.dbg.agent; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import ghidra.async.AsyncUtils; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.dbg.util.CollectionUtils.Delta; -import ghidra.dbg.util.PathUtils; import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator; import ghidra.util.Msg; +import ghidra.util.datastruct.ListenerSet; /** * A default implementation of {@link TargetObject} suitable for cases where the implementation @@ -39,14 +42,44 @@ public class DefaultTargetObject extends AbstractTargetObject

{ /** Note modifying this directly subverts notifications */ - protected final Map elements = new TreeMap<>(TargetObjectKeyComparator.ELEMENT); + protected final Map elements = + new TreeMap<>(TargetObjectKeyComparator.ELEMENT); + protected final Map cbElements = + new TreeMap<>(TargetObjectKeyComparator.ELEMENT); + protected final Map roCbElements = + Collections.unmodifiableMap(cbElements); protected CompletableFuture curElemsRequest; /** Note modifying this directly subverts notifications */ protected final Map attributes = new TreeMap<>(TargetObjectKeyComparator.ATTRIBUTE); + protected final Map cbAttributes = + new TreeMap<>(TargetObjectKeyComparator.ATTRIBUTE); + protected final Map roCbAttributes = + Collections.unmodifiableMap(cbAttributes); protected CompletableFuture curAttrsRequest; + /*protected static Set> dependencySet = Set.of(// + TargetProcess.class, // + TargetThread.class, // + TargetStack.class, // + TargetStackFrame.class, // + TargetRegisterBank.class, // + TargetRegisterContainer.class, // + TargetRegister.class, // + TargetMemory.class, // + TargetMemoryRegion.class, // + TargetModule.class, // + TargetModuleContainer.class, // + TargetSection.class, // + TargetBreakpointSpecContainer.class, // + TargetBreakpointSpec.class, // + TargetBreakpointLocation.class, // + TargetEventScope.class, // + TargetFocusScope.class, // + TargetExecutionStateful.class // + );*/ + /** * Construct a new default target object whose schema is derived from the parent * @@ -117,10 +150,9 @@ public class DefaultTargetObject AbstractDebuggerObjectModel model, P parent, String key, String typeHint, TargetObjectSchema schema) { super(proxyFactory, proxyInfo, model, parent, key, typeHint, schema); - changeAttributes(List.of(), List.of(), Map.ofEntries( - Map.entry(DISPLAY_ATTRIBUTE_NAME, key == null ? "" : key), - Map.entry(UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.UNSOLICITED)), - "Initialized"); + changeAttributes(List.of(), List.of(), + Map.ofEntries(Map.entry(DISPLAY_ATTRIBUTE_NAME, key == null ? "" : key)), + "Default"); } public DefaultTargetObject(ProxyFactory proxyFactory, I proxyInfo, @@ -157,8 +189,7 @@ public class DefaultTargetObject @Override public CompletableFuture resync(boolean refreshAttributes, boolean refreshElements) { - return CompletableFuture.allOf( - fetchAttributes(refreshAttributes), + return CompletableFuture.allOf(fetchAttributes(refreshAttributes), fetchElements(refreshElements)); } @@ -186,6 +217,14 @@ public class DefaultTargetObject return AsyncUtils.NIL; } + private boolean shouldRequestElements(boolean refresh) { + if (refresh) { + return true; + } + ResyncMode resync = getSchema().getElementResyncMode(); + return resync.shouldResync(curElemsRequest); + } + /** * {@inheritDoc} * @@ -199,11 +238,10 @@ public class DefaultTargetObject public CompletableFuture> fetchElements(boolean refresh) { CompletableFuture req; synchronized (elements) { - if (refresh || curElemsRequest == null || curElemsRequest.isCompletedExceptionally() || - getUpdateMode() == TargetUpdateMode.SOLICITED) { - curElemsRequest = requestElements(refresh).thenCompose(model::gateFuture); + if (shouldRequestElements(refresh)) { + curElemsRequest = model.gateFuture(requestElements(refresh)); } - req = curElemsRequest; + req = curElemsRequest == null ? AsyncUtils.NIL : curElemsRequest; } return req.thenApply(__ -> getCachedElements()); } @@ -220,6 +258,11 @@ public class DefaultTargetObject } } + @Override + public Map getCallbackElements() { + return roCbElements; + } + /** * {@inheritDoc} * @@ -230,22 +273,13 @@ public class DefaultTargetObject return fetchElements().thenApply(elems -> elems.get(index)); } - protected Map combineElements(Collection canonical, - Map links) { + protected Map combineElements(Collection autoKeyed, + Map mapKeyed) { Map asMap = new LinkedHashMap<>(); - for (E e : canonical) { - if (!PathUtils.parent(e.getPath()).equals(getPath())) { - Msg.error(this, "Link found in canonical elements of " + parent + ": " + e); - } + for (E e : autoKeyed) { asMap.put(e.getIndex(), e); } - for (Map.Entry ent : links.entrySet()) { - if (!PathUtils.isLink(getPath(), PathUtils.makeKey(ent.getKey()), - ent.getValue().getPath())) { - //Msg.error(this, "Canonical element found in links: " + ent); - } - asMap.put(ent.getKey(), ent.getValue()); - } + asMap.putAll(mapKeyed); return asMap; } @@ -256,14 +290,14 @@ public class DefaultTargetObject * An existing element is left in place if it's identical to its replacement as in {@code ==}. * This method also invalidates the sub-trees of removed elements, if any. * - * @param canonical the desired set of canonical elements - * @param links the desired map of linked elements + * @param autoKeyed the desired set of elements where keys are given by the elements + * @param mapKeyed the desired map of elements with specified keys (usually for links) * @param reason the reason for the change (used as the reason for invalidation) * @return the delta from the previous elements */ - public Delta setElements(Collection canonical, - Map links, String reason) { - Map elements = combineElements(canonical, links); + public Delta setElements(Collection autoKeyed, + Map mapKeyed, String reason) { + Map elements = combineElements(autoKeyed, mapKeyed); return setElements(elements, reason); } @@ -274,6 +308,15 @@ public class DefaultTargetObject return setElements(elements, Map.of(), reason); } + private void updateCallbackElements(Delta delta) { + CompletableFuture.runAsync(() -> { + delta.apply(this.cbElements, Delta.SAME); + }, model.clientExecutor).exceptionally(ex -> { + Msg.error(this, "Error updating elements before callback"); + return null; + }); + } + private Delta setElements(Map elements, String reason) { Delta delta; synchronized (model.lock) { @@ -283,8 +326,9 @@ public class DefaultTargetObject if (schemax != null) { schemax.validateElementDelta(getPath(), delta, enforcesStrictSchema()); } - doInvalidateElements(delta.removed.values(), reason); + doInvalidateElements(delta.removed, reason); if (!delta.isEmpty()) { + updateCallbackElements(delta); listeners.fire.elementsChanged(getProxy(), delta.getKeysRemoved(), delta.added); } return delta; @@ -298,15 +342,14 @@ public class DefaultTargetObject * This method also invalidates the sub-trees of removed elements, if any. * * @param remove the set of indices to remove - * @param addCanonical the set of canonical elements to add - * @param addLinks the map of linked elements to add + * @param autoKeyed the set of elements to add with the elements' keys + * @param mapKeyed the map of elements to add with given keys (usually for links) * @param reason the reason for the change (used as the reason for invalidation) * @return the actual delta from the previous to the current elements */ - public Delta changeElements(Collection remove, - Collection addCanonical, Map addLinks, - String reason) { - Map add = combineElements(addCanonical, addLinks); + public Delta changeElements(Collection remove, Collection autoKeyed, + Map mapKeyed, String reason) { + Map add = combineElements(autoKeyed, mapKeyed); return changeElements(remove, add, reason); } @@ -318,7 +361,7 @@ public class DefaultTargetObject return changeElements(remove, add, Map.of(), reason); } - private Delta changeElements(Collection remove, Map add, + public Delta changeElements(Collection remove, Map add, String reason) { Delta delta; synchronized (model.lock) { @@ -328,8 +371,9 @@ public class DefaultTargetObject if (schemax != null) { schemax.validateElementDelta(getPath(), delta, enforcesStrictSchema()); } - doInvalidateElements(delta.removed.values(), reason); + doInvalidateElements(delta.removed, reason); if (!delta.isEmpty()) { + updateCallbackElements(delta); listeners.fire.elementsChanged(getProxy(), delta.getKeysRemoved(), delta.added); } return delta; @@ -353,6 +397,14 @@ public class DefaultTargetObject return AsyncUtils.NIL; } + private boolean shouldRequestAttributes(boolean refresh) { + if (refresh) { + return true; + } + ResyncMode resync = getSchema().getAttributeResyncMode(); + return resync.shouldResync(curAttrsRequest); + } + /** * {@inheritDoc} * @@ -367,10 +419,10 @@ public class DefaultTargetObject CompletableFuture req; synchronized (attributes) { // update_mode does not affect attributes. They always behave as if UNSOLICITED. - if (refresh || curAttrsRequest == null || curAttrsRequest.isCompletedExceptionally()) { - curAttrsRequest = requestAttributes(refresh).thenCompose(model::gateFuture); + if (shouldRequestAttributes(refresh)) { + curAttrsRequest = model.gateFuture(requestAttributes(refresh)); } - req = curAttrsRequest; + req = curAttrsRequest == null ? AsyncUtils.NIL : curAttrsRequest; } return req.thenApply(__ -> { synchronized (model.lock) { @@ -394,6 +446,11 @@ public class DefaultTargetObject } } + @Override + public Map getCallbackAttributes() { + return roCbAttributes; + } + @Override public Object getCachedAttribute(String name) { synchronized (model.lock) { @@ -401,25 +458,13 @@ public class DefaultTargetObject } } - protected Map combineAttributes( - Collection canonicalObjects, Map linksAndValues) { + protected Map combineAttributes(Collection autoKeyed, + Map mapKeyed) { Map asMap = new LinkedHashMap<>(); - for (TargetObject ca : canonicalObjects) { - if (!PathUtils.parent(ca.getPath()).equals(getPath())) { - Msg.error(this, "Link found in canonical attributes: " + ca); - } + for (TargetObject ca : autoKeyed) { asMap.put(ca.getName(), ca); } - for (Map.Entry ent : linksAndValues.entrySet()) { - Object av = ent.getValue(); - /*if (av instanceof TargetObject) { - TargetObject link = (TargetObject) av; - if (!PathUtils.isLink(getPath(), ent.getKey(), link.getPath())) { - //Msg.error(this, "Canonical attribute found in links: " + ent); - } - }*/ - asMap.put(ent.getKey(), av); - } + asMap.putAll(mapKeyed); return asMap; } @@ -431,22 +476,31 @@ public class DefaultTargetObject * defined by {@link Objects#equals(Object, Object)}. This method also invalidates the sub-trees * of removed non-reference object-valued attributes. * - * @param canonicalObjects the desired set of canonical object-valued attributes - * @param linksAndValues the desired map of other attributes + * @param autoKeyed the desired set of object-valued attributes using the objects' keys + * @param mapKeyed the desired map of other attributes (usually links and primitive values) * @param reason the reason for the change (used as the reason for invalidation) * @return the delta from the previous attributes */ - public Delta setAttributes(Collection canonicalObjects, - Map linksAndValues, String reason) { - Map attributes = combineAttributes(canonicalObjects, linksAndValues); + public Delta setAttributes(Collection autoKeyed, + Map mapKeyed, String reason) { + Map attributes = combineAttributes(autoKeyed, mapKeyed); return setAttributes(attributes, reason); } + private void updateCallbackAttributes(Delta delta) { + CompletableFuture.runAsync(() -> { + delta.apply(this.cbAttributes, Delta.EQUAL); + }, model.clientExecutor).exceptionally(ex -> { + Msg.error(this, "Error updating elements before callback"); + return null; + }); + } + /** * TODO: Document me. */ public Delta setAttributes(Map attributes, String reason) { - Delta delta; + Delta delta; synchronized (model.lock) { delta = Delta.computeAndSet(this.attributes, attributes, Delta.EQUAL); } @@ -456,6 +510,7 @@ public class DefaultTargetObject } doInvalidateAttributes(delta.removed, reason); if (!delta.isEmpty()) { + updateCallbackAttributes(delta); listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added); } return delta; @@ -470,18 +525,33 @@ public class DefaultTargetObject * of removed non-reference object-valued attributes. * * @param remove the set of names to remove - * @param addCanonicalObjects the set of canonical object-valued attributes to add - * @param addLinksAndValues the map of other attributes to add + * @param autoKeyed the set of object-valued attributes to add using the objects' keys + * @param mapKeyed the map of other attributes to add (usually links and primitives) * @param reason the reason for the change (used as the reason for invalidation) * @return the actual delta from the previous to the current attributes */ public Delta changeAttributes(List remove, - Collection addCanonicalObjects, - Map addLinksAndValues, String reason) { - Map add = combineAttributes(addCanonicalObjects, addLinksAndValues); + Collection autoKeyed, Map mapKeyed, String reason) { + Map add = combineAttributes(autoKeyed, mapKeyed); return changeAttributes(remove, add, reason); } + public Map filterValid(String name, Map map) { + return map.entrySet().stream().filter(ent -> { + T val = ent.getValue(); + if (!(val instanceof TargetObject)) { + return true; + } + TargetObject obj = (TargetObject) val; + if (obj.isValid()) { + return true; + } + Msg.error(this, name + " " + ent.getKey() + " of " + getJoinedPath(".") + + " linked to invalid object: " + obj.getJoinedPath(".")); + return false; + }).collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); + } + /** * This method may soon be made private. Consider * {@link DefaultTargetObject#changeAttributes(List, Collection, Map, String)} instead. @@ -491,7 +561,8 @@ public class DefaultTargetObject * used to specify the "canonical" location. */ public Delta changeAttributes(List remove, Map add, String reason) { - Delta delta; + // add = filterValid("Attribute", add); + Delta delta; synchronized (model.lock) { delta = Delta.apply(this.attributes, remove, add, Delta.EQUAL); } @@ -500,9 +571,87 @@ public class DefaultTargetObject schemax.validateAttributeDelta(getPath(), delta, enforcesStrictSchema()); } doInvalidateAttributes(delta.removed, reason); - if (!delta.isEmpty()) { + if (!delta.isEmpty()/* && !reason.equals("Default")*/) { + updateCallbackAttributes(delta); listeners.fire.attributesChanged(getProxy(), delta.getKeysRemoved(), delta.added); } return delta; } + + @Override + public ListenerSet getListeners() { + return listeners; + } + + /* + private CompletableFuture findDependencies(TargetObjectListener l) { + //System.err.println("findDependencies " + this); + Map resultAttrs = new HashMap<>(); + Map resultElems = new HashMap<>(); + AsyncFence fence = new AsyncFence(); + fence.include(fetchAttributes(false).thenCompose(attrs -> { + AsyncFence af = new AsyncFence(); + for (String key : attrs.keySet()) { //requiredObjKeys) { + Object object = attrs.get(key); + if (!(object instanceof TargetObjectRef)) { + continue; + } + TargetObjectRef ref = (TargetObjectRef) object; + if (PathUtils.isLink(getPath(), key, ref.getPath())) { + continue; + } + af.include(ref.fetch().thenAccept(obj -> { + if (isDependency(obj)) { + synchronized (this) { + resultAttrs.put(key, obj); + obj.addListener(l); + } + } + })); + } + return af.ready(); + })); + fence.include(fetchElements(false).thenCompose(elems -> { + AsyncFence ef = new AsyncFence(); + for (Entry entry : elems.entrySet()) { + ef.include(entry.getValue().fetch().thenAccept(obj -> { + synchronized (this) { + resultElems.put(entry.getKey(), obj); + obj.addListener(l); + } + })); + } + return ef.ready(); + })); + return fence.ready(); + } + + public boolean isDependency(TargetObject object) { + String name = object.getName(); + if (name != null) { + if (name.equals("Debug")) + return true; + if (name.equals("Stack")) + return true; + } + + Set> interfaces = object.getSchema().getInterfaces(); + for (Class ifc : interfaces) { + if (dependencySet.contains(ifc)) { + return true; + } + } + return false; + } + */ + + @Override + public void addListener(DebuggerModelListener l) { + listeners.add(l); + /* + if (isDependency(this)) { + findDependencies(l); + } + */ + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/InvalidatableTargetObjectIf.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/InvalidatableTargetObjectIf.java index cf36a939fc..c03a87eb85 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/InvalidatableTargetObjectIf.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/agent/InvalidatableTargetObjectIf.java @@ -59,4 +59,16 @@ public interface InvalidatableTargetObjectIf extends TargetObject { * @param reason a human-consumable explanation for the removal */ void invalidateSubtree(TargetObject branch, String reason); + + /** + * Invalidate this subtree, without locking + * + *

+ * This really only exists to avoid reentering a lock. This should be called when a thread has + * already acquired the relevant lock(s). + * + * @param branch + * @param reason + */ + void doInvalidateSubtree(TargetObject branch, String reason); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetAccessConditioned.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetAccessConditioned.java index 2b51020e27..518ce58038 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetAccessConditioned.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetAccessConditioned.java @@ -36,10 +36,4 @@ public interface TargetAccessConditioned extends TargetObject { public default boolean isAccessible() { return getTypedAttributeNowByName(ACCESSIBLE_ATTRIBUTE_NAME, Boolean.class, true); } - - public interface TargetAccessibilityListener extends TargetObjectListener { - default void accessibilityChanged(TargetAccessConditioned object, - boolean accessibe) { - } - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointLocation.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointLocation.java index 1f5f6768d0..b82eb1bdab 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointLocation.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointLocation.java @@ -16,7 +16,6 @@ package ghidra.dbg.target; import ghidra.dbg.DebuggerTargetObjectIface; -import ghidra.dbg.attributes.TargetObjectList; import ghidra.dbg.target.schema.TargetAttributeType; import ghidra.program.model.address.Address; @@ -25,13 +24,14 @@ import ghidra.program.model.address.Address; * *

* If the native debugger does not separate the concepts of specification and location, then - * breakpoint objects should implement both the specification and location interfaces. + * breakpoint objects should implement both the specification and location interfaces. If the + * location is user-togglable independent of its specification, it should implement + * {@link TargetTogglable} as well. */ @DebuggerTargetObjectIface("BreakpointLocation") public interface TargetBreakpointLocation extends TargetObject { String ADDRESS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "address"; - String AFFECTS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "affects"; // NOTE: address and length are treated separately (not using AddressRange) // On GDB, e.g., the length may not be offered immediately. String LENGTH_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "length"; @@ -47,21 +47,6 @@ public interface TargetBreakpointLocation extends TargetObject { return getTypedAttributeNowByName(ADDRESS_ATTRIBUTE_NAME, Address.class, null); } - /** - * A list of object to which this breakpoint applies - * - *

- * This list may be empty, in which case, this location is conventionally assumed to apply - * everywhere its container's location/scope suggests. - * - * @return the list of affected objects' references - */ - @TargetAttributeType(name = AFFECTS_ATTRIBUTE_NAME, hidden = true) - public default TargetObjectList getAffects() { - return getTypedAttributeNowByName(AFFECTS_ATTRIBUTE_NAME, TargetObjectList.class, - TargetObjectList.of()); - } - /** * If available, get the length in bytes, of the range covered by the breakpoint. * diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointLocationContainer.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointLocationContainer.java new file mode 100644 index 0000000000..17b47aa2ac --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointLocationContainer.java @@ -0,0 +1,33 @@ +/* ### + * 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 ghidra.dbg.DebuggerTargetObjectIface; + +/** + * A place for breakpoint locations to reside + * + *

+ * This is just a marker interface for finding where a target's breakpoints are given. In some + * models, notably GDB, the locations belong to a global set of specifications. The only way to + * indicate that a location applies to a target is for it to be a successor of that target, at least + * by linking. To ease discovery, the breakpoint location container for the target must be a + * canonical successor of the target. The locations in the container may be canonical or links. + */ +@DebuggerTargetObjectIface("BreakpointLocationContainer") +public interface TargetBreakpointLocationContainer extends TargetObject { + // Nothing here aside from a marker +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpec.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpec.java index 524cf314ff..5ec7bc1139 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpec.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpec.java @@ -21,7 +21,7 @@ import java.util.concurrent.CompletableFuture; import ghidra.dbg.DebugModelConventions; import ghidra.dbg.DebuggerTargetObjectIface; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.target.schema.TargetAttributeType; /** @@ -35,18 +35,40 @@ import ghidra.dbg.target.schema.TargetAttributeType; * object include the resolved {@link TargetBreakpointLocation}s. If the debugger does not share * this same concept, then its breakpoints should implement both the specification and the location; * the specification need not have any children. + * + *

+ * This object extends {@link TargetTogglable} for a transitional period only. Implementations whose + * breakpoint specifications can be toggled should declare this interface explicitly. When the + * specification is user togglable, toggling it should effectively toggle all locations -- whether + * or not the locations are user togglable. */ @DebuggerTargetObjectIface("BreakpointSpec") -public interface TargetBreakpointSpec extends TargetObject { +public interface TargetBreakpointSpec extends TargetObject, /*@Transitional*/ TargetTogglable { public enum TargetBreakpointKind { - READ, WRITE, EXECUTE, SOFTWARE; + /** + * A read breakpoint, likely implemented in hardware + */ + READ, + /** + * A write breakpoint, likely implemented in hardware + */ + WRITE, + /** + * An execution breakpoint implemented in hardware, i.e., without modifying the target's + * program memory + */ + HW_EXECUTE, + /** + * An execution breakpoint implemented in software, i.e., by modifying the target's program + * memory + */ + SW_EXECUTE; } String CONTAINER_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "container"; String EXPRESSION_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "expression"; String KINDS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "kinds"; - String ENABLED_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "enabled"; /** * Get the container of this breakpoint. @@ -59,8 +81,9 @@ public interface TargetBreakpointSpec extends TargetObject { * @return a reference to the container */ @TargetAttributeType(name = CONTAINER_ATTRIBUTE_NAME, required = true, hidden = true) - public default TargetBreakpointContainer getContainer() { - return getTypedAttributeNowByName(CONTAINER_ATTRIBUTE_NAME, TargetBreakpointContainer.class, + public default TargetBreakpointSpecContainer getContainer() { + return getTypedAttributeNowByName(CONTAINER_ATTRIBUTE_NAME, + TargetBreakpointSpecContainer.class, null); } @@ -89,16 +112,6 @@ public interface TargetBreakpointSpec extends TargetObject { TargetBreakpointKindSet.EMPTY); } - /** - * Check if the breakpoint is enabled - * - * @return true if enabled, false otherwise - */ - @TargetAttributeType(name = ENABLED_ATTRIBUTE_NAME, required = true, hidden = true) - public default boolean isEnabled() { - return getTypedAttributeNowByName(ENABLED_ATTRIBUTE_NAME, Boolean.class, false); - } - /** * Add an action to execute locally when this breakpoint traps execution * @@ -132,25 +145,6 @@ public interface TargetBreakpointSpec extends TargetObject { TargetBreakpointLocation breakpoint); } - /** - * Disable all breakpoints resulting from this specification - */ - public CompletableFuture disable(); - - /** - * Enable all breakpoints resulting from this specification - */ - public CompletableFuture enable(); - - /** - * Enable or disable all breakpoints resulting from this specification - * - * @param enabled true to enable, false to disable - */ - public default CompletableFuture toggle(boolean enabled) { - return enabled ? enable() : disable(); - } - /** * Get the locations created by this specification. * @@ -171,9 +165,4 @@ public interface TargetBreakpointSpec extends TargetObject { } // TODO: Make hit count part of the common interface? - - public interface TargetBreakpointSpecListener extends TargetObjectListener { - default void breakpointToggled(TargetBreakpointSpec spec, boolean enabled) { - } - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointContainer.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpecContainer.java similarity index 81% rename from Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointContainer.java rename to Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpecContainer.java index 5880bba0c6..f79345f895 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetBreakpointSpecContainer.java @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import ghidra.dbg.DebuggerTargetObjectIface; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.schema.TargetAttributeType; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.CollectionUtils.AbstractEmptySet; import ghidra.dbg.util.CollectionUtils.AbstractNSet; import ghidra.program.model.address.*; @@ -31,9 +32,17 @@ import ghidra.program.model.address.*; *

* This interface provides for the placement (creation) of breakpoints and as a listening point for * breakpoint events. Typically, it is implemented by an object whose elements are breakpoints. + * + *

+ * TODO: Rename this to {@code TargetBreakpointOperations}. Conventionally, it is a container of + * breakpoints, but it doesn't technically have to be. A client searching for the breakpoint + * (location) container should use {@link TargetObjectSchema#searchForCanonicalContainer(Class)}, + * passing {@link TargetBreakpointLocation}. A client seeking to place breakpoints should use + * {@link TargetObjectSchema#searchForSuitable(Class, java.util.List)}, passing + * {@link TargetBreakpointSpecContainer}. */ -@DebuggerTargetObjectIface("BreakpointContainer") -public interface TargetBreakpointContainer extends TargetObject { +@DebuggerTargetObjectIface("BreakpointSpecContainer") +public interface TargetBreakpointSpecContainer extends TargetObject { String SUPPORTED_BREAK_KINDS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "supported_breakpoint_kinds"; @@ -132,26 +141,4 @@ public interface TargetBreakpointContainer extends TargetObject { Set kinds) { return placeBreakpoint(new AddressRangeImpl(address, address), kinds); } - - public interface TargetBreakpointListener extends TargetObjectListener { - /** - * A breakpoint trapped execution - * - *

- * The program counter can be obtained in a few ways. The most reliable is to get the - * address of the effective breakpoint. If available, the frame will also contain the - * program counter. Finally, the trapped object or one of its relatives may offer the - * program counter. - * - * @param container the container whose breakpoint trapped execution - * @param trapped the object whose execution was trapped - * @param frame the innermost stack frame, if available, of the trapped object - * @param spec the breakpoint specification - * @param breakpoint the breakpoint location that actually trapped execution - */ - default void breakpointHit(TargetBreakpointContainer container, TargetObject trapped, - TargetStackFrame frame, TargetBreakpointSpec spec, - TargetBreakpointLocation breakpoint) { - } - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetConsole.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetConsole.java index e67d95660e..0fb61d5a82 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetConsole.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetConsole.java @@ -54,37 +54,4 @@ public interface TargetConsole extends TargetObject { * @return a future which completes when the data is sent */ public CompletableFuture write(byte[] data); - - public interface TargetConsoleListener extends TargetObjectListener { - /** - * The console has produced output - * - * @param console the console producing the output - * @param channel identifies the "output stream", stdout or stderr - * @param data the output data - */ - default void consoleOutput(TargetObject console, Channel channel, byte[] data) { - } - - /** - * The console has produced output - * - * @implNote Overriding this method is not a substitute for overriding - * {@link #consoleOutput(TargetObject, Channel, byte[])}. Some models may invoke - * this {@code String} variant as a convenience, which by default, invokes the - * {@code byte[]} variant, but models are only expected to invoke the - * {@code byte[]} variant. A client may override this method simply to avoid - * back-and-forth conversions between {@code String}s and {@code byte[]}s. - * - * @param console the console producing the output - * @param channel identifies the "output stream", stdout or stderr - * @param text the output text - */ - default void consoleOutput(TargetObject console, Channel channel, String text) { - consoleOutput(console, channel, text.getBytes(CHARSET)); - } - } - - public interface TargetTextConsoleListener extends TargetConsoleListener { - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetEventScope.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetEventScope.java index cd986c3d82..c89f66b69b 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetEventScope.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetEventScope.java @@ -15,8 +15,6 @@ */ package ghidra.dbg.target; -import java.util.List; - import ghidra.dbg.DebuggerTargetObjectIface; import ghidra.dbg.target.schema.TargetAttributeType; @@ -36,7 +34,7 @@ public interface TargetEventScope extends TargetObject { /** * The session has stopped for an unspecified reason */ - STOPPED, + STOPPED(true), /** * The session is running for an unspecified reason * @@ -44,40 +42,40 @@ public interface TargetEventScope extends TargetObject { * Note that execution state changes are communicated via {@link TargetExecutionStateful}, * since the sessiopn may specify such state on a per-target and/or per-thread basis. */ - RUNNING, + RUNNING(false), /** * A new target process was created by this session * *

* If the new process is part of the session, too, it must be passed as a parameter. */ - PROCESS_CREATED, + PROCESS_CREATED(false), /** * A target process in this session has exited */ - PROCESS_EXITED, + PROCESS_EXITED(false), /** * A new target thread was created by this session * *

* The new thread must be part of the session, too, and must be given as the event thread. */ - THREAD_CREATED, + THREAD_CREATED(false), /** * A target thread in this session has exited */ - THREAD_EXITED, + THREAD_EXITED(false), /** * A new module has been loaded by this session * *

* The new module must be passed as a parameter. */ - MODULE_LOADED, + MODULE_LOADED(false), /** * A module has been unloaded by this session */ - MODULE_UNLOADED, + MODULE_UNLOADED(false), /** * The session has stopped, because one if its targets was trapped by a breakpoint * @@ -85,7 +83,7 @@ public interface TargetEventScope extends TargetObject { * If the breakpoint (specification) is part of the session, too, it must be passed as a * parameter. The trapped target must also be passed as a parameter. */ - BREAKPOINT_HIT, + BREAKPOINT_HIT(true), /** * The session has stopped, because a stepping command has completed * @@ -93,7 +91,7 @@ public interface TargetEventScope extends TargetObject { * The target completing the command must also be passed as a parameter, unless it is the * event thread. If it is a thread, it must be given as the event thread. */ - STEP_COMPLETED, + STEP_COMPLETED(true), /** * The session has stopped, because one if its targets was trapped on an exception * @@ -101,7 +99,7 @@ public interface TargetEventScope extends TargetObject { * The trapped target must also be passed as a parameter, unless it is the event thread. If * it is a thread, it must be given as the event thread. */ - EXCEPTION, + EXCEPTION(false), /** * The session has stopped, because one of its targets was trapped on a signal * @@ -109,7 +107,13 @@ public interface TargetEventScope extends TargetObject { * The trapped target must also be passed as a parameter, unless it is the event thread. If * it is a thread, it must be given as the event thread. */ - SIGNAL, + SIGNAL(false); + + public final boolean impliesStop; + + private TargetEventType(boolean impliesStop) { + this.impliesStop = impliesStop; + } } /** @@ -125,7 +129,7 @@ public interface TargetEventScope extends TargetObject { * @return the process or reference */ @TargetAttributeType(name = EVENT_PROCESS_ATTRIBUTE_NAME, hidden = true) - public default /*TODO: TypedTargetObjectRef>*/ String getEventProcess() { + public default /*TODO: TargetProcess*/ String getEventProcess() { return getTypedAttributeNowByName(EVENT_PROCESS_ATTRIBUTE_NAME, String.class, null); } @@ -138,43 +142,7 @@ public interface TargetEventScope extends TargetObject { * @return the thread or reference */ @TargetAttributeType(name = EVENT_THREAD_ATTRIBUTE_NAME, hidden = true) - public default /*TODO: TypedTargetObjectRef>*/ String getEventThread() { + public default /*TODO: TargetThread*/ String getEventThread() { return getTypedAttributeNowByName(EVENT_THREAD_ATTRIBUTE_NAME, String.class, null); } - - public interface TargetEventScopeListener extends TargetObjectListener { - /** - * An event affecting a target in this scope has occurred - * - *

- * When present, this callback must be invoked before any other callback which results from - * this event, except creation events. E.g., for PROCESS_EXITED, this must be called before - * the affected process is removed from the tree. - * - *

- * Whenever possible, event thread must be given. This is often the thread given focus by - * the debugger immediately upon stopping for the event. Parameters are not (yet) strictly - * specified, but it should include the stopped target, if that target is not already given - * by the event thread. It may optionally contain other useful information, such as an exit - * code, but no listener should depend on that information being given. - * - *

- * The best way to communicate to users what has happened is via the description. Almost - * every other result of an event is communicated by other means in the model, e.g., state - * changes, object creation, destruction. The description should contain as much information - * as possible to cue users as to why the other changes have occurred, and point them to - * relevant objects. For example, if trapped on a breakpoint, the description might contain - * the breakpoint's identifier. If the debugger prints a message for this event, that - * message is probably a sufficient description. - * - * @param object the event scope - * @param eventThread if applicable, the thread causing the event - * @param type the type of event - * @param description a human-readable description of the event - * @param parameters extra parameters for the event. TODO: Specify these for each type - */ - default void event(TargetEventScope object, TargetThread eventThread, TargetEventType type, - String description, List parameters) { - } - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetExecutionStateful.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetExecutionStateful.java index d6725771a6..2c225cd8de 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetExecutionStateful.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetExecutionStateful.java @@ -33,7 +33,8 @@ public interface TargetExecutionStateful extends TargetObject { /** * The object has been created, but it not yet alive * - * This may apply, e.g., to a GDB "Inferior" which has no yet been used to launch or attach + *

+ * This may apply, e.g., to a GDB "Inferior," which has no yet been used to launch or attach * to a process. */ INACTIVE { @@ -55,6 +56,13 @@ public interface TargetExecutionStateful extends TargetObject { /** * The object is alive, but its execution state is unspecified + * + *

+ * Implementations should use {@link #STOPPED} and {@link #RUNNING} whenever possible. For + * some objects, e.g., a process, this is conventionally determined by its parts, e.g., + * threads: A process is running when any of its threads are running. It is stopped + * when all of its threads are stopped. For the clients' sakes, all models should + * implement these conventions internally. */ ALIVE { @Override @@ -124,7 +132,7 @@ public interface TargetExecutionStateful extends TargetObject { *

* The object still exists but no longer represents something alive. This could be used for * stale handles to objects which may still be queried (e.g., for a process exit code), or - * e.g., a GDB "Inferior" which could be re-used to launch or attach to another process. + * e.g., a GDB "Inferior," which could be re-used to launch or attach to another process. */ TERMINATED { @Override @@ -173,18 +181,6 @@ public interface TargetExecutionStateful extends TargetObject { @TargetAttributeType(name = STATE_ATTRIBUTE_NAME, required = true, hidden = true) public default TargetExecutionState getExecutionState() { return getTypedAttributeNowByName(STATE_ATTRIBUTE_NAME, TargetExecutionState.class, - TargetExecutionState.STOPPED); - } - - public interface TargetExecutionStateListener extends TargetObjectListener { - /** - * The object has entered a different execution state - * - * @param object the object - * @param state the new state - */ - default void executionStateChanged(TargetExecutionStateful object, - TargetExecutionState state) { - } + TargetExecutionState.INACTIVE); } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetFocusScope.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetFocusScope.java index 78852c48bc..cea02bbe8d 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetFocusScope.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetFocusScope.java @@ -50,39 +50,25 @@ public interface TargetFocusScope extends TargetObject { /** * Get the focused object in this scope * + *

+ * Note that client UIs should be careful about event loops and user intuition when listening + * for changes of this attribute. The client should avoid calling + * {@link #requestFocus(TargetObject)} in response. Perhaps the simplest way is to only request + * focus when the selected object has actually changed. The debugger may "adjust" the focus. For + * example, when focusing a thread, the debugger may instead focus a particular frame in that + * thread (a successor). Or, when focusing a memory region, the debugger may only focus the + * owning process (an ancestor). The suggested strategy (a work in progress) is the "same level, + * same type" rule. It may be appropriate to highlight the actual focused object to cue the user + * in, but the user's selection should remain at the same level. If an ancestor or successor + * receives focus, leave the user's selection as is. If a sibling element or one of its + * successors receives focus, select that sibling. A similar rule applies to "cousin" elements, + * so long as they have the same type. In most other cases, it's appropriate to select the + * focused element. TODO: Implement this rule in {@link DebugModelConventions} + * * @return a reference to the focused object or {@code null} if no object is focused. */ @TargetAttributeType(name = FOCUS_ATTRIBUTE_NAME, required = true, hidden = true) default TargetObject getFocus() { return getTypedAttributeNowByName(FOCUS_ATTRIBUTE_NAME, TargetObject.class, null); } - - public interface TargetFocusScopeListener extends TargetObjectListener { - /** - * Focused has changed within this scope - * - *

- * Note that client UIs should be careful about event loops and user intuition when - * receiving this event. The client should avoid calling - * {@link TargetFocusScope#requestFocus(TargetObject)} in response to this event. Perhaps - * the simplest way is to only request focus when the selected object has actually changed. - * Also, the debugger may "adjust" the focus. For example, when focusing a thread, the - * debugger may additionally focus a particular frame in that thread (a successor). Or, when - * focusing a memory region, the debugger may only focus the owning process (an ancestor). - * The suggested strategy (a work in progress) is the "same level, same type" rule. It may - * be appropriate to highlight the actual focused object to cue the user in, but the user's - * selection should remain at the same level. If an ancestor or successor receives focus, - * leave the user's selection as is. If a sibling element or one of its successors receives - * focus, select that sibling. A similar rule applies to "cousin" elements, so long as they - * have the same type. In most other cases, it's appropriate to select the focused element. - * - *

- * TODO: Implement this rule in {@link DebugModelConventions} - * - * @param object this scope - * @param focused the object receiving focus - */ - default void focusChanged(TargetFocusScope object, TargetObject focused) { - } - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetInterpreter.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetInterpreter.java index cade7e57bb..7f8070ba8c 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetInterpreter.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetInterpreter.java @@ -18,8 +18,6 @@ package ghidra.dbg.target; import java.util.concurrent.CompletableFuture; import ghidra.dbg.DebuggerTargetObjectIface; -import ghidra.dbg.target.TargetConsole.Channel; -import ghidra.dbg.target.TargetConsole.TargetTextConsoleListener; import ghidra.dbg.target.schema.TargetAttributeType; /** @@ -72,30 +70,4 @@ public interface TargetInterpreter extends TargetObject { public default String getPrompt() { return getTypedAttributeNowByName(PROMPT_ATTRIBUTE_NAME, String.class, ">"); } - - public interface TargetInterpreterListener extends TargetTextConsoleListener { - /** - * {@inheritDoc} - * - *

- * This should only receive console output for non-captured commands. See - * {@link TargetInterpreter#executeCapture(String)}. - */ - @Override - default void consoleOutput(TargetObject console, Channel channel, String text) { - TargetTextConsoleListener.super.consoleOutput(console, channel, text); - } - - /** - * The interpreter's prompt has changed - * - *

- * Any UI elements presenting the prompt should be updated immediately. - * - * @param interpreter the interpreter whose prompt changed - * @param prompt the new prompt - */ - default void promptChanged(TargetInterpreter interpreter, String prompt) { - } - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMemory.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMemory.java index d5daa4667d..f58b4c686b 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMemory.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMemory.java @@ -19,9 +19,7 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import ghidra.dbg.DebuggerTargetObjectIface; -import ghidra.dbg.error.DebuggerMemoryAccessException; import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressRange; /** * The memory model of a target object @@ -90,38 +88,4 @@ public interface TargetMemory extends TargetObject { public default CompletableFuture> getRegions() { return fetchChildrenSupporting((Class) TargetMemoryRegion.class); } - - public interface TargetMemoryListener extends TargetObjectListener { - /** - * Memory was successfully read or written - * - *

- * If the implementation employs a cache, then it need only report reads or writes which - * updated that cache. However, that cache must be invalidated whenever any other event - * occurs which could change memory, e.g., the target stepping or running. - * - *

- * If the implementation can detect memory reads or writes driven by the debugger - * then it is also acceptable to call this method for those events. However, this method - * must not be called for memory changes driven by the target. In other - * words, this method should only be called for reads or writes requested by the user. - * - * @param memory this memory object - * @param address the starting address of the affected range - * @param data the new data for the affected range - */ - default void memoryUpdated(TargetMemory memory, Address address, byte[] data) { - } - - /** - * An attempt to read memory failed - * - * @param memory the memory object - * @param range the range for the read which generated the error - * @param e the error - */ - default void memoryReadError(TargetMemory memory, AddressRange range, - DebuggerMemoryAccessException e) { - } - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java index 13583f4350..0e2633b6ea 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetMethod.java @@ -153,16 +153,17 @@ public interface TargetMethod extends TargetObject { return (T) arguments.get(name); } if (required) { - throw new DebuggerIllegalArgumentException("Missing required parameter " + name); + throw new DebuggerIllegalArgumentException( + "Missing required parameter '" + name + "'"); } return defaultValue; } @Override public String toString() { - return String.format("> - implements TargetParameterMap { + public static class ImmutableTargetParameterMap extends + AbstractNMap> implements TargetParameterMap { public ImmutableTargetParameterMap(Map> map) { super(map); @@ -200,8 +200,7 @@ public interface TargetMethod extends TargetObject { * @return a map of descriptions by name */ static TargetParameterMap makeParameters(Stream> params) { - return TargetParameterMap.copyOf( - params.collect(Collectors.toMap(p -> p.name, p -> p))); + return TargetParameterMap.copyOf(params.collect(Collectors.toMap(p -> p.name, p -> p))); } /** @@ -210,8 +209,7 @@ public interface TargetMethod extends TargetObject { * @param params the descriptions * @return a map of descriptions by name */ - static TargetParameterMap makeParameters( - Collection> params) { + static TargetParameterMap makeParameters(Collection> params) { return makeParameters(params.stream()); } @@ -221,8 +219,7 @@ public interface TargetMethod extends TargetObject { * @param params the descriptions * @return a map of descriptions by name */ - static TargetParameterMap makeParameters( - ParameterDescription... params) { + static TargetParameterMap makeParameters(ParameterDescription... params) { return makeParameters(Stream.of(params)); } @@ -240,8 +237,7 @@ public interface TargetMethod extends TargetObject { if (!parameters.keySet().containsAll(arguments.keySet())) { Set extraneous = new TreeSet<>(arguments.keySet()); extraneous.removeAll(parameters.keySet()); - throw new DebuggerIllegalArgumentException( - "Extraneous parameters: " + extraneous); + throw new DebuggerIllegalArgumentException("Extraneous parameters: " + extraneous); } } Map valid = new LinkedHashMap<>(); @@ -290,8 +286,8 @@ public interface TargetMethod extends TargetObject { * @return the parameter map */ static TargetParameterMap getParameters(TargetObject obj) { - return obj.getTypedAttributeNowByName(PARAMETERS_ATTRIBUTE_NAME, - TargetParameterMap.class, TargetParameterMap.of()); + return obj.getTypedAttributeNowByName(PARAMETERS_ATTRIBUTE_NAME, TargetParameterMap.class, + TargetParameterMap.of()); } /** @@ -299,11 +295,7 @@ public interface TargetMethod extends TargetObject { * * @return the name-description map of parameters */ - @TargetAttributeType( - name = PARAMETERS_ATTRIBUTE_NAME, - required = true, - fixed = true, - hidden = true) + @TargetAttributeType(name = PARAMETERS_ATTRIBUTE_NAME, required = true, fixed = true, hidden = true) default public TargetParameterMap getParameters() { return getParameters(this); } @@ -318,14 +310,9 @@ public interface TargetMethod extends TargetObject { * * @return the return type */ - @TargetAttributeType( - name = RETURN_TYPE_ATTRIBUTE_NAME, - required = true, - fixed = true, - hidden = true) + @TargetAttributeType(name = RETURN_TYPE_ATTRIBUTE_NAME, required = true, fixed = true, hidden = true) default public Class getReturnType() { - return getTypedAttributeNowByName(RETURN_TYPE_ATTRIBUTE_NAME, Class.class, - Object.class); + return getTypedAttributeNowByName(RETURN_TYPE_ATTRIBUTE_NAME, Class.class, Object.class); } // TODO: Allow extra parameters, i.e., varargs? diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetModuleContainer.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetModuleContainer.java index a96cff79f5..c8132552a3 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetModuleContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetModuleContainer.java @@ -19,6 +19,7 @@ import java.util.concurrent.CompletableFuture; import ghidra.dbg.DebuggerTargetObjectIface; import ghidra.dbg.target.schema.TargetAttributeType; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.lifecycle.Experimental; /** @@ -32,6 +33,12 @@ import ghidra.lifecycle.Experimental; * TODO: Experiment with the idea of "synthetic modules" as presented by {@code dbgeng.dll}. Is * there a similar idea in GDB? This could allow us to expose Ghidra's symbol table and types to the * native debugger. + * + *

+ * TODO: Rename this to {@code TargetModuleOperations}. Conventionally, it is a container of + * modules, but it doesn't technically have to be. If we don't eventually go forward with synthetic + * modules, then we could remove this interface altogether. A client searching for the module + * container should use {@link TargetObjectSchema#searchForCanonicalContainer(Class)}. */ @DebuggerTargetObjectIface("ModuleContainer") public interface TargetModuleContainer extends TargetObject { diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java index d5d7593965..1a4ecb4bf5 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetObject.java @@ -24,7 +24,6 @@ import ghidra.async.AsyncFence; import ghidra.async.AsyncUtils; import ghidra.dbg.*; import ghidra.dbg.error.DebuggerModelTypeException; -import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.schema.*; import ghidra.dbg.util.PathUtils; import ghidra.dbg.util.PathUtils.PathComparator; @@ -161,16 +160,18 @@ public interface TargetObject extends Comparable { Set> ALL_INTERFACES = Set.of(TargetAccessConditioned.class, TargetAggregate.class, TargetAttachable.class, TargetAttacher.class, - TargetBreakpointContainer.class, TargetBreakpointSpec.class, TargetDataTypeMember.class, - TargetDataTypeNamespace.class, TargetDeletable.class, TargetDetachable.class, - TargetBreakpointLocation.class, TargetEnvironment.class, TargetEventScope.class, + TargetBreakpointLocation.class, TargetBreakpointLocationContainer.class, + TargetBreakpointSpec.class, TargetBreakpointSpecContainer.class, TargetConsole.class, + TargetDataTypeMember.class, TargetDataTypeNamespace.class, TargetDeletable.class, + TargetDetachable.class, TargetEnvironment.class, TargetEventScope.class, TargetExecutionStateful.class, TargetFocusScope.class, TargetInterpreter.class, - TargetInterruptible.class, TargetKillable.class, TargetLauncher.class, TargetMethod.class, - TargetMemory.class, TargetMemoryRegion.class, TargetModule.class, + TargetInterruptible.class, TargetKillable.class, TargetLauncher.class, TargetMemory.class, + TargetMemoryRegion.class, TargetMethod.class, TargetModule.class, TargetModuleContainer.class, TargetNamedDataType.class, TargetProcess.class, TargetRegister.class, TargetRegisterBank.class, TargetRegisterContainer.class, - TargetResumable.class, TargetSection.class, TargetStack.class, TargetStackFrame.class, - TargetSteppable.class, TargetSymbol.class, TargetSymbolNamespace.class, TargetThread.class); + TargetResumable.class, TargetSection.class, TargetSectionContainer.class, TargetStack.class, + TargetStackFrame.class, TargetSteppable.class, TargetSymbol.class, + TargetSymbolNamespace.class, TargetThread.class, TargetTogglable.class); Map> INTERFACES_BY_NAME = initInterfacesByName(); /** @@ -256,7 +257,7 @@ public interface TargetObject extends Comparable { } } - protected static Collection getInterfaceNamesOf(Class cls) { + public static Collection getInterfaceNamesOf(Class cls) { return INTERFACE_NAMES_BY_CLASS.computeIfAbsent(cls, Protected::doGetInterfaceNamesOf); } @@ -276,32 +277,6 @@ public interface TargetObject extends Comparable { } } - enum TargetUpdateMode { - /** - * The object's elements are kept up to date via unsolicited push notifications / callbacks. - * - *

- * This is the default. - */ - UNSOLICITED, - /** - * The object's elements are only updated when requested. - * - *

- * The request may still generate push notifications / callbacks if others are listening - */ - SOLICITED, - /** - * The object's elements will not change. - * - *

- * This is a promise made by the model implementation. Once the {@code update_mode} - * attribute has this value, it should never be changed back. Note that other attributes of - * this object are still expected to be kept up to date, if they change. - */ - FIXED; - } - /** * Check for target object equality * @@ -603,10 +578,28 @@ public interface TargetObject extends Comparable { /** * Get the cached elements of this object * + * @see #getCallbackElements() * @return the map of indices to element references */ public Map getCachedElements(); + /** + * Get the cached elements of this object synchronized with the callbacks + * + *

+ * Whereas {@link #getCachedElements()} gets the map of elements right now, it's + * possible that view of elements is far ahead of the callback processing queue. This view is of + * the elements as the change callbacks have been processed so far. When accessing this from the + * {@link DebuggerModelListener#elementsChanged(TargetObject, Collection, Map)} callback, this + * map will have just had the given delta applied to it. + * + *

+ * WARNING:The returned map must only be accessed by the callback thread. + * + * @return the map of indices to element references + */ + public Map getCallbackElements(); + /** * Fetch all the elements of this object * @@ -881,32 +874,6 @@ public interface TargetObject extends Comparable { return getTypedAttributeNowByName(KIND_ATTRIBUTE_NAME, String.class, getDisplay()); } - /** - * Get the element update mode for this object - * - *

- * The update mode informs the client's caching implementation. If set to - * {@link TargetUpdateMode#UNSOLICITED}, the client will assume its cache is kept up to date via - * listener callbacks, and may avoid querying for the object's elements. If set to - * {@link TargetUpdateMode#FIXED}, the client can optionally remove its listener for element - * changes but still assume its cache is up to date, since the object's elements are no longer - * changing. If set to {@link TargetUpdateMode#SOLICITED}, the client must re-validate its cache - * whenever the elements are requested. It is still recommended that the client listen for - * element changes, since the local cache may be updated (resulting in callbacks) when handling - * requests from another client. - * - *

- * IMPORTANT: Update mode does not apply to attributes. Except in rare circumstances, the model - * must keep an object's attributes up to date. - * - * @return the update mode - */ - @TargetAttributeType(name = UPDATE_MODE_ATTRIBUTE_NAME, hidden = true) - public default TargetUpdateMode getUpdateMode() { - return getTypedAttributeNowByName(UPDATE_MODE_ATTRIBUTE_NAME, TargetUpdateMode.class, - TargetUpdateMode.UNSOLICITED); - } - /** * A custom ordinal for positioning this item on screen * @@ -976,10 +943,13 @@ public interface TargetObject extends Comparable { * Refresh the children of this object * *

- * This is necessary when {@link #getUpdateMode()} is {@link TargetUpdateMode#SOLICITED}. It is - * also useful when the user believes things are out of sync. This causes the model to update - * its attributes and/or elements. If either of the {@code refresh} parameters are set, the - * model should be aggressive in ensuring its caches are up to date. + * The default fetch implementations follow the prescription of + * {@link TargetObjectSchema#getElementResyncMode()} and + * {@link TargetObjectSchema#getAttributeResyncMode()}. The client may call this method when, + * for unknown reasons, the respective cache(s) are out of sync with the target debugger. Such + * circumstances indicate an implementation error, but this method may provide a means to + * recover. When a {@code refresh} parameter is set, the model should be aggressive in updating + * its respective cache(s). * * @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 @@ -1032,10 +1002,28 @@ public interface TargetObject extends Comparable { /** * Get the cached attributes of this object * + * @see #getCallbackAttributes() * @return the cached name-value map of attributes */ public Map getCachedAttributes(); + /** + * Get the cached attributes of this object synchronized with the callbacks + * + *

+ * Whereas {@link #getCachedAttributes()} gets the map of attributes right now, it's + * possible that view of attributes if far ahead of the callback processing queue. This view is + * of the attributes as the change callbacks have been processed so far. When accessing this + * from the {@link DebuggerModelListener#attributesChanged(TargetObject, Collection, Map)} + * callback, this map will have just had the given delta applied to it. + * + *

+ * WARNING:The returned map must only be accessed by the callback thread. + * + * @return the cached name-value map of attributes + */ + public Map getCallbackAttributes(); + /** * Get the named attribute from the cache * @@ -1106,7 +1094,7 @@ public interface TargetObject extends Comparable { * @see DebuggerObjectModel#addModelListener(DebuggerModelListener) * @param l the listener */ - public default void addListener(TargetObjectListener l) { + public default void addListener(DebuggerModelListener l) { throw new UnsupportedOperationException(); } @@ -1118,107 +1106,7 @@ public interface TargetObject extends Comparable { * * @param l the listener */ - public default void removeListener(TargetObjectListener l) { + public default void removeListener(DebuggerModelListener l) { throw new UnsupportedOperationException(); } - - public interface TargetObjectListener { - - /** - * The object was created - * - *

- * 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 created(TargetObject object) { - } - - /** - * The object is no longer valid - * - *

- * This should be the final callback ever issued for this object. Invalidation of an object - * implies invalidation of all its successors; nevertheless, the implementation MUST - * explicitly invoke this callback for those successors in preorder. Users need only listen - * for invalidation by installing a listener on the object of interest. However, a user must - * be able to ignore invalidation events on an object it has already removed and/or - * invalidated. For models that are managed by a client connection, disconnecting or - * otherwise terminating the session should invalidate the root, and thus every object must - * receive this callback. - * - *

- * If an invalidated object is replaced (i.e., a new object with the same path is added to - * the model), the implementation must be careful to issue all invalidations related to the - * removed object before the replacement is added, so that delayed invalidations are not - * mistakenly applied to the replacement or its successors. - * - * @param object the now-invalid object - * @param branch the root of the sub-tree being invalidated - * @param reason an informational, human-consumable reason, if applicable - */ - default void invalidated(TargetObject object, 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) { - } - - /** - * The object's elements changed - * - * @param parent the object whose children changed - * @param removed the list of removed children - * @param added a map of indices to new children references - */ - default void elementsChanged(TargetObject parent, Collection removed, - Map added) { - } - - /** - * The object's attributes changed - * - *

- * In the case of an object-valued attribute, changes to that object do not constitute a - * changed attribute. The attribute is considered changed only when that attribute is - * assigned to a completely different object. - * - * @param parent the object whose attributes changed - * @param removed the list of removed attributes - * @param added a map of names to new/changed attributes - */ - default void attributesChanged(TargetObject parent, Collection removed, - Map added) { - } - - /** - * The model has requested the user invalidate caches associated with this object - * - *

- * For objects with methods exposing contents which transcend elements and attributes (e.g., - * memory contents), this callback requests that any caches associated with that content be - * invalidated. Most notably, this usually occurs when an object (e.g., thread) enters the - * {@link TargetExecutionState#RUNNING} state, to inform proxies that they should invalidate - * their memory and register caches. In most cases, users need not worry about this - * callback. Protocol implementations that use the model, however, should forward this - * request to the client implementation. - * - *

- * Note caches of elements and attributes are not affected by this callback. See - * {@link TargetObject#invalidateCaches()}. - * - * @param object the object whose caches must be invalidated - */ - default void invalidateCacheRequested(TargetObject object) { - } - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegister.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegister.java index 9e8c747f54..ea02024bc5 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegister.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegister.java @@ -68,6 +68,12 @@ public interface TargetRegister extends TargetObject { return getTypedAttributeNowByName(LENGTH_ATTRIBUTE_NAME, Integer.class, 0); } + /** + * Get the name of this register + * + *

+ * TODO: Instead of overriding getIndex, we should introduce getRegisterName. + */ @Override public default String getIndex() { return PathUtils.isIndex(getPath()) ? PathUtils.getIndex(getPath()) diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegisterBank.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegisterBank.java index 64ee8a0dce..b9417ff5d9 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegisterBank.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegisterBank.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import ghidra.dbg.DebuggerTargetObjectIface; import ghidra.dbg.error.DebuggerRegisterAccessException; import ghidra.dbg.target.schema.TargetAttributeType; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.util.Msg; /** @@ -36,14 +37,23 @@ import ghidra.util.Msg; public interface TargetRegisterBank extends TargetObject { String DESCRIPTIONS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "descriptions"; + // TODO: Remove this stopgap once we implement register-value replay + String REGISTERVALS_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "register_values"; /** * Get the object describing the registers in this bank * + *

+ * TODO: {@link TargetRegisterContainer} ought to be removed. However, some models present a + * complex structure for their register banks and containers, splitting the set into, e.g., + * User, Vector, etc. I suspect the simplest way for a client to accommodate this is to use + * {@link TargetObjectSchema#searchFor(Class, boolean)}, passing {@link TargetRegister}. The + * "canonical container" concept doesn't really work here, as that will yield each set, rather + * than the full descriptions container. + * * @return a future which completes with object */ @TargetAttributeType(name = DESCRIPTIONS_ATTRIBUTE_NAME) - @SuppressWarnings("unchecked") public default TargetRegisterContainer getDescriptions() { return getTypedAttributeNowByName(DESCRIPTIONS_ATTRIBUTE_NAME, TargetRegisterContainer.class, null); @@ -199,20 +209,4 @@ public interface TargetRegisterBank extends TargetObject { return null; }); } - - public interface TargetRegisterBankListener extends TargetObjectListener { - /** - * Registers were successfully read or written - * - *

- * If the implementation employs a cache, then it need only report reads or writes which - * updated that cache. However, that cache must be invalidated whenever any other event - * occurs which could change register values, e.g., the target stepping or running. - * - * @param bank this register bank object - * @param updates a name-value map of updated registers - */ - default void registersUpdated(TargetRegisterBank bank, Map updates) { - } - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegisterContainer.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegisterContainer.java index 6dc55d72a2..126117f3cf 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegisterContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetRegisterContainer.java @@ -20,9 +20,16 @@ import java.util.concurrent.CompletableFuture; import ghidra.dbg.DebugModelConventions; import ghidra.dbg.DebuggerTargetObjectIface; +import ghidra.dbg.target.schema.TargetObjectSchema; /** * A container of register descriptions + * + *

+ * TODO: Remove this. It really doesn't add anything that can't be discovered via the schema. A + * client searching for a register (description) container should use + * {@link TargetObjectSchema#searchForCanonicalContainer(Class)}, or discover the bank first, and + * ask for its descriptions. */ @DebuggerTargetObjectIface("RegisterContainer") public interface TargetRegisterContainer extends TargetObject { @@ -39,7 +46,9 @@ public interface TargetRegisterContainer extends TargetObject { * @implNote By default, this method collects all successor registers ordered by path. * Overriding that behavior is not yet supported. * @return the register descriptions + * @deprecated I don't think this has any actual utility. */ + @Deprecated(forRemoval = true) default CompletableFuture> getRegisters() { return DebugModelConventions.collectSuccessors(this, TargetRegister.class); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetSectionContainer.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetSectionContainer.java new file mode 100644 index 0000000000..586373df31 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetSectionContainer.java @@ -0,0 +1,29 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.dbg.target; + +import ghidra.dbg.DebuggerTargetObjectIface; + +/** + * A place for breakpoint locations to reside + * + *

+ * This is just a marker interface for finding where a target's sections are given. + */ +@DebuggerTargetObjectIface("SectionContainer") +public interface TargetSectionContainer extends TargetObject { + // Nothing here aside from a marker +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetStack.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetStack.java index 2134f63660..0ab2468392 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetStack.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetStack.java @@ -27,6 +27,10 @@ import ghidra.dbg.DebuggerTargetObjectIface; *

* Conventionally, if the debugger can also unwind register values, then each frame should present a * register bank. Otherwise, the same object presenting this stack should present the register bank. + * + *

+ * TODO: Probably remove this. It serves only as a container of {@link TargetStackFrame}, which can + * be discovered using the schema. */ @DebuggerTargetObjectIface("Stack") public interface TargetStack extends TargetObject { diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetTogglable.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetTogglable.java new file mode 100644 index 0000000000..d87858e48a --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/TargetTogglable.java @@ -0,0 +1,59 @@ +/* ### + * 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.concurrent.CompletableFuture; + +import ghidra.dbg.DebuggerTargetObjectIface; +import ghidra.dbg.target.schema.TargetAttributeType; + +/** + * An object which can be toggled + */ +@DebuggerTargetObjectIface("Togglable") +public interface TargetTogglable extends TargetObject { + + String ENABLED_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "enabled"; + + /** + * Check if the object is enabled + * + * @return true if enabled, false otherwise + */ + @TargetAttributeType(name = ENABLED_ATTRIBUTE_NAME, required = true, hidden = true) + public default boolean isEnabled() { + return getTypedAttributeNowByName(ENABLED_ATTRIBUTE_NAME, Boolean.class, false); + } + + /** + * Disable this object + */ + public CompletableFuture disable(); + + /** + * Enable this object + */ + public CompletableFuture enable(); + + /** + * Enable or disable this object + * + * @param enabled true to enable, false to disable + */ + public default CompletableFuture toggle(boolean enabled) { + return enabled ? enable() : disable(); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/AnnotatedSchemaContext.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/AnnotatedSchemaContext.java index 69980566d6..f6691f1e27 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/AnnotatedSchemaContext.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/AnnotatedSchemaContext.java @@ -133,7 +133,13 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext { protected final Map, TargetObjectSchema> schemasByClass = new LinkedHashMap<>(); - protected SchemaName nameFromAnnotatedClass(Class cls) { + /** + * Get the schema name for an annotated target object class + * + * @param cls the class + * @return the schema name + */ + public SchemaName nameFromAnnotatedClass(Class cls) { synchronized (namesByClass) { TargetObjectSchemaInfo info = cls.getAnnotation(TargetObjectSchemaInfo.class); if (info == null) { @@ -203,75 +209,99 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext { " annotation, or that the class was referenced by accident."); } return schemasByClass.computeIfAbsent(cls, c -> { - TargetObjectSchemaInfo info = cls.getAnnotation(TargetObjectSchemaInfo.class); - if (info == null) { - throw new IllegalArgumentException("Class " + cls + " is not annotated with @" + - TargetObjectSchemaInfo.class.getSimpleName()); - } - SchemaBuilder builder = builder(name); - - Set> allParents = ReflectionUtilities.getAllParents(cls); - for (Class parent : allParents) { - DebuggerTargetObjectIface ifaceAnnot = - parent.getAnnotation(DebuggerTargetObjectIface.class); - if (ifaceAnnot != null) { - builder.addInterface(parent.asSubclass(TargetObject.class)); - } - } - - builder.setCanonicalContainer(info.canonicalContainer()); - - boolean sawDefaultElementType = false; - for (TargetElementType et : info.elements()) { - if (et.index().equals("")) { - sawDefaultElementType = true; - } - builder.addElementSchema(et.index(), nameFromClass(et.type()), et); - } - if (!sawDefaultElementType) { - Set> bounds = getBoundsOfFetchElements(cls); - if (bounds.size() != 1) { - // TODO: Compile-time validation? - throw new IllegalArgumentException( - "Could not identify unique element class (" + bounds + ") for " + cls); - } - else { - Class bound = bounds.iterator().next(); - SchemaName schemaName; - try { - schemaName = nameFromClass(bound); - } - catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - "Could not get schema name from bound " + bound + " of " + cls + - ".fetchElements()", - e); - } - builder.setDefaultElementSchema(schemaName); - } - } - - addPublicMethodsFromClass(builder, cls, cls); - for (Class parent : allParents) { - if (TargetObject.class.isAssignableFrom(parent)) { - addPublicMethodsFromClass(builder, parent.asSubclass(TargetObject.class), - cls); - } - } - for (TargetAttributeType at : info.attributes()) { - AnnotatedAttributeSchema attrSchema = attributeSchemaFromAnnotation(at); - AttributeSchema exists = builder.getAttributeSchema(attrSchema.getName()); - if (exists != null) { - attrSchema = attrSchema.lower((AnnotatedAttributeSchema) exists); - } - builder.replaceAttributeSchema(attrSchema, at); - } + SchemaBuilder builder = builderForClass(cls, name); return builder.buildAndAdd(); }); } } + /** + * Get a populated builder for an annotated target object class + * + * @param cls the class + * @return the builder + */ + public SchemaBuilder builderForClass(Class cls) { + return builderForClass(cls, nameFromAnnotatedClass(cls)); + } + + /** + * Get a populated builder for an annotated target object class + * + * @param cls the class + * @param name a custom name for the schema + * @return the builder + */ + public SchemaBuilder builderForClass(Class cls, SchemaName name) { + TargetObjectSchemaInfo info = cls.getAnnotation(TargetObjectSchemaInfo.class); + if (info == null) { + throw new IllegalArgumentException("Class " + cls + " is not annotated with @" + + TargetObjectSchemaInfo.class.getSimpleName()); + } + SchemaBuilder builder = builder(name); + + Set> allParents = ReflectionUtilities.getAllParents(cls); + for (Class parent : allParents) { + DebuggerTargetObjectIface ifaceAnnot = + parent.getAnnotation(DebuggerTargetObjectIface.class); + if (ifaceAnnot != null) { + builder.addInterface(parent.asSubclass(TargetObject.class)); + } + } + + builder.setCanonicalContainer(info.canonicalContainer()); + builder.setElementResyncMode(info.elementResync()); + builder.setAttributeResyncMode(info.attributeResync()); + + boolean sawDefaultElementType = false; + for (TargetElementType et : info.elements()) { + if (et.index().equals("")) { + sawDefaultElementType = true; + } + builder.addElementSchema(et.index(), nameFromClass(et.type()), et); + } + if (!sawDefaultElementType) { + Set> bounds = getBoundsOfFetchElements(cls); + if (bounds.size() != 1) { + // TODO: Compile-time validation? + throw new IllegalArgumentException( + "Could not identify unique element class (" + bounds + ") for " + cls); + } + else { + Class bound = bounds.iterator().next(); + SchemaName schemaName; + try { + schemaName = nameFromClass(bound); + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Could not get schema name from bound " + bound + " of " + cls + + ".fetchElements()", + e); + } + builder.setDefaultElementSchema(schemaName); + } + } + + addPublicMethodsFromClass(builder, cls, cls); + for (Class parent : allParents) { + if (TargetObject.class.isAssignableFrom(parent)) { + addPublicMethodsFromClass(builder, parent.asSubclass(TargetObject.class), + cls); + } + } + for (TargetAttributeType at : info.attributes()) { + AnnotatedAttributeSchema attrSchema = attributeSchemaFromAnnotation(at); + AttributeSchema exists = builder.getAttributeSchema(attrSchema.getName()); + if (exists != null) { + attrSchema = attrSchema.lower((AnnotatedAttributeSchema) exists); + } + builder.replaceAttributeSchema(attrSchema, at); + } + return builder; + } + protected String attributeNameFromBean(String beanName, boolean isBool) { beanName = isBool ? StringUtils.removeStartIgnoreCase(beanName, "is") : StringUtils.removeStartIgnoreCase(beanName, "get"); @@ -346,6 +376,16 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext { return true; } + /** + * Get the schema for an annotated target object class + * + *

+ * This will ensure all the schemas of the given class' dependencies are constructed and added + * to the context. + * + * @param cls the class + * @return the schema + */ public TargetObjectSchema getSchemaForClass(Class cls) { TargetObjectSchema schema = fromAnnotatedClass(cls); fillDependencies(); diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java index 63a970b9d1..0f47b86785 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java @@ -124,24 +124,34 @@ public class DefaultTargetObjectSchema private final Class type; private final Set> interfaces; private final boolean isCanonicalContainer; + private final Map elementSchemas; private final SchemaName defaultElementSchema; + private final ResyncMode elementResync; + private final Map attributeSchemas; private final AttributeSchema defaultAttributeSchema; + private final ResyncMode attributeResync; DefaultTargetObjectSchema(SchemaContext context, SchemaName name, Class type, Set> interfaces, boolean isCanonicalContainer, Map elementSchemas, SchemaName defaultElementSchema, - Map attributeSchemas, AttributeSchema defaultAttributeSchema) { + ResyncMode elementResync, + Map attributeSchemas, AttributeSchema defaultAttributeSchema, + ResyncMode attributeResync) { this.context = context; this.name = name; this.type = type; this.interfaces = Collections.unmodifiableSet(new LinkedHashSet<>(interfaces)); this.isCanonicalContainer = isCanonicalContainer; + this.elementSchemas = Collections.unmodifiableMap(new LinkedHashMap<>(elementSchemas)); this.defaultElementSchema = defaultElementSchema; + this.elementResync = elementResync; + this.attributeSchemas = Collections.unmodifiableMap(new LinkedHashMap<>(attributeSchemas)); this.defaultAttributeSchema = defaultAttributeSchema; + this.attributeResync = attributeResync; } @Override @@ -179,6 +189,11 @@ public class DefaultTargetObjectSchema return defaultElementSchema; } + @Override + public ResyncMode getElementResyncMode() { + return elementResync; + } + @Override public Map getAttributeSchemas() { return attributeSchemas; @@ -189,6 +204,11 @@ public class DefaultTargetObjectSchema return defaultAttributeSchema; } + @Override + public ResyncMode getAttributeResyncMode() { + return attributeResync; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -209,11 +229,11 @@ public class DefaultTargetObjectSchema sb.append(" "); } sb.append("]\n" + INDENT); - sb.append("elements = "); + sb.append("elements(resync " + elementResync + ") = "); sb.append(elementSchemas); sb.append(" default " + defaultElementSchema); sb.append("\n" + INDENT); - sb.append("attributes = "); + sb.append("attributes(resync " + attributeResync + ") = "); sb.append(attributeSchemas); sb.append(" default " + defaultAttributeSchema); sb.append("\n}"); @@ -257,12 +277,18 @@ public class DefaultTargetObjectSchema if (!Objects.equals(this.defaultElementSchema, that.defaultElementSchema)) { return false; } + if (!Objects.equals(this.elementResync, that.elementResync)) { + return false; + } if (!Objects.equals(this.attributeSchemas, that.attributeSchemas)) { return false; } if (!Objects.equals(this.defaultAttributeSchema, that.defaultAttributeSchema)) { return false; } + if (!Objects.equals(this.attributeResync, that.attributeResync)) { + return false; + } return true; } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java index fabcbd7443..7f862332b7 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java @@ -20,13 +20,12 @@ import java.util.*; import ghidra.dbg.attributes.TargetDataType; import ghidra.dbg.attributes.TargetObjectList; import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.TargetMethod.TargetParameterMap; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.TargetObject.TargetUpdateMode; import ghidra.dbg.target.TargetSteppable.TargetStepKindSet; -import ghidra.dbg.util.PathPattern; +import ghidra.dbg.util.PathMatcher; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRange; @@ -88,8 +87,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema { SET_ATTACH_KIND("SET_ATTACH_KIND", TargetAttachKindSet.class), // TODO: Limited built-in generics SET_BREAKPOINT_KIND("SET_BREAKPOINT_KIND", TargetBreakpointKindSet.class), SET_STEP_KIND("SET_STEP_KIND", TargetStepKindSet.class), - EXECUTION_STATE("EXECUTION_STATE", TargetExecutionState.class), - UPDATE_MODE("UPDATE_MODE", TargetUpdateMode.class); + EXECUTION_STATE("EXECUTION_STATE", TargetExecutionState.class); public static final class MinimalSchemaContext extends DefaultSchemaContext { public static final SchemaContext INSTANCE = new MinimalSchemaContext(); @@ -172,6 +170,11 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema { return VOID.getName(); } + @Override + public ResyncMode getElementResyncMode() { + return TargetObjectSchema.DEFAULT_ELEMENT_RESYNC; + } + @Override public Map getAttributeSchemas() { return Map.of(); @@ -183,8 +186,22 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema { } @Override - public void searchFor(Set result, List prefix, boolean parentIsCanonical, - Class type, boolean requireCanonical) { - return; + public ResyncMode getAttributeResyncMode() { + return TargetObjectSchema.DEFAULT_ATTRIBUTE_RESYNC; + } + + @Override + public PathMatcher searchFor(Class type, boolean requireCanonical) { + return new PathMatcher(); + } + + @Override + public List searchForCanonicalContainer(Class type) { + return null; + } + + @Override + public List searchForSuitable(Class type, List path) { + return null; } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java index eb7a705f5b..0c73051e86 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java @@ -18,8 +18,7 @@ package ghidra.dbg.target.schema; import java.util.*; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; -import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; +import ghidra.dbg.target.schema.TargetObjectSchema.*; public class SchemaBuilder { private final DefaultSchemaContext context; @@ -28,10 +27,14 @@ public class SchemaBuilder { private Class type = TargetObject.class; private Set> interfaces = new LinkedHashSet<>(); private boolean isCanonicalContainer = false; + private Map elementSchemas = new LinkedHashMap<>(); private SchemaName defaultElementSchema = EnumerableTargetObjectSchema.OBJECT.getName(); + private ResyncMode elementResync = TargetObjectSchema.DEFAULT_ELEMENT_RESYNC; + private Map attributeSchemas = new LinkedHashMap<>(); private AttributeSchema defaultAttributeSchema = AttributeSchema.DEFAULT_ANY; + private ResyncMode attributeResync = TargetObjectSchema.DEFAULT_ATTRIBUTE_RESYNC; private Map elementOrigins = new LinkedHashMap<>(); private Map attributeOrigins = new LinkedHashMap<>(); @@ -65,6 +68,11 @@ public class SchemaBuilder { return this; } + public SchemaBuilder removeInterface(Class iface) { + this.interfaces.remove(iface); + return this; + } + public SchemaBuilder setCanonicalContainer(boolean isCanonicalContainer) { this.isCanonicalContainer = isCanonicalContainer; return this; @@ -74,12 +82,6 @@ public class SchemaBuilder { return isCanonicalContainer; } - public SchemaBuilder setElementSchemas(Map elementSchemas) { - this.elementSchemas.clear(); - this.elementSchemas.putAll(elementSchemas); - return this; - } - /** * Define the schema for a child element * @@ -101,6 +103,15 @@ public class SchemaBuilder { return this; } + public SchemaBuilder removeElementSchema(String index) { + if (index.equals("")) { + return setDefaultElementSchema(EnumerableTargetObjectSchema.OBJECT.getName()); + } + elementSchemas.remove(index); + elementOrigins.remove(index); + return this; + } + public Map getElementSchemas() { return Map.copyOf(elementSchemas); } @@ -114,12 +125,15 @@ public class SchemaBuilder { return defaultElementSchema; } - public SchemaBuilder setAttributeSchemas(Map attributeSchemas) { - this.attributeSchemas.clear(); - this.attributeSchemas.putAll(attributeSchemas); + public SchemaBuilder setElementResyncMode(ResyncMode elementResync) { + this.elementResync = elementResync; return this; } + public ResyncMode getElementResyncMode() { + return elementResync; + } + /** * Define the schema for a child attribute. * @@ -144,6 +158,15 @@ public class SchemaBuilder { return this; } + public SchemaBuilder removeAttributeSchema(String name) { + if (name.equals("")) { + return setDefaultAttributeSchema(AttributeSchema.DEFAULT_ANY); + } + attributeSchemas.remove(name); + attributeOrigins.remove(name); + return this; + } + public Map getAttributeSchemas() { return Map.copyOf(attributeSchemas); } @@ -170,6 +193,15 @@ public class SchemaBuilder { return defaultAttributeSchema; } + public SchemaBuilder setAttributeResyncMode(ResyncMode attributeResync) { + this.attributeResync = attributeResync; + return this; + } + + public ResyncMode getAttributeResyncMode() { + return attributeResync; + } + public TargetObjectSchema buildAndAdd() { TargetObjectSchema schema = build(); context.putSchema(schema); @@ -177,7 +209,9 @@ public class SchemaBuilder { } public TargetObjectSchema build() { - return new DefaultTargetObjectSchema(context, name, type, interfaces, isCanonicalContainer, - elementSchemas, defaultElementSchema, attributeSchemas, defaultAttributeSchema); + return new DefaultTargetObjectSchema( + context, name, type, interfaces, isCanonicalContainer, + elementSchemas, defaultElementSchema, elementResync, + attributeSchemas, defaultAttributeSchema, attributeResync); } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java index cf26057676..9ed4c65689 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java @@ -17,15 +17,15 @@ package ghidra.dbg.target.schema; import java.util.*; import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import ghidra.dbg.agent.DefaultTargetObject; +import ghidra.dbg.target.TargetAggregate; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema; +import ghidra.dbg.util.*; import ghidra.dbg.util.CollectionUtils.Delta; -import ghidra.dbg.util.PathPattern; -import ghidra.dbg.util.PathUtils; -import ghidra.lifecycle.Internal; import ghidra.util.Msg; /** @@ -44,6 +44,9 @@ import ghidra.util.Msg; * which matches any key. Similarly, the wild-card index is {@code []}. */ public interface TargetObjectSchema { + public static final ResyncMode DEFAULT_ELEMENT_RESYNC = ResyncMode.NEVER; + public static final ResyncMode DEFAULT_ATTRIBUTE_RESYNC = ResyncMode.NEVER; + /** * An identifier for schemas within a context. * @@ -99,6 +102,71 @@ public interface TargetObjectSchema { } } + /** + * A mode describing what "promise" a model makes when keeping elements or attributes up to date + * + *

+ * Each object specifies a element sync mode, and an attribute sync mode. These describe when + * the client must call {@link TargetObject#resync(boolean, boolean)} to refresh/resync to + * ensure it has a fresh cache of elements and/or attributes. Note that any client requesting a + * resync will cause all clients to receive the updates. + */ + enum ResyncMode { + /** + * The object's elements are kept up to date via unsolicited push notifications / callbacks + * + *

+ * The client should never have to call {@link TargetObject#resync()}. This is the default, + * and it is preferred for attributes. It is most appropriate for small-ish collections that + * change often and that the client is likely to need, e.g., the process, thread, and module + * lists. In general, if the native debugger or API offers callbacks for updating the + * collection, then this is the mode to use. + */ + NEVER { + @Override + public boolean shouldResync(CompletableFuture curRequest) { + return false; + } + }, + /** + * The object must be explicitly synchronized once + * + *

+ * This mode is appropriate for large collections, e.g., the symbols of a module. To push + * these without solicitation could be expensive, both for the model to retrieve them from + * the debugger, and for the client to process the collection. They should only be retrieved + * when asked, via {@link TargetObject#resync()}. Such collections are typically fixed, and + * so do not require later updates. Nevertheless, if the collection does change, + * then those updates must be pushed without further solicitation. + */ + ONCE { + @Override + public boolean shouldResync(CompletableFuture curRequest) { + return curRequest == null || curRequest.isCompletedExceptionally(); + } + }, + /** + * The object's elements are only updated when requested + * + *

+ * This is the default for elements. It is appropriate for collections where the client + * doesn't necessarily need an up-to-date copy. Please note the higher likelihood that the + * client may make requests involving an object that has since become invalid. The model + * must be prepared to reject those requests gracefully. The most common example is the list + * of attachable processes: It should only be retrieved when requested, and there's no need + * to keep it up to date. If a process terminates, and the client later requests to attach + * to it, the request may be rejected. + */ + ALWAYS { + @Override + public boolean shouldResync(CompletableFuture curRequest) { + return true; + } + }; + + public abstract boolean shouldResync(CompletableFuture curRequest); + } + /** * Schema descriptor for a child attribute. */ @@ -258,6 +326,13 @@ public interface TargetObjectSchema { return getDefaultElementSchema(); } + /** + * Get the re-synchronization mode for the object's elements + * + * @return the element re-synchronization mode + */ + ResyncMode getElementResyncMode(); + /** * Get the map of attribute names to named schemas * @@ -299,6 +374,13 @@ public interface TargetObjectSchema { return getDefaultAttributeSchema(); } + /** + * Get the re-synchronization mode for attributes + * + * @return the attribute re-synchronization mode + */ + ResyncMode getAttributeResyncMode(); + /** * Get the named schema for a child having the given key * @@ -345,6 +427,13 @@ public interface TargetObjectSchema { return childSchema.getSuccessorSchema(path.subList(1, path.size())); } + /** + * Do the same as {@link #searchFor(Class, List, boolean)} with an empty prefix + */ + default PathMatcher searchFor(Class type, boolean requireCanonical) { + return searchFor(type, List.of(), requireCanonical); + } + /** * Find (sub) path patterns that match objects implementing a given interface * @@ -353,44 +442,290 @@ public interface TargetObjectSchema { * successor implementing the interface. * * @param type the sub-type of {@link TargetObject} to search for + * @param prefix the prefix for each relative path pattern * @param requireCanonical only return patterns matching a canonical location for the type * @return a set of patterns where such objects could be found */ - default Set searchFor(Class type, + default PathMatcher searchFor(Class type, List prefix, boolean requireCanonical) { if (type == TargetObject.class) { throw new IllegalArgumentException("Must provide a specific interface"); } - Set result = new LinkedHashSet<>(); - searchFor(result, List.of(), false, type, requireCanonical); + PathMatcher result = new PathMatcher(); + Private.searchFor(this, result, prefix, true, type, requireCanonical, new HashSet<>()); return result; } - @Internal // TODO: Make a separate internal interface? - default void searchFor(Set result, List prefix, boolean parentIsCanonical, - Class type, boolean requireCanonical) { - if (getInterfaces().contains(type) && parentIsCanonical) { - result.add(new PathPattern(prefix)); + class Private { + private abstract static class BreadthFirst { + Set allOnLevel = new HashSet<>(); + + public BreadthFirst(Set seed) { + allOnLevel.addAll(seed); + } + + public void expandAttributes(Set nextLevel, T ent) { + SchemaContext ctx = ent.schema.getContext(); + for (AttributeSchema as : ent.schema.getAttributeSchemas().values()) { + try { + SchemaName schema = as.getSchema(); + TargetObjectSchema child = ctx.getSchema(schema); + expandAttribute(nextLevel, ent, child, + PathUtils.extend(ent.path, as.getName())); + } + catch (NullPointerException npe) { + Msg.error(this, "Null schema for " + as); + } + } + } + + public void expandDefaultAttribute(Set nextLevel, T ent) { + SchemaContext ctx = ent.schema.getContext(); + AttributeSchema das = ent.schema.getDefaultAttributeSchema(); + TargetObjectSchema child = ctx.getSchema(das.getSchema()); + expandAttribute(nextLevel, ent, child, PathUtils.extend(ent.path, das.getName())); + } + + public void expandElements(Set nextLevel, T ent) { + SchemaContext ctx = ent.schema.getContext(); + for (Map.Entry elemEnt : ent.schema.getElementSchemas() + .entrySet()) { + TargetObjectSchema child = ctx.getSchema(elemEnt.getValue()); + expandElement(nextLevel, ent, child, + PathUtils.index(ent.path, elemEnt.getKey())); + } + } + + public void expandDefaultElement(Set nextLevel, T ent) { + SchemaContext ctx = ent.schema.getContext(); + TargetObjectSchema child = ctx.getSchema(ent.schema.getDefaultElementSchema()); + expandElement(nextLevel, ent, child, PathUtils.index(ent.path, "")); + } + + public void nextLevel() { + Set nextLevel = new HashSet<>(); + for (T ent : allOnLevel) { + if (!descend(ent)) { + continue; + } + expandAttributes(nextLevel, ent); + expandDefaultAttribute(nextLevel, ent); + expandElements(nextLevel, ent); + expandDefaultElement(nextLevel, ent); + } + allOnLevel = nextLevel; + } + + public boolean descend(T ent) { + return true; + } + + public void expandAttribute(Set nextLevel, T ent, TargetObjectSchema schema, + List path) { + } + + public void expandElement(Set nextLevel, T ent, TargetObjectSchema schema, + List path) { + } } - for (Entry ent : getElementSchemas().entrySet()) { - List extended = PathUtils.index(prefix, ent.getKey()); - TargetObjectSchema elemSchema = getContext().getSchema(ent.getValue()); - elemSchema.searchFor(result, extended, isCanonicalContainer(), type, requireCanonical); - } - List deExtended = PathUtils.extend(prefix, "[]"); - TargetObjectSchema deSchema = getContext().getSchema(getDefaultElementSchema()); - deSchema.searchFor(result, deExtended, isCanonicalContainer(), type, requireCanonical); + private static class SearchEntry { + final List path; + final TargetObjectSchema schema; - for (Entry ent : getAttributeSchemas().entrySet()) { - List extended = PathUtils.extend(prefix, ent.getKey()); - TargetObjectSchema attrSchema = getContext().getSchema(ent.getValue().getSchema()); - attrSchema.searchFor(result, extended, isCanonicalContainer(), type, requireCanonical); + public SearchEntry(List path, TargetObjectSchema schema) { + this.path = path; + this.schema = schema; + } } - List daExtended = PathUtils.extend(prefix, ""); - TargetObjectSchema daSchema = - getContext().getSchema(getDefaultAttributeSchema().getSchema()); - daSchema.searchFor(result, daExtended, isCanonicalContainer(), type, requireCanonical); + + private static class CanonicalSearchEntry extends SearchEntry { + final boolean parentIsCanonical; + + public CanonicalSearchEntry(List path, boolean parentIsCanonical, + TargetObjectSchema schema) { + super(path, schema); + this.parentIsCanonical = parentIsCanonical; + } + } + + private static void searchFor(TargetObjectSchema sch, PathMatcher result, + List prefix, boolean parentIsCanonical, Class type, + boolean requireCanonical, Set visited) { + if (!visited.add(sch)) { + return; + } + + if (sch.getInterfaces().contains(type) && parentIsCanonical) { + result.addPattern(prefix); + } + SchemaContext ctx = sch.getContext(); + boolean isCanonical = sch.isCanonicalContainer(); + for (Entry ent : sch.getElementSchemas().entrySet()) { + List extended = PathUtils.index(prefix, ent.getKey()); + TargetObjectSchema elemSchema = ctx.getSchema(ent.getValue()); + searchFor(elemSchema, result, extended, isCanonical, type, requireCanonical, + visited); + } + List deExtended = PathUtils.extend(prefix, "[]"); + TargetObjectSchema deSchema = ctx.getSchema(sch.getDefaultElementSchema()); + searchFor(deSchema, result, deExtended, isCanonical, type, requireCanonical, visited); + + for (Entry ent : sch.getAttributeSchemas().entrySet()) { + List extended = PathUtils.extend(prefix, ent.getKey()); + TargetObjectSchema attrSchema = ctx.getSchema(ent.getValue().getSchema()); + searchFor(attrSchema, result, extended, parentIsCanonical, type, requireCanonical, + visited); + } + List daExtended = PathUtils.extend(prefix, ""); + TargetObjectSchema daSchema = + ctx.getSchema(sch.getDefaultAttributeSchema().getSchema()); + searchFor(daSchema, result, daExtended, parentIsCanonical, type, requireCanonical, + visited); + + visited.remove(sch); + } + + static List searchForSuitableInAggregate(TargetObjectSchema seed, + Class type) { + Set init = Set.of(new SearchEntry(List.of(), seed)); + BreadthFirst breadth = new BreadthFirst<>(init) { + final Set visited = new HashSet<>(); + + @Override + public boolean descend(SearchEntry ent) { + return ent.schema.getInterfaces().contains(TargetAggregate.class); + } + + @Override + public void expandAttribute(Set nextLevel, SearchEntry ent, + TargetObjectSchema schema, List path) { + if (visited.add(schema)) { + nextLevel.add(new SearchEntry(path, schema)); + } + } + + @Override + public void expandDefaultAttribute(Set nextLevel, SearchEntry ent) { + } + + @Override + public void expandElements(Set nextLevel, SearchEntry ent) { + } + + @Override + public void expandDefaultElement(Set nextLevel, SearchEntry ent) { + } + }; + while (!breadth.allOnLevel.isEmpty()) { + Set found = breadth.allOnLevel.stream() + .filter(ent -> ent.schema.getInterfaces().contains(type)) + .collect(Collectors.toSet()); + if (!found.isEmpty()) { + if (found.size() == 1) { + return found.iterator().next().path; + } + return null; + } + breadth.nextLevel(); + } + return null; + } + } + + /** + * Find the (sub) path to the canonical container for objects implementing a given interface + * + *

+ * If more than one container is found having the shortest path, then {@code null} is returned. + * + * @param type the sub-type of {@link TargetObject} to search for + * @return the single path to that container + */ + default List searchForCanonicalContainer(Class type) { + if (type == TargetObject.class) { + throw new IllegalArgumentException("Must provide a specific interface"); + } + SchemaContext ctx = getContext(); + Set visited = new HashSet<>(); + Set visitedAsElement = new HashSet<>(); + Set allOnLevel = new HashSet<>(); + allOnLevel.add(new Private.CanonicalSearchEntry(List.of(), false, this)); + while (!allOnLevel.isEmpty()) { + List found = null; + for (Private.CanonicalSearchEntry ent : allOnLevel) { + if (ent.schema.getInterfaces().contains(type) && ent.parentIsCanonical) { + // Check for final being index is in parentIsCanonical. + if (found != null) { + return null; // Non-unique answer + } + found = PathUtils.parent(ent.path); + } + } + if (found != null) { + return List.copyOf(found); // Unique shortest answer + } + + Set nextLevel = new HashSet<>(); + for (Private.CanonicalSearchEntry ent : allOnLevel) { + if (PathPattern.isWildcard(PathUtils.getKey(ent.path))) { + continue; + } + for (Map.Entry attrEnt : ent.schema.getAttributeSchemas() + .entrySet()) { + TargetObjectSchema attrSchema = ctx.getSchema(attrEnt.getValue().getSchema()); + if (TargetObject.class.isAssignableFrom(attrSchema.getType()) && + visited.add(attrSchema)) { + nextLevel.add(new Private.CanonicalSearchEntry( + PathUtils.extend(ent.path, attrEnt.getKey()), false, // If child is not element, this is not is canonical container + attrSchema)); + } + } + for (Map.Entry elemEnt : ent.schema.getElementSchemas() + .entrySet()) { + TargetObjectSchema elemSchema = ctx.getSchema(elemEnt.getValue()); + visited.add(elemSchema); // Add but do not condition + if (visitedAsElement.add(elemSchema)) { + nextLevel.add(new Private.CanonicalSearchEntry( + PathUtils.index(ent.path, elemEnt.getKey()), + ent.schema.isCanonicalContainer(), elemSchema)); + } + } + TargetObjectSchema deSchema = ctx.getSchema(ent.schema.getDefaultElementSchema()); + visited.add(deSchema); + if (visitedAsElement.add(deSchema)) { + nextLevel.add(new Private.CanonicalSearchEntry(PathUtils.index(ent.path, ""), + ent.schema.isCanonicalContainer(), deSchema)); + } + } + allOnLevel = nextLevel; + } + // We exhausted the reachable schemas + return null; + } + + default List searchForSuitable(Class type, List path) { + for (; path != null; path = PathUtils.parent(path)) { + TargetObjectSchema schema = getSuccessorSchema(path); + if (schema.getInterfaces().contains(type)) { + return path; + } + List inAgg = Private.searchForSuitableInAggregate(schema, type); + if (inAgg != null) { + return PathUtils.extend(path, inAgg); + } + } + return null; + } + + default List searchForAncestor(Class type, List path) { + for (; path != null; path = PathUtils.parent(path)) { + TargetObjectSchema schema = getSuccessorSchema(path); + if (schema.getInterfaces().contains(type)) { + return path; + } + } + return null; } /** @@ -433,8 +768,8 @@ public interface TargetObjectSchema { String path = key == null ? null : PathUtils.toString(PathUtils.extend(parentPath, key)); String msg = path == null - ? "Value " + value + " does not conform to required type " + - getType() + " of schema " + this + ? "Value " + value + " does not conform to required type " + getType() + + " of schema " + this : "Value " + value + " for " + path + " does not conform to required type " + getType() + " of schema " + this; Msg.error(this, msg); @@ -445,8 +780,8 @@ public interface TargetObjectSchema { for (Class iface : getInterfaces()) { if (!iface.isAssignableFrom(cls)) { // TODO: Should this throw an exception, eventually? - String msg = "Value " + value + " does not implement required interface " + - iface + " of schema " + this; + String msg = "Value " + value + " does not implement required interface " + iface + + " of schema " + this; Msg.error(this, msg); if (strict) { throw new AssertionError(msg); @@ -465,8 +800,7 @@ public interface TargetObjectSchema { */ default void validateRequiredAttributes(TargetObject object, boolean strict) { Set present = object.getCachedAttributes().keySet(); - Set missing = getAttributeSchemas() - .values() + Set missing = getAttributeSchemas().values() .stream() .filter(AttributeSchema::isRequired) .map(AttributeSchema::getName) @@ -553,8 +887,7 @@ public interface TargetObjectSchema { * @param delta the delta, before or after the fact */ default void validateElementDelta(List parentPath, - Delta delta, - boolean strict) { + Delta delta, boolean strict) { for (Map.Entry ent : delta.added.entrySet()) { TargetObject element = ent.getValue(); TargetObjectSchema schema = getContext().getSchema(getElementSchema(ent.getKey())); diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchemaInfo.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchemaInfo.java index 50bbb5900b..172dd161e6 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchemaInfo.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchemaInfo.java @@ -17,6 +17,8 @@ package ghidra.dbg.target.schema; import java.lang.annotation.*; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; + @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface TargetObjectSchemaInfo { @@ -26,5 +28,9 @@ public @interface TargetObjectSchemaInfo { TargetElementType[] elements() default {}; + ResyncMode elementResync() default ResyncMode.NEVER; + TargetAttributeType[] attributes() default {}; + + ResyncMode attributeResync() default ResyncMode.NEVER; } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java index 8475d81506..b56447baa3 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java @@ -24,8 +24,7 @@ import org.jdom.input.SAXBuilder; import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema; -import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; -import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; +import ghidra.dbg.target.schema.TargetObjectSchema.*; import ghidra.util.Msg; import ghidra.util.xml.XmlUtilities; @@ -88,6 +87,10 @@ public class XmlSchemaContext extends DefaultSchemaContext { if (schema.isCanonicalContainer()) { XmlUtilities.setStringAttr(result, "canonical", "yes"); } + XmlUtilities.setStringAttr(result, "elementResync", + schema.getElementResyncMode().name()); + XmlUtilities.setStringAttr(result, "attributeResync", + schema.getAttributeResyncMode().name()); for (Map.Entry ent : schema.getElementSchemas().entrySet()) { Element elemElem = new Element("element"); @@ -162,6 +165,10 @@ public class XmlSchemaContext extends DefaultSchemaContext { } builder.setCanonicalContainer(parseBoolean(schemaElem, "canonical")); + builder.setElementResyncMode( + ResyncMode.valueOf(schemaElem.getAttributeValue("elementResync"))); + builder.setAttributeResyncMode( + ResyncMode.valueOf(schemaElem.getAttributeValue("attributeResync"))); for (Element elemElem : XmlUtilities.getChildren(schemaElem, "element")) { SchemaName schema = name(elemElem.getAttributeValue("schema")); diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/CollectionUtils.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/CollectionUtils.java index 80debde376..5487e92800 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/CollectionUtils.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/CollectionUtils.java @@ -346,7 +346,8 @@ public enum CollectionUtils { return removed.isEmpty() && added.isEmpty(); } - public Delta apply(Map mutable, BiPredicate equals) { + public Delta apply(Map mutable, + BiPredicate equals) { return apply(mutable, removed.keySet(), added, equals); } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/DebuggerCallbackReorderer.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/DebuggerCallbackReorderer.java new file mode 100644 index 0000000000..7383b8898f --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/DebuggerCallbackReorderer.java @@ -0,0 +1,283 @@ +/* ### + * 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.util; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; + +import ghidra.async.AsyncFence; +import ghidra.async.AsyncUtils; +import ghidra.dbg.DebuggerModelClosedReason; +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetConsole.Channel; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.util.Msg; + +/** + * A mechanism for re-ordering model callbacks + * + *

+ * When this is added to the model, {@code replay} must be true, or behavior of the mechanism is + * undefined. + */ +public class DebuggerCallbackReorderer implements DebuggerModelListener { + + private class ObjectRecord { + private final TargetObject obj; + private final CompletableFuture addedToParent = new CompletableFuture<>(); + private final CompletableFuture complete; + + ObjectRecord(TargetObject obj) { + this.obj = obj; + TargetObject parent = obj.getParent(); + ObjectRecord parentRecord = parent == null ? null : records.get(parent); + if (parentRecord == null) { + complete = addedToParent.thenApply(this::completed); + } + else { + complete = parentRecord.complete.thenCompose(__ -> addedToParent) + .thenApply(this::completed); + } + } + + TargetObject completed(TargetObject obj) { + records.remove(obj); + // NB. We should already be on the clientExecutor + Map attributes = obj.getCallbackAttributes(); + if (!attributes.isEmpty()) { + defensive(() -> listener.attributesChanged(obj, List.of(), attributes), + "attributesChanged(r)"); + } + Map elements = obj.getCallbackElements(); + if (!elements.isEmpty()) { + defensive(() -> listener.elementsChanged(obj, List.of(), elements), + "elementsChanged(r)"); + } + return obj; + } + + void added() { + if (!addedToParent.isDone()) { + addedToParent.complete(obj); + } + } + + void removed() { + if (!addedToParent.isDone()) { + addedToParent.cancel(false); + } + } + } + + private final DebuggerModelListener listener; + + private final Map records = new HashMap<>(); + private CompletableFuture lastEvent = AsyncUtils.NIL; + + public DebuggerCallbackReorderer(DebuggerModelListener listener) { + this.listener = listener; + } + + private void defensive(Runnable r, String cb) { + try { + r.run(); + } + catch (Throwable t) { + Msg.error(this, "Listener " + listener + " caused exception processing " + cb, t); + } + } + + @Override + public void catastrophic(Throwable t) { + listener.catastrophic(t); + } + + @Override + public void modelClosed(DebuggerModelClosedReason reason) { + listener.modelClosed(reason); + } + + @Override + public void modelOpened() { + listener.modelOpened(); + } + + @Override + public void modelStateChanged() { + listener.modelStateChanged(); + } + + @Override + public void created(TargetObject object) { + //System.err.println("created object='" + object.getJoinedPath(".") + "'"); + records.put(object, new ObjectRecord(object)); + defensive(() -> listener.created(object), "created"); + } + + @Override + public void invalidated(TargetObject object, TargetObject branch, String reason) { + ObjectRecord remove = records.remove(object); + if (remove != null) { + remove.removed(); + } + defensive(() -> listener.invalidated(object, branch, reason), "invalidated"); + } + + @Override + public void rootAdded(TargetObject root) { + defensive(() -> listener.rootAdded(root), "rootAdded"); + records.get(root).added(); + } + + @Override + public void attributesChanged(TargetObject object, Collection removed, + Map added) { + //System.err.println("attributesChanged object=" + object.getJoinedPath(".") + ",removed=" + + // removed + ",added=" + added); + ObjectRecord record = records.get(object); + if (record == null) { + defensive(() -> listener.attributesChanged(object, removed, added), + "attributesChanged"); + } + // Removed taken care of via invalidation + for (Entry ent : added.entrySet()) { + //System.err.println(" " + ent.getKey()); + Object val = ent.getValue(); + if (val instanceof TargetObject) { + TargetObject obj = (TargetObject) val; + if (!PathUtils.isLink(object.getPath(), ent.getKey(), obj.getPath())) { + ObjectRecord rec = records.get(obj); + rec.added(); + } + } + } + } + + @Override + public void elementsChanged(TargetObject object, Collection removed, + Map added) { + //System.err.println("elementsChanged object=" + object.getJoinedPath(".") + ",removed=" + + // removed + ",added=" + added); + ObjectRecord record = records.get(object); + if (record == null) { + defensive(() -> listener.elementsChanged(object, removed, added), "elementsChanged"); + } + // Removed taken care of via invalidation + for (Entry ent : added.entrySet()) { + //System.err.println(" " + ent.getKey()); + TargetObject obj = ent.getValue(); + if (!PathUtils.isElementLink(object.getPath(), ent.getKey(), obj.getPath())) { + ObjectRecord rec = records.get(obj); + if (rec != null) { + rec.added(); + } + } + } + } + + private synchronized void orderedOnObjects(Collection objects, Runnable r, + String cb) { + AsyncFence fence = new AsyncFence(); + fence.include(lastEvent); + for (TargetObject obj : objects) { + ObjectRecord record = records.get(obj); + if (record != null) { + fence.include(record.complete); + } + } + lastEvent = fence.ready().thenAccept(__ -> { + defensive(r, cb); + }).exceptionally(ex -> { + Msg.error(this, "Callback " + cb + " dropped for error in dependency", ex); + return null; + }); + } + + @Override + public void breakpointHit(TargetObject container, TargetObject trapped, TargetStackFrame frame, + TargetBreakpointSpec spec, TargetBreakpointLocation breakpoint) { + List args = frame == null + ? List.of(container, trapped, spec, breakpoint) + : List.of(container, trapped, frame, spec, breakpoint); + orderedOnObjects(args, () -> { + listener.breakpointHit(container, trapped, frame, spec, breakpoint); + }, "breakpointHit"); + } + + @Override + public void consoleOutput(TargetObject console, Channel channel, byte[] data) { + orderedOnObjects(List.of(console), () -> { + listener.consoleOutput(console, channel, data); + }, "consoleOutput"); + } + + private Collection gatherObjects(Collection... collections) { + Set objs = new HashSet<>(); + for (Collection col : collections) { + for (Object val : col) { + if (val instanceof TargetObject) { + objs.add((TargetObject) val); + } + } + } + return objs; + } + + @Override + public void event(TargetObject object, TargetThread eventThread, TargetEventType type, + String description, List parameters) { + List objs = eventThread == null + ? List.of(object) + : List.of(object, eventThread); + orderedOnObjects(gatherObjects(objs, parameters), () -> { + listener.event(object, eventThread, type, description, parameters); + }, "event(" + type + ") " + description); + } + + @Override + public void invalidateCacheRequested(TargetObject object) { + orderedOnObjects(List.of(object), () -> { + listener.invalidateCacheRequested(object); + }, "invalidateCacheRequested"); + } + + @Override + public void memoryReadError(TargetObject memory, AddressRange range, + DebuggerMemoryAccessException e) { + orderedOnObjects(List.of(memory), () -> { + listener.memoryReadError(memory, range, e); + }, "invalidateCacheRequested"); + } + + @Override + public void memoryUpdated(TargetObject memory, Address address, byte[] data) { + orderedOnObjects(List.of(memory), () -> { + listener.memoryUpdated(memory, address, data); + }, "invalidateCacheRequested"); + } + + @Override + public void registersUpdated(TargetObject bank, Map updates) { + orderedOnObjects(List.of(bank), () -> { + listener.registersUpdated(bank, updates); + }, "invalidateCacheRequested"); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathMatcher.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathMatcher.java index 1b5d885a63..9f84141eae 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathMatcher.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathMatcher.java @@ -18,13 +18,26 @@ package ghidra.dbg.util; import java.util.*; import java.util.function.Predicate; +import org.apache.commons.lang3.StringUtils; + public class PathMatcher implements PathPredicates { + protected static final Set WILD_SINGLETON = Set.of(""); + protected final Set patterns = new HashSet<>(); public void addPattern(List pattern) { patterns.add(new PathPattern(pattern)); } + public void addPattern(PathPattern pattern) { + patterns.add(pattern); + } + + @Override + public String toString() { + return String.format("", StringUtils.join(patterns, "\n ")); + } + /** * TODO: We could probably do a lot better, esp. for many patterns, by using a trie. */ @@ -43,12 +56,66 @@ public class PathMatcher implements PathPredicates { } @Override - public boolean successorCouldMatch(List path) { - return anyPattern(p -> p.successorCouldMatch(path)); + public boolean successorCouldMatch(List path, boolean strict) { + return anyPattern(p -> p.successorCouldMatch(path, strict)); } @Override - public boolean ancestorMatches(List path) { - return anyPattern(p -> p.ancestorMatches(path)); + public boolean ancestorMatches(List path, boolean strict) { + return anyPattern(p -> p.ancestorMatches(path, strict)); + } + + @Override + public List getSingletonPath() { + if (patterns.size() != 1) { + return null; + } + return patterns.iterator().next().getSingletonPath(); + } + + @Override + public PathPattern getSingletonPattern() { + if (patterns.size() != 1) { + return null; + } + return patterns.iterator().next(); + } + + @Override + public Set getNextNames(List path) { + Set result = new HashSet<>(); + for (PathPattern pattern : patterns) { + result.addAll(pattern.getNextNames(path)); + if (result.contains("")) { + return WILD_SINGLETON; + } + } + return result; + } + + @Override + public Set getNextIndices(List path) { + Set result = new HashSet<>(); + for (PathPattern pattern : patterns) { + result.addAll(pattern.getNextIndices(path)); + if (result.contains("")) { + return WILD_SINGLETON; + } + } + return result; + } + + @Override + public boolean isEmpty() { + return patterns.isEmpty(); + } + + @Override + public PathMatcher applyIndices(List indices) { + PathMatcher result = new PathMatcher(); + for (PathPattern pat : patterns) { + result.addPattern(pat.applyIndices(indices)); + } + return result; } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPattern.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPattern.java index 3716bdff1e..e14097105c 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPattern.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPattern.java @@ -15,8 +15,7 @@ */ package ghidra.dbg.util; -import java.util.List; -import java.util.Objects; +import java.util.*; public class PathPattern implements PathPredicates { private final List pattern; @@ -44,6 +43,11 @@ public class PathPattern implements PathPredicates { this.pattern = List.copyOf(pattern); } + @Override + public String toString() { + return String.format("", PathUtils.toString(pattern)); + } + @Override public boolean equals(Object obj) { if (!(obj instanceof PathPattern)) { @@ -58,6 +62,10 @@ public class PathPattern implements PathPredicates { return pattern.hashCode(); } + public static boolean isWildcard(String pat) { + return "[]".equals(pat) || "".equals(pat); + } + public static boolean keyMatches(String pat, String key) { if (key.equals(pat)) { return true; @@ -65,7 +73,7 @@ public class PathPattern implements PathPredicates { if ("[]".equals(pat) && PathUtils.isIndex(key)) { return true; } - if ("".equals(pat) && PathUtils.isName(pat)) { + if ("".equals(pat) && PathUtils.isName(key)) { return true; } return false; @@ -89,18 +97,101 @@ public class PathPattern implements PathPredicates { } @Override - public boolean successorCouldMatch(List path) { + public boolean successorCouldMatch(List path, boolean strict) { if (path.size() > pattern.size()) { return false; } + if (strict && path.size() == pattern.size()) { + return false; + } return matchesUpTo(path, path.size()); } @Override - public boolean ancestorMatches(List path) { + public boolean ancestorMatches(List path, boolean strict) { if (path.size() < pattern.size()) { return false; } + if (strict && path.size() == pattern.size()) { + return false; + } return matchesUpTo(path, pattern.size()); } + + protected static boolean containsWildcards(List pattern) { + for (String pat : pattern) { + if (isWildcard(pat)) { + return true; + } + } + return false; + } + + @Override + public List getSingletonPath() { + if (containsWildcards(pattern)) { + return null; + } + return pattern; + } + + public int countWildcards() { + return (int) pattern.stream().filter(k -> isWildcard(k)).count(); + } + + @Override + public PathPattern getSingletonPattern() { + return this; + } + + @Override + public Set getNextNames(List path) { + if (path.size() >= pattern.size()) { + return Set.of(); + } + String pat = pattern.get(path.size()); + if (PathUtils.isName(pat)) { + return Set.of(pat); + } + return Set.of(); + } + + @Override + public Set getNextIndices(List path) { + if (path.size() >= pattern.size()) { + return Set.of(); + } + String pat = pattern.get(path.size()); + if (PathUtils.isIndex(pat)) { + return Set.of(PathUtils.parseIndex(pat)); + } + return Set.of(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public PathPattern applyIndices(List indices) { + List result = new ArrayList<>(pattern.size()); + Iterator it = indices.iterator(); + for (String pat : pattern) { + if (it.hasNext() && isWildcard(pat)) { + String index = it.next(); + if (PathUtils.isIndex(pat)) { + result.add(PathUtils.makeKey(index)); + } + else { + // NB. Rare for attribute wildcards, but just in case + result.add(index); + } + } + else { + result.add(pat); + } + } + return new PathPattern(result); + } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java index 147cac1263..ce0772334c 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/PathPredicates.java @@ -15,7 +15,12 @@ */ package ghidra.dbg.util; -import java.util.List; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import ghidra.async.AsyncFence; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.util.PathUtils.PathComparator; public interface PathPredicates { /** @@ -29,6 +34,7 @@ public interface PathPredicates { /** * Check if the given path could have a matching successor * + *

* This essentially checks if the given path is a viable prefix to the matcher. * * @implNote this method could become impractical for culling queries if we allow too @@ -38,15 +44,235 @@ public interface PathPredicates { * * * @param path the path (prefix) to check + * @param strict true to exclude the case where {@link #matches(List)} would return true * @return true if a successor could match, false otherwise */ - boolean successorCouldMatch(List path); + boolean successorCouldMatch(List path, boolean strict); /** * Check if the given path has an ancestor that matches * * @param path the path to check + * @param strict true to exclude the case where {@link #matches(List)} would return true * @return true if an ancestor matches, false otherwise */ - boolean ancestorMatches(List path); + boolean ancestorMatches(List path, boolean strict); + + /** + * Assuming a successor of path could match, get the patterns for the next possible key + * + *

+ * If the pattern could accept a name next, get all patterns describing those names + * + * @param path the ancestor path + * @return a set of patterns + */ + Set getNextNames(List path); + + /** + * Assuming a successor of path could match, get the patterns for the next possible index + * + *

+ * If the pattern could accept an index next, get all patterns describing those indices + * + * @param path the ancestor path + * @return a set of patterns, without brack@Override ets ({@code []) + */ + Set getNextIndices(List path); + + /** + * If this predicate is known to match only one path, i.e., no wildcards, get that path + * + * @return the singleton path, or {@code null} + */ + List getSingletonPath(); + + /** + * If this predicate consists of a single pattern, get that pattern + * + * @return the singleton pattern, or {@code null} + */ + PathPattern getSingletonPattern(); + + static boolean anyMatches(Set pats, String key) { + for (String pat : pats) { + if ("".equals(pat)) { + return true; + } + if (key.equals(pat)) { + return true; + } + } + return false; + } + + default NavigableMap, ?> getCachedValues(TargetObject seed) { + return getCachedValues(List.of(), seed); + } + + default NavigableMap, ?> getCachedValues(List path, Object val) { + NavigableMap, Object> result = new TreeMap<>(PathComparator.KEYED); + getCachedValues(result, path, val); + return result; + } + + default void getCachedValues(Map, Object> result, List path, Object val) { + if (matches(path)) { + result.put(path, val); + } + if (val instanceof TargetObject && successorCouldMatch(path, true)) { + TargetObject cur = (TargetObject) val; + Set nextNames = getNextNames(path); + if (!nextNames.isEmpty()) { + for (Map.Entry ent : cur.getCachedAttributes().entrySet()) { + Object value = ent.getValue(); + String name = ent.getKey(); + if (!anyMatches(nextNames, name)) { + continue; + } + getCachedValues(result, PathUtils.extend(path, name), value); + } + } + Set nextIndices = getNextIndices(path); + if (!nextIndices.isEmpty()) { + for (Map.Entry ent : cur.getCachedElements().entrySet()) { + Object obj = ent.getValue(); + String index = ent.getKey(); + if (!anyMatches(nextIndices, index)) { + continue; + } + getCachedValues(result, PathUtils.index(path, index), obj); + } + } + } + } + + default NavigableMap, TargetObject> getCachedSuccessors(TargetObject seed) { + NavigableMap, TargetObject> result = new TreeMap<>(PathComparator.KEYED); + getCachedSuccessors(result, List.of(), seed); + return result; + } + + default void getCachedSuccessors(Map, TargetObject> result, + List path, TargetObject cur) { + if (matches(path)) { + result.put(path, cur); + } + if (successorCouldMatch(path, true)) { + Set nextNames = getNextNames(path); + if (!nextNames.isEmpty()) { + for (Map.Entry ent : cur.getCachedAttributes().entrySet()) { + Object value = ent.getValue(); + if (!(value instanceof TargetObject)) { + continue; + } + String name = ent.getKey(); + if (!anyMatches(nextNames, name)) { + continue; + } + TargetObject obj = (TargetObject) value; + getCachedSuccessors(result, PathUtils.extend(path, name), obj); + } + } + Set nextIndices = getNextIndices(path); + if (!nextIndices.isEmpty()) { + for (Map.Entry ent : cur.getCachedElements() + .entrySet()) { + TargetObject obj = ent.getValue(); + String index = ent.getKey(); + if (!anyMatches(nextIndices, index)) { + continue; + } + getCachedSuccessors(result, PathUtils.index(path, index), obj); + } + } + } + } + + default CompletableFuture, TargetObject>> fetchSuccessors( + TargetObject seed) { + NavigableMap, TargetObject> result = new TreeMap<>(PathComparator.KEYED); + return fetchSuccessors(result, List.of(), seed).thenApply(__ -> result); + } + + default CompletableFuture fetchSuccessors(Map, TargetObject> result, + List path, TargetObject cur) { + AsyncFence fence = new AsyncFence(); + if (matches(path)) { + synchronized (result) { + result.put(path, cur); + } + } + if (successorCouldMatch(path, true)) { + Set nextNames = getNextNames(path); + if (!nextNames.isEmpty()) { + fence.include(cur.fetchAttributes().thenCompose(attrs -> { + AsyncFence aFence = new AsyncFence(); + for (Map.Entry ent : attrs.entrySet()) { + Object value = ent.getValue(); + if (!(value instanceof TargetObject)) { + continue; + } + String name = ent.getKey(); + if (!anyMatches(nextNames, name)) { + continue; + } + TargetObject obj = (TargetObject) value; + aFence.include( + fetchSuccessors(result, PathUtils.extend(path, name), obj)); + } + return aFence.ready(); + })); + } + Set nextIndices = getNextIndices(path); + if (!nextIndices.isEmpty()) { + fence.include(cur.fetchElements().thenCompose(elems -> { + AsyncFence eFence = new AsyncFence(); + for (Map.Entry ent : elems.entrySet()) { + TargetObject obj = ent.getValue(); + String index = ent.getKey(); + if (!anyMatches(nextIndices, index)) { + continue; + } + eFence.include( + fetchSuccessors(result, PathUtils.index(path, index), obj)); + } + return eFence.ready(); + })); + } + } + return fence.ready(); + } + + /** + * Substitute wildcards from left to right for the given list of indices + * + *

+ * Takes each pattern and substitutes its wildcards for the given indices, starting from the + * left and working right. This object is unmodified, and the result is returned. + * + *

+ * If there are fewer wildcards in a pattern than given, only the left-most indices are taken. + * If there are fewer indices than wildcards in a pattern, then the right-most wildcards are + * left in the resulting pattern. Note while rare, attribute wildcards are substituted, too. + * + * @param indices the indices to substitute + * @return the pattern or matcher with the applied substitutions + */ + PathPredicates applyIndices(List indices); + + default PathPredicates applyIndices(String... indices) { + return applyIndices(List.of(indices)); + } + + /** + * Test if any patterns are contained here + * + *

+ * Note that the presence of a pattern does not guarantee the presence of a matching object. + * However, the absence of any pattern does guarantee no object can match. + * + * @return + */ + boolean isEmpty(); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/AnnotatedDebuggerAttributeListenerTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/AnnotatedDebuggerAttributeListenerTest.java new file mode 100644 index 0000000000..c1539d9dde --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/AnnotatedDebuggerAttributeListenerTest.java @@ -0,0 +1,56 @@ +/* ### + * 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; + +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import ghidra.async.AsyncReference; +import ghidra.dbg.agent.AbstractDebuggerObjectModel; +import ghidra.dbg.agent.DefaultTargetModelRoot; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.testutil.DebuggerModelTestUtils; +import ghidra.program.model.address.AddressFactory; + +public class AnnotatedDebuggerAttributeListenerTest implements DebuggerModelTestUtils { + @Test + public void testAnnotatedListener() throws Throwable { + AbstractDebuggerObjectModel model = new AbstractDebuggerObjectModel() { + @Override + public AddressFactory getAddressFactory() { + return null; + } + }; + DefaultTargetModelRoot obj = new DefaultTargetModelRoot(model, "Test"); + + AsyncReference display = new AsyncReference<>(); + DebuggerModelListener l = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + @AttributeCallback("_test") + private void testChanged(TargetObject object, String disp) { + display.set(disp, null); + } + }; + obj.addListener(l); + obj.changeAttributes(List.of(), Map.ofEntries(Map.entry("_test", "Testing")), "Because"); + waitOn(display.waitValue("Testing")); + + obj.changeAttributes(List.of("_test"), Map.of(), "Because"); + waitOn(display.waitValue(null)); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/DebugModelConventionsTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/DebugModelConventionsTest.java index 8783590903..b00bd26034 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/DebugModelConventionsTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/DebugModelConventionsTest.java @@ -38,8 +38,8 @@ public class DebugModelConventionsTest { mb.createTestModel(); mb.createTestProcessesAndThreads(); - TargetBreakpointContainer bpts = DebugModelConventions - .findSuitable(TargetBreakpointContainer.class, mb.testProcess1) + TargetBreakpointSpecContainer bpts = DebugModelConventions + .findSuitable(TargetBreakpointSpecContainer.class, mb.testProcess1) .get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); assertEquals(mb.testProcess1.breaks, bpts); } @@ -49,8 +49,8 @@ public class DebugModelConventionsTest { mb.createTestModel(); mb.createTestProcessesAndThreads(); - TargetBreakpointContainer bpts = DebugModelConventions - .findSuitable(TargetBreakpointContainer.class, mb.testProcess1.threads) + TargetBreakpointSpecContainer bpts = DebugModelConventions + .findSuitable(TargetBreakpointSpecContainer.class, mb.testProcess1.threads) .get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); assertEquals(mb.testProcess1.breaks, bpts); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/agent/DefaultDebuggerObjectModelTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/agent/DefaultDebuggerObjectModelTest.java index b1d293cf4f..25850a3c1d 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/agent/DefaultDebuggerObjectModelTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/agent/DefaultDebuggerObjectModelTest.java @@ -15,7 +15,7 @@ */ package ghidra.dbg.agent; -import static ghidra.lifecycle.Unfinished.TODO; +import static ghidra.lifecycle.Unfinished.*; import static org.junit.Assert.*; import java.util.*; @@ -30,11 +30,10 @@ import ghidra.async.AsyncTestUtils; import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetRegisterBank; -import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener; -import ghidra.dbg.util.*; -import ghidra.dbg.util.AttributesChangedListener.AttributesChangedInvocation; -import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation; -import ghidra.dbg.util.InvalidatedListener.InvalidatedInvocation; +import ghidra.dbg.testutil.*; +import ghidra.dbg.testutil.AttributesChangedListener.AttributesChangedInvocation; +import ghidra.dbg.testutil.ElementsChangedListener.ElementsChangedInvocation; +import ghidra.dbg.testutil.InvalidatedListener.InvalidatedInvocation; import ghidra.program.model.address.AddressFactory; import ghidra.program.model.address.AddressSpace; @@ -278,7 +277,7 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils { } @Override - public void registersUpdated(TargetRegisterBank bank, Map updates) { + public void registersUpdated(TargetObject bank, Map updates) { record.add(new ImmutablePair<>("registersUpdated", bank)); } } @@ -291,19 +290,16 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils { FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A"); FakeTargetRegisterBank fakeA1rb = new FakeTargetRegisterBank(model, fakeA, "[1]"); - fakeA1rb.listeners.fire(TargetRegisterBankListener.class) - .registersUpdated(fakeA1rb, Map.of()); + fakeA1rb.listeners.fire.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), + assertEquals(List.of(new ImmutablePair<>("created", fakeA), new ImmutablePair<>("created", fakeA1rb), new ImmutablePair<>("registersUpdated", fakeA1rb), - new ImmutablePair<>("addedElem", fakeA1rb), - new ImmutablePair<>("addedAttr", fakeA)), + new ImmutablePair<>("addedElem", fakeA1rb), new ImmutablePair<>("addedAttr", fakeA)), listener.record); } @@ -312,8 +308,7 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils { FakeTargetObject fakeA = new FakeTargetObject(model, model.root, "A"); FakeTargetRegisterBank fakeA1rb = new FakeTargetRegisterBank(model, fakeA, "[1]"); - fakeA1rb.listeners.fire(TargetRegisterBankListener.class) - .registersUpdated(fakeA1rb, Map.of()); + fakeA1rb.listeners.fire.registersUpdated(fakeA1rb, Map.of()); fakeA.setElements(List.of(fakeA1rb), "Init"); model.root.setAttributes(List.of(fakeA), Map.of(), "Init"); EventRecordingListener listener = new EventRecordingListener(); @@ -321,12 +316,9 @@ public class DefaultDebuggerObjectModelTest implements AsyncTestUtils { 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)), + assertEquals(List.of(new ImmutablePair<>("created", model.root), + new ImmutablePair<>("created", fakeA), new ImmutablePair<>("created", fakeA1rb), + new ImmutablePair<>("addedElem", fakeA1rb), new ImmutablePair<>("addedAttr", fakeA)), listener.record); } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/AbstractTestTargetRegisterBank.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/AbstractTestTargetRegisterBank.java index 03c643e49b..316ee6c313 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/AbstractTestTargetRegisterBank.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/AbstractTestTargetRegisterBank.java @@ -54,10 +54,10 @@ public abstract class AbstractTestTargetRegisterBank

} result.put(n, v); } - return regs.getModel().future(result).thenApply(__ -> { - listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, result); + return model.gateFuture(regs.getModel().future(result).thenApply(__ -> { + listeners.fire.registersUpdated(this, result); return result; - }).thenCompose(model::gateFuture); + })); } protected CompletableFuture writeRegs(Map values, @@ -80,9 +80,9 @@ public abstract class AbstractTestTargetRegisterBank

} } future.thenAccept(__ -> { - listeners.fire(TargetRegisterBankListener.class).registersUpdated(this, updates); - }).thenCompose(model::gateFuture); - return future; + listeners.fire.registersUpdated(this, updates); + }); + return model.gateFuture(future); } public void setFromBank(AbstractTestTargetRegisterBank bank) { diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/EmptyDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/EmptyDebuggerObjectModel.java new file mode 100644 index 0000000000..8060b36761 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/EmptyDebuggerObjectModel.java @@ -0,0 +1,43 @@ +/* ### + * 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.model; + +import ghidra.dbg.agent.AbstractDebuggerObjectModel; +import ghidra.dbg.agent.SpiTargetObject; +import ghidra.program.model.address.*; + +public class EmptyDebuggerObjectModel extends AbstractDebuggerObjectModel { + protected final AddressSpace ram = new GenericAddressSpace("ram", 64, AddressSpace.TYPE_RAM, 0); + protected final AddressFactory factory = new DefaultAddressFactory(new AddressSpace[] { ram }); + + @Override + public AddressFactory getAddressFactory() { + return factory; + } + + public Address addr(long off) { + return ram.getAddress(off); + } + + public AddressRange range(long min, long max) { + return new AddressRangeImpl(addr(min), addr(max)); + } + + @Override + public void addModelRoot(SpiTargetObject root) { + super.addModelRoot(root); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java index fb180a93c6..b44003cced 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestDebuggerObjectModel.java @@ -15,14 +15,17 @@ */ package ghidra.dbg.model; +import java.io.IOException; import java.util.concurrent.*; -import ghidra.dbg.agent.AbstractDebuggerObjectModel; +import org.jdom.JDOMException; + import ghidra.dbg.target.TargetObject; -import ghidra.program.model.address.*; +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.target.schema.XmlSchemaContext; // TODO: Refactor with other Fake and Test model stuff. -public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel { +public class TestDebuggerObjectModel extends EmptyDebuggerObjectModel { public static final String TEST_MODEL_STRING = "Test Model"; protected static final int DELAY_MILLIS = 250; @@ -34,9 +37,19 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel { ASYNC, DELAYED; } - protected final AddressSpace ram = - new GenericAddressSpace("ram", 64, AddressSpace.TYPE_RAM, 0); - protected final AddressFactory factory = new DefaultAddressFactory(new AddressSpace[] { ram }); + public static final XmlSchemaContext SCHEMA_CTX; + public static final TargetObjectSchema ROOT_SCHEMA; + static { + try { + SCHEMA_CTX = XmlSchemaContext.deserialize( + EmptyDebuggerObjectModel.class.getResourceAsStream("test_schema.xml")); + ROOT_SCHEMA = SCHEMA_CTX.getSchema(SCHEMA_CTX.name("Test")); + } + catch (IOException | JDOMException e) { + throw new AssertionError(e); + } + } + public final TestTargetSession session; protected int invalidateCachesCount; @@ -50,25 +63,25 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel { } public TestDebuggerObjectModel(String rootHint) { - this.session = new TestTargetSession(this, rootHint); + this.session = new TestTargetSession(this, rootHint, ROOT_SCHEMA); addModelRoot(session); } + @Override + public TargetObjectSchema getRootSchema() { + return ROOT_SCHEMA; + } + @Override public String toString() { return TEST_MODEL_STRING; } - @Override + @Override // TODO: Give test writer control of addModelRoot public CompletableFuture fetchModelRoot() { return future(session); } - @Override - public AddressFactory getAddressFactory() { - return factory; - } - @Override public CompletableFuture close() { session.invalidateSubtree(session, "Model closed"); @@ -87,14 +100,6 @@ public class TestDebuggerObjectModel extends AbstractDebuggerObjectModel { return session.requestFocus(obj); } - public Address addr(long off) { - return ram.getAddress(off); - } - - public AddressRange range(long min, long max) { - return new AddressRangeImpl(addr(min), addr(max)); - } - @Override public synchronized void invalidateAllLocalCaches() { invalidateCachesCount++; diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpoint.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpoint.java index c7bb66b240..9632fa1baa 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpoint.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpoint.java @@ -18,10 +18,8 @@ package ghidra.dbg.model; import java.util.*; import java.util.concurrent.CompletableFuture; -import ghidra.dbg.attributes.TargetObjectList; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetBreakpointContainer.TargetBreakpointKindSet; -import ghidra.dbg.util.CollectionUtils.Delta; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.Address; @@ -36,7 +34,6 @@ public class TestTargetBreakpoint changeAttributes(List.of(), Map.of( SPEC_ATTRIBUTE_NAME, this, ADDRESS_ATTRIBUTE_NAME, address, - AFFECTS_ATTRIBUTE_NAME, TargetObjectList.of(parent.getParent()), ENABLED_ATTRIBUTE_NAME, true, EXPRESSION_ATTRIBUTE_NAME, address.toString(), KINDS_ATTRIBUTE_NAME, TargetBreakpointKindSet.copyOf(kinds), @@ -56,23 +53,17 @@ public class TestTargetBreakpoint @Override public CompletableFuture disable() { - Delta delta = changeAttributes(List.of(), Map.of( + changeAttributes(List.of(), Map.of( ENABLED_ATTRIBUTE_NAME, false // ), "Disabled Breakpoint"); - if (delta.added.containsKey(ENABLED_ATTRIBUTE_NAME)) { - listeners.fire(TargetBreakpointSpecListener.class).breakpointToggled(this, false); - } return getModel().future(null); } @Override public CompletableFuture enable() { - Delta delta = changeAttributes(List.of(), Map.of( + changeAttributes(List.of(), Map.of( ENABLED_ATTRIBUTE_NAME, true // ), "Enabled Breakpoint"); - if (delta.added.containsKey(ENABLED_ATTRIBUTE_NAME)) { - listeners.fire(TargetBreakpointSpecListener.class).breakpointToggled(this, true); - } return getModel().future(null); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java index 6cbd5a053b..bdeaee77ac 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetBreakpointContainer.java @@ -19,7 +19,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; -import ghidra.dbg.target.TargetBreakpointContainer; +import ghidra.dbg.target.TargetBreakpointSpecContainer; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.program.model.address.AddressRange; @@ -31,7 +31,7 @@ import ghidra.program.model.address.AddressRange; public class TestTargetBreakpointContainer extends DefaultTestTargetObject - implements TargetBreakpointContainer { + implements TargetBreakpointSpecContainer { protected static final TargetBreakpointKindSet ALL_KINDS = TargetBreakpointKindSet.of(TargetBreakpointKind.values()); diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetInterpreter.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetInterpreter.java index 9971fca50c..219b811033 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetInterpreter.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetInterpreter.java @@ -66,18 +66,16 @@ public class TestTargetInterpreter changeAttributes(List.of(), Map.of( DISPLAY_ATTRIBUTE_NAME, display // ), "Display changed"); - listeners.fire.displayChanged(this, display); } public void setPrompt(String prompt) { changeAttributes(List.of(), Map.of( PROMPT_ATTRIBUTE_NAME, prompt // ), "Prompt changed"); - listeners.fire(TargetInterpreterListener.class).promptChanged(this, prompt); } public void output(Channel channel, String line) { - listeners.fire(TargetInterpreterListener.class).consoleOutput(this, channel, line + "\n"); + listeners.fire.consoleOutput(this, channel, line + "\n"); } public void clearCalls() { diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java index b2349dc19a..25d9d3577a 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetMemory.java @@ -47,7 +47,7 @@ public class TestTargetMemory memory.getData(address.getOffset(), data); CompletableFuture future = getModel().future(data); future.thenAccept(__ -> { - listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, data); + listeners.fire.memoryUpdated(this, address, data); }); return future; } @@ -62,7 +62,7 @@ public class TestTargetMemory setMemory(address, data); CompletableFuture future = getModel().future(null); future.thenAccept(__ -> { - listeners.fire(TargetMemoryListener.class).memoryUpdated(this, address, data); + listeners.fire.memoryUpdated(this, address, data); }); return future; } @@ -78,7 +78,6 @@ public class TestTargetMemory changeAttributes(List.of(), Map.ofEntries( Map.entry(ACCESSIBLE_ATTRIBUTE_NAME, accessible)), "Set Test Memory Accessibility"); - listeners.fire(TargetAccessibilityListener.class).accessibilityChanged(this, accessible); return old; } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java index d887386fb1..879cdab4d1 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetSession.java @@ -23,6 +23,8 @@ import ghidra.async.AsyncUtils; import ghidra.dbg.agent.DefaultTargetModelRoot; import ghidra.dbg.target.*; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.schema.EnumerableTargetObjectSchema; +import ghidra.dbg.target.schema.TargetObjectSchema; public class TestTargetSession extends DefaultTargetModelRoot implements TestTargetObject, TargetFocusScope, TargetEventScope, TargetLauncher { @@ -33,18 +35,20 @@ public class TestTargetSession extends DefaultTargetModelRoot public final TestMimickJavaLauncher mimickJavaLauncher; public TestTargetSession(TestDebuggerObjectModel model, String rootHint) { - super(model, rootHint); + this(model, rootHint, EnumerableTargetObjectSchema.OBJECT); + } + + public TestTargetSession(TestDebuggerObjectModel model, String rootHint, + TargetObjectSchema schema) { + super(model, rootHint, schema); environment = new TestTargetEnvironment(this); processes = new TestTargetProcessContainer(this); interpreter = new TestTargetInterpreter(this); mimickJavaLauncher = new TestMimickJavaLauncher(this); - changeAttributes(List.of(), List.of( - environment, - processes, - interpreter, - mimickJavaLauncher), - Map.of(), "Initialized"); + changeAttributes(List.of(), + List.of(environment, processes, interpreter, mimickJavaLauncher), Map.of(), + "Initialized"); } public TestTargetProcess addProcess(int pid) { @@ -58,19 +62,16 @@ public class TestTargetSession extends DefaultTargetModelRoot @Override public CompletableFuture requestFocus(TargetObject obj) { - return getModel().future(null).thenAccept(__ -> { - changeAttributes(List.of(), List.of(), Map.of( - FOCUS_ATTRIBUTE_NAME, obj // + return model.gateFuture(getModel().future(null).thenAccept(__ -> { + changeAttributes(List.of(), List.of(), Map.of(FOCUS_ATTRIBUTE_NAME, obj // ), "Focus requested"); - listeners.fire(TargetFocusScopeListener.class).focusChanged(this, obj); - }).thenCompose(model::gateFuture); + })); } public void simulateStep(TestTargetThread eventThread) { eventThread.setState(TargetExecutionState.RUNNING); - listeners.fire(TargetEventScopeListener.class) - .event(this, eventThread, TargetEventType.STEP_COMPLETED, - "Test thread completed a step", List.of()); + listeners.fire.event(this, eventThread, TargetEventType.STEP_COMPLETED, + "Test thread completed a step", List.of()); eventThread.setState(TargetExecutionState.STOPPED); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThread.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThread.java index 819e2d56e0..eafabf3ddf 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThread.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/model/TestTargetThread.java @@ -58,8 +58,5 @@ public class TestTargetThread Delta delta = changeAttributes(List.of(), List.of(), Map.of( STATE_ATTRIBUTE_NAME, state // ), "Changed state"); - if (delta.added.containsKey(STATE_ATTRIBUTE_NAME)) { - listeners.fire(TargetExecutionStateListener.class).executionStateChanged(this, state); - } } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java index 17890b534d..6d14f6fd7d 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/AnnotatedTargetObjectSchemaTest.java @@ -25,6 +25,7 @@ import org.junit.Test; import ghidra.dbg.agent.*; import ghidra.dbg.target.*; import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema; +import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode; import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; public class AnnotatedTargetObjectSchemaTest { @@ -40,8 +41,6 @@ public class AnnotatedTargetObjectSchemaTest { EnumerableTargetObjectSchema.STRING.getName(), false, false, true), null); builder.addAttributeSchema(new DefaultAttributeSchema("_kind", EnumerableTargetObjectSchema.STRING.getName(), false, true, true), null); - builder.addAttributeSchema(new DefaultAttributeSchema("_update_mode", - EnumerableTargetObjectSchema.UPDATE_MODE.getName(), false, false, true), null); builder.addAttributeSchema(new DefaultAttributeSchema("_order", EnumerableTargetObjectSchema.INT.getName(), false, false, true), null); builder.addAttributeSchema(new DefaultAttributeSchema("_modified", @@ -235,6 +234,29 @@ public class AnnotatedTargetObjectSchemaTest { assertEquals("TestAnnotatedTargetRootWithListedAttrs", schema.getName().toString()); } + @TargetObjectSchemaInfo(elementResync = ResyncMode.ONCE, attributeResync = ResyncMode.ALWAYS) + static class TestAnnotatedTargetRootWithResyncModes extends DefaultTargetModelRoot { + + public TestAnnotatedTargetRootWithResyncModes(AbstractDebuggerObjectModel model, + String typeHint) { + super(model, typeHint); + } + } + + @Test + public void testAnnotatedRootWithResyuncModes() { + AnnotatedSchemaContext ctx = new AnnotatedSchemaContext(); + TargetObjectSchema schema = + ctx.getSchemaForClass(TestAnnotatedTargetRootWithResyncModes.class); + + TargetObjectSchema exp = addBasicAttributes(ctx.builder(schema.getName())) + .addInterface(TargetAggregate.class) + .setElementResyncMode(ResyncMode.ONCE) + .setAttributeResyncMode(ResyncMode.ALWAYS) + .build(); + assertEquals(exp, schema); + } + static class NotAPrimitive { } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/TargetObjectSchemaValidationTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/TargetObjectSchemaValidationTest.java index 38ffdeb022..2c2a26932f 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/TargetObjectSchemaValidationTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/TargetObjectSchemaValidationTest.java @@ -27,7 +27,6 @@ import org.junit.Test; import ghidra.dbg.agent.*; import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetObject.TargetUpdateMode; import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema; import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; @@ -268,8 +267,6 @@ public class TargetObjectSchemaValidationTest { ValidatedObject createRepleteValidatedObject() { TargetObjectSchema schema = ctx.builder(new SchemaName("test")) - .addAttributeSchema(new DefaultAttributeSchema("_update_mode", - EnumerableTargetObjectSchema.UPDATE_MODE.getName(), true, false, false), null) .addAttributeSchema(new DefaultAttributeSchema("_display", EnumerableTargetObjectSchema.STRING.getName(), true, false, false), null) .addAttributeSchema(new DefaultAttributeSchema("int", @@ -287,12 +284,10 @@ public class TargetObjectSchemaValidationTest { ValidatedObject obj = createRepleteValidatedObject(); obj.setAttributes(List.of(), Map.of( "_display", "Hello", - "_update_mode", TargetUpdateMode.SOLICITED, "int", 5), "Test"); obj.setAttributes(List.of(), Map.of( "_display", "World", - "_update_mode", TargetUpdateMode.FIXED, "int", 6), "Test"); } @@ -309,7 +304,6 @@ public class TargetObjectSchemaValidationTest { ValidatedObject obj = createRepleteValidatedObject(); obj.setAttributes(List.of(), Map.of( "_display", "World", - "_update_mode", TargetUpdateMode.UNSOLICITED, "int", 7.0), "Test"); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/XmlTargetObjectSchemaTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/XmlTargetObjectSchemaTest.java index 3ca06b9be3..b07128631d 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/XmlTargetObjectSchemaTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/XmlTargetObjectSchemaTest.java @@ -25,13 +25,12 @@ import org.junit.Test; import ghidra.dbg.target.TargetInterpreter; import ghidra.dbg.target.TargetProcess; import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema; -import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema; -import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; +import ghidra.dbg.target.schema.TargetObjectSchema.*; public class XmlTargetObjectSchemaTest { protected static final String SCHEMA_XML = "" + "\n" + - " \n" + + " \n" + " \n" + " \n" + " \n" + @@ -40,7 +39,7 @@ public class XmlTargetObjectSchemaTest { " \n" + - " \n" + + " \n" + " \n" + " \n" + @@ -55,13 +54,17 @@ public class XmlTargetObjectSchemaTest { .setCanonicalContainer(true) .addElementSchema("reserved", EnumerableTargetObjectSchema.VOID.getName(), null) .addElementSchema("", NAME_DOWN1, null) + .setElementResyncMode(ResyncMode.NEVER) .addAttributeSchema(new DefaultAttributeSchema("some_int", EnumerableTargetObjectSchema.INT.getName(), false, false, false), null) .addAttributeSchema(new DefaultAttributeSchema("some_object", EnumerableTargetObjectSchema.OBJECT.getName(), true, true, true), null) + .setAttributeResyncMode(ResyncMode.ONCE) .buildAndAdd(); protected static final TargetObjectSchema SCHEMA_DOWN1 = CTX.builder(NAME_DOWN1) + .setElementResyncMode(ResyncMode.ALWAYS) .setDefaultAttributeSchema(AttributeSchema.DEFAULT_VOID) + .setAttributeResyncMode(ResyncMode.ALWAYS) .buildAndAdd(); @Test diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelAttacherTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelAttacherTest.java new file mode 100644 index 0000000000..53b55a9cb6 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelAttacherTest.java @@ -0,0 +1,309 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; +import static org.junit.Assume.*; + +import java.lang.invoke.MethodHandles; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.agent.AbstractDebuggerObjectModel; +import ghidra.dbg.agent.DefaultTargetModelRoot; +import ghidra.dbg.error.DebuggerIllegalArgumentException; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetMethod.TargetParameterMap; +import ghidra.dbg.testutil.ElementTrackingListener; +import ghidra.program.model.address.AddressFactory; + +public abstract class AbstractDebuggerModelAttacherTest extends AbstractDebuggerModelTest + implements RequiresAttachSpecimen { + + public List getExpectedAttacherPath() { + return null; + } + + public List getExpectedAttachableContainerPath() { + return null; + } + + public List getExpectedProcessesContainerPath() { + return null; + } + + public abstract TargetParameterMap getExpectedAttachParameters(); + + public abstract void assertEnvironment(TargetEnvironment environment); + + @Test + public void testAttacherIsWhereExpected() throws Throwable { + List expectedAttacherPath = getExpectedAttacherPath(); + assumeNotNull(expectedAttacherPath); + m.build(); + + TargetAttacher attacher = findAttacher(); + assertEquals(expectedAttacherPath, attacher.getPath()); + } + + @Test + public void testProcessContainerIsWhereExpected() throws Throwable { + List expectedProcessContainerPath = getExpectedProcessesContainerPath(); + assumeNotNull(expectedProcessContainerPath); + m.build(); + + TargetObject container = findProcessContainer(); + assertEquals(expectedProcessContainerPath, container.getPath()); + } + + @Test + public void testAttachableContainerIsWhereExpected() throws Throwable { + List expectedAttachableContainerPath = getExpectedAttachableContainerPath(); + assumeNotNull(expectedAttachableContainerPath); + m.build(); + + TargetObject container = findAttachableContainer(); + assertEquals(expectedAttachableContainerPath, container.getPath()); + } + + protected void runTestListAttachable(TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + waitAcc(container); + Collection attachables = fetchAttachables(container); + assertNotNull(getAttachable(attachables, specimen, dummy, this)); + } + + @Test + public void testListAttachable() throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assumeTrue(m.hasAttachableContainer()); + m.build(); + dummy = specimen.runDummy(); + + TargetObject container = findAttachableContainer(); + runTestListAttachable(container); + } + + // TODO: Attacher parameters, when we go that way. + + protected void runTestAttachByPid(TargetAttacher attacher) throws Throwable { + waitAcc(attacher); + waitOn(attacher.attach(dummy.pid)); + } + + @Test + public void testAttachByPid() throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + m.build(); + dummy = specimen.runDummy(); + + var listener = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + CompletableFuture observedCreated = new CompletableFuture<>(); + + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + public void stateChanged(TargetObject object, TargetExecutionState state) { + // We're only expecting one process, so this should be fine + TargetProcess process = DebugModelConventions.liveProcessOrNull(object); + if (process == null) { + return; + } + try { + TargetEnvironment env = findEnvironment(process.getPath()); + assertEnvironment(env); + observedCreated.complete(null); + } + catch (Throwable e) { + observedCreated.completeExceptionally(e); + } + } + }; + // NB. I've intentionally omitted the reorderer here. The model should get it right. + m.getModel().addModelListener(listener); + + TargetAttacher attacher = m.find(TargetAttacher.class, List.of()); + runTestAttachByPid(attacher); + waitOn(listener.observedCreated); + } + + protected void runTestAttachByObj(TargetAttacher attacher, TargetObject container) + throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + Collection attachables = fetchAttachables(container); + TargetAttachable target = getAttachable(attachables, specimen, dummy, this); + waitAcc(attacher); + waitOn(attacher.attach(target)); + } + + @Test + public void testAttachByObj() throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assumeTrue(m.hasAttachableContainer()); + m.build(); + dummy = specimen.runDummy(); + + TargetAttacher attacher = findAttacher(); + TargetObject container = findAttachableContainer(); + runTestAttachByObj(attacher, container); + } + + protected static class BogusObjectModel extends AbstractDebuggerObjectModel { + @Override + public AddressFactory getAddressFactory() { + return null; + } + } + + protected static class BogusTargetAttachable extends DefaultTargetModelRoot + implements TargetAttachable { + public BogusTargetAttachable(AbstractDebuggerObjectModel model) { + super(model, "Bogus"); + } + } + + protected void runTestAttachByObjBogusThrowsException(TargetAttacher attacher) + throws Throwable { + waitAcc(attacher); + BogusObjectModel bogusModel = new BogusObjectModel(); + TargetAttachable bogusAttachable = new BogusTargetAttachable(bogusModel); + waitOn(attacher.attach(bogusAttachable)); + } + + @Test(expected = DebuggerIllegalArgumentException.class) + public void testAttachByObjBogusThrowsException() throws Throwable { + m.build(); + + TargetAttacher attacher = m.find(TargetAttacher.class, List.of()); + runTestAttachByObjBogusThrowsException(attacher); + } + + protected void runTestAttachByPidThenDetach(TargetAttacher attacher, TargetObject container) + throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assertNull(getProcessRunning(container, specimen, this)); + runTestAttachByPid(attacher); + runTestDetach(container, specimen); + assertTrue(dummy.process.isAlive()); + } + + @Test + public void testAttachByPidThenDetach() throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assumeTrue(m.hasDetachableProcesses()); + m.build(); + dummy = specimen.runDummy(); + + TargetAttacher attacher = findAttacher(); + TargetObject container = findProcessContainer(); + runTestAttachByPidThenDetach(attacher, container); + } + + protected void runTestAttachByPidThenKill(TargetAttacher attacher, TargetObject container) + throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assertNull(getProcessRunning(container, specimen, this)); + runTestAttachByPid(attacher); + runTestKill(container, specimen); + retryVoid(() -> assertFalse(dummy.process.isAlive()), List.of(AssertionError.class)); + } + + @Test + public void testAttachByPidThenKill() throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assumeTrue(m.hasKillableProcesses()); + m.build(); + dummy = specimen.runDummy(); + + TargetAttacher attacher = findAttacher(); + TargetObject container = findProcessContainer(); + runTestAttachByPidThenKill(attacher, container); + } + + protected void runTestAttachByPidThenResumeInterrupt(TargetAttacher attacher, + TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assertNull(getProcessRunning(container, specimen, this)); + runTestAttachByPid(attacher); + runTestResumeInterruptMany(container, specimen, 3); + assertTrue(dummy.process.isAlive()); + } + + @Test + public void testAttachByPidThenResumeInterrupt() throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assumeTrue(m.hasResumableProcesses()); + m.build(); + dummy = specimen.runDummy(); + + TargetAttacher attacher = findAttacher(); + TargetObject container = findProcessContainer(); + runTestAttachByPidThenResumeInterrupt(attacher, container); + } + + protected void runTestAttachShowsInProcessContainer(TargetAttacher attacher, + TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assertNull(getProcessRunning(container, specimen, this)); + runTestAttachByPid(attacher); + retryForProcessRunning(container, specimen, this); + } + + @Test + public void testAttachShowsInProcessContainer() throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assumeTrue(m.hasProcessContainer()); + m.build(); + dummy = specimen.runDummy(); + + TargetAttacher attacher = findAttacher(); + TargetObject container = findProcessContainer(); + runTestAttachShowsInProcessContainer(attacher, container); + } + + protected void runTestAttachShowsInProcessContainerViaListener(TargetAttacher attacher, + TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + ElementTrackingListener procListener = + new ElementTrackingListener<>(TargetProcess.class); + container.addListener(procListener); + // NB. Have to express interest, otherwise model is not obligated to invoke listener + Collection procsBefore = fetchProcesses(container); + procListener.putAll(container.getCachedElements()); + assertNull(getProcessRunning(procsBefore, specimen, this)); + runTestAttachByPid(attacher); + retryVoid(() -> { + // Cannot fetch elements. rely only on listener. + assertNotNull(getProcessRunning(procListener.elements.values(), specimen, this)); + }, List.of(AssertionError.class)); + } + + @Test + public void testAttachShowsInProcessContainerViaListener() throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assumeTrue(m.hasProcessContainer()); + m.build(); + dummy = specimen.runDummy(); + + TargetAttacher attacher = findAttacher(); + TargetObject container = findProcessContainer(); + runTestAttachShowsInProcessContainerViaListener(attacher, container); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelBreakpointsTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelBreakpointsTest.java new file mode 100644 index 0000000000..f042174e1e --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelBreakpointsTest.java @@ -0,0 +1,358 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; +import static org.junit.Assume.*; + +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.junit.Test; + +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.dbg.util.DebuggerCallbackReorderer; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.AddressRangeImpl; +import ghidra.util.Msg; + +/** + * Tests the functionality of breakpoints + * + *

+ * Note that this test does not check for nuances regarding specification vs. + * location, as it is meant to generalize across models for interests of the UI + * only. As such, we only test that we can set breakpoints at given addresses + * and that a location manifests there, regardless of the intervening + * mechanisms. We also test some basic operations on the breakpoint (location) + * itself. Models which have separate specifications from locations, or for + * which you want to test non-address specifications will need to add their own + * tests, tailored to the semantics of that model's breakpoint specifications. + * + *

+ * TODO: Enable, disable (if supported), delete (if supported), manipulation via + * CLI is synced + */ +public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebuggerModelTest + implements RequiresTarget { + + /** + * Get the expected (absolute) path of the target's breakpoint container + * + * @param targetPath the path of the target + * @return the expected path, or {@code null} for no assertion + */ + public List getExpectedBreakpointContainerPath(List targetPath) { + return null; + } + + public abstract TargetBreakpointKindSet getExpectedSupportedKinds(); + + public abstract AddressRange getSuitableRangeForBreakpoint(TargetObject target, + TargetBreakpointKind kind) throws Throwable; + + public boolean isSupportsTogglableLocations() { + return false; + } + + public boolean isSupportsDeletableLocations() { + return false; + } + + @Test + public void testBreakpointContainerIsWhereExpected() throws Throwable { + m.build(); + + TargetObject target = obtainTarget(); + List expectedBreakpointContainerPath = + getExpectedBreakpointContainerPath(target.getPath()); + assumeNotNull(expectedBreakpointContainerPath); + TargetBreakpointSpecContainer container = + m.suitable(TargetBreakpointSpecContainer.class, target.getPath()); + assertEquals(expectedBreakpointContainerPath, container.getPath()); + } + + @Test + public void testBreakpointContainerSupportsExpectedKinds() throws Throwable { + m.build(); + + TargetObject target = obtainTarget(); + TargetBreakpointSpecContainer container = + m.suitable(TargetBreakpointSpecContainer.class, target.getPath()); + waitOn(container.fetchAttributes()); + assertEquals(getExpectedSupportedKinds(), container.getSupportedBreakpointKinds()); + } + + @Test + public void testBreakpointsSupportTogglableAsExpected() throws Throwable { + m.build(); + + for (TargetObjectSchema schema : m.getModel() + .getRootSchema() + .getContext() + .getAllSchemas()) { + Set> ifs = schema.getInterfaces(); + if (ifs.contains(TargetBreakpointLocation.class)) { + boolean supportsTogglableLocations = ifs.contains(TargetTogglable.class) && + !ifs.contains(TargetBreakpointSpec.class); + assertEquals(isSupportsTogglableLocations(), supportsTogglableLocations); + } + } + } + + @Test + public void testBreakpointLocationsSupportDeletableAsExpected() throws Throwable { + m.build(); + + for (TargetObjectSchema schema : m.getModel() + .getRootSchema() + .getContext() + .getAllSchemas()) { + Set> ifs = schema.getInterfaces(); + if (ifs.contains(TargetBreakpointLocation.class)) { + boolean supportsDeletableLocations = ifs.contains(TargetDeletable.class) && + !ifs.contains(TargetBreakpointSpec.class); + assertEquals(isSupportsDeletableLocations(), supportsDeletableLocations); + } + } + } + + protected TargetBreakpointLocation assertAtLeastOneLocCovers( + Collection locs, AddressRange range, + TargetBreakpointKind kind) throws Throwable { + for (TargetBreakpointLocation l : locs) { + TargetBreakpointSpec spec = l.getSpecification(); + if (spec == null) { // Mid construction? + continue; + } + if (l.getAddress() == null || l.getLength() == null) { + continue; + } + AddressRange actualRange = new AddressRangeImpl(l.getAddress(), l.getLength()); + if (!actualRange.contains(range.getMinAddress()) || + !actualRange.contains(range.getMaxAddress())) { + continue; + } + if (spec.getKinds() == null) { + continue; + } + if (!spec.getKinds().contains(kind)) { + continue; + } + return l; + } + fail("No location covers expected breakpoint"); + return null; + } + + protected void runTestPlaceBreakpoint(TargetBreakpointKind kind) throws Throwable { + assumeTrue(getExpectedSupportedKinds().contains(kind)); + m.build(); + + var monitor = new DebuggerModelListener() { + DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this); + + @Override + public void created(TargetObject object) { + if (!object.getJoinedPath(".").contains("reak")) { + return; + } + Msg.debug(this, "CREATED " + object.getJoinedPath(".")); + } + + protected String logDisp(Object val) { + if (val == null) { + return ""; // Should never happen + } + if (val instanceof TargetObject) { + TargetObject obj = (TargetObject) val; + return "obj-" + obj.getJoinedPath("."); + } + return val.toString(); + } + + @Override + public void attributesChanged(TargetObject object, Collection removed, + Map added) { + if (!object.getJoinedPath(".").contains("reak")) { + return; + } + Msg.debug(this, + "ATTRIBUTES: object=" + object.getJoinedPath(".") + ",removed=" + removed); + for (Entry ent : added.entrySet()) { + Msg.debug(this, + " ATTR_added: " + ent.getKey() + "=" + logDisp(ent.getValue())); + } + } + + @Override + public void elementsChanged(TargetObject object, Collection removed, + Map added) { + if (!object.getJoinedPath(".").contains("reak")) { + return; + } + Msg.debug(this, + "ELEMENTS: object=" + object.getJoinedPath(".") + ",removed=" + removed); + for (Entry ent : added.entrySet()) { + Msg.debug(this, + " ELEM_added: " + ent.getKey() + "=" + logDisp(ent.getValue())); + } + } + }; + m.getModel().addModelListener(monitor.reorderer, true); + + TargetObject target = obtainTarget(); + TargetBreakpointSpecContainer container = findBreakpointSpecContainer(target.getPath()); + AddressRange range = getSuitableRangeForBreakpoint(target, kind); + waitOn(container.placeBreakpoint(range, Set.of(kind))); + retryVoid(() -> { + Collection found = + m.findAll(TargetBreakpointLocation.class, target.getPath()).values(); + assertAtLeastOneLocCovers(found, range, kind); + }, List.of(AssertionError.class)); + } + + @Test + public void testPlaceSoftwareBreakpoint() throws Throwable { + runTestPlaceBreakpoint(TargetBreakpointKind.SW_EXECUTE); + } + + @Test + public void testPlaceHardwareBreakpoint() throws Throwable { + runTestPlaceBreakpoint(TargetBreakpointKind.HW_EXECUTE); + } + + @Test + public void testPlaceReadBreakpoint() throws Throwable { + runTestPlaceBreakpoint(TargetBreakpointKind.READ); + } + + @Test + public void testPlaceWriteBreakpoint() throws Throwable { + runTestPlaceBreakpoint(TargetBreakpointKind.WRITE); + } + + protected Set createLocations() throws Throwable { + // TODO: Test with multiple targets? + TargetObject target = obtainTarget(); + TargetBreakpointSpecContainer container = findBreakpointSpecContainer(target.getPath()); + assertNotNull("No breakpoint spec container", container); + Set locs = new HashSet<>(); + for (TargetBreakpointKind kind : getExpectedSupportedKinds()) { + AddressRange range = getSuitableRangeForBreakpoint(target, kind); + waitOn(container.placeBreakpoint(range, Set.of(kind))); + locs.add(retry(() -> { + Collection found = + m.findAll(TargetBreakpointLocation.class, target.getPath()).values(); + return assertAtLeastOneLocCovers(found, range, kind); + }, List.of(AssertionError.class))); + } + Msg.debug(this, "Have locations: " + + locs.stream().map(l -> l.getJoinedPath(".")).collect(Collectors.toSet())); + return locs; + } + + protected void runToggleTest(Set set) throws Throwable { + List order = new ArrayList<>(set); + Collections.shuffle(order); + // Disable each + for (TargetTogglable t : order) { + waitOn(t.disable()); + retryVoid(() -> { + assertFalse(t.isEnabled()); + }, List.of(AssertionError.class)); + } + // Repeat it for fun. Should have no effect + for (TargetTogglable t : order) { + waitOn(t.disable()); + retryVoid(() -> { + assertFalse(t.isEnabled()); + }, List.of(AssertionError.class)); + } + + // Enable each + for (TargetTogglable t : order) { + waitOn(t.enable()); + retryVoid(() -> { + assertTrue(t.isEnabled()); + }, List.of(AssertionError.class)); + } + // Repeat it for fun. Should have no effect + for (TargetTogglable t : order) { + waitOn(t.enable()); + retryVoid(() -> { + assertTrue(t.isEnabled()); + }, List.of(AssertionError.class)); + } + } + + @Test + public void testToggleBreakpoints() throws Throwable { + m.build(); + + Set locs = createLocations(); + runToggleTest(locs.stream() + .map(l -> l.getSpecification().as(TargetTogglable.class)) + .collect(Collectors.toSet())); + } + + @Test + public void testToggleBreakpointLocations() throws Throwable { + assumeTrue(isSupportsTogglableLocations()); + m.build(); + + Set locs = createLocations(); + runToggleTest( + locs.stream().map(l -> l.as(TargetTogglable.class)).collect(Collectors.toSet())); + } + + protected void runDeleteTest(Set set) throws Throwable { + List order = new ArrayList<>(set); + Collections.shuffle(order); + // Disable each + for (TargetDeletable d : order) { + waitOn(d.delete()); + retryVoid(() -> { + assertFalse(d.isValid()); + }, List.of(AssertionError.class)); + } + } + + @Test + public void testDeleteBreakpoints() throws Throwable { + m.build(); + + Set locs = createLocations(); + runDeleteTest(locs.stream() + .map(l -> l.getSpecification().as(TargetDeletable.class)) + .collect(Collectors.toSet())); + } + + @Test + public void testDeleteBreakpointLocations() throws Throwable { + assumeTrue(isSupportsDeletableLocations()); + m.build(); + + Set locs = createLocations(); + runDeleteTest( + locs.stream().map(l -> l.as(TargetDeletable.class)).collect(Collectors.toSet())); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelEnvironmentTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelEnvironmentTest.java new file mode 100644 index 0000000000..29c1eb021d --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelEnvironmentTest.java @@ -0,0 +1,28 @@ +/* ### + * 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.test; + +import java.util.Map; + +import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen; + +public abstract class AbstractDebuggerModelEnvironmentTest { + protected void doTestLaunchEnvironment(DebuggerTestSpecimen specimen, + Map expectedEnvironment) { + // TODO: Check that the environment is as expected before PROCESS_CREATED is emitted + // For models without event scope, before TargetProcess gets added. + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelFactoryTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelFactoryTest.java new file mode 100644 index 0000000000..53cb38ccb8 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelFactoryTest.java @@ -0,0 +1,93 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import ghidra.dbg.DebugModelConventions.AsyncAccess; +import ghidra.dbg.error.DebuggerModelTerminatingException; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.schema.EnumerableTargetObjectSchema; +import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.util.Msg; + +public abstract class AbstractDebuggerModelFactoryTest extends AbstractDebuggerModelTest { + + protected abstract Map getFailingFactoryOptions(); + + @Test + public void testBuildAndClose() throws Throwable { + m.build(); + assertNotNull(m.getModel()); + } + + @Test + public void testBuildFailingOptionsErr() throws Throwable { + for (Map.Entry bad : getFailingFactoryOptions().entrySet()) { + Map options = new HashMap<>(m.getFactoryOptions()); + options.put(bad.getKey(), bad.getValue()); + try { + m.buildModel(options); + fail(); + } + catch (Exception ex) { + if (!DebuggerModelTerminatingException.isIgnorable(ex)) { + throw ex; + } + // Pass + } + } + } + + @Test + public void testPing() throws Throwable { + m.build(); + waitOn(m.getModel().ping("Hello, Ghidra Async Debugging!")); + } + + @Test + public void testWaitRootAccess() throws Throwable { + m.build(); + + TargetObject root = m.getRoot(); + AsyncAccess access = access(root); + waitAcc(access); + } + + @Test + public void testHasNonEnumerableRootSchema() throws Throwable { + m.build(); + + TargetObjectSchema rootSchema = m.getModel().getRootSchema(); + Msg.info(this, rootSchema.getContext()); + assertFalse(rootSchema instanceof EnumerableTargetObjectSchema); + } + + @Test + public void testNonExistentPathGivesNull() throws Throwable { + m.build(); + + TargetObject root = m.getRoot(); + waitAcc(root); + TargetObject noExist = waitOn(root.fetchSuccessor(m.getBogusPath())); + assertNull(noExist); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelFocusTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelFocusTest.java new file mode 100644 index 0000000000..2e16ed0bbb --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelFocusTest.java @@ -0,0 +1,115 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +import java.util.List; +import java.util.Set; + +import org.junit.Test; + +import ghidra.dbg.target.TargetFocusScope; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.util.PathUtils; + +public abstract class AbstractDebuggerModelFocusTest extends AbstractDebuggerModelTest { + + /** + * Get (possibly generate) things for this focus test to try out + * + * @throws Throwable if anything goes wrong + */ + protected abstract Set getFocusableThings() throws Throwable; + + /** + * Governs whether assertions permit the actual object to be a successor of the expected object + * + * @return true to permit successors, false to require exact + */ + protected boolean permitSuccessor() { + return true; + } + + protected void assertSuccessorOrExact(TargetObject expected, TargetObject actual) { + assertNotNull(actual); + if (permitSuccessor()) { + assertTrue("Expected successor of '" + expected.getJoinedPath(".") + + "' got '" + actual.getJoinedPath(".") + "'", + PathUtils.isAncestor(expected.getPath(), actual.getPath())); + } + else { + assertSame(expected, actual); + } + } + + /** + * If the default focus is one of the focusable things (after generation), assert its path + * + * @return the path of the expected default focus, or {@code null} for no assertion + */ + protected List getExpectedDefaultFocus() { + return null; + } + + @Test + public void testDefaultFocusIsAsExpected() throws Throwable { + List expectedDefaultFocus = getExpectedDefaultFocus(); + assumeNotNull(expectedDefaultFocus); + m.build(); + + TargetFocusScope scope = findFocusScope(); + Set focusable = getFocusableThings(); + // The default must be one of the focusable objects + assertTrue(focusable.stream().anyMatch(f -> f.getPath().equals(expectedDefaultFocus))); + retryVoid(() -> { + assertEquals(expectedDefaultFocus, scope.getFocus().getPath()); + }, List.of(AssertionError.class)); + } + + @Test + public void testFocusEachOnce() throws Throwable { + m.build(); + + TargetFocusScope scope = findFocusScope(); + Set focusable = getFocusableThings(); + for (TargetObject obj : focusable) { + waitOn(scope.requestFocus(obj)); + retryVoid(() -> { + assertSuccessorOrExact(obj, scope.getFocus()); + }, List.of(AssertionError.class)); + } + } + + @Test + public void testFocusEachTwice() throws Throwable { + m.build(); + + TargetFocusScope scope = findFocusScope(); + Set focusable = getFocusableThings(); + for (TargetObject obj : focusable) { + waitOn(scope.requestFocus(obj)); + retryVoid(() -> { + assertSuccessorOrExact(obj, scope.getFocus()); + }, List.of(AssertionError.class)); + waitOn(scope.requestFocus(obj)); + retryVoid(() -> { + assertSuccessorOrExact(obj, scope.getFocus()); + }, List.of(AssertionError.class)); + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelInterpreterTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelInterpreterTest.java new file mode 100644 index 0000000000..6513117dbd --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelInterpreterTest.java @@ -0,0 +1,196 @@ +/* ### + * 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.test; + +import static ghidra.lifecycle.Unfinished.*; +import static org.junit.Assert.*; +import static org.junit.Assume.*; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Ignore; +import org.junit.Test; + +import ghidra.async.AsyncReference; +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.error.DebuggerModelTerminatingException; +import ghidra.dbg.target.TargetConsole.Channel; +import ghidra.dbg.target.TargetInterpreter; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.testutil.CatchOffThread; +import ghidra.util.Msg; + +public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebuggerModelTest + implements RequiresAttachSpecimen, RequiresLaunchSpecimen { + + public List getExpectedInterpreterPath() { + return null; + } + + protected abstract String getEchoCommand(String msg); + + protected abstract String getQuitCommand(); + + /** + * Get the CLI command to attach to the {@link #dummy} process + * + * @return the command + */ + protected abstract String getAttachCommand(); + + @Test + public void testInterpreterIsWhereExpected() throws Throwable { + List expectedInterpreterPath = getExpectedInterpreterPath(); + assumeNotNull(expectedInterpreterPath); + m.build(); + + TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of()); + assertEquals(expectedInterpreterPath, interpreter.getPath()); + } + + protected void runTestExecute(TargetInterpreter interpreter, String cmd) throws Throwable { + AsyncReference lastOut = new AsyncReference<>(); + DebuggerModelListener l = new DebuggerModelListener() { + @Override + public void consoleOutput(TargetObject interpreter, Channel channel, byte[] out) { + String str = new String(out); + Msg.debug(this, "Got " + channel + " output: " + str); + for (String line : str.split("\n")) { + lastOut.set(line.trim(), null); + } + } + }; + interpreter.addListener(l); + waitAcc(interpreter); + waitOn(interpreter.execute(cmd)); + waitOn(lastOut.waitValue("test")); + } + + @Test + public void testExecute() throws Throwable { + String cmd = getEchoCommand("test"); + assumeNotNull(cmd); + m.build(); + + TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of()); + runTestExecute(interpreter, cmd); + } + + protected void runTestExecuteCapture(TargetInterpreter interpreter, String cmd) + throws Throwable { + waitAcc(interpreter); + try (CatchOffThread off = new CatchOffThread()) { + DebuggerModelListener l = new DebuggerModelListener() { + @Override + public void consoleOutput(TargetObject interpreter, Channel channel, byte[] out) { + String str = new String(out); + Msg.debug(this, "Got " + channel + " output: " + str); + if (!str.contains("test")) { + return; + } + off.catching(() -> fail("Unexpected output:" + str)); + } + }; + interpreter.addListener(l); + waitAcc(interpreter); + String out = waitOn(interpreter.executeCapture(cmd)); + // Not the greatest, but allow extra lines + List lines = + Stream.of(out.split("\n")).map(s -> s.trim()).collect(Collectors.toList()); + assertTrue(lines.contains("test")); + } + } + + @Test + public void testExecuteCapture() throws Throwable { + String cmd = getEchoCommand("test"); + assumeNotNull(cmd); + m.build(); + + TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of()); + runTestExecuteCapture(interpreter, cmd); + } + + @Test(expected = DebuggerModelTerminatingException.class) + public void testExecuteQuit() throws Throwable { + String cmd = getQuitCommand(); + assumeNotNull(cmd); + m.build(); + + TargetInterpreter interpreter = m.find(TargetInterpreter.class, List.of()); + runTestExecute(interpreter, cmd); + } + + @Test + @Ignore + public void testFocusIsSynced() throws Throwable { + TODO(); + } + + @Test + @Ignore + public void testBreakpointsAreSynced() throws Throwable { + TODO(); + // TODO: Place different kinds + // TODO: Enable/disable + // TODO: Delete (spec vs. loc?) + } + + protected void runTestLaunchViaInterpreterShowsInProcessContainer(TargetInterpreter interpreter, + TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getLaunchSpecimen(); + assertNull(getProcessRunning(container, specimen, this)); + for (String line : specimen.getLaunchScript()) { + waitOn(interpreter.execute(line)); + } + retryForProcessRunning(container, specimen, this); + } + + @Test + public void testLaunchViaInterpreterShowsInProcessContainer() throws Throwable { + assumeTrue(m.hasProcessContainer()); + m.build(); + + TargetInterpreter interpreter = findInterpreter(); + TargetObject container = findProcessContainer(); + assertNotNull("No process container", container); + runTestLaunchViaInterpreterShowsInProcessContainer(interpreter, container); + } + + protected void runTestAttachViaInterpreterShowsInProcessContainer(TargetInterpreter interpreter, + TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assertNull(getProcessRunning(container, specimen, this)); + String cmd = getAttachCommand(); + waitOn(interpreter.execute(cmd)); + retryForProcessRunning(container, specimen, this); + } + + @Test + public void testAttachViaInterpreterShowsInProcessContainer() throws Throwable { + DebuggerTestSpecimen specimen = getAttachSpecimen(); + assumeTrue(m.hasProcessContainer()); + m.build(); + dummy = specimen.runDummy(); + + TargetInterpreter interpreter = findInterpreter(); + TargetObject container = findProcessContainer(); + assertNotNull("No process container", container); + runTestAttachViaInterpreterShowsInProcessContainer(interpreter, container); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelLauncherTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelLauncherTest.java new file mode 100644 index 0000000000..2665d96790 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelLauncherTest.java @@ -0,0 +1,224 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; +import static org.junit.Assume.assumeTrue; + +import java.lang.invoke.MethodHandles; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetMethod.TargetParameterMap; +import ghidra.dbg.testutil.ElementTrackingListener; + +public abstract class AbstractDebuggerModelLauncherTest extends AbstractDebuggerModelTest + implements RequiresLaunchSpecimen { + + public List getExpectedLauncherPath() { + return null; + } + + public List getExpectedProcessesContainerPath() { + return null; + } + + public abstract TargetParameterMap getExpectedLauncherParameters(); + + public abstract void assertEnvironment(TargetEnvironment environment); + + @Test + public void testLauncherIsWhereExpected() throws Throwable { + List expectedLauncherPath = getExpectedLauncherPath(); + assumeNotNull(expectedLauncherPath); + m.build(); + + TargetLauncher launcher = findLauncher(); + assertEquals(expectedLauncherPath, launcher.getPath()); + } + + @Test + public void testProcessContainerIsWhereExpected() throws Throwable { + List expectedProcessContainerPath = getExpectedProcessesContainerPath(); + assumeNotNull(expectedProcessContainerPath); + m.build(); + + TargetObject container = findProcessContainer(); + assertEquals(expectedProcessContainerPath, container.getPath()); + } + + protected void runTestLaunchParameters(TargetLauncher launcher, + TargetParameterMap expectedParameters) throws Throwable { + waitAcc(launcher); + waitOn(launcher.fetchAttributes()); + assertEquals(expectedParameters, launcher.getParameters()); + } + + @Test + public void testLaunchParameters() throws Throwable { + TargetParameterMap expectedParameters = getExpectedLauncherParameters(); + assumeNotNull(expectedParameters); + m.build(); + + TargetLauncher launcher = findLauncher(); + runTestLaunchParameters(launcher, expectedParameters); + } + + protected void runTestLaunch(TargetLauncher launcher) throws Throwable { + DebuggerTestSpecimen specimen = getLaunchSpecimen(); + waitAcc(launcher); + waitOn(launcher.launch(specimen.getLauncherArgs())); + } + + @Test + public void testLaunch() throws Throwable { + m.build(); + + var listener = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + CompletableFuture observedCreated = new CompletableFuture<>(); + + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + public void stateChanged(TargetObject object, TargetExecutionState state) { + // We're only expecting one process, so this should be fine + TargetProcess process = DebugModelConventions.liveProcessOrNull(object); + if (process == null) { + return; + } + try { + TargetEnvironment env = findEnvironment(process.getPath()); + assertEnvironment(env); + observedCreated.complete(null); + } + catch (Throwable e) { + observedCreated.completeExceptionally(e); + } + } + }; + // NB. I've intentionally omitted the reorderer here. The model should get it right. + m.getModel().addModelListener(listener); + + TargetLauncher launcher = findLauncher(); + runTestLaunch(launcher); + waitOn(listener.observedCreated); + } + + protected void runTestLaunchThenDetach(TargetLauncher launcher, + TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getLaunchSpecimen(); + assertNull(getProcessRunning(container, specimen, this)); + runTestLaunch(launcher); + runTestDetach(container, specimen); + } + + @Test + public void testLaunchThenDetach() throws Throwable { + assumeTrue(m.hasDetachableProcesses()); + m.build(); + + TargetLauncher launcher = findLauncher(); + TargetObject container = findProcessContainer(); + runTestLaunchThenDetach(launcher, container); + } + + protected void runTestLaunchThenKill(TargetLauncher launcher, + TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getLaunchSpecimen(); + assertNull(getProcessRunning(container, specimen, this)); + runTestLaunch(launcher); + runTestKill(container, specimen); + } + + @Test + public void testLaunchThenKill() throws Throwable { + assumeTrue(m.hasKillableProcesses()); + m.build(); + + TargetLauncher launcher = findLauncher(); + TargetObject container = findProcessContainer(); + runTestLaunchThenKill(launcher, container); + } + + protected void runTestLaunchThenResume(TargetLauncher launcher, + TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getLaunchSpecimen(); + assertNull(getProcessRunning(container, specimen, this)); + runTestLaunch(launcher); + runTestResumeTerminates(container, specimen); + } + + @Test + public void testLaunchThenResume() throws Throwable { + assumeTrue(m.hasKillableProcesses()); + m.build(); + + TargetLauncher launcher = findLauncher(); + TargetObject container = findProcessContainer(); + runTestLaunchThenResume(launcher, container); + } + + protected void runTestLaunchShowsInProcessContainer(TargetLauncher launcher, + TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getLaunchSpecimen(); + assertNull(getProcessRunning(container, specimen, this)); + runTestLaunch(launcher); + retryForProcessRunning(container, specimen, this); + } + + @Test + public void testLaunchShowsInProcessContainer() throws Throwable { + assumeTrue(m.hasProcessContainer()); + m.build(); + + TargetLauncher launcher = findLauncher(); + TargetObject container = findProcessContainer(); + runTestLaunchShowsInProcessContainer(launcher, container); + } + + protected void runTestLaunchShowsInProcessContainerViaListener( + TargetLauncher launcher, TargetObject container) throws Throwable { + DebuggerTestSpecimen specimen = getLaunchSpecimen(); + ElementTrackingListener procListener = + new ElementTrackingListener<>(TargetProcess.class); + container.addListener(procListener); + // NB. Have to express interest, otherwise model is not obligated to invoke listener + Collection procsBefore = fetchProcesses(container); + procListener.putAll(container.getCachedElements()); + assertNull(getProcessRunning(procsBefore, specimen, this)); + runTestLaunch(launcher); + retryVoid(() -> { + // Cannot fetch elements. rely only on listener. + assertNotNull(getProcessRunning(procListener.elements.values(), specimen, this)); + }, List.of(AssertionError.class)); + } + + @Test + public void testLaunchShowsInProcessContainerViaListener() throws Throwable { + assumeTrue(m.hasProcessContainer()); + m.build(); + + TargetLauncher launcher = findLauncher(); + TargetObject container = findProcessContainer(); + runTestLaunchShowsInProcessContainerViaListener(launcher, container); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelMultiprocessTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelMultiprocessTest.java new file mode 100644 index 0000000000..5c62e9d6a7 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelMultiprocessTest.java @@ -0,0 +1,32 @@ +/* ### + * 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.test; + +/** + * TODO: We need more tests to verify that commands affect the specified thing. + * Some models seem to just use the "current," which is usually correct + * in practice because of focus syncing, but it may not be, esp., if the user is + * scripting. In particular, when it comes to actions on processes and threads: + * + *

    + *
  • Process.kill
  • + *
  • Process.detach
  • + *
  • Thread.step
  • + *
+ */ +public class AbstractDebuggerModelMultiprocessTest { + +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelRegistersTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelRegistersTest.java new file mode 100644 index 0000000000..fbbbfd037b --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelRegistersTest.java @@ -0,0 +1,225 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +import java.math.BigInteger; +import java.util.*; +import java.util.Map.Entry; + +import org.junit.Test; + +import ghidra.dbg.target.*; +import ghidra.dbg.target.schema.TargetObjectSchema; + +/** + * Tests the functionality of a register bank + * + *

+ * Note that multiple sub-cases of this test can be generated to separate + * testing various types of registers. E.g., one for user registers, one for + * control registers, another for vector registers, etc. The model developer + * should be thorough, and decide how best to break the tests down, depending on + * the mechanism the model uses to read and/or write to each set. Even if + * different cases are not used, the total register set should exercise all the + * various register types. + */ +public abstract class AbstractDebuggerModelRegistersTest extends AbstractDebuggerModelTest + implements RequiresTarget { + + /** + * Get the expected (absolute) path of the target's (inner-most) register + * bank + * + * @param threadPath the path of the target (usually a thread) + * @return the expected path, or {@code null} for no assertion + */ + public List getExpectedRegisterBankPath(List threadPath) { + return null; + } + + /** + * This has been a popular convention, and may soon become required + * + *

+ * Background: Technically, the descriptions (register container) can be + * higher up the model tree, e.g., to apply to an entire process, rather + * than to specific threads. Of course, this might imply all threads have + * the same set of registers. That assumption seems intuitive, but on some + * platforms, e.g., dbgeng with WoW64, some threads may only have the 32-bit + * registers available. Even then, a process could present two register + * containers, one for the 64-bit and one for the 32-bit registers, + * assigning the bank's {@link TargetRegisterBank#getDescriptions()} + * attribute accordingly. + * + *

+ * However, none of that really matters if you choose the + * banks-are-containers convention. The primary motivation for doing this is + * to present register values as attributes in the tree. This makes them + * accessible from the "Objects" window of the UI, which is significant, + * because using the "Registers" window requires the target be recorded into + * a trace. Thus, if this test detects that the model's + * {@link TargetRegisterBank}s are also {@link TargetRegisterContainer}s, + * then this method must return true, and it will further verify the + * {@link TargetObject#getValue()} attribute of each register object + * correctly reports the same values as + * {@link TargetRegisterBank#readRegistersNamed(Collection)}. TODO: + * Currently the value is given as a string, encoding the value in base-16. + * I'd rather it were a byte array. + * + * @return true if the convention is expected and should be tested, false if + * not + */ + public boolean isRegisterBankAlsoContainer() { + return true; + } + + /** + * Get the values to write to the registers + * + *

+ * This collection is used for validation in other tests. The descriptions + * are validated to have lengths consistent with the written values, and the + * read values are expected to have lengths equal to the written values. + * + * @return the name-value map to write, and use for validation + */ + public abstract Map getRegisterWrites(); + + /** + * This various slightly from the usual find pattern, since we attempt to + * find any thread first + * + * @param seedPath the path to the target or thread + * @return the bank, or {@code null} if one cannot be uniquely identified in + * a thread, or the target + * @throws Throwable if anything goes wrong + */ + protected TargetRegisterBank findRegisterBank(List seedPath) throws Throwable { + return m.findWithIndex(TargetRegisterBank.class, "0", seedPath); + } + + @Test + public void testRegisterBankIsWhereExpected() throws Throwable { + m.build(); + + TargetObject target = maybeSubstituteThread(obtainTarget()); + List expectedRegisterBankPath = + getExpectedRegisterBankPath(target.getPath()); + assumeNotNull(expectedRegisterBankPath); + + TargetRegisterBank bank = findRegisterBank(target.getPath()); + assertEquals(expectedRegisterBankPath, bank.getPath()); + } + + @Test + public void testBanksAreContainersConventionIsAsExpected() throws Throwable { + m.build(); + + boolean banksAreContainers = true; + for (TargetObjectSchema schema : m.getModel() + .getRootSchema() + .getContext() + .getAllSchemas()) { + if (schema.getInterfaces().contains(TargetRegisterBank.class)) { + banksAreContainers &= + schema.getInterfaces().contains(TargetRegisterContainer.class); + } + } + assertEquals(isRegisterBankAlsoContainer(), banksAreContainers); + } + + @Test + public void testRegistersHaveExpectedSizes() throws Throwable { + m.build(); + + TargetObject target = maybeSubstituteThread(obtainTarget()); + TargetRegisterBank bank = m.findWithIndex(TargetRegisterBank.class, "0", target.getPath()); + TargetObject descriptions = bank.getDescriptions(); + for (Entry ent : getRegisterWrites().entrySet()) { + String regName = ent.getKey(); + TargetRegister reg = + m.findWithIndex(TargetRegister.class, regName, descriptions.getPath()); + assertEquals(ent.getValue().length, (reg.getBitLength() + 7) / 8); + } + } + + // TODO: Test cases for writing to non-existing registers (by name) + + @Test + public void testReadRegisters() throws Throwable { + m.build(); + + TargetObject target = maybeSubstituteThread(obtainTarget()); + TargetRegisterBank bank = m.findWithIndex(TargetRegisterBank.class, "0", target.getPath()); + Map exp = getRegisterWrites(); + Map read = waitOn(bank.readRegistersNamed(exp.keySet())); + assertEquals("Not all registers were read, or extras were read", exp.keySet(), + read.keySet()); + + // NB. The specimen is not expected to control the register values. Just validate lengths + for (String name : exp.keySet()) { + assertEquals(exp.get(name).length, read.get(name).length); + } + + if (!isRegisterBankAlsoContainer()) { + return; // pass + } + + for (String name : exp.keySet()) { + expectRegisterObjectValue(bank, name, read.get(name)); + } + } + + protected void expectRegisterObjectValue(TargetRegisterBank bank, String name, byte[] value) + throws Throwable { + retryVoid(() -> { + TargetRegister reg = m.findWithIndex(TargetRegister.class, name, bank.getPath()); + assertNotNull(reg); + String actualHex = (String) reg.getValue(); + assertNotNull(actualHex); + assertEquals(new BigInteger(1, value), new BigInteger(actualHex, 16)); + }, List.of(AssertionError.class)); + } + + @Test + public void testWriteRegisters() throws Throwable { + m.build(); + + TargetObject target = maybeSubstituteThread(obtainTarget()); + TargetRegisterBank bank = m.findWithIndex(TargetRegisterBank.class, "0", target.getPath()); + Map write = getRegisterWrites(); + waitOn(bank.writeRegistersNamed(write)); + // NB. This only really tests the cache, if applicable. A scenario checks for efficacy. + Map read = waitOn(bank.readRegistersNamed(write.keySet())); + assertEquals("Not all registers were read, or extras were read", write.keySet(), + read.keySet()); + + for (String name : write.keySet()) { + assertArrayEquals(write.get(name), read.get(name)); + } + + if (!isRegisterBankAlsoContainer()) { + return; // pass + } + + for (String name : write.keySet()) { + expectRegisterObjectValue(bank, name, read.get(name)); + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioCloneExitTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioCloneExitTest.java new file mode 100644 index 0000000000..663598f1c5 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioCloneExitTest.java @@ -0,0 +1,158 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; + +import java.lang.invoke.MethodHandles; +import java.util.*; + +import org.junit.Test; + +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AsyncState; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.util.Msg; + +/** + * A scenario which tests a single process with two threads + */ +public abstract class AbstractDebuggerModelScenarioCloneExitTest extends AbstractDebuggerModelTest { + /** + * Time to wait to observe the child from the clone before resuming again + */ + public static final long WAIT_FOR_CHILD_MS = 1000; + + /** + * This specimen must clone or similar, and then both parent and child must exit immediately + * + *

+ * They may optionally print information, but they cannot spin, sleep, or otherwise hang around. + * + * @return the specimen + */ + protected abstract DebuggerTestSpecimen getSpecimen(); + + /** + * Perform whatever preparation is necessary to ensure the child will remain attached and be + * trapped upon its being clone from its parent + * + *

+ * If this cannot be done without a handle to the parent process, override + * {@link #postLaunch(TargetObject, TargetProcess)} instead. + * + * @param launcher the launcher + * @throws Throwable if anything goes wrong + */ + protected void preLaunch(TargetLauncher launcher) throws Throwable { + } + + /** + * Perform whatever preparation is necessary to ensure the child will remain attached and be + * trapped upon its being cloned from its parent + * + *

+ * For most debuggers, no special setup is necessary. + * + * @param process the process trapped at launch -- typically at {@code main()}. + * @throws Throwable if anything goes wrong + */ + protected void postLaunch(TargetProcess process) throws Throwable { + } + + /** + * Get a breakpoint expression that will trap both threads post-clone + * + * @return the expression + */ + protected abstract String getBreakpointExpression(); + + /** + * Test the following scenario: + * + *

    + *
  1. Obtain a launcher and use it to start the specimen
  2. + *
  3. Place a breakpoint on the new process
  4. + *
  5. Resume the process until it is TERMINATED
  6. + *
  7. Verify exactly two unique threads were trapped by the breakpoint
  8. + *
+ * + *

+ * Note because some platforms, notably Windows, may produce additional threads before executing + * {@code main}, we cannot simply count THREAD_CREATED events. We mitigate this by using a + * breakpoint which should only trap threads executing user code. Note that we do not verify + * which thread is trapped first, since we do not control thread scheduling. + */ + @Test + public void testScenario() throws Throwable { + DebuggerTestSpecimen specimen = getSpecimen(); + m.build(); + + List trapped = new ArrayList<>(); + var monitor = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + // For model developer diagnostics + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + private void stateChanged(TargetObject obj, TargetExecutionState state) { + Msg.debug(this, obj.getJoinedPath(".") + " is now " + state); + } + + @Override + public void breakpointHit(TargetObject container, TargetObject thread, + TargetStackFrame frame, TargetBreakpointSpec spec, + TargetBreakpointLocation breakpoint) { + Msg.debug(this, thread.getJoinedPath(".") + " trapped by " + + breakpoint.getJoinedPath(".") + " (" + spec.getExpression() + ")"); + if (spec.getExpression().equals(getBreakpointExpression())) { + Msg.debug(this, " Counted"); + trapped.add(thread); + } + } + }; + m.getModel().addModelListener(monitor); + + TargetLauncher launcher = findLauncher(); + preLaunch(launcher); + Msg.debug(this, "Launching " + specimen); + waitOn(launcher.launch(specimen.getLauncherArgs())); + Msg.debug(this, " Done launching"); + TargetObject processContainer = findProcessContainer(); + assertNotNull("No process container", processContainer); + TargetProcess process = retryForProcessRunning(processContainer, specimen, this); + postLaunch(process); + TargetBreakpointSpecContainer breakpointContainer = + findBreakpointSpecContainer(process.getPath()); + Msg.debug(this, "Placing breakpoint"); + waitOn(breakpointContainer.placeBreakpoint(getBreakpointExpression(), + Set.of(TargetBreakpointKind.SW_EXECUTE))); + + assertTrue(DebugModelConventions.isProcessAlive(process)); + AsyncState state = + new AsyncState(m.suitable(TargetExecutionStateful.class, process.getPath())); + + for (int i = 1; DebugModelConventions.isProcessAlive(process); i++) { + Msg.debug(this, "(" + i + ") Resuming process until terminated"); + resume(process); + Msg.debug(this, " Done " + i); + waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING)); + } + + assertEquals(2, trapped.size()); + assertEquals(2, Set.copyOf(trapped).size()); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioForkExitTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioForkExitTest.java new file mode 100644 index 0000000000..340edcf20b --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioForkExitTest.java @@ -0,0 +1,258 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; + +import java.lang.invoke.MethodHandles; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AsyncState; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.util.Msg; + +/** + * A scenario which tests multiple processes -- a child forked from a parent. The debugger must + * become attached to both. If the debugger does not support attaching to the child, while remaining + * attached to the parent as well, then this scenario cannot be applied to test it. + */ +public abstract class AbstractDebuggerModelScenarioForkExitTest extends AbstractDebuggerModelTest { + /** + * Time to wait to observe the child from the fork before resuming again + */ + public static final long WAIT_FOR_CHILD_MS = 1000; + + /** + * This specimen must fork or similar, and then both parent and child must exit immediately + * + *

+ * They may optionally print information, but they cannot spin, sleep, or otherwise hang around. + * For platforms without {@code fork()}, e.g., Windows, the nearest equivalent behavior should + * be performed, e.g., roughly {@code CreateProcess("SameSpecimen.exe /child")}. + * + * @return the specimen + */ + protected abstract DebuggerTestSpecimen getSpecimen(); + + /** + * Perform whatever preparation is necessary to ensure the child will remain attached and be + * trapped upon its being forked from its parent + * + *

+ * If this cannot be done without a handle to the parent process, override + * {@link #postLaunch(TargetObject, TargetProcess)} instead. + * + * @param launcher the launcher + * @throws Throwable if anything goes wrong + */ + protected void preLaunch(TargetLauncher launcher) throws Throwable { + } + + /** + * Perform whatever preparation is necessary to ensure the child will remain attached and be + * trapped upon its being forked from its parent + * + * @param parentProcess the parent process + * @throws Throwable if anything goes wrong + */ + protected void postLaunch(TargetProcess parentProcess) throws Throwable { + } + + protected void postFork(TargetProcess parentProcess, TargetProcess childProcess) + throws Throwable { + } + + /** + * Get a breakpoint expression that will trap the parent post-fork + * + *

+ * Ideally, this same expression will trap the child post-fork as well. Note this test presumes + * the child will be trapped by the debugger upon fork. See + * {@link #getChildBreakpointExpression()} + * + * @return the expression + */ + protected abstract String getParentBreakpointExpression(); + + /** + * Get a breakpoint expression that will trap the child post-fork + * + *

+ * If breakpoints are not passed from parent to child, the test will need to set a breakpoint to + * trap the child. Override this with a suitable expression, if needed. + * + * @return the expression + */ + protected String getChildBreakpointExpression() { + return null; + } + + /** + * This is invoked for both the launch of the specimen and upon fork + * + *

+ * Because one is forked from the other, we should expect to see the same environment attributes + * for both processes. That said, if a tester does need to distinguish, and please + * think carefully about whether or not you should, you can examine the environment's path to + * determine which process it applies to. + * + * @param environment the environment at the time the process became alive + */ + public abstract void assertEnvironment(TargetEnvironment environment); + + /** + * Test the following scenario: + * + *

    + *
  1. Obtain a launcher and use it to start the specimen
  2. + *
  3. Place a breakpoint on the new (parent) process
  4. + *
  5. Resume the process until the fork is observed, generating the child process
  6. + *
  7. Verify both processes are ALIVE
  8. + *
  9. Resume the parent process until it is TERMINATED
  10. + *
  11. Verify the child process is still ALIVE
  12. + *
  13. Resume the child process until it is TERMIANTED
  14. + *
+ */ + @Test + public void testScenario() throws Throwable { + DebuggerTestSpecimen specimen = getSpecimen(); + m.build(); + + var stateMonitor = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + Set observed = new HashSet<>(); + CompletableFuture observedParent = new CompletableFuture<>(); + CompletableFuture observedChild = new CompletableFuture<>(); + List> futures = + List.of(observedParent, observedChild); + + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + private void stateChanged(TargetObject obj, TargetExecutionState state) { + Msg.debug(this, "STATE: " + obj.getJoinedPath(".") + " is now " + state); + + TargetProcess process = DebugModelConventions.liveProcessOrNull(obj); + if (process == null) { + return; + } + + CompletableFuture f = futures.get(observed.size()); + if (observed.add(process)) { + try { + TargetEnvironment env = findEnvironment(process.getPath()); + assertEnvironment(env); + f.complete(process); + } + catch (Throwable e) { + f.completeExceptionally(e); + } + } + } + + @Override + public void event(TargetObject object, TargetThread eventThread, + TargetEventType type, String description, List parameters) { + Msg.debug(this, "EVENT: " + object.getJoinedPath(".") + " emitted " + type + + "(desc=" + description + ",params=" + parameters + ")"); + } + }; + m.getModel().addModelListener(stateMonitor); + + TargetLauncher launcher = findLauncher(); + preLaunch(launcher); + Msg.debug(this, "Launching " + specimen); + waitOn(launcher.launch(specimen.getLauncherArgs())); + Msg.debug(this, " Done launching"); + TargetObject processContainer = findProcessContainer(); + assertNotNull("No process container", processContainer); + TargetProcess parentProcess = waitOn(stateMonitor.observedParent); + Msg.debug(this, "Parent is " + parentProcess.getJoinedPath(".")); + postLaunch(parentProcess); + + AsyncState parentState = + new AsyncState(m.suitable(TargetExecutionStateful.class, parentProcess.getPath())); + waitOn(parentState.waitValue(TargetExecutionState.STOPPED)); + + placeBreakpoint("parent", parentProcess, getParentBreakpointExpression()); + + TargetProcess childProcess = null; + for (int i = 1; childProcess == null; i++) { + Msg.debug(this, "(" + i + ") Resuming until fork"); + resume(parentProcess); + Msg.debug(this, " Done " + i); + waitAcc(access(parentProcess)); + try { + childProcess = retryForOtherProcessRunning(processContainer, specimen, this, + p -> p != parentProcess, WAIT_FOR_CHILD_MS); + } + catch (AssertionError e) { + // Try resuming again + } + } + Msg.debug(this, "Child is " + childProcess.getJoinedPath(".")); + assertNotSame(parentProcess, childProcess); + assertNotEquals(parentProcess, childProcess); + assertSame(childProcess, waitOn(stateMonitor.observedChild)); + + assertTrue(DebugModelConventions.isProcessAlive(parentProcess)); + AsyncState childState = + new AsyncState(m.suitable(TargetExecutionStateful.class, childProcess.getPath())); + waitOn(parentState.waitUntil(s -> s == TargetExecutionState.STOPPED)); + postFork(parentProcess, childProcess); + + placeChildBreakpoint(childProcess); + + for (int i = 1; DebugModelConventions.isProcessAlive(parentProcess); i++) { + Msg.debug(this, "(" + i + ") Resuming parent until terminated"); + resume(parentProcess); + Msg.debug(this, " Done " + i); + TargetExecutionState state = + waitOn(parentState.waitUntil(s -> s != TargetExecutionState.RUNNING)); + Msg.debug(this, "Parent state after resume-wait-not-running: " + state); + Msg.debug(this, " And Child: " + childState.get()); + } + + assertTrue(DebugModelConventions.isProcessAlive(childProcess)); + waitOn(childState.waitUntil(s -> s == TargetExecutionState.STOPPED)); + for (int i = 1; DebugModelConventions.isProcessAlive(childProcess); i++) { + Msg.debug(this, "Resuming child until terminated"); + resume(childProcess); + Msg.debug(this, " Done " + i); + waitOn(childState.waitUntil(s -> s != TargetExecutionState.RUNNING)); + } + } + + protected void placeBreakpoint(String who, TargetProcess process, String expression) + throws Throwable { + TargetBreakpointSpecContainer container = + findBreakpointSpecContainer(process.getPath()); + Msg.debug(this, "Placing breakpoint (on " + who + ")"); + waitOn(container.placeBreakpoint(expression, Set.of(TargetBreakpointKind.SW_EXECUTE))); + } + + protected void placeChildBreakpoint(TargetProcess childProcess) throws Throwable { + String expression = getChildBreakpointExpression(); + if (expression != null) { + placeBreakpoint("child", childProcess, expression); + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioMemoryTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioMemoryTest.java new file mode 100644 index 0000000000..4199ab289c --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioMemoryTest.java @@ -0,0 +1,172 @@ +/* ### + * 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.test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; + +import java.lang.invoke.MethodHandles; +import java.util.Objects; + +import org.junit.Assert; +import org.junit.Test; + +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AsyncState; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.program.model.address.Address; +import ghidra.util.Msg; + +/** + * A scenario that verifies memory writes affect the target + */ +public abstract class AbstractDebuggerModelScenarioMemoryTest extends AbstractDebuggerModelTest { + + /** + * This specimen must perform some observable action, which can be affected by a memory write + * + *

+ * A common example is to exit with a code read from memory. It may also be useful for debugging + * purposes to print a message from the same memory. + * + * @return the specimen + */ + protected abstract DebuggerTestSpecimen getSpecimen(); + + /** + * Get the destination address to write to + * + *

+ * The most reliable way to do this is to ensure an easily identifiable symbol for the + * destination address is exported, then use the debugging API to obtain its address. + * + * @param process the process running the specimen + * @return the destination address + * @throws Throwable if anything goes wrong + */ + protected abstract Address getAddressToWrite(TargetProcess process) throws Throwable; + + /** + * Get the bytes to write + * + *

+ * It's probably best to use a string encoded in the platform's preferred format. + * + * @return the bytes + */ + protected abstract byte[] getBytesToWrite(); + + /** + * Get the expected bytes after read + * + *

+ * This should be the same as {@link #getBytesToWrite()}, but preferably includes some + * additional bytes after, so that memory reads can be verified to come from the actual target, + * and not just from a cached write. A common scenario is to partially write over a string, then + * read the entire string, verifying both the overwritten part and the remainder. + * + * @return the bytes + */ + protected abstract byte[] getExpectedBytes(); + + /** + * Perform whatever preparation is necessary to observe the expected effect + * + * @param process the process trapped at launch -- typically at {@code main()}. + * @throws Throwable if anything goes wrong + */ + protected void postLaunch(TargetProcess process) throws Throwable { + } + + /** + * Verify, using {@link Assert}, that the target exhibited the effect of the memory write + * + *

+ * Note that the given process may be invalid, depending on the model's implementation. The + * tester should know how the model under test behaves. If the object is invalid, it's possible + * its attributes were updated immediately preceding invalidation with observable information, + * but this is usually not the case. The better approach is to devise an effect that can be + * observed in an event callback. To install such a listener, override + * {@link #postLaunch(TargetProcess)} and record the relevant information to be validated here. + * Do not place assertions in the event callback, since the failures they could produce will not + * be recorded as test failures. If the effect can be observed in multiple ways, it is best to + * verify all of them. + * + * @param process the target process, which may no longer be valid + * @throws Throwable if anything goes wrong or an assertion fails + */ + protected abstract void verifyExpectedEffect(TargetProcess process) throws Throwable; + + /** + * Test the following scenario: + * + *

    + *
  1. Obtain a launcher and use it to start the specimen
  2. + *
  3. Overwrite bytes at a designated address in memory
  4. + *
  5. Read those bytes and verify they were modified
  6. + *
  7. Resume the process until it is TERMINATED
  8. + *
  9. Verify some effect, usually the exit code
  10. + *
+ */ + @Test + public void testScenario() throws Throwable { + DebuggerTestSpecimen specimen = getSpecimen(); + m.build(); + + // For model developer diagnostics + var stateMonitor = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + private void stateChanged(TargetObject obj, TargetExecutionState state) { + Msg.debug(this, obj.getJoinedPath(".") + " is now " + state); + } + }; + m.getModel().addModelListener(stateMonitor); + + TargetLauncher launcher = findLauncher(); + Msg.debug(this, "Launching " + specimen); + waitOn(launcher.launch(specimen.getLauncherArgs())); + Msg.debug(this, " Done launching"); + TargetObject processContainer = findProcessContainer(); + TargetProcess process = retryForProcessRunning(processContainer, specimen, this); + postLaunch(process); + + Address address = Objects.requireNonNull(getAddressToWrite(process)); + byte[] data = Objects.requireNonNull(getBytesToWrite()); + TargetMemory memory = Objects.requireNonNull(findMemory(process.getPath())); + Msg.debug(this, "Writing memory"); + waitOn(memory.writeMemory(address, data)); + Msg.debug(this, " Done"); + byte[] expected = getExpectedBytes(); + byte[] read = waitOn(memory.readMemory(address, expected.length)); + assertArrayEquals(expected, read); + + assertTrue(DebugModelConventions.isProcessAlive(process)); + AsyncState state = + new AsyncState(m.suitable(TargetExecutionStateful.class, process.getPath())); + + for (int i = 1; DebugModelConventions.isProcessAlive(process); i++) { + Msg.debug(this, "(" + i + ") Resuming process until terminated"); + resume(process); + Msg.debug(this, " Done " + i); + waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING)); + Msg.debug(this, "Parent state after resume-wait-not-running: " + state); + } + + verifyExpectedEffect(process); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioRegistersTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioRegistersTest.java new file mode 100644 index 0000000000..83e158a857 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioRegistersTest.java @@ -0,0 +1,189 @@ +/* ### + * 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.test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.lang.invoke.MethodHandles; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import org.junit.Assert; +import org.junit.Test; + +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AsyncState; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.util.Msg; + +public abstract class AbstractDebuggerModelScenarioRegistersTest extends AbstractDebuggerModelTest { + + /** + * This specimen must have an observable behavior which can be effected by writing a register + * + *

+ * The simplest is probably to exit with a code from a known register. This can probably be best + * accomplished by having main call {@code exit(some_func(0));} where {@code some_func} has a + * known calling convention and simply returns its parameter. This way, the test can break on + * {@code some_func} and write to the register for that first parameter. On architectures where + * the standard calling convention passes parameters via memory, you may be able to select an + * alternative that uses registers, or you may have to use inline/pure assembly. + * + * @return the specimen + */ + protected abstract DebuggerTestSpecimen getSpecimen(); + + /** + * Perform any work needed after the specimen has been launched + * + * @param process the process running the specimen + * @throws Throwable if anything goes wrong + */ + protected void postLaunch(TargetProcess process) throws Throwable { + } + + /** + * Get the expression to break when the register write should be made + * + *

+ * More than likely, this should just be the symbol for that function. + * + * @return the expression + */ + protected abstract String getBreakpointExpression(); + + /** + * Get the registers and values to write to achieve the desired effect + * + * @return the name-value map of registers to write + */ + protected abstract Map getRegisterWrites(); + + /** + * Verify, using {@link Assert}, that the target exhibited the effect of the register write + * + *

+ * Note that the given process may be invalid, depending on the model's implementation. The + * tester should know how the model under test behaves. If the object is invalid, it's possible + * its attributes were updated immediately preceding invalidation with observable information, + * but this is usually not the case. The better approach is to devise an effect that can be + * observed in an event callback. To install such a listener, override + * {@link #postLaunch(TargetProcess)} and record the relevant information to be validated here. + * Do not place assertions in the event callback, since the failures they could produce will not + * be recorded as test failures. If the effect can be observed in multiple ways, it is best to + * verify all of them. + * + * @param process the target process, which may no longer be valid + * @throws Throwable if anything goes wrong or an assertion fails + */ + protected abstract void verifyExpectedEffect(TargetProcess process) throws Throwable; + + /** + * Test the following scenario + * + *

    + *
  1. Obtain a launcher and use it to start the specimen
  2. + *
  3. Place a breakpoint
  4. + *
  5. Continue until a thread hits the breakpoint
  6. + *
  7. Write that thread's registers
  8. + *
  9. Resume the process until it is TERMINATED
  10. + *
  11. Verify some effect, usually the exit code
  12. + *
+ */ + @Test + public void testScenario() throws Throwable { + DebuggerTestSpecimen specimen = getSpecimen(); + m.build(); + + // For model developer diagnostics + var bpMonitor = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + CompletableFuture trapped = new CompletableFuture<>(); + + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + private void stateChanged(TargetObject obj, TargetExecutionState state) { + Msg.debug(this, obj.getJoinedPath(".") + " is now " + state); + } + + @Override + public void breakpointHit(TargetObject container, TargetObject trapped, + TargetStackFrame frame, TargetBreakpointSpec spec, + TargetBreakpointLocation breakpoint) { + Msg.debug(this, "TRAPPED by " + spec); + if (getBreakpointExpression().equals(spec.getExpression())) { + this.trapped.complete(trapped); + Msg.debug(this, " Counted"); + } + } + }; + m.getModel().addModelListener(bpMonitor); + + TargetLauncher launcher = findLauncher(); + Msg.debug(this, "Launching " + specimen); + waitOn(launcher.launch(specimen.getLauncherArgs())); + Msg.debug(this, " Done launching"); + TargetObject processContainer = findProcessContainer(); + assertNotNull(processContainer); + TargetProcess process = retryForProcessRunning(processContainer, specimen, this); + postLaunch(process); + + TargetBreakpointSpecContainer breakpointContainer = + findBreakpointSpecContainer(process.getPath()); + Msg.debug(this, "Placing breakpoint"); + waitOn(breakpointContainer.placeBreakpoint(getBreakpointExpression(), + Set.of(TargetBreakpointKind.SW_EXECUTE))); + + assertTrue(DebugModelConventions.isProcessAlive(process)); + AsyncState state = + new AsyncState(m.suitable(TargetExecutionStateful.class, process.getPath())); + + /** + * NB. If an assert(isAlive) is failing, check that breakpointHit() is emitted before + * attributeChanged(state=STOPPED) + */ + for (int i = 1; !bpMonitor.trapped.isDone(); i++) { + assertTrue(state.get().isAlive()); + Msg.debug(this, "(" + i + ") Resuming process until breakpoint hit"); + resume(process); + Msg.debug(this, " Done " + i); + waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING)); + } + assertTrue(state.get().isAlive()); + TargetObject target = waitOn(bpMonitor.trapped); + + Map toWrite = getRegisterWrites(); + TargetRegisterBank bank = Objects + .requireNonNull(m.findWithIndex(TargetRegisterBank.class, "0", target.getPath())); + Msg.debug(this, "Writing registers: " + toWrite.keySet()); + waitOn(bank.writeRegistersNamed(toWrite)); + Msg.debug(this, " Done"); + + assertTrue(DebugModelConventions.isProcessAlive(process)); + + for (int i = 1; DebugModelConventions.isProcessAlive(process); i++) { + Msg.debug(this, "(" + i + ") Resuming process until terminated"); + resume(process); + Msg.debug(this, " Done " + i); + waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING)); + Msg.debug(this, "Parent state after resume-wait-not-running: " + state); + } + + verifyExpectedEffect(process); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioStackTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioStackTest.java new file mode 100644 index 0000000000..e115213742 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelScenarioStackTest.java @@ -0,0 +1,176 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; + +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.Set; + +import org.junit.Test; + +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AsyncState; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.util.PathMatcher; +import ghidra.dbg.util.PathPattern; +import ghidra.program.model.address.Address; +import ghidra.util.Msg; + +public abstract class AbstractDebuggerModelScenarioStackTest extends AbstractDebuggerModelTest { + + /** + * This specimen must create a stack easily recognizable by examination of 4 frames' PCs + * + *

+ * This is accomplished by writing 4 functions where each calls the next, the innermost + * function's symbol providing an easily-placed breakpoint. When the breakpoint is hit, frame 0 + * should be at the entry of the innermost function, and the pc for each frame after that should + * be within the body of its respective following function. + * + * @return the specimen + */ + protected abstract DebuggerTestSpecimen getSpecimen(); + + /** + * Perform any work needed after the specimen has been launched + * + * @param process the process running the specimen + * @throws Throwable if anything goes wrong + */ + protected void postLaunch(TargetProcess process) throws Throwable { + } + + /** + * Get the expression to break at the innermost recognizable function + * + *

+ * More than likely, this should just be the symbol for that function. + * + * @return the expression + */ + protected abstract String getBreakpointExpression(); + + /** + * Examine the address of the given frame and verify it is where expected + * + *

+ * Note if this validation needs access to the process, it should at least record where that + * process is by overriding {@link #postLaunch(TargetProcess)}. Ideally, it can perform all of + * the necessary lookups, e.g., to record symbol values, there instead of here. + * + * @param index the index + * @param pc the program counter + */ + protected abstract void validateFramePC(int index, Address pc); + + /** + * Test the following scenario: + * + *

    + *
  1. Obtain a launcher and use it to start the specimen
  2. + *
  3. Place the breakpoint on the new process
  4. + *
  5. Resume the process until the breakpoint is hit
  6. + *
  7. Read the stack and verify the PC for each frame
  8. + *
  9. Resume the process until it is TERMINATED
  10. + *
+ */ + @Test + public void testScenario() throws Throwable { + DebuggerTestSpecimen specimen = getSpecimen(); + m.build(); + + var bpMonitor = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + boolean hit = false; + + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + private void stateChanged(TargetObject object, TargetExecutionState state) { + Msg.debug(this, "STATE " + object.getJoinedPath(".") + " is now " + state); + } + + @Override + public void breakpointHit(TargetObject container, TargetObject trapped, + TargetStackFrame frame, TargetBreakpointSpec spec, + TargetBreakpointLocation breakpoint) { + Msg.debug(this, "TRAPPED by " + spec); + if (getBreakpointExpression().equals(spec.getExpression())) { + hit = true; + Msg.debug(this, " Counted"); + } + } + }; + m.getModel().addModelListener(bpMonitor); + + Msg.debug(this, "Launching " + specimen); + TargetLauncher launcher = findLauncher(); + waitOn(launcher.launch(specimen.getLauncherArgs())); + Msg.debug(this, " Done launching"); + TargetObject processContainer = findProcessContainer(); + TargetProcess process = retryForProcessRunning(processContainer, specimen, this); + postLaunch(process); + + TargetBreakpointSpecContainer breakpointContainer = + findBreakpointSpecContainer(process.getPath()); + Msg.debug(this, "Placing breakpoint"); + waitOn(breakpointContainer.placeBreakpoint(getBreakpointExpression(), + Set.of(TargetBreakpointKind.SW_EXECUTE))); + + assertTrue(DebugModelConventions.isProcessAlive(process)); + AsyncState state = + new AsyncState(m.suitable(TargetExecutionStateful.class, process.getPath())); + + /** + * NB. If an assert(isAlive) is failing, check that breakpointHit() is emitted before + * attributeChanged(state=STOPPED) + */ + for (int i = 1; !bpMonitor.hit; i++) { + assertTrue(state.get().isAlive()); + Msg.debug(this, "(" + i + ") Resuming process until breakpoint hit"); + resume(process); + Msg.debug(this, " Done " + i); + waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING)); + } + assertTrue(state.get().isAlive()); + + TargetStack stack = findStack(process.getPath()); + PathMatcher matcher = stack.getSchema().searchFor(TargetStackFrame.class, true); + PathPattern pattern = matcher.getSingletonPattern(); + assertNotNull("Frames are not clearly indexable", pattern); + assertEquals("Frames are not clearly indexable", 1, pattern.countWildcards()); + // Sort by path should present them innermost to outermost + List frames = retry(() -> { + List result = + List.copyOf(m.findAll(TargetStackFrame.class, stack.getPath()).values()); + assertTrue("Fewer than 4 frames", result.size() > 4); + return result; + }, List.of(AssertionError.class)); + for (int i = 0; i < 4; i++) { + TargetStackFrame f = frames.get(i); + validateFramePC(i, f.getProgramCounter()); + } + + for (int i = 1; DebugModelConventions.isProcessAlive(process); i++) { + Msg.debug(this, "(" + i + ") Resuming process until terminated"); + resume(process); + Msg.debug(this, " Done " + i); + waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING)); + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelSteppableTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelSteppableTest.java new file mode 100644 index 0000000000..e2c5240e0f --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelSteppableTest.java @@ -0,0 +1,231 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import ghidra.async.AsyncDebouncer; +import ghidra.async.AsyncTimer; +import ghidra.dbg.DebugModelConventions.AsyncState; +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; + +/** + * Tests the functionality of a single-stepping a target + * + *

+ * Note that multiple sub-cases of this test can be generated in order to test each steppable object + * in the model. Take care when selecting which object (usually a thread) to put under test. If, for + * example, the selected thread is performing a blocking system call, then the tests will almost + * certainly time out. + */ +public abstract class AbstractDebuggerModelSteppableTest extends AbstractDebuggerModelTest + implements RequiresTarget { + + /** + * Get the expected (absolute) path of the steppable under test + * + * @param threadPath the path of the target (usually a thread) + * @return the expected path, or {@code null} for no assertion + */ + public List getExpectedSteppablePath(List threadPath) { + return null; + } + + @Test + public void testSteppableIsWhereExpected() throws Throwable { + m.build(); + + TargetObject target = maybeSubstituteThread(obtainTarget()); + List expectedSteppablePath = getExpectedSteppablePath(target.getPath()); + assumeNotNull(expectedSteppablePath); + + TargetSteppable steppable = findSteppable(target.getPath()); + assertEquals(expectedSteppablePath, steppable.getPath()); + } + + /** + * An arbitrary number + * + *

+ * Should be enough to prove that stepping works consistently, but not so high that the test + * drags on. Definitely 2 or greater :) + * + * @return the number of steps in the test + */ + protected int getStepCount() { + return 5; + } + + /** + * This just steps the target some number of times and verifies the execution state between + * + * @throws Throwable if anything goes wrong + */ + @Test + public void testStep() throws Throwable { + m.build(); + + TargetObject target = maybeSubstituteThread(obtainTarget()); + AsyncState state = + new AsyncState(m.suitable(TargetExecutionStateful.class, target.getPath())); + TargetSteppable steppable = findSteppable(target.getPath()); + for (int i = 0; i < getStepCount(); i++) { + waitOn(steppable.step()); + TargetExecutionState st = + waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING)); + assertTrue("Target terminated while stepping", st.isAlive()); + } + } + + // TODO: Test other kinds of steps + // TODO: Test expected step kinds (or expected parameters) + // TODO: Once there, use the generic method invocation style + + /** + * The window of silence necessary to assume no more callbacks will occur + * + * @return the window in milliseconds + */ + protected long getDebounceWindowMs() { + return 1000; + } + + enum CallbackType { + EVENT_RUNNING, + EVENT_STOPPED, + REGS_UPDATED, + CACHE_INVALIDATED; + } + + /** + * Test event order for single stepping + * + *

+ * This tests that the {@link DebuggerModelListener#registersUpdated(TargetObject, Map)} + * callback occurs "last" following a step. While other callbacks may intervene, the order ought + * to be {@code event(RUNNING)}, {@code event(STOPPED)}, {@code registersUpdated()}. An + * {@code event()} cannot follow, as that would cause the snap to advance, making registers + * appear stale . Worse, if {@code registersUpdated} precedes {@code STOPPED}, the recorder will + * write values into the snap previous to the one it ought. This principle applies to all + * {@code event()}s, but is easiest to test for single-stepping. We also check that the + * registers (cached) are not invalidated after they are updated for the step. Note that + * {@code STOPPED} can be substituted for any event which implies the target is stopped. + * + * @throws Throwable if anything goes wrong + */ + @Test + public void testStepEventOrder() throws Throwable { + m.build(); + + var listener = new DebuggerModelListener() { + List callbacks = new ArrayList<>(); + List log = new ArrayList<>(); + AsyncDebouncer debouncer = + new AsyncDebouncer(AsyncTimer.DEFAULT_TIMER, getDebounceWindowMs()); + + @Override + public void event(TargetObject object, TargetThread eventThread, TargetEventType type, + String description, List parameters) { + synchronized (callbacks) { + if (type == TargetEventType.RUNNING) { + callbacks.add(CallbackType.EVENT_RUNNING); + } + else if (type.impliesStop) { + callbacks.add(CallbackType.EVENT_STOPPED); + } + log.add("event(" + type + "): " + description); + } + debouncer.contact(null); + } + + @Override + public void registersUpdated(TargetObject bank, Map updates) { + synchronized (callbacks) { + callbacks.add(CallbackType.REGS_UPDATED); + log.add("registersUpdated()"); + } + debouncer.contact(null); + } + + @Override + public void invalidateCacheRequested(TargetObject object) { + synchronized (callbacks) { + callbacks.add(CallbackType.CACHE_INVALIDATED); + log.add("invalidateCacheRequested()"); + } + debouncer.contact(null); + } + + @Override + public void attributesChanged(TargetObject object, Collection removed, + Map added) { + debouncer.contact(null); + } + + @Override + public void elementsChanged(TargetObject object, Collection removed, + Map added) { + debouncer.contact(null); + } + }; + m.getModel().addModelListener(listener); + + CompletableFuture settledBefore = listener.debouncer.settled(); + TargetObject target = maybeSubstituteThread(obtainTarget()); + AsyncState state = + new AsyncState(m.suitable(TargetExecutionStateful.class, target.getPath())); + TargetSteppable steppable = findSteppable(target.getPath()); + waitOnNoValidate(settledBefore); + synchronized (listener.callbacks) { + listener.callbacks.clear(); + listener.log.add("CLEARED callbacks"); + } + + CompletableFuture settledAfter = listener.debouncer.settled(); + waitOn(steppable.step()); + TargetExecutionState st = + waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING)); + assertTrue("Target terminated while stepping", st.isAlive()); + waitOnNoValidate(settledAfter); + + List callbacks; + synchronized (listener.callbacks) { + callbacks = List.copyOf(listener.callbacks); + } + + int stoppedIdx = callbacks.indexOf(CallbackType.EVENT_STOPPED); + assertNotEquals(-1, stoppedIdx); + List follows = callbacks.subList(stoppedIdx + 1, callbacks.size()); + assertFalse("Observed multiple event(STOPPED/OTHER) callbacks for one step", + follows.contains(CallbackType.EVENT_STOPPED)); + int regsUpdatedIdx = callbacks.indexOf(CallbackType.REGS_UPDATED); + assertNotEquals("Did not observe a registersUpdated() callback", -1, regsUpdatedIdx); + assertTrue("registersUpdated() must follow event(STOPPED/OTHER)", + regsUpdatedIdx > stoppedIdx); + int invalidatedIdx = follows.indexOf(CallbackType.CACHE_INVALIDATED); + assertTrue("Observed an invalidateCacheRequest() after registersUpdated()", + invalidatedIdx < regsUpdatedIdx); // absent or precedes + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelTest.java new file mode 100644 index 0000000000..3286281d06 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractDebuggerModelTest.java @@ -0,0 +1,345 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import org.junit.After; +import org.junit.Before; + +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AsyncState; +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.testutil.*; +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; +import ghidra.util.Msg; + +/** + * + *
    + *
  • TODO: ensure order: created(Thread), event(THREAD_CREATED), created(RegisterBank) ?
  • + *
  • TODO: ensure registersUpdated(RegisterBank) immediately upon created(RegisterBank) ?
  • + *
+ */ +public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadlessIntegrationTest + implements TestDebuggerModelProvider, DebuggerModelTestUtils { + + protected DummyProc dummy; + public ModelHost m; + + /** + * The default seed path to use when searching for a type of object + * + * @return the seed path + */ + protected List seedPath() { + return List.of(); + } + + protected TargetObject findAttachableContainer() throws Throwable { + return m.findContainer(TargetAttachable.class, seedPath()); + } + + protected TargetAttacher findAttacher() throws Throwable { + return m.find(TargetAttacher.class, seedPath()); + } + + protected TargetFocusScope findFocusScope() throws Throwable { + return m.find(TargetFocusScope.class, seedPath()); + } + + protected TargetInterpreter findInterpreter() throws Throwable { + return m.find(TargetInterpreter.class, seedPath()); + } + + /** + * Get the launcher under test + * + *

+ * This can be overridden to force a different launcher under the test. + * + * @return the launcher + * @throws Throwable if anything goes wrong + */ + protected TargetLauncher findLauncher() throws Throwable { + return m.find(TargetLauncher.class, seedPath()); + } + + /** + * Get the process container under test + * + *

+ * This can be overridden to force a different object under the test. + * + * @return the process container + * @throws Throwable if anything goes wrong + */ + protected TargetObject findProcessContainer() throws Throwable { + return m.findContainer(TargetProcess.class, seedPath()); + } + + /** + * Get the breakpoint container of a target under test + * + * @param seedPath the path to the target + * @return the breakpoint container + * @throws Throwable if anything goes wrong + */ + protected TargetBreakpointSpecContainer findBreakpointSpecContainer(List seedPath) + throws Throwable { + return m.suitable(TargetBreakpointSpecContainer.class, seedPath); + } + + protected TargetEnvironment findEnvironment(List seedPath) throws Throwable { + return m.suitable(TargetEnvironment.class, seedPath); + } + + /** + * Get the steppable object of a target under test + * + * @param seedPath the path to the target + * @return the steppable object + * @throws Throwable if anything goes wrong + */ + protected TargetSteppable findSteppable(List seedPath) throws Throwable { + return m.find(TargetSteppable.class, seedPath); + } + + /** + * Get the memory of a target under test + * + * @param seedPath the path to the target + * @return the memory + * @throws Throwable if anything goes wrong + */ + protected TargetMemory findMemory(List seedPath) throws Throwable { + return m.find(TargetMemory.class, seedPath); + } + + /** + * Find any thread to put under test + * + * @param seedPath the path to the target process + * @return the thread + * @throws Throwable if anything goes wrong + */ + protected TargetThread findAnyThread(List seedPath) throws Throwable { + return m.findAny(TargetThread.class, seedPath); + } + + // TODO: Seems TargetStack is just a container for TargetStackFrame + // This could be replaced by findContainer(TargetStackFrame) + protected TargetStack findStack(List seedPath) throws Throwable { + return m.find(TargetStack.class, seedPath); + } + + protected TargetRegisterBank findAnyRegisterBank(List seedPath) throws Throwable { + return m.findAny(TargetRegisterBank.class, seedPath); + } + + protected TargetStackFrame findAnyStackFrame(List seedPath) throws Throwable { + return m.findAny(TargetStackFrame.class, seedPath); + } + + protected TargetObject maybeSubstituteThread(TargetObject target) throws Throwable { + TargetThread thread = findAnyThread(target.getPath()); + return thread == null ? target : thread; + } + + @Override + public void validateCompletionThread() { + m.validateCompletionThread(); + } + + @Before + public void setUpDebuggerModelTest() throws Throwable { + m = modelHost(); + } + + @After + public void tearDownDebuggerModelTest() throws Throwable { + /** + * NB. Model has to be closed before dummy. If dummy is suspended by a debugger, terminating + * it, even forcibly, may fail. + */ + if (m != null) { + m.close(); + } + if (dummy != null) { + dummy.close(); + } + } + + public interface DebuggerTestSpecimen { + /** + * Run the specimen outside the debugger + * + *

+ * This is really only applicable to processes which are going to run/wait indefinitely, + * since this is likely used in tests involving attach. + * + * @return a handle to the process + * @throws Throwable if anything goes wrong + */ + DummyProc runDummy() throws Throwable; + + /** + * Get the arguments to launch this specimen using the model's launcher + * + * @return the arguments + */ + Map getLauncherArgs(); + + /** + * Get the script to launch this specimen via the interpreter + */ + List getLaunchScript(); + + /** + * Check if this specimen is the image for the given process + * + * @param process the process to examine + * @param test the test case + * @return true if the specimen is the image, false otherwise + * @throws Throwable if anything goes wrong + */ + boolean isRunningIn(TargetProcess process, AbstractDebuggerModelTest test) throws Throwable; + + /** + * Check if this specimen is the image for the given attachable process + * + *

+ * The actual check is usually done by the OS-assigned PID. + * + * @param dummy the dummy process whose image is known to be this specimen + * @param attachable the attachable process presented by the model + * @param test the test case + * @return true if the attachable process represents the given dummy + * @throws Throwable if anything goes wrong + */ + boolean isAttachable(DummyProc dummy, TargetAttachable attachable, + AbstractDebuggerModelTest test) throws Throwable; + } + + /** + * Set a software breakpoint and resume until it is hit + * + * @param bpExpression the expression for the breakpoint + * @param target the target to resume + * @return the object which is actually trapped, often a thread + * @throws Throwable if anything goes wrong + */ + protected TargetObject trapAt(String bpExpression, TargetObject target) throws Throwable { + var listener = new DebuggerModelListener() { + CompletableFuture trapped = new CompletableFuture<>(); + + @Override + public void event(TargetObject object, TargetThread eventThread, TargetEventType type, + String description, List parameters) { + Msg.debug(this, "EVENT " + type + " '" + description + "'"); + } + + @Override + public void breakpointHit(TargetObject container, TargetObject trapped, + TargetStackFrame frame, TargetBreakpointSpec spec, + TargetBreakpointLocation breakpoint) { + if (bpExpression.equals(spec.getExpression())) { + this.trapped.complete(trapped); + } + } + }; + target.getModel().addModelListener(listener); + + TargetBreakpointSpecContainer breakpoints = findBreakpointSpecContainer(target.getPath()); + waitOn(breakpoints.placeBreakpoint(bpExpression, Set.of(TargetBreakpointKind.SW_EXECUTE))); + + AsyncState state = + new AsyncState(m.suitable(TargetExecutionStateful.class, target.getPath())); + while (!listener.trapped.isDone()) { + resume(target); + TargetExecutionState st = + waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING)); + assertTrue("Target terminated before it was trapped", st.isAlive()); + } + target.getModel().removeModelListener(listener); + return waitOn(listener.trapped); + } + + protected void runTestDetach(TargetObject container, DebuggerTestSpecimen specimen) + throws Throwable { + TargetProcess process = retryForProcessRunning(container, specimen, this); + TargetDetachable detachable = m.suitable(TargetDetachable.class, process.getPath()); + waitAcc(detachable); + waitOn(detachable.detach()); + assertFalse(DebugModelConventions.isProcessAlive(process)); + } + + protected void runTestKill(TargetObject container, DebuggerTestSpecimen specimen) + throws Throwable { + TargetProcess process = retryForProcessRunning(container, specimen, this); + TargetKillable killable = m.suitable(TargetKillable.class, process.getPath()); + waitAcc(killable); + waitOn(killable.kill()); + assertFalse(DebugModelConventions.isProcessAlive(process)); + } + + protected void runTestResumeTerminates(TargetObject container, DebuggerTestSpecimen specimen) + throws Throwable { + TargetProcess process = retryForProcessRunning(container, specimen, this); + TargetResumable resumable = m.suitable(TargetResumable.class, process.getPath()); + AsyncState state = + new AsyncState(m.suitable(TargetExecutionStateful.class, process.getPath())); + TargetExecutionState st = waitOn(state.waitUntil(s -> s != TargetExecutionState.RUNNING)); + assertTrue(st.isAlive()); + waitOn(resumable.resume()); + retryVoid(() -> assertFalse(DebugModelConventions.isProcessAlive(process)), + List.of(AssertionError.class)); + } + + protected void runTestResumeInterruptMany(TargetObject container, DebuggerTestSpecimen specimen, + int repetitions) throws Throwable { + TargetProcess process = retryForProcessRunning(container, specimen, this); + TargetResumable resumable = m.suitable(TargetResumable.class, process.getPath()); + TargetInterruptible interruptible = + m.suitable(TargetInterruptible.class, process.getPath()); + TargetExecutionStateful stateful = + m.suitable(TargetExecutionStateful.class, process.getPath()); + for (int i = 0; i < repetitions; i++) { + waitAcc(resumable); + waitOn(resumable.resume()); + if (stateful != null) { + retryVoid(() -> { + assertEquals(TargetExecutionState.RUNNING, stateful.getExecutionState()); + }, List.of(AssertionError.class)); + } + // NB. Never have to wait to interrupt. (Hmmmm, do we believe this?) + waitOn(interruptible.interrupt()); + if (stateful != null) { + retryVoid(() -> { + assertEquals(TargetExecutionState.STOPPED, stateful.getExecutionState()); + }, List.of(AssertionError.class)); + } + } + waitOn(container.getModel().ping("Are you still there?")); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractModelHost.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractModelHost.java new file mode 100644 index 0000000000..36e361c0d6 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/AbstractModelHost.java @@ -0,0 +1,245 @@ +/* ### + * 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.test; + +import java.util.*; +import java.util.Map.Entry; + +import ghidra.dbg.DebuggerModelFactory; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.testutil.*; +import ghidra.dbg.testutil.TestDebuggerModelProvider.ModelHost; +import ghidra.dbg.util.*; +import ghidra.dbg.util.ConfigurableFactory.Property; +import ghidra.dbg.util.PathUtils.PathComparator; + +public abstract class AbstractModelHost implements ModelHost, DebuggerModelTestUtils { + protected DebuggerObjectModel model; + public CallbackValidator callbackValidator; + public EventValidator eventValidator; + public TargetObjectAddedWaiter waiter; + public DebuggerConsole console; + + protected boolean validateCallbacks = true; + protected boolean validateEvents = true; + protected boolean provideConsole = true; + + @Override + public DebuggerObjectModel buildModel(Map options) throws Throwable { + DebuggerModelFactory factory = getModelFactory(); + for (Map.Entry opt : options.entrySet()) { + @SuppressWarnings("unchecked") + Property property = + (Property) factory.getOptions().get(opt.getKey()); + property.setValue(opt.getValue()); + } + DebuggerObjectModel model = waitOn(factory.build()); + if (validateCallbacks) { + callbackValidator = new CallbackValidator(model); + } + if (validateEvents) { + eventValidator = new EventValidator(model); + } + if (provideConsole) { + console = new DebuggerConsole(model); + } + waiter = new TargetObjectAddedWaiter(model); + return model; + } + + @Override + public AbstractModelHost build() throws Throwable { + model = buildModel(getFactoryOptions()); + return this; + } + + @Override + public DebuggerObjectModel getModel() { + return model; + } + + @Override + public void validateCompletionThread() { + if (callbackValidator != null) { + callbackValidator.validateCompletionThread(); + } + } + + @Override + public TargetObject getRoot() throws Throwable { + // Nothing waits on root unless they call this. Cannot use getModelRoot() + return waitOn(model.fetchModelRoot()); + } + + @Override + public void close() throws Exception { + if (model != null) { + try { + waitOn(model.close()); + } + catch (Exception e) { + throw e; + } + catch (Throwable e) { + throw new AssertionError(e); + } + } + if (callbackValidator != null) { + callbackValidator.close(); + } + if (eventValidator != null) { + eventValidator.close(); + } + if (console != null) { + console.close(); + } + if (waiter != null) { + waiter.close(); + } + } + + public abstract DebuggerModelFactory getModelFactory(); + + @Override + public List getBogusPath() { + return PathUtils.parse("THIS.PATH[SHOULD].NEVER[EXIST]"); + } + + @Override + public boolean hasDetachableProcesses() { + return true; + } + + @Override + public boolean hasInterruptibleProcesses() { + return true; + } + + @Override + public boolean hasKillableProcesses() { + return true; + } + + @Override + public boolean hasResumableProcesses() { + return true; + } + + @Override + public boolean hasAttachableContainer() { + return true; + } + + @Override + public boolean hasAttacher() { + return true; + } + + @Override + public boolean hasEventScope() { + return true; + } + + @Override + public boolean hasLauncher() { + return true; + } + + @Override + public boolean hasProcessContainer() { + return true; + } + + @Override + public TargetObjectAddedWaiter getAddedWaiter() { + return waiter; + } + + @Override + public T find(Class cls, List seedPath) throws Throwable { + PathMatcher matcher = + model.getRootSchema().getSuccessorSchema(seedPath).searchFor(cls, seedPath, true); + if (matcher.isEmpty()) { + return null; + } + return cls.cast(assertUniqueShortest(waitOn(waiter.waitAtLeastOne(matcher)))); + } + + @Override + public T findWithIndex(Class cls, String index, + List seedPath) throws Throwable { + Objects.requireNonNull(index); // Use find if no index is expected + PathPredicates matcher = model.getRootSchema() + .getSuccessorSchema(seedPath) + .searchFor(cls, seedPath, true) + .applyIndices(index); + if (matcher.isEmpty()) { + return null; + } + return cls.cast(waitOn(waiter.wait(matcher.getSingletonPath()))); + } + + @Override + public T findAny(Class cls, List seedPath) + throws Throwable { + PathMatcher matcher = + model.getRootSchema().getSuccessorSchema(seedPath).searchFor(cls, seedPath, true); + if (matcher.isEmpty()) { + return null; + } + return cls.cast(waitOn(waiter.waitAtLeastOne(matcher)).firstEntry().getValue()); + } + + @Override + public NavigableMap, T> findAll(Class cls, + List seedPath) throws Throwable { + PathMatcher matcher = + model.getRootSchema().getSuccessorSchema(seedPath).searchFor(cls, seedPath, false); + if (matcher.isEmpty()) { + return new TreeMap<>(); + } + // NB. Outside of testing, an "unsafe" cast of the map should be fine. + // During testing, we should expend the energy to verify the heap. + NavigableMap, T> result = new TreeMap<>(PathComparator.KEYED); + for (Entry, ?> ent : waitOn(waiter.waitAtLeastOne(matcher)).entrySet()) { + result.put(ent.getKey(), cls.cast(ent.getValue())); + } + return result; + } + + @Override + public TargetObject findContainer(Class cls, List seedPath) + throws Throwable { + List foundSub = + model.getRootSchema().getSuccessorSchema(seedPath).searchForCanonicalContainer(cls); + if (foundSub == null) { + return null; + } + List path = PathUtils.extend(seedPath, foundSub); + return (TargetObject) waitOn(waiter.wait(path)); + } + + @Override + public T suitable(Class cls, List seedPath) + throws Throwable { + List path = model.getRootSchema().searchForSuitable(cls, seedPath); + if (path == null) { + return null; + } + return cls.cast(waitOn(waiter.wait(path))); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/ProvidesTargetViaAttachSpecimen.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/ProvidesTargetViaAttachSpecimen.java new file mode 100644 index 0000000000..3399e3b740 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/ProvidesTargetViaAttachSpecimen.java @@ -0,0 +1,40 @@ +/* ### + * 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.test; + +import ghidra.dbg.target.TargetAttacher; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen; +import ghidra.dbg.testutil.DummyProc; + +public interface ProvidesTargetViaAttachSpecimen extends RequiresTarget, RequiresAttachSpecimen { + + void setDummy(DummyProc dummy); + + AbstractDebuggerModelTest getTest(); + + @Override + default TargetObject obtainTarget() throws Throwable { + TargetAttacher attacher = getTest().findAttacher(); + TargetObject container = getTest().findProcessContainer(); + DebuggerTestSpecimen specimen = getAttachSpecimen(); + waitAcc(attacher); + DummyProc dummy = specimen.runDummy(); + setDummy(dummy); + attacher.attach(dummy.pid); + return retryForProcessRunning(container, specimen, getTest()); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/ProvidesTargetViaLaunchSpecimen.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/ProvidesTargetViaLaunchSpecimen.java new file mode 100644 index 0000000000..3dcdde7ef3 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/ProvidesTargetViaLaunchSpecimen.java @@ -0,0 +1,44 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; + +import ghidra.dbg.target.TargetLauncher; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen; + +public interface ProvidesTargetViaLaunchSpecimen extends RequiresTarget, RequiresLaunchSpecimen { + + /** + * Probably just {@code return this;} + * + * @return the test + */ + AbstractDebuggerModelTest getTest(); + + @Override + default TargetObject obtainTarget() throws Throwable { + TargetLauncher launcher = getTest().findLauncher(); + assertNotNull("No launcher found", launcher); + TargetObject container = getTest().findProcessContainer(); + assertNotNull("No process container found", container); + DebuggerTestSpecimen specimen = getLaunchSpecimen(); + waitAcc(launcher); + launcher.launch(specimen.getLauncherArgs()); + return retryForProcessRunning(container, specimen, getTest()); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/RequiresAttachSpecimen.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/RequiresAttachSpecimen.java new file mode 100644 index 0000000000..753d92d662 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/RequiresAttachSpecimen.java @@ -0,0 +1,32 @@ +/* ### + * 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.test; + +import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen; + +public interface RequiresAttachSpecimen { + + /** + * Get the specimen to use for attach tests + * + *

+ * This specimen should live indefinitely. It should only terminate when the debugger or + * operating system kills it. A good example on UNIX is "{@code dd}". + * + * @return the specimen + */ + DebuggerTestSpecimen getAttachSpecimen(); +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/RequiresLaunchSpecimen.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/RequiresLaunchSpecimen.java new file mode 100644 index 0000000000..f5995e6ea5 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/RequiresLaunchSpecimen.java @@ -0,0 +1,33 @@ +/* ### + * 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.test; + +import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen; + +public interface RequiresLaunchSpecimen { + + /** + * Get the specimen to use for launch tests + * + *

+ * This specimen should live for only a short period of time. When left to execute freely, it + * should immediately terminate on its own without error. A good example on UNIX is + * "{@code echo Hello, World!}" + * + * @return the specimen + */ + DebuggerTestSpecimen getLaunchSpecimen(); +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/RequiresTarget.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/RequiresTarget.java new file mode 100644 index 0000000000..fde6929180 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/test/RequiresTarget.java @@ -0,0 +1,34 @@ +/* ### + * 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.test; + +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetProcess; +import ghidra.dbg.testutil.DebuggerModelTestUtils; + +public interface RequiresTarget extends DebuggerModelTestUtils { + + /** + * Perform whatever minimal setup is necessary to obtain a target suitable for testing + * + *

+ * For user-mode debugging this is almost certainly a {@link TargetProcess}. + * + * @return the target + * @throws Throwable if anything goes wrong + */ + TargetObject obtainTarget() throws Throwable; +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AbstractInvocationListener.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/AbstractInvocationListener.java similarity index 97% rename from Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AbstractInvocationListener.java rename to Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/AbstractInvocationListener.java index c742f45514..c8ce2b7bb4 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AbstractInvocationListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/AbstractInvocationListener.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.dbg.util; +package ghidra.dbg.testutil; import java.util.LinkedList; diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/AddressValidator.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/AddressValidator.java new file mode 100644 index 0000000000..b5f28de63a --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/AddressValidator.java @@ -0,0 +1,21 @@ +/* ### + * 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.testutil; + +public class AddressValidator { + // TODO: Ensure 'Memory' exists before any address/range is in any callback / attribute + // TODO: Should it also verify it is covered by a known region? +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AttributesChangedListener.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/AttributesChangedListener.java similarity index 87% rename from Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AttributesChangedListener.java rename to Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/AttributesChangedListener.java index d83686ed3d..4bded7eb47 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AttributesChangedListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/AttributesChangedListener.java @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.dbg.util; +package ghidra.dbg.testutil; import java.util.Collection; import java.util.Map; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.TargetObject.TargetObjectListener; -import ghidra.dbg.util.AttributesChangedListener.AttributesChangedInvocation; +import ghidra.dbg.testutil.AttributesChangedListener.AttributesChangedInvocation; public class AttributesChangedListener extends - AbstractInvocationListener implements TargetObjectListener { + AbstractInvocationListener implements DebuggerModelListener { public static class AttributesChangedInvocation { public final TargetObject parent; public final Collection removed; diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/CallbackValidator.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/CallbackValidator.java new file mode 100644 index 0000000000..a20db578a9 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/CallbackValidator.java @@ -0,0 +1,377 @@ +/* ### + * 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.testutil; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.Map.Entry; + +import ghidra.dbg.*; +import ghidra.dbg.agent.AbstractTargetObject; +import ghidra.dbg.attributes.TargetObjectList; +import ghidra.dbg.attributes.TargetStringList; +import ghidra.dbg.error.DebuggerMemoryAccessException; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetAttacher.TargetAttachKind; +import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet; +import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; +import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; +import ghidra.dbg.target.TargetConsole.Channel; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetMethod.TargetParameterMap; +import ghidra.dbg.target.TargetSteppable.TargetStepKind; +import ghidra.dbg.target.TargetSteppable.TargetStepKindSet; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; + +public class CallbackValidator implements DebuggerModelListener, AutoCloseable { + + public CatchOffThread off = new CatchOffThread(); + + public final DebuggerObjectModel model; + public Thread thread = null; + public Set valid = new HashSet<>(); + + // Knobs + // TODO: Make these methods instead? + public boolean log = false; + public boolean requireSameThread = true; + public boolean requireSameModel = true; + public boolean requireValid = true; + public boolean requireProxy = true; + public Set> allowedTypes = Set.of( // When not a TargetObject + Boolean.class, boolean.class, Byte.class, byte.class, byte[].class, Character.class, + char.class, Double.class, double.class, Float.class, float.class, Integer.class, int.class, + Long.class, long.class, String.class, TargetStringList.class, Address.class, + AddressRange.class, TargetAttachKind.class, TargetAttachKindSet.class, + TargetBreakpointKind.class, TargetBreakpointKindSet.class, TargetStepKind.class, + TargetStepKindSet.class, TargetExecutionState.class, TargetEventType.class, + TargetParameterMap.class, TargetObjectList.class); + + public CallbackValidator(DebuggerObjectModel model) { + this.model = model; + model.addModelListener(this, true); + } + + public void validateCallbackThread(String callback) { + if (thread == null) { + thread = Thread.currentThread(); + } + if (requireSameThread) { + assertEquals("Callback " + callback + " came from an unexpected thread", thread, + Thread.currentThread()); + } + } + + public void validateCompletionThread() { + if (thread == null) { + thread = Thread.currentThread(); + } + if (requireSameThread) { + if (thread != Thread.currentThread()) { + throw new AssertionError("Completion came from an unexpected thread expected:" + + thread + " but got " + Thread.currentThread()); + } + assertEquals("Completion came from an unexpected thread", thread, + Thread.currentThread()); + } + } + + public void validateObjectModel(String callback, TargetObject obj) { + if (requireSameModel) { + assertEquals("Callback " + callback + " included foreign object " + obj, model, + obj.getModel()); + } + } + + public void validateObjectProxy(String callback, TargetObject obj) { + if (requireProxy && obj instanceof AbstractTargetObject) { + AbstractTargetObject ato = (AbstractTargetObject) obj; + assertEquals( + "Non-proxy object " + obj.getJoinedPath(".") + "leaked into callback " + callback, + obj, ato.getProxy()); + } + } + + public void validateObjectValid(String callback, TargetObject obj) { + if (requireValid) { + assertTrue("Object " + obj.getJoinedPath(".") + " invalid during callback " + callback, + valid.contains(obj)); + } + } + + public void validateObjectInvalid(String callback, TargetObject obj) { + if (requireValid) { + assertFalse("Object " + obj.getJoinedPath(".") + " valid during callback " + callback + + ", but should have been invalid", valid.contains(obj)); + } + } + + public void validateInvalidObject(String callback, TargetObject obj) { + assertNotNull(obj); + validateObjectModel(callback, obj); + validateObjectProxy(callback, obj); + validateObjectInvalid(callback, obj); + } + + public void validateObject(String callback, TargetObject obj) { + assertNotNull(obj); + validateObjectModel(callback, obj); + validateObjectProxy(callback, obj); + validateObjectValid(callback, obj); + } + + public void validateObjectOptional(String callback, TargetObject obj) { + if (obj != null) { + validateObject(callback, obj); + } + } + + public void validateObjects(String callback, Collection objs) { + for (TargetObject obj : objs) { + validateObject(callback, obj); + } + } + + public void validateObjectsInMap(String callback, Map map) { + for (Entry ent : map.entrySet()) { + Object obj = ent.getValue(); + validateObjectOrAllowedType(callback + "(key=" + ent.getKey() + ")", obj); + } + } + + public void validateObjectOrAllowedType(String callback, Object obj) { + if (obj instanceof TargetObject) { + validateObject(callback, (TargetObject) obj); + return; + } + for (Class cls : allowedTypes) { + if (cls.isInstance(obj)) { + return; + } + } + fail( + "Invalid object type in callback " + callback + " " + obj + "(" + obj.getClass() + ")"); + } + + public void validateObjectsInCollection(String callback, Collection objs) { + for (Object obj : objs) { + validateObjectOrAllowedType(callback, obj); + } + } + + @Override + public synchronized void catastrophic(Throwable t) { + if (log) { + Msg.info(this, "catastrophic(t=" + t + ")"); + } + off.catching(() -> { + throw new AssertionError("Catastrophic error", t); + }); + } + + @Override + public void modelClosed(DebuggerModelClosedReason reason) { + if (log) { + Msg.info(this, "modelClosed(reason=" + reason + ")"); + } + } + + @Override + public synchronized void elementsChanged(TargetObject object, Collection removed, + Map added) { + if (log) { + Msg.info(this, "elementsChanged(object=" + object + ",removed=" + removed + ",added=" + + added + ")"); + } + off.catching(() -> { + validateCallbackThread("elementsChanged"); + validateObject("elementsChanged.object", object); + validateObjectsInMap("elementsChanged.added(object=" + object.getJoinedPath(".") + ")", + added); + }); + } + + @Override + public synchronized void attributesChanged(TargetObject object, Collection removed, + Map added) { + if (log) { + Msg.info(this, "attributesChanged(object=" + object + ",removed=" + removed + + ",added=" + added + ")"); + } + off.catching(() -> { + validateCallbackThread("attributesChanged"); + validateObject("attributesChanged.object", object); + validateObjectsInMap( + "attributesChanged.added(object=" + object.getJoinedPath(".") + ")", added); + }); + } + + @Override + public synchronized void breakpointHit(TargetObject container, TargetObject trapped, + TargetStackFrame frame, TargetBreakpointSpec spec, + TargetBreakpointLocation breakpoint) { + if (log) { + Msg.info(this, "breakpointHit(container=" + container + ",trapped=" + trapped + + ",frame=" + frame + ",spec=" + spec + ",breakpoint=" + breakpoint + ")"); + } + off.catching(() -> { + validateCallbackThread("breakpointHit"); + validateObject("breakpointHit.container", container); + validateObject("breakpointHit.trapped", trapped); + validateObjectOptional("breakpointHit.frame", frame); + validateObject("breakpointHit.spec", spec); + validateObject("breakpointHit.breakpoint", breakpoint); + }); + } + + @Override + public synchronized void consoleOutput(TargetObject console, Channel channel, byte[] data) { + if (log) { + Msg.info(this, "consoleOutput(console=" + console + ",channel=" + channel + ",data=" + + new String(data) + ")"); + } + off.catching(() -> { + validateCallbackThread("consoleOutput"); + validateObject("consoleOutput", console); + assertNotNull(data); + }); + } + + @Override + public synchronized void created(TargetObject object) { + if (log) { + Msg.info(this, "created(object=" + object + ")"); + } + valid.add(object); + off.catching(() -> { + validateCallbackThread("created"); + validateObject("created", object); + }); + } + + @Override + public synchronized void event(TargetObject object, TargetThread eventThread, + TargetEventType type, String description, List parameters) { + if (log) { + Msg.info(this, "event(object=" + object + ",eventThread=" + eventThread + ",type=" + + type + ",description=" + description + ",parameters=" + parameters + ")"); + } + off.catching(() -> { + validateCallbackThread("event"); + validateObject("event", object); + if (type == TargetEventType.THREAD_CREATED || type == TargetEventType.THREAD_EXITED) { + validateObject("event", eventThread); + } + else { + validateObjectOptional("event.eventThread", eventThread); + } + assertNotNull(type); + assertNotNull(description); + validateObjectsInCollection("event.parameters", parameters); + }); + } + + @Override + public synchronized void invalidateCacheRequested(TargetObject object) { + if (log) { + Msg.info(this, "invalidateCacheRequested(object=" + object + ")"); + } + off.catching(() -> { + validateCallbackThread("invalidateCacheRequested"); + validateObject("invalidateCacheRequested", object); + }); + } + + @Override + public synchronized void invalidated(TargetObject object, TargetObject branch, String reason) { + if (log) { + Msg.info(this, + "invalidated(object=" + object + ",branch=" + branch + ",reason=" + reason + ")"); + } + off.catching(() -> { + validateCallbackThread("invalidated"); + validateObject("invalidated", object); + valid.remove(object); + validateInvalidObject("invalidated", branch); // pre-ordered callbacks + assertNotNull(reason); + }); + } + + @Override + public synchronized void memoryReadError(TargetObject memory, AddressRange range, + DebuggerMemoryAccessException e) { + if (log) { + Msg.info(this, + "memoryReadError(memory=" + memory + ",range=" + range + ",e=" + e + ")"); + } + off.catching(() -> { + validateCallbackThread("memoryReadError"); + validateObject("memoryReadError", memory); + assertNotNull(range); + throw new AssertionError("Memory read error", e); + }); + } + + @Override + public void memoryUpdated(TargetObject memory, Address address, byte[] data) { + if (log) { + Msg.info(this, "memoryUpdated(memory=" + memory + ",address=" + address + ",data=" + + NumericUtilities.convertBytesToString(data) + ")"); + } + off.catching(() -> { + validateCallbackThread("memoryUpdated"); + validateObject("memoryUpdated", memory); + assertNotNull(address); + // TODO: Validate address for regions + assertNotNull(data); + }); + } + + @Override + public void registersUpdated(TargetObject bank, Map updates) { + if (log) { + Msg.info(this, "registersUpdated(bank=" + bank + ",updates=" + + DebuggerModelTestUtils.hexlify(updates) + ")"); + } + off.catching(() -> { + validateCallbackThread("registersUpdated"); + validateObject("registersUpdated", bank); + assertNotNull(updates); + // TODO: Validate names to descriptions, including lengths of values + }); + } + + @Override + public void rootAdded(TargetObject root) { + if (log) { + Msg.info(this, "rootAdded(root=" + root + ")"); + } + off.catching(() -> { + validateCallbackThread("rootAdded"); + validateObject("rootAdded", root); + }); + } + + @Override + public synchronized void close() throws Exception { + off.close(); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/CatchOffThread.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/CatchOffThread.java new file mode 100644 index 0000000000..44b1493a18 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/CatchOffThread.java @@ -0,0 +1,46 @@ +/* ### + * 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.testutil; + +import ghidra.util.Msg; + +public class CatchOffThread implements AutoCloseable { + protected Throwable caught; + + public void catching(Runnable runnable) { + try { + runnable.run(); + } + catch (Throwable e) { + if (caught == null) { + caught = e; + } + Msg.error(this, "Off-thread exception: " + e); + } + } + + @Override + public void close() throws Exception { + if (caught != null) { + if (caught instanceof Exception) { + throw (Exception) caught; + } + else { + throw new AssertionError("Off-thread exception", caught); + } + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DebuggerConsole.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DebuggerConsole.java new file mode 100644 index 0000000000..ae930f124c --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DebuggerConsole.java @@ -0,0 +1,84 @@ +/* ### + * 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.testutil; + +import java.io.*; + +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.TargetConsole.Channel; +import ghidra.dbg.target.TargetInterpreter; +import ghidra.dbg.target.TargetObject; + +public class DebuggerConsole extends Thread implements DebuggerModelListener, AutoCloseable { + private final DebuggerObjectModel model; + private final BufferedReader reader; + + private TargetInterpreter interpreter; + private boolean closed = false; + + public DebuggerConsole(DebuggerObjectModel model) { + this.model = model; + this.reader = new BufferedReader(new InputStreamReader(System.in)); + + model.addModelListener(this); + setDaemon(true); + start(); + } + + @Override + public void consoleOutput(TargetObject console, Channel channel, byte[] data) { + if (console instanceof TargetInterpreter) { + if (interpreter == null) { + System.out.println("Found interpreter: " + console); + interpreter = (TargetInterpreter) console; + } + } + String text = new String(data); + System.out.println(text); + } + + @Override + public void run() { + try { + while (!closed) { + String line = reader.readLine(); + if (interpreter == null) { + System.err.println("Have not found interpreter, yet"); + continue; + } + interpreter.execute(line).whenComplete((__, ex) -> { + if (ex != null) { + System.err.println("Command error: " + ex.getMessage()); + } + else { + System.out.println("Command finished"); + } + }); + } + } + catch (IOException e) { + System.err.println("IOException on console: " + e); + } + } + + @Override + public void close() throws Exception { + model.removeModelListener(this); + closed = true; + interrupt(); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DebuggerModelTestUtils.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DebuggerModelTestUtils.java new file mode 100644 index 0000000000..c601814d6c --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DebuggerModelTestUtils.java @@ -0,0 +1,209 @@ +/* ### + * 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.testutil; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.*; +import java.util.Map.Entry; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import ghidra.async.AsyncReference; +import ghidra.async.AsyncTestUtils; +import ghidra.dbg.DebugModelConventions; +import ghidra.dbg.DebugModelConventions.AsyncAccess; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetSteppable.TargetStepKind; +import ghidra.dbg.test.AbstractDebuggerModelTest; +import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen; +import ghidra.util.NumericUtilities; + +public interface DebuggerModelTestUtils extends AsyncTestUtils { + + default byte[] arr(String hex) { + return NumericUtilities.convertStringToBytes(hex); + } + + /** + * Performs the cast, after verifying the schema, too + * + * @param the type of the object + * @param type the class of the object + * @param obj the object to cast + * @return the same object + */ + default T cast(Class type, TargetObject obj) { + assertTrue(obj.getSchema().getInterfaces().contains(type)); + return type.cast(obj); + } + + default T ancestor(Class type, TargetObject seed) throws Throwable { + return DebugModelConventions.ancestor(type, seed); + } + + default AsyncAccess access(TargetObject obj) throws Throwable { + return new AsyncAccess( + ancestor(TargetAccessConditioned.class, Objects.requireNonNull(obj))); + } + + default void waitAcc(TargetObject obj) throws Throwable { + AsyncAccess acc = access(obj); + waitAcc(acc); + acc.dispose(); + } + + default void waitAcc(AsyncReference access) throws Throwable { + waitOn(access.waitValue(true)); + } + + default void cli(TargetObject interpreter, String cmd) throws Throwable { + TargetInterpreter as = interpreter.as(TargetInterpreter.class); + waitOn(as.execute(cmd)); + } + + default String captureCli(TargetObject interpreter, String cmd) throws Throwable { + TargetInterpreter as = interpreter.as(TargetInterpreter.class); + return waitOn(as.executeCapture(cmd)); + } + + default void launch(TargetObject launcher, Map args) throws Throwable { + TargetLauncher as = launcher.as(TargetLauncher.class); + waitOn(as.launch(args)); + } + + default void resume(TargetObject resumable) throws Throwable { + TargetResumable as = resumable.as(TargetResumable.class); + waitOn(as.resume()); + } + + default void step(TargetObject steppable, TargetStepKind kind) throws Throwable { + TargetSteppable as = steppable.as(TargetSteppable.class); + waitOn(as.step(kind)); + } + + default TargetObject getFocus(TargetObject scope) { + TargetFocusScope as = scope.as(TargetFocusScope.class); + return as.getFocus(); + } + + default void focus(TargetObject scope, TargetObject focus) throws Throwable { + TargetFocusScope as = scope.as(TargetFocusScope.class); + waitOn(as.requestFocus(focus)); + } + + static Map hexlify(Map map) { + return map.entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, + e -> NumericUtilities.convertBytesToString(e.getValue()))); + } + + /** + * Assert that there is a single reference with shortest path, and get it + * + * @param the type of object reference + * @param refs the map of paths to references, sorted shortest-key-first + * @return the value of the entry with shortest key + */ + default T assertUniqueShortest(NavigableMap, T> refs) { + assertTrue(refs.size() >= 1); + Iterator, T>> rit = refs.entrySet().iterator(); + Entry, T> shortest = rit.next(); + if (!rit.hasNext()) { + return shortest.getValue(); + } + Entry, T> next = rit.next(); + assertTrue("Shortest is not unique: " + refs, + next.getKey().size() > shortest.getKey().size()); + + return shortest.getValue(); + } + + default TargetAttachable getAttachable(Collection attachables, + DebuggerTestSpecimen specimen, DummyProc dummy, AbstractDebuggerModelTest test) { + return attachables.stream().filter(a -> { + try { + return specimen.isAttachable(dummy, a, test); + } + catch (Throwable e) { + throw new AssertionError(e); + } + }).findFirst().orElse(null); + } + + default TargetProcess getProcessRunning(Collection processes, + DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test) { + return getProcessRunning(processes, specimen, test, p -> true); + } + + default TargetProcess getProcessRunning(Collection processes, + DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test, + Predicate predicate) { + return processes.stream().filter(p -> { + try { + return predicate.test(p) && specimen.isRunningIn(p, test); + } + catch (Throwable e) { + throw new AssertionError(e); + } + }).findFirst().orElse(null); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + default Collection fetchProcesses(TargetObject container) + throws Throwable { + return (Collection) waitOn(container.fetchElements(true)).values(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + default Collection fetchAttachables(TargetObject container) + throws Throwable { + return (Collection) waitOn(container.fetchElements(true)).values(); + } + + default TargetProcess getProcessRunning(TargetObject container, + DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test) throws Throwable { + return getProcessRunning(container, specimen, test, p -> true); + } + + default TargetProcess getProcessRunning(TargetObject container, + DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test, + Predicate predicate) throws Throwable { + return getProcessRunning(fetchProcesses(container), specimen, test, predicate); + } + + default TargetProcess retryForProcessRunning(TargetObject container, + DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test) throws Throwable { + return retry(() -> { + TargetProcess process = getProcessRunning(container, specimen, test); + assertNotNull(process); + return process; + }, List.of(AssertionError.class)); + } + + default TargetProcess retryForOtherProcessRunning(TargetObject container, + DebuggerTestSpecimen specimen, AbstractDebuggerModelTest test, + Predicate predicate, long timeoutMs) + throws Throwable { + return retry(timeoutMs, () -> { + TargetProcess process = getProcessRunning(container, specimen, test, predicate); + assertNotNull(process); + return process; + }, List.of(AssertionError.class)); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DummyProc.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DummyProc.java index 3b7fcb7e3b..ba172292c2 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DummyProc.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DummyProc.java @@ -16,14 +16,13 @@ package ghidra.dbg.testutil; import java.io.*; -import java.lang.reflect.Field; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import ghidra.framework.Application; public class DummyProc implements AutoCloseable { - final Process process; + public final Process process; public final long pid; public static String which(String cmd) { @@ -33,40 +32,37 @@ public class DummyProc implements AutoCloseable { catch (Exception e) { // fallback to system } + if (new File(cmd).canExecute()) { + return cmd; + } + String line; try { - Process exec = new ProcessBuilder("which", cmd).start(); + boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows"); + Process exec = new ProcessBuilder(isWindows ? "where" : "which", cmd).start(); exec.waitFor(); BufferedReader reader = new BufferedReader(new InputStreamReader(exec.getInputStream())); - return reader.readLine().trim(); + line = reader.readLine(); } catch (Exception e) { throw new RuntimeException(e); } + if (line == null) { + throw new RuntimeException("Cannot find " + cmd); + } + return line.trim(); } - public static DummyProc run(String... args) throws NoSuchFieldException, SecurityException, - IllegalArgumentException, IllegalAccessException, IOException { + public static DummyProc run(String... args) throws IOException { DummyProc proc = new DummyProc(args); return proc; } - DummyProc(String... args) throws IOException, NoSuchFieldException, SecurityException, - IllegalArgumentException, IllegalAccessException { + DummyProc(String... args) throws IOException { args[0] = which(args[0]); process = new ProcessBuilder(args).start(); - @SuppressWarnings("hiding") - long pid = -1; - try { - Field pidFld = process.getClass().getDeclaredField("pid"); - pidFld.setAccessible(true); - pid = pidFld.getLong(process); - } - catch (NoSuchFieldException | SecurityException e) { - throw new AssertionError("Could not get pid for DummyProc", e); - } - this.pid = pid; + pid = process.pid(); } @Override diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/ElementTrackingListener.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/ElementTrackingListener.java similarity index 86% rename from Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/ElementTrackingListener.java rename to Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/ElementTrackingListener.java index 0deda1ba58..a38020615d 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/ElementTrackingListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/ElementTrackingListener.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.dbg.util; +package ghidra.dbg.testutil; import java.util.*; @@ -21,11 +21,11 @@ import org.apache.commons.lang3.reflect.TypeUtils; import org.apache.commons.lang3.reflect.Typed; import ghidra.async.AsyncReference; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.TargetObject.TargetObjectListener; import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator; -public class ElementTrackingListener implements TargetObjectListener { +public class ElementTrackingListener implements DebuggerModelListener { public final Class valType; public final Map elements = new TreeMap<>(TargetObjectKeyComparator.ELEMENT); public final AsyncReference size = new AsyncReference<>(); @@ -40,11 +40,6 @@ public class ElementTrackingListener implements TargetOb this((Class) TypeUtils.getRawType(valType.getType(), null)); } - @Override - public void displayChanged(TargetObject object, String display) { - // Don't care - } - public synchronized AsyncReference refElement(String index) { T elem = elements.get(index); AsyncReference ref = new AsyncReference<>(elem); @@ -70,10 +65,4 @@ public class ElementTrackingListener implements TargetOb } size.set(elements.size(), null); } - - @Override - public void attributesChanged(TargetObject parent, Collection removed, - Map added) { - // Don't care - } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/ElementsChangedListener.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/ElementsChangedListener.java similarity index 87% rename from Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/ElementsChangedListener.java rename to Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/ElementsChangedListener.java index 949186316f..98dd5021b9 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/ElementsChangedListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/ElementsChangedListener.java @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.dbg.util; +package ghidra.dbg.testutil; import java.util.Collection; import java.util.Map; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.TargetObject.TargetObjectListener; -import ghidra.dbg.util.ElementsChangedListener.ElementsChangedInvocation; +import ghidra.dbg.testutil.ElementsChangedListener.ElementsChangedInvocation; public class ElementsChangedListener extends AbstractInvocationListener - implements TargetObjectListener { + implements DebuggerModelListener { public static class ElementsChangedInvocation { public final TargetObject parent; public final Collection removed; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/EventSequenceListener.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/EventSequenceListener.java similarity index 83% rename from Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/EventSequenceListener.java rename to Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/EventSequenceListener.java index 8d4d2ce47a..15c9eaf1b9 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/EventSequenceListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/EventSequenceListener.java @@ -13,24 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.model; +package ghidra.dbg.testutil; import java.util.*; -import ghidra.dbg.target.TargetEventScope; -import ghidra.dbg.target.TargetEventScope.TargetEventScopeListener; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetThread; -public class EventSequenceListener implements TargetEventScopeListener { +public class EventSequenceListener implements DebuggerModelListener { public static class EventRecord { - public final TargetEventScope object; + public final TargetObject object; public final TargetThread eventThread; public final TargetEventType type; public final String description; public final List parameters; - public EventRecord(TargetEventScope object, TargetThread eventThread, TargetEventType type, + public EventRecord(TargetObject object, TargetThread eventThread, TargetEventType type, String description, List parameters) { this.object = object; this.eventThread = eventThread; @@ -78,7 +78,7 @@ public class EventSequenceListener implements TargetEventScopeListener { public final List events = new ArrayList<>(); @Override - public void event(TargetEventScope object, TargetThread eventThread, TargetEventType type, + public void event(TargetObject object, TargetThread eventThread, TargetEventType type, String description, List parameters) { events.add(new EventRecord(object, eventThread, type, description, parameters)); } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/EventValidator.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/EventValidator.java new file mode 100644 index 0000000000..33d3559173 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/EventValidator.java @@ -0,0 +1,303 @@ +/* ### + * 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.testutil; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.*; +import java.util.function.Function; + +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.util.Msg; + +public class EventValidator + implements DebuggerModelListener, AutoCloseable, DebuggerModelTestUtils { + public CatchOffThread off = new CatchOffThread(); + + interface Observation { + String getEvent(); + + TargetObject getObject(); + + Observation inParameter(String event); + + default Observation inAncestor(String event, TargetObject successor) { + return inParameter("Parent of " + successor.getJoinedPath(".") + " in " + event); + } + + Observation inCreated(String event); + + Observation inDestroyed(String event); + } + + static abstract class AbstractObservation implements Observation { + private final String event; + private final TargetObject object; + + public AbstractObservation(String event, TargetObject object) { + this.event = event; + this.object = object; + } + + @Override + public String getEvent() { + return event; + } + + @Override + public TargetObject getObject() { + return object; + } + + protected String getPath() { + return object.getJoinedPath("."); + } + } + + static class NoObservation extends AbstractObservation { + public NoObservation(TargetObject object) { + super("[none]", object); + } + + @Override + public Observation inParameter(String event) { + return new UseObservation(event, getObject()); + } + + @Override + public Observation inCreated(String event) { + return new CreatedObservation(event, getObject()); + } + + @Override + public Observation inDestroyed(String event) { + return new DestroyedObservation(event, getObject()); + } + } + + static class UseObservation extends AbstractObservation { + public UseObservation(String event, TargetObject object) { + super(event, object); + } + + @Override + public Observation inParameter(String event) { + return this; + } + + @Override + public Observation inCreated(String event) { + throw new AssertionError( + "Observed " + getEvent() + " for " + getPath() + " before " + event); + } + + @Override + public Observation inDestroyed(String event) { + return new DestroyedObservation(event, getObject()); + } + } + + static class CreatedObservation extends AbstractObservation { + public CreatedObservation(String event, TargetObject object) { + super(event, object); + } + + @Override + public Observation inParameter(String event) { + return this; + } + + @Override + public Observation inCreated(String event) { + throw new AssertionError("Observed double-" + event + " of " + getPath()); + } + + @Override + public Observation inDestroyed(String event) { + return new DestroyedObservation(event, getObject()); + } + } + + static class DestroyedObservation extends AbstractObservation { + public DestroyedObservation(String event, TargetObject object) { + super(event, object); + } + + @Override + public Observation inParameter(String event) { + throw new AssertionError( + "Observed " + event + " of " + getPath() + " after " + getEvent()); + } + + @Override + public Observation inCreated(String event) { + return new CreatedObservation(event, getObject()); + } + + @Override + public Observation inDestroyed(String event) { + throw new AssertionError("Observed double-" + event + " of " + getPath()); + } + } + + public final DebuggerObjectModel model; + public Map processes = new HashMap<>(); + public Map threads = new HashMap<>(); + public Map modules = new HashMap<>(); + + // Knobs + public boolean log = false; + + public EventValidator(DebuggerObjectModel model) { + this.model = model; + model.addModelListener(this); + } + + @Override + public void invalidated(TargetObject object, TargetObject branch, String reason) { + if (log) { + Msg.info(this, + "invalidated(object=" + object + ",branch=" + branch + ",reason=" + reason + ")"); + } + processes.remove(object); + threads.remove(object); + modules.remove(object); + } + + @Override + public synchronized void event(TargetObject object, TargetThread eventThread, + TargetEventType type, String description, List parameters) { + if (log) { + Msg.info(this, + "event(object=" + object + ",eventThread=" + eventThread + ",type=" + type + + ",description=" + description + ",parameters=" + parameters + ")"); + } + off.catching(() -> { + switch (type) { + case PROCESS_CREATED: + validateCreated(type.name(), TargetProcess.class, processes, parameters); + break; + case PROCESS_EXITED: + validateDestroyed(type.name(), TargetProcess.class, processes, parameters); + break; + case THREAD_CREATED: + validateCreated(type.name(), TargetThread.class, threads, parameters); + break; + case THREAD_EXITED: + validateDestroyed(type.name(), TargetThread.class, threads, parameters); + break; + case MODULE_LOADED: + validateCreated(type.name(), TargetModule.class, modules, parameters); + break; + case MODULE_UNLOADED: + validateDestroyed(type.name(), TargetModule.class, modules, parameters); + break; + case STOPPED: + case RUNNING: + case BREAKPOINT_HIT: + case STEP_COMPLETED: + case EXCEPTION: + case SIGNAL: + validateParameters(type.name(), parameters); + break; + default: + fail("Unexpected event type"); + } + }); + } + + protected void observe(Map map, T object, + Function func) { + map.compute(object, (__, observation) -> { + if (observation == null) { + observation = new NoObservation(object); + } + return func.apply(observation); + }); + } + + protected void validateParameters(String event, List objects) { + for (Object obj : objects) { + if (obj instanceof TargetProcess) { + observe(processes, (TargetProcess) obj, o -> o.inParameter(event)); + } + if (obj instanceof TargetThread) { + observe(threads, (TargetThread) obj, o -> o.inParameter(event)); + } + if (obj instanceof TargetModule) { + observe(modules, (TargetModule) obj, o -> o.inParameter(event)); + } + } + } + + protected void validateAncestors(String event, TargetObject object) { + TargetObject ancestor = object; + while (null != (ancestor = ancestor.getParent())) { // Yes, pre-step to parent + if (ancestor instanceof TargetProcess) { + observe(processes, (TargetProcess) ancestor, o -> o.inAncestor(event, object)); + } + if (ancestor instanceof TargetThread) { + observe(threads, (TargetThread) ancestor, o -> o.inAncestor(event, object)); + } + if (ancestor instanceof TargetModule) { + observe(modules, (TargetModule) ancestor, o -> o.inAncestor(event, object)); + } + } + } + + protected T doGetFirstAs(Class cls, List objects) { + if (objects.isEmpty()) { + return null; + } + Object first = objects.get(0); + if (!cls.isInstance(first)) { + return null; + } + return cls.cast(first); + } + + protected T getFirstAs(String event, Class cls, List objects) { + T result = doGetFirstAs(cls, objects); + assertNotNull("The first parameter of " + event + " must be a " + cls.getSimpleName(), + result); + return result; + } + + protected void validateCreated(String event, Class cls, + Map map, List objects) { + T t = getFirstAs(event, cls, objects); + observe(map, t, o -> o.inCreated(event)); + validateAncestors(event, t); + validateParameters(event, objects.subList(1, objects.size())); + } + + protected void validateDestroyed(String event, Class cls, + Map map, List objects) { + T t = getFirstAs(event, cls, objects); + observe(map, t, o -> o.inDestroyed(event)); + validateAncestors(event, t); + validateParameters(event, objects.subList(1, objects.size())); + } + + @Override + public synchronized void close() throws Exception { + off.close(); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/InvalidatedListener.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/InvalidatedListener.java similarity index 81% rename from Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/InvalidatedListener.java rename to Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/InvalidatedListener.java index 7f7c3d9fd8..2e5c326601 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/InvalidatedListener.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/InvalidatedListener.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.dbg.util; +package ghidra.dbg.testutil; +import ghidra.dbg.DebuggerModelListener; import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.TargetObject.TargetObjectListener; -import ghidra.dbg.util.InvalidatedListener.InvalidatedInvocation; +import ghidra.dbg.testutil.InvalidatedListener.InvalidatedInvocation; -public class InvalidatedListener extends - AbstractInvocationListener implements TargetObjectListener { +public class InvalidatedListener extends AbstractInvocationListener + implements DebuggerModelListener { public static class InvalidatedInvocation { public final TargetObject object; public final TargetObject branch; diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/RegisterUpdateValidator.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/RegisterUpdateValidator.java new file mode 100644 index 0000000000..b3197ed4e7 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/RegisterUpdateValidator.java @@ -0,0 +1,20 @@ +/* ### + * 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.testutil; + +public class RegisterUpdateValidator { + // Ensure updates are for registers that exist in the descriptions +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/TargetObjectAddedWaiter.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/TargetObjectAddedWaiter.java new file mode 100644 index 0000000000..9e693b2aaa --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/TargetObjectAddedWaiter.java @@ -0,0 +1,124 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.dbg.testutil; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; + +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.util.*; + +public class TargetObjectAddedWaiter + implements DebuggerModelListener, DebuggerModelTestUtils, AutoCloseable { + private final DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this); + private final Map, CompletableFuture> pathBacklog = new HashMap<>(); + private final Map, ?>>> predBacklog = + new HashMap<>(); + private final DebuggerObjectModel model; + + public TargetObjectAddedWaiter(DebuggerObjectModel model) { + this.model = model; + model.addModelListener(reorderer, true); + } + + @Override + public void close() throws Exception { + model.removeModelListener(reorderer); + } + + protected void retryBacklogs() { + synchronized (predBacklog) { + // NB. getModelRoot() can be non-null before rootAdded. Use fetch.getNow instead. + TargetObject root = model.fetchModelRoot().getNow(null); + if (root != null) { + for (Iterator, ?>>>> it = + predBacklog.entrySet().iterator(); it.hasNext();) { + Entry, ?>>> ent = + it.next(); + NavigableMap, ?> values = ent.getKey().getCachedValues(root); + if (!values.isEmpty()) { + // NB. This is completed with a lock, but tests should just use waitOn + ent.getValue().complete(values); + it.remove(); + } + } + } + } + } + + @Override + public void rootAdded(TargetObject root) { + retryBacklogs(); + } + + @Override + public void attributesChanged(TargetObject object, Collection removed, + Map added) { + for (Entry ent : added.entrySet()) { + List attrPath = PathUtils.extend(object.getPath(), ent.getKey()); + CompletableFuture cf = pathBacklog.remove(attrPath); + if (cf != null) { + cf.complete(ent.getValue()); + } + } + retryBacklogs(); + } + + @Override + public void elementsChanged(TargetObject object, Collection removed, + Map added) { + for (Entry ent : added.entrySet()) { + List elemPath = PathUtils.index(object.getPath(), ent.getKey()); + CompletableFuture cf = pathBacklog.remove(elemPath); + if (cf != null) { + cf.complete(ent.getValue()); + } + } + retryBacklogs(); + } + + public CompletableFuture wait(List path) { + Objects.requireNonNull(path); + synchronized (pathBacklog) { + Object val = model.getModelValue(path); + if (val != null) { + return CompletableFuture.completedFuture(val); + } + CompletableFuture promise = new CompletableFuture<>(); + pathBacklog.put(path, promise); + return promise; + } + } + + public CompletableFuture, ?>> waitAtLeastOne( + PathPredicates predicates) { + synchronized (predBacklog) { + TargetObject root = model.getModelRoot(); + if (root != null) { + NavigableMap, ?> result = predicates.getCachedValues(root); + if (!result.isEmpty()) { + return CompletableFuture.completedFuture(result); + } + } + CompletableFuture, ?>> promise = new CompletableFuture<>(); + predBacklog.put(predicates, promise); + return promise; + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/TargetObjectCreatedWaiter.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/TargetObjectCreatedWaiter.java new file mode 100644 index 0000000000..3cefb85e7a --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/TargetObjectCreatedWaiter.java @@ -0,0 +1,87 @@ +/* ### + * 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.testutil; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.TargetObject; + +public class TargetObjectCreatedWaiter implements DebuggerModelListener { + private final Map, CompletableFuture> pathBacklog = new HashMap<>(); + private final Map, CompletableFuture>> predBacklog = + new HashMap<>(); + private final DebuggerObjectModel model; + + public TargetObjectCreatedWaiter(DebuggerObjectModel model) { + this.model = model; + model.addModelListener(this, false); + } + + @Override + public void created(TargetObject object) { + CompletableFuture cf; + synchronized (pathBacklog) { + cf = pathBacklog.remove(object.getPath()); + } + if (cf != null) { + cf.complete(object); + } + Map, CompletableFuture>> matched; + synchronized (predBacklog) { + matched = predBacklog.entrySet() + .stream() + .filter(e -> e.getKey().test(object)) + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); + predBacklog.keySet().removeAll(matched.keySet()); + } + if (!matched.isEmpty()) { + Set result = Set.of(object); + for (CompletableFuture> promise : matched.values()) { + promise.complete(result); + } + } + } + + public CompletableFuture wait(List path) { + synchronized (pathBacklog) { + TargetObject obj = model.getModelObject(path); + if (obj != null) { + return CompletableFuture.completedFuture(obj); + } + CompletableFuture promise = new CompletableFuture<>(); + pathBacklog.put(path, promise); + return promise; + } + } + + public CompletableFuture> waitAtLeastOne( + Predicate predicate) { + synchronized (predBacklog) { + Set result = model.getModelObjects(predicate); + if (!result.isEmpty()) { + return CompletableFuture.completedFuture(result); + } + CompletableFuture> promise = new CompletableFuture<>(); + predBacklog.put(predicate, promise); + return promise; + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/TestDebuggerModelProvider.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/TestDebuggerModelProvider.java new file mode 100644 index 0000000000..71c03edf7f --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/TestDebuggerModelProvider.java @@ -0,0 +1,88 @@ +/* ### + * 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.testutil; + +import java.util.List; +import java.util.Map; + +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.TargetObject; + +public interface TestDebuggerModelProvider { + interface ModelHost extends AutoCloseable { + Map getFactoryOptions(); + + ModelHost build() throws Throwable; + + DebuggerObjectModel buildModel(Map options) throws Throwable; + + DebuggerObjectModel getModel(); + + void validateCompletionThread(); + + TargetObject getRoot() throws Throwable; + + List getBogusPath(); + + boolean hasDetachableProcesses(); + + boolean hasInterruptibleProcesses(); + + boolean hasKillableProcesses(); + + boolean hasResumableProcesses(); + + boolean hasAttachableContainer(); + + boolean hasAttacher(); + + boolean hasEventScope(); + + boolean hasLauncher(); + + boolean hasProcessContainer(); + + T find(Class cls, List seedPath) throws Throwable; + + /** + * Use the schema to find the appropriate path, substituting the given index for a wildcard + * at most once, then get or wait for that object + * + * @param the type of object + * @param cls the class giving the type + * @param index the index when needed + * @param seedPath the seed path for the search. The result will be a successor + * @return the found object, or {@code null} if the schema does not give it + * @throws Throwable if anything goes wrong + */ + T findWithIndex(Class cls, String index, List seedPath) + throws Throwable; + + T findAny(Class cls, List seedPath) throws Throwable; + + Map, T> findAll(Class cls, List seedPath) + throws Throwable; + + TargetObject findContainer(Class cls, List seedPath) + throws Throwable; + + T suitable(Class cls, List seedPath) throws Throwable; + + TargetObjectAddedWaiter getAddedWaiter(); + } + + ModelHost modelHost() throws Throwable; +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AllTargetObjectListenerAdapter.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AllTargetObjectListenerAdapter.java deleted file mode 100644 index 5d5cb6db5d..0000000000 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/AllTargetObjectListenerAdapter.java +++ /dev/null @@ -1,99 +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.util; - -import static org.junit.Assert.fail; - -import java.util.Collection; -import java.util.Map; - -import ghidra.dbg.error.DebuggerMemoryAccessException; -import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibilityListener; -import ghidra.dbg.target.TargetConsole.Channel; -import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; -import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionStateListener; -import ghidra.dbg.target.TargetFocusScope.TargetFocusScopeListener; -import ghidra.dbg.target.TargetInterpreter.TargetInterpreterListener; -import ghidra.dbg.target.TargetMemory.TargetMemoryListener; -import ghidra.dbg.target.TargetObject.TargetObjectListener; -import ghidra.dbg.target.TargetRegisterBank.TargetRegisterBankListener; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressRange; - -public interface AllTargetObjectListenerAdapter - extends TargetObjectListener, TargetAccessibilityListener, - TargetExecutionStateListener, TargetFocusScopeListener, TargetInterpreterListener, - TargetMemoryListener, TargetRegisterBankListener { - @Override - default void accessibilityChanged(TargetAccessConditioned object, boolean accessible) { - //fail(); - } - - @Override - default void elementsChanged(TargetObject parent, Collection removed, - Map added) { - //fail(); - } - - @Override - default void attributesChanged(TargetObject parent, Collection removed, - Map added) { - //fail(); - } - - @Override - default void consoleOutput(TargetObject console, Channel channel, byte[] out) { - //fail(); - } - - @Override - default void displayChanged(TargetObject object, String display) { - //fail(); - } - - @Override - default void executionStateChanged(TargetExecutionStateful object, - TargetExecutionState state) { - //fail(); - } - - @Override - default void focusChanged(TargetFocusScope object, TargetObject focused) { - //fail(); - } - - @Override - default void memoryUpdated(TargetMemory memory, Address address, byte[] data) { - //fail(); - } - - @Override - default void memoryReadError(TargetMemory memory, AddressRange range, - DebuggerMemoryAccessException e) { - fail(); - } - - @Override - default void promptChanged(TargetInterpreter interpreter, String prompt) { - //fail(); - } - - @Override - default void registersUpdated(TargetRegisterBank bank, Map updates) { - //fail(); - } -} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/DebuggerCallbackReordererTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/DebuggerCallbackReordererTest.java new file mode 100644 index 0000000000..68f3ac3d30 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/DebuggerCallbackReordererTest.java @@ -0,0 +1,406 @@ +/* ### + * 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.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import org.junit.Test; + +import ghidra.dbg.DebuggerModelListener; +import ghidra.dbg.agent.AbstractDebuggerObjectModel; +import ghidra.dbg.agent.DefaultDebuggerObjectModelTest.FakeTargetObject; +import ghidra.dbg.agent.DefaultDebuggerObjectModelTest.FakeTargetRegisterBank; +import ghidra.dbg.agent.DefaultTargetModelRoot; +import ghidra.dbg.model.EmptyDebuggerObjectModel; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetEventScope.TargetEventType; +import ghidra.dbg.target.schema.*; +import ghidra.dbg.testutil.DebuggerModelTestUtils; + +public class DebuggerCallbackReordererTest implements DebuggerModelTestUtils { + + @TargetObjectSchemaInfo + public static class EmptyTargetSession extends DefaultTargetModelRoot { + public EmptyTargetSession(AbstractDebuggerObjectModel model, String typeHint, + TargetObjectSchema schema) { + super(model, typeHint, schema); + } + } + + public static final AnnotatedSchemaContext SCHEMA_CTX = new AnnotatedSchemaContext(); + public static final TargetObjectSchema EMPTY_SCHEMA = + SCHEMA_CTX.getSchemaForClass(EmptyTargetSession.class); + + public static class TestReorderedListener implements DebuggerModelListener { + protected final DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this); + protected final Map, CompletableFuture> waits; + protected final List added = new ArrayList<>(); + + public TestReorderedListener(Collection> paths) { + waits = + paths.stream().collect(Collectors.toMap(p -> p, p -> new CompletableFuture<>())); + } + + protected void done(TargetObject obj) { + synchronized (added) { + added.add(obj); + } + CompletableFuture cf = waits.get(obj.getPath()); + if (cf != null) { + cf.complete(obj); + } + } + + public CompletableFuture get(List path) { + return waits.get(path); + } + + public List getAdded() { + synchronized (added) { + assertEquals("Duplicates added: " + added, added.size(), Set.copyOf(added).size()); + return List.copyOf(added); + } + } + + @Override + public void rootAdded(TargetObject root) { + done(root); + } + + @Override + public void attributesChanged(TargetObject object, Collection removed, + Map added) { + for (Object val : added.values()) { + if (val instanceof TargetObject) { + done((TargetObject) val); + } + } + } + + @Override + public void elementsChanged(TargetObject object, Collection removed, + Map added) { + for (TargetObject obj : added.values()) { + done(obj); + } + } + } + + @Test + public void testRootOnly() throws Throwable { + EmptyDebuggerObjectModel model = new EmptyDebuggerObjectModel(); + TestReorderedListener listener = new TestReorderedListener(List.of(PathUtils.parse(""))); + model.addModelListener(listener.reorderer); + + assertEquals(List.of(), listener.getAdded()); + DefaultTargetModelRoot root = + new DefaultTargetModelRoot(model, "Root", model.getRootSchema()); + model.addModelRoot(root); + assertEquals(root, waitOn(listener.get(PathUtils.parse("")))); + } + + @Test + public void testChain2TopDown() throws Throwable { + EmptyDebuggerObjectModel model = new EmptyDebuggerObjectModel(); + TestReorderedListener listener = + new TestReorderedListener(List.of(PathUtils.parse("A[1]"))); + model.addModelListener(listener.reorderer); + + DefaultTargetModelRoot root = + new DefaultTargetModelRoot(model, "Root", model.getRootSchema()); + FakeTargetObject toA = new FakeTargetObject(model, root, "A"); + FakeTargetObject toA1 = new FakeTargetObject(model, toA, "[1]"); + + model.addModelRoot(root); + root.changeAttributes(List.of(), List.of(toA), Map.of(), "Test"); + toA.changeElements(List.of(), List.of(toA1), "Test"); + + assertEquals(toA1, waitOn(listener.get(PathUtils.parse("A[1]")))); + assertEquals(List.of(root, toA, toA1), listener.getAdded()); + } + + @Test + public void testChain2BottomUp() throws Throwable { + EmptyDebuggerObjectModel model = new EmptyDebuggerObjectModel(); + TestReorderedListener listener = + new TestReorderedListener(List.of(PathUtils.parse("A[1]"))); + model.addModelListener(listener.reorderer); + + DefaultTargetModelRoot root = + new DefaultTargetModelRoot(model, "Root", model.getRootSchema()); + FakeTargetObject toA = new FakeTargetObject(model, root, "A"); + FakeTargetObject toA1 = new FakeTargetObject(model, toA, "[1]"); + + toA.changeElements(List.of(), List.of(toA1), "Test"); + root.changeAttributes(List.of(), List.of(toA), Map.of(), "Test"); + model.addModelRoot(root); + + assertEquals(toA1, waitOn(listener.get(PathUtils.parse("A[1]")))); + assertEquals(List.of(root, toA, toA1), listener.getAdded()); + } + + @Test + public void testChain3RootLast() throws Throwable { + EmptyDebuggerObjectModel model = new EmptyDebuggerObjectModel(); + TestReorderedListener listener = + new TestReorderedListener(List.of(PathUtils.parse("A[1].i"))); + model.addModelListener(listener.reorderer); + + DefaultTargetModelRoot root = + new DefaultTargetModelRoot(model, "Root", model.getRootSchema()); + FakeTargetObject toA = new FakeTargetObject(model, root, "A"); + FakeTargetObject toA1 = new FakeTargetObject(model, toA, "[1]"); + FakeTargetObject toA1i = new FakeTargetObject(model, toA1, "i"); + + toA.changeElements(List.of(), List.of(toA1), "Test"); + root.changeAttributes(List.of(), List.of(toA), Map.of(), "Test"); + toA1.changeAttributes(List.of(), List.of(toA1i), Map.of(), "Test"); + model.addModelRoot(root); + + assertEquals(toA1i, waitOn(listener.get(PathUtils.parse("A[1].i")))); + assertEquals(List.of(root, toA, toA1, toA1i), listener.getAdded()); + } + + @Test + public void test2xChain2BottomUpBreadth() throws Throwable { + EmptyDebuggerObjectModel model = new EmptyDebuggerObjectModel(); + TestReorderedListener listener = new TestReorderedListener(List.of( + PathUtils.parse("A[1]"), + PathUtils.parse("B[2]"))); + model.addModelListener(listener.reorderer); + + DefaultTargetModelRoot root = + new DefaultTargetModelRoot(model, "Root", model.getRootSchema()); + FakeTargetObject toA = new FakeTargetObject(model, root, "A"); + FakeTargetObject toA1 = new FakeTargetObject(model, toA, "[1]"); + FakeTargetObject toB = new FakeTargetObject(model, root, "B"); + FakeTargetObject toB2 = new FakeTargetObject(model, toB, "[2]"); + + toA.changeElements(List.of(), List.of(toA1), "Test"); + toB.changeElements(List.of(), List.of(toB2), "Test"); + root.changeAttributes(List.of(), List.of(toA, toB), Map.of(), "Test"); + model.addModelRoot(root); + + assertEquals(toA1, waitOn(listener.get(PathUtils.parse("A[1]")))); + assertEquals(toB2, waitOn(listener.get(PathUtils.parse("B[2]")))); + // TODO: Not sure I can rely on add order where there's no dependency + // E.g., will toA always precede toB, just because it was listed first? + assertEquals(List.of(root, toA, toB, toA1, toB2), listener.getAdded()); + } + + @Test + public void test2xChain2BottomUpDepthRootBefore2nd() throws Throwable { + EmptyDebuggerObjectModel model = new EmptyDebuggerObjectModel(); + TestReorderedListener listener = new TestReorderedListener(List.of( + PathUtils.parse("A[1]"), + PathUtils.parse("B[2]"))); + model.addModelListener(listener.reorderer); + + DefaultTargetModelRoot root = + new DefaultTargetModelRoot(model, "Root", model.getRootSchema()); + FakeTargetObject toA = new FakeTargetObject(model, root, "A"); + FakeTargetObject toA1 = new FakeTargetObject(model, toA, "[1]"); + FakeTargetObject toB = new FakeTargetObject(model, root, "B"); + FakeTargetObject toB2 = new FakeTargetObject(model, toB, "[2]"); + + toA.changeElements(List.of(), List.of(toA1), "Test"); + root.changeAttributes(List.of(), List.of(toA), Map.of(), "Test"); + model.addModelRoot(root); + toB.changeElements(List.of(), List.of(toB2), "Test"); + root.changeAttributes(List.of(), List.of(toB), Map.of(), "Test"); + + assertEquals(toA1, waitOn(listener.get(PathUtils.parse("A[1]")))); + assertEquals(toB2, waitOn(listener.get(PathUtils.parse("B[2]")))); + assertEquals(List.of(root, toA, toA1, toB, toB2), listener.getAdded()); + } + + @Test + public void testEventOrdering() throws Throwable { + EmptyDebuggerObjectModel model = new EmptyDebuggerObjectModel(); + var listener = new TestReorderedListener(List.of(PathUtils.parse("A[r1].i"))) { + List captured; + + @Override + public void registersUpdated(TargetObject bank, Map updates) { + captured = getAdded(); // NB. "duplicates" exception may cause NPE + } + }; + model.addModelListener(listener.reorderer); + + DefaultTargetModelRoot root = + new DefaultTargetModelRoot(model, "Root", model.getRootSchema()); + FakeTargetObject toA = new FakeTargetRegisterBank(model, root, "A"); + FakeTargetObject toA1 = new FakeTargetObject(model, toA, "[r1]"); + FakeTargetObject toA1i = new FakeTargetObject(model, toA1, "i"); + + /** + * Because A's elements will be added before registersUpdated is replayed, we wait on a + * child of [r1], to guarantee registersUpdated has happened + */ + toA.changeElements(List.of(), List.of(toA1), "Test"); + toA.getListeners().fire.registersUpdated(toA, Map.of("r1", new byte[4])); + root.changeAttributes(List.of(), List.of(toA), Map.of(), "Test"); + /** + * CFs may get queued in depth, so add root here to ensure registersUpdated comes before + * toA1i + */ + model.addModelRoot(root); + toA1.changeAttributes(List.of(), List.of(toA1i), Map.of(), "Test"); + + assertEquals(toA1i, waitOn(listener.get(PathUtils.parse("A[r1].i")))); + assertEquals(List.of(root, toA, toA1), listener.captured); + listener.getAdded(); + } + + public static class FakeTargetRoot extends DefaultTargetModelRoot implements TargetEventScope { + public FakeTargetRoot(AbstractDebuggerObjectModel model, String typeHint, + TargetObjectSchema schema) { + super(model, typeHint, schema); + } + } + + public static class FakeTargetThread extends FakeTargetObject implements TargetThread { + public FakeTargetThread(AbstractDebuggerObjectModel model, TargetObject parent, + String name) { + super(model, parent, name); + } + } + + public static class FakeTargetProcess extends FakeTargetObject implements TargetProcess { + public FakeTargetProcess(AbstractDebuggerObjectModel model, TargetObject parent, + String name) { + super(model, parent, name); + } + } + + @Test + public void testEventOrderingResilient() throws Throwable { + EmptyDebuggerObjectModel model = new EmptyDebuggerObjectModel(); + List toWait = PathUtils.parse("Processes[1].Threads[1].i"); + var listener = new TestReorderedListener(List.of(toWait)) { + Map> events = new LinkedHashMap<>(); + + @Override + public void event(TargetObject object, TargetThread eventThread, + TargetEventType type, + String description, List parameters) { + while (events.containsKey(description)) { + description = description + " (DUPLICATE)"; + } + events.put(description, getAdded()); + // Test that errors do not prevent processing of next event(s) + throw new RuntimeException("This stack is supposed to appear"); + } + }; + model.addModelListener(listener.reorderer); + + FakeTargetRoot root = new FakeTargetRoot(model, "Root", model.getRootSchema()); + FakeTargetObject processes = new FakeTargetObject(model, root, "Processes"); + FakeTargetProcess proc1 = new FakeTargetProcess(model, processes, "[1]"); + root.getListeners().fire.event(root, null, TargetEventType.PROCESS_CREATED, + "Process 1 created", List.of(proc1)); + FakeTargetObject p1threads = new FakeTargetObject(model, proc1, "Threads"); + FakeTargetThread thread1 = new FakeTargetThread(model, p1threads, "[1]"); + root.getListeners().fire.event(root, thread1, TargetEventType.THREAD_CREATED, + "Thread 1 created", List.of()); + + p1threads.changeElements(List.of(), List.of(thread1), "Test"); + proc1.changeAttributes(List.of(), List.of(p1threads), Map.of(), "Test"); + processes.changeElements(List.of(), List.of(proc1), "Test"); + root.changeAttributes(List.of(), List.of(processes), Map.of(), "Test"); + model.addModelRoot(root); + thread1.changeAttributes(List.of(), List.of(new FakeTargetObject(model, thread1, "i")), + Map.of(), "Dummy"); + + waitOn(listener.get(toWait)); + assertEquals(List.of("Process 1 created", "Thread 1 created"), + List.copyOf(listener.events.keySet())); + assertTrue(listener.events.get("Process 1 created").contains(proc1)); + assertTrue(listener.events.get("Thread 1 created").contains(thread1)); + } + + @Test + public void testEventOrderingCareful() throws Throwable { + EmptyDebuggerObjectModel model = new EmptyDebuggerObjectModel(); + List toWait = PathUtils.parse("Processes[1].Threads[1].i"); + var listener = new TestReorderedListener(List.of(toWait)) { + Map> events = new LinkedHashMap<>(); + + @Override + public void event(TargetObject object, TargetThread eventThread, + TargetEventType type, + String description, List parameters) { + while (events.containsKey(description)) { + description = description + " (DUPLICATE)"; + } + events.put(description, getAdded()); + // Test that errors do not prevent processing of next event(s) + throw new RuntimeException("This stack is supposed to appear"); + } + }; + model.addModelListener(listener.reorderer); + FakeTargetRoot root = new FakeTargetRoot(model, "Root", model.getRootSchema()); + FakeTargetObject processes = new FakeTargetObject(model, root, "Processes"); + FakeTargetProcess proc1 = new FakeTargetProcess(model, processes, "[1]"); + root.getListeners().fire.event(root, null, TargetEventType.PROCESS_CREATED, + "Process 1 created", List.of(proc1)); + FakeTargetObject p1threads = new FakeTargetObject(model, proc1, "Threads"); + FakeTargetThread thread1 = new FakeTargetThread(model, p1threads, "[1]"); + root.getListeners().fire.event(root, thread1, TargetEventType.THREAD_CREATED, + "Thread 1 created", List.of()); + FakeTargetThread thread2 = new FakeTargetThread(model, p1threads, "[2]"); + root.getListeners().fire.event(root, thread1, TargetEventType.THREAD_CREATED, + "Thread 2 created", List.of()); + + p1threads.changeElements(List.of(), List.of(thread2), "Test"); + proc1.changeAttributes(List.of(), List.of(p1threads), Map.of(), "Test"); + processes.changeElements(List.of(), List.of(proc1), "Test"); + root.changeAttributes(List.of(), List.of(processes), Map.of(), "Test"); + model.addModelRoot(root); + + assertTrue(listener.events.isEmpty()); + + p1threads.changeElements(List.of(), List.of(thread1), "Test"); + thread1.changeAttributes(List.of(), List.of(new FakeTargetObject(model, thread1, "i")), + Map.of(), "Dummy"); + + waitOn(listener.get(toWait)); + assertEquals(List.of("Process 1 created", "Thread 1 created", "Thread 2 created"), + List.copyOf(listener.events.keySet())); + assertTrue(listener.events.get("Process 1 created").contains(proc1)); + assertTrue(listener.events.get("Thread 1 created").contains(thread1)); + assertTrue(listener.events.get("Thread 2 created").contains(thread2)); + } + + @Test + public void testRootLink() throws Throwable { + EmptyDebuggerObjectModel model = new EmptyDebuggerObjectModel(); + TestReorderedListener listener = new TestReorderedListener(List.of(PathUtils.parse(""))); + model.addModelListener(listener.reorderer); + + assertEquals(List.of(), listener.getAdded()); + DefaultTargetModelRoot root = + new DefaultTargetModelRoot(model, "Root", model.getRootSchema()); + root.changeAttributes(List.of(), Map.of("link", root), "Test"); + model.addModelRoot(root); + assertEquals(root, waitOn(listener.get(PathUtils.parse("")))); + } +} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/DebuggerModelTestUtils.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/DebuggerModelTestUtils.java deleted file mode 100644 index 6011636c98..0000000000 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/util/DebuggerModelTestUtils.java +++ /dev/null @@ -1,81 +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.util; - -import java.util.Map; - -import ghidra.async.AsyncTestUtils; -import ghidra.dbg.DebugModelConventions; -import ghidra.dbg.DebugModelConventions.AllRequiredAccess; -import ghidra.dbg.DebuggerObjectModel; -import ghidra.dbg.target.*; -import ghidra.dbg.target.TargetSteppable.TargetStepKind; - -public interface DebuggerModelTestUtils extends AsyncTestUtils { - - default TargetObject root(DebuggerObjectModel model) throws Throwable { - return waitOn(model.fetchModelRoot()); - } - - default T suitable(Class type, TargetObject seed) - throws Throwable { - return waitOn(DebugModelConventions.findSuitable(type, seed)); - } - - default AllRequiredAccess access(TargetObject obj) throws Throwable { - return waitOn(DebugModelConventions.trackAccessibility(obj)); - } - - default void waitAcc(AllRequiredAccess access) throws Throwable { - waitOn(access.waitValue(true)); - } - - default void cli(TargetObject interpreter, String cmd) throws Throwable { - TargetInterpreter as = interpreter.as(TargetInterpreter.class); - waitOn(as.execute(cmd)); - } - - default String captureCli(TargetObject interpreter, String cmd) throws Throwable { - TargetInterpreter as = interpreter.as(TargetInterpreter.class); - return waitOn(as.executeCapture(cmd)); - } - - default void launch(TargetObject launcher, Map args) throws Throwable { - TargetLauncher as = launcher.as(TargetLauncher.class); - waitOn(as.launch(args)); - } - - default void resume(TargetObject resumable) throws Throwable { - TargetResumable as = resumable.as(TargetResumable.class); - waitOn(as.resume()); - } - - default void step(TargetObject steppable, TargetStepKind kind) throws Throwable { - TargetSteppable as = steppable.as(TargetSteppable.class); - waitOn(as.step(kind)); - } - - default TargetObject getFocus(TargetObject scope) { - TargetFocusScope as = scope.as(TargetFocusScope.class); - return as.getFocus(); - } - - default void focus(TargetObject scope, TargetObject focus) throws Throwable { - TargetFocusScope as = scope.as(TargetFocusScope.class); - waitOn(as.requestFocus(focus)); - } - -} diff --git a/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml b/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml new file mode 100644 index 0000000000..10bf631390 --- /dev/null +++ b/Ghidra/Debug/Framework-Debugging/src/test/resources/ghidra/dbg/model/test_schema.xml @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpoint.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpoint.java index 260d299c69..3e2d025f29 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpoint.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/breakpoint/TraceBreakpoint.java @@ -23,6 +23,7 @@ import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRange; import ghidra.trace.model.Trace; import ghidra.trace.model.thread.TraceThread; +import ghidra.util.database.ObjectKey; import ghidra.util.exception.DuplicateNameException; /** @@ -37,6 +38,13 @@ public interface TraceBreakpoint { */ Trace getTrace(); + /** + * Get an opaque unique id for this object, whose hash is immutable + * + * @return the opaque object id + */ + ObjectKey getObjectKey(); + /** * Get the "full name" of this breakpoint * diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java index 8ee51d8e55..fdc5d26e67 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceMemoryOperations.java @@ -179,6 +179,7 @@ public interface TraceMemoryOperations { /** * Set the state of memory over a given time and address range * + *

* Setting state to {@link TraceMemoryState#KNOWN} via this method is not recommended. Setting * bytes will automatically update the state accordingly. * @@ -221,6 +222,7 @@ public interface TraceMemoryOperations { /** * Get the entry recording the most recent state at the given snap and address * + *

* The entry includes the entire entry at that snap. Parts occluded by more recent snaps are not * subtracted from the entry's address range. * diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java index 66daab6d7d..e5d0c2595f 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RowWrappedEnumeratedColumnTableModel.java @@ -21,23 +21,34 @@ import java.util.stream.Collectors; import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn; -public class RowWrappedEnumeratedColumnTableModel & EnumeratedTableColumn, R, T> +/** + * A table model where the columns are enumerated, and the rows are wrappers on the objects being + * displayed + * + * @param the type of columns + * @param the type of (immutable) keys for uniquely identifying an object + * @param the type of rows + * @param the type of objects being wrapped + */ +public class RowWrappedEnumeratedColumnTableModel & EnumeratedTableColumn, K, R, T> extends DefaultEnumeratedColumnTableModel { + private final Function keyFunc; private final Function wrapper; - private final Map map = new HashMap<>(); + private final Map map = new HashMap<>(); public RowWrappedEnumeratedColumnTableModel(String name, Class colType, - Function wrapper) { + Function keyFunc, Function wrapper) { super(name, colType); + this.keyFunc = keyFunc; this.wrapper = wrapper; } protected synchronized R rowFor(T t) { - return map.computeIfAbsent(t, wrapper); + return map.computeIfAbsent(keyFunc.apply(t), k -> wrapper.apply(t)); } protected synchronized R delFor(T t) { - return map.remove(t); + return map.remove(keyFunc.apply(t)); } protected synchronized List rowsFor(Collection c) { @@ -69,7 +80,7 @@ public class RowWrappedEnumeratedColumnTableModel & Enumerated map.keySet().removeAll(c); } - public synchronized Map getMap() { + public synchronized Map getMap() { return Map.copyOf(map); } } diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/lifecycle/Experimental.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/lifecycle/Experimental.java index 32446012e5..355d3cc0ea 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/lifecycle/Experimental.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/lifecycle/Experimental.java @@ -22,6 +22,7 @@ import java.lang.annotation.Target; /** * An annotation for experimental things * + *

* The items are intended to become part of the public API, but the interfaces are unstable, and * there's no guarantee they will ever become public. */ diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/lifecycle/Transitional.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/lifecycle/Transitional.java new file mode 100644 index 0000000000..c7267d84a3 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/lifecycle/Transitional.java @@ -0,0 +1,24 @@ +/* ### + * 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.lifecycle; + +/** + * The item is present for transitional purposes only and will soon be removed + */ +public @interface Transitional { + +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBAnnotatedObject.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBAnnotatedObject.java index c0941bc1c4..991e6de6d1 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBAnnotatedObject.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBAnnotatedObject.java @@ -37,7 +37,7 @@ public class DBAnnotatedObject extends DatabaseObject { this.store = store; this.record = record; if (store != null) { - this.adapter = store.errHandler; + this.adapter = store.adapter; this.codecs = (List) store.codecs; } else { @@ -46,6 +46,15 @@ public class DBAnnotatedObject extends DatabaseObject { } } + /** + * Get an opaque unique id for this object, whose hash is immutable + * + * @return the opaque object id + */ + public ObjectKey getObjectKey() { + return new ObjectKey(store.adapter, store.table.getName(), key); + } + @SuppressWarnings({ "rawtypes", "unchecked" }) protected void write(DBObjectColumn column) { DBFieldCodec codec = codecs.get(column.columnNumber); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStore.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStore.java index 17081382ce..737b75719f 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStore.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/DBCachedObjectStore.java @@ -366,7 +366,7 @@ public class DBCachedObjectStore implements ErrorHa return it.hasNext(); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return false; } } @@ -377,7 +377,7 @@ public class DBCachedObjectStore implements ErrorHa return fromRaw(it.next()); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return null; } } @@ -388,7 +388,7 @@ public class DBCachedObjectStore implements ErrorHa it.delete(); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); } } }; @@ -402,7 +402,7 @@ public class DBCachedObjectStore implements ErrorHa return iterator(rawIterator(direction, keyRange)); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return null; } } @@ -418,7 +418,7 @@ public class DBCachedObjectStore implements ErrorHa } } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); } } @@ -433,7 +433,7 @@ public class DBCachedObjectStore implements ErrorHa } } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); } } @@ -479,7 +479,7 @@ public class DBCachedObjectStore implements ErrorHa } } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); } return result; } @@ -685,7 +685,7 @@ public class DBCachedObjectStore implements ErrorHa } }; - final ErrorHandler errHandler; + final DBCachedDomainObjectAdapter adapter; final DBHandle dbh; final DBObjectCache cache; private final Class objectType; @@ -704,7 +704,7 @@ public class DBCachedObjectStore implements ErrorHa public DBCachedObjectStore(DBCachedDomainObjectAdapter adapter, Class objectType, DBAnnotatedObjectFactory factory, Table table) { - this.errHandler = adapter; + this.adapter = adapter; this.dbh = adapter.getDBHandle(); this.objectType = objectType; this.factory = factory; @@ -715,13 +715,13 @@ public class DBCachedObjectStore implements ErrorHa this.lock = adapter.getReadWriteLock(); this.codecs = DBCachedObjectStoreFactory.getCodecs(objectType); - this.asForwardMap = new DBCachedObjectStoreMap<>(this, errHandler, lock, Direction.FORWARD); + this.asForwardMap = new DBCachedObjectStoreMap<>(this, adapter, lock, Direction.FORWARD); this.asForwardKeySet = - new DBCachedObjectStoreKeySet(this, errHandler, lock, Direction.FORWARD); + new DBCachedObjectStoreKeySet(this, adapter, lock, Direction.FORWARD); this.asForwardValueCollection = - new DBCachedObjectStoreValueCollection<>(this, errHandler, lock, Direction.FORWARD); + new DBCachedObjectStoreValueCollection<>(this, adapter, lock, Direction.FORWARD); this.asForwardEntrySet = - new DBCachedObjectStoreEntrySet<>(this, errHandler, lock, Direction.FORWARD); + new DBCachedObjectStoreEntrySet<>(this, adapter, lock, Direction.FORWARD); } /** @@ -777,7 +777,7 @@ public class DBCachedObjectStore implements ErrorHa } } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); } return i; } @@ -815,7 +815,7 @@ public class DBCachedObjectStore implements ErrorHa return keyRange.contains(rec.getKey()); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return false; } } @@ -825,7 +825,7 @@ public class DBCachedObjectStore implements ErrorHa return keys.typedContains(key); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return false; } } @@ -835,7 +835,7 @@ public class DBCachedObjectStore implements ErrorHa return objects.typedContains(obj); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return false; } } @@ -862,7 +862,7 @@ public class DBCachedObjectStore implements ErrorHa return doCreate(key); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return null; } } @@ -877,7 +877,7 @@ public class DBCachedObjectStore implements ErrorHa return doCreate(table.getKey()); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return null; } } @@ -904,7 +904,7 @@ public class DBCachedObjectStore implements ErrorHa } @SuppressWarnings("unchecked") DBFieldCodec castCodec = (DBFieldCodec) codec; - return new DBCachedObjectIndex<>(this, errHandler, castCodec, columnIndex, Range.all(), + return new DBCachedObjectIndex<>(this, adapter, castCodec, columnIndex, Range.all(), Direction.FORWARD); } @@ -922,7 +922,7 @@ public class DBCachedObjectStore implements ErrorHa return objects.typedRemove(obj) != null; } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return false; } } @@ -932,7 +932,7 @@ public class DBCachedObjectStore implements ErrorHa return keys.typedRemove(key); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return null; } } @@ -943,7 +943,7 @@ public class DBCachedObjectStore implements ErrorHa cache.invalidate(); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); } } @@ -962,7 +962,7 @@ public class DBCachedObjectStore implements ErrorHa cache.delete(List.of(new KeyRange(min, max))); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); } } @@ -975,7 +975,7 @@ public class DBCachedObjectStore implements ErrorHa return supplier.get(); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return null; } } @@ -985,7 +985,7 @@ public class DBCachedObjectStore implements ErrorHa return objects.get(key); } catch (IOException e) { - errHandler.dbError(e); + adapter.dbError(e); return null; } } @@ -1013,7 +1013,7 @@ public class DBCachedObjectStore implements ErrorHa protected DBCachedObjectStoreFoundKeysValueCollection findObjects(int columnIndex, Field field) throws IOException { Field[] found = table.findRecords(field, columnIndex); - return new DBCachedObjectStoreFoundKeysValueCollection<>(this, errHandler, lock, found); + return new DBCachedObjectStoreFoundKeysValueCollection<>(this, adapter, lock, found); } protected Iterator iterator(int columnIndex, Range fieldRange, Direction direction) @@ -1041,7 +1041,7 @@ public class DBCachedObjectStore implements ErrorHa @Override public void dbError(IOException e) { - errHandler.dbError(e); + adapter.dbError(e); } /** diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/ObjectKey.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/ObjectKey.java new file mode 100644 index 0000000000..92e915cd1a --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/database/ObjectKey.java @@ -0,0 +1,83 @@ +/* ### + * 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.util.database; + +import java.util.Objects; + +import ghidra.framework.data.DomainObjectAdapterDB; + +/** + * Enough information to uniquely identify a trace object + */ +public class ObjectKey implements Comparable { + + private final DomainObjectAdapterDB adapter; + private final String tableName; + private final long key; + + private final int hash; + + public ObjectKey(DomainObjectAdapterDB adapter, String tableName, long key) { + this.adapter = adapter; + this.tableName = tableName; + this.key = key; + this.hash = Objects.hash(System.identityHashCode(adapter), tableName, key); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ObjectKey)) { + return false; + } + ObjectKey that = (ObjectKey) obj; + if (this.adapter != that.adapter) { + return false; + } + if (!(Objects.equals(this.tableName, that.tableName))) { + return false; + } + if (this.key != that.key) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public int compareTo(ObjectKey that) { + int result; + if (this.adapter != that.adapter) { + result = this.adapter.getName().compareTo(that.adapter.getName()); + if (result != 0) { + return result; + } + return System.identityHashCode(this.adapter) - System.identityHashCode(that.adapter); + } + result = this.tableName.compareTo(that.tableName); + if (result != 0) { + return result; + } + result = Long.compareUnsigned(this.key, that.key); + if (result != 0) { + return result; + } + return 0; + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerMap.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerMap.java index 19aa2db55d..d1fd7148bc 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerMap.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/util/datastruct/ListenerMap.java @@ -61,8 +61,13 @@ public class ListenerMap { protected static final AtomicReference firstExc = new AtomicReference<>(); protected static void reportError(Object listener, Throwable e) { - Msg.error(listener, "Listener " + listener + " caused unexpected exception", e); - firstExc.accumulateAndGet(e, (o, n) -> o == null ? n : o); + if (e instanceof RejectedExecutionException) { + Msg.trace(listener, "Listener invocation rejected: " + e); + } + else { + Msg.error(listener, "Listener " + listener + " caused unexpected exception", e); + firstExc.accumulateAndGet(e, (o, n) -> o == null ? n : o); + } } /** @@ -129,9 +134,6 @@ public class ListenerMap { try { method.invoke(l, args); } - catch (RejectedExecutionException e) { - Msg.trace(this, "Listener invocation rejected", e); - } catch (InvocationTargetException e) { Throwable cause = e.getCause(); reportError(l, cause); @@ -230,6 +232,12 @@ public class ListenerMap { @SuppressWarnings("unchecked") public T fire(Class ext) { + if (ext == iface) { + return ext.cast(fire); + } + if (!iface.isAssignableFrom(ext)) { + throw new IllegalArgumentException("Cannot fire on less-specific interface"); + } return (T) extFires.computeIfAbsent(ext, e -> (P) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] { iface, ext }, new ListenerHandler<>(ext))); diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/ProxyUtilities.java b/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/ProxyUtilities.java index 21d9b01584..3a3b72edff 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/ProxyUtilities.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/ProxyUtilities.java @@ -75,7 +75,7 @@ public enum ProxyUtilities { public static T composeOnDelegate(Class iface, T delegate, List> mixins, MethodHandles.Lookup lookup) { Class[] allIface = new Class[1 + mixins.size()]; - mixins.toArray(allIface); + allIface = mixins.toArray(allIface); allIface[allIface.length - 1] = iface; ComposedHandler handler = new ComposedHandler(delegate, lookup); return (T) Proxy.newProxyInstance(delegate.getClass().getClassLoader(), allIface, handler);