/* ### * 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 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.*; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.junit.*; import agent.dbgeng.dbgeng.DbgEngTest; import agent.dbgeng.model.iface1.DbgModelTargetLauncher; import agent.dbgeng.testutil.DummyProc; import ghidra.async.*; import ghidra.dbg.*; import ghidra.dbg.DebugModelConventions.AllRequiredAccess; import ghidra.dbg.error.DebuggerModelNoSuchPathException; import ghidra.dbg.error.DebuggerModelTypeException; import ghidra.dbg.target.*; import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetConsole.Channel; import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.XmlSchemaContext; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.Address; import ghidra.test.AbstractGhidraHeadlessIntegrationTest; import ghidra.util.*; public abstract class AbstractModelForDbgTest extends AbstractGhidraHeadlessIntegrationTest { protected static final Map AMD64_TEST_REG_VALUES = Map.of( // "rax", NumericUtilities.convertStringToBytes("0123456789abcdef"), // "ymm0", NumericUtilities.convertStringToBytes("" + // "0011223344556677" + "8899aabbccddeeff")); // TODO: for some reason, the dbgeng thinks ymm0 is VECTOR128 //"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() throws Exception; 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 static T waitOn(CompletableFuture future) throws Throwable { try { return future.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS); } catch (ExecutionException e) { throw e.getCause(); } } @Before public void setUpDbgEngTest() { DbgEngTest.assumeDbgengDLLLoadable(); } @Test public void testInitFinish() throws Throwable { try (ModelHost m = modelHost()) { waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).finish()); } } @Test public void testNonExistentPathGivesNull() throws Throwable { try (ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { model.fetchModelObject("Doesn't exist").handle(seq::next); }, TypeSpec.cls(TargetObject.class)).then((obj, seq) -> { assertNull(obj); seq.exit(); }).finish()); } } @Test public void testSessionLaunch() throws Throwable { try (ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference launcher = new AtomicReference<>(); TypeSpec> t = TypeSpec.auto(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(DbgModelTargetLauncher.class, root.get()) .handle(seq::next); }, launcher).then(seq -> { Msg.debug(this, "Launching..."); launcher.get().launch("notepad", "junk.txt").handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting Processes (after launch)..."); model.fetchObjectElements(List.of("Sessions", "[0]")) .handle(seq::next); }, t).then((children, seq) -> { Msg.debug(this, "Processes after: " + children); assertEquals(1, children.size()); seq.exit(); }).finish()); } } @Test public void testProcessLaunch() throws Throwable { try (ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference session = new AtomicReference<>(); AtomicReference rootAccess = new AtomicReference<>(); AtomicReference launcher = new AtomicReference<>(); AtomicReference launcherAccess = new AtomicReference<>(); TypeSpec> t = TypeSpec.auto(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, rootAccess).then(seq -> { Msg.debug(this, "Waiting for session access..."); rootAccess.get().waitValue(true).handle(seq::next); }).then(seq -> { model.fetchModelObject(List.of("Sessions", "[0]")).handle(seq::next); }, session).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(DbgModelTargetLauncher.class, session.get()).handle(seq::next); }, TypeSpec.cls(TargetObject.class)).then((obj, seq) -> { assertTrue(obj.getInterfaceNames().contains("Launcher")); launcher.set((DbgModelTargetLauncher) obj); Msg.debug(this, "Tracking process access..."); DebugModelConventions.trackAccessibility(obj).handle(seq::next); }, launcherAccess).then(seq -> { Msg.debug(this, "Waiting for process access..."); launcherAccess.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Launching..."); launcher.get().launch("notepad", "junk.txt").handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Waiting for session access (again)..."); rootAccess.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting Processes (after launch)..."); model.fetchObjectElements(List.of("Sessions", "[0]", "Processes")) .handle(seq::next); }, t).then((elements, seq) -> { Msg.debug(this, "Processes after: " + elements); assertEquals(1, elements.size()); seq.exit(); }).finish()); } } @Test public void testListAvailableProcesses() throws Throwable { try (DummyProc np = runProc("notepad"); ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { model.fetchObjectElements(List.of("Sessions", "[0]", "Available")) .handle(seq::next); }, DebuggerObjectModel.ELEMENT_MAP_TYPE).then((available, seq) -> { assertTrue(available.containsKey(Long.toString(np.pid))); seq.exit(); }).finish()); } } @Test public void testListProcesses() throws Throwable { try (DummyProc np = runProc("notepad"); ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference proc = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(TargetAttacher.class, root.get()) .handle( seq::next); }, proc).then(seq -> { Msg.debug(this, "Attaching to bogus path..."); TargetAttacher attacher = proc.get().as(TargetAttacher.class); TODO(); seq.next(null, null); }).then(seq -> { model.fetchObjectElements(List.of("Sessions", "[0]", "Processes")) .handle(seq::next); // NB: listProcesses will fail if no process is being debugged }, DebuggerObjectModel.ELEMENT_MAP_TYPE).then((processes, seq) -> { assertEquals(1, processes.size()); seq.exit(); }).finish()); } } @Test public void testSessionAttachKill() throws Throwable { try (DummyProc np = runProc("notepad"); ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference attacher = new AtomicReference<>(); AtomicReference attachable = new AtomicReference<>(); TypeSpec> t = TypeSpec.auto(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { // Msg.debug(this, "Getting Processes (before attach)..."); // model.getObjectElements(List.of("Sessions", "[0]", "Processes")).handle(seq::next); // }, t).then((children, seq) -> { // Msg.debug(this, "Processes before: " + children); // assertEquals(1, children.size()); AsyncFence fence = new AsyncFence(); Msg.debug(this, "Finding TargetAttacher..."); fence.include(DebugModelConventions.findSuitable(TargetAttacher.class, root.get()) .thenAccept(a -> { Msg.debug(this, " Got TargetAttacher: " + a); attacher.set(a); })); fence.include(model.fetchModelObject("Sessions", "[0]", "Available", "[" + np.pid + "]") .thenAccept(o -> { Msg.debug(this, " Got Attachable: " + o); assertTrue(o.getInterfaceNames().contains("Attachable")); attachable.set((TargetAttachable) o); })); fence.ready().handle(seq::next); }).then(seq -> { Msg.debug(this, "Attaching..."); attacher.get().attach(attachable.get()).handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Waiting for session access (again)..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting Processes (after attach)..."); model.fetchObjectElements(List.of("Sessions", "[0]", "Processes")) .handle(seq::next); }, t).then((elements, seq) -> { Msg.debug(this, "Processes after: " + elements); assertEquals(1, elements.size()); Msg.debug(this, "Killing..."); TargetObject attached = elements.get("0"); assertTrue(attached.getInterfaceNames().contains("Killable")); TargetKillable killable = (TargetKillable) attached; killable.kill().handle(seq::next); }).finish()); } } @Test public void testProcessAttachKill() throws Throwable { try (DummyProc np = runProc("notepad"); ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference obj = new AtomicReference<>(); TypeSpec> t = TypeSpec.auto(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(TargetAttacher.class, root.get()) .handle( seq::next); }, obj).then(seq -> { Msg.debug(this, "Attaching..."); TargetAttacher attacher = obj.get().as(TargetAttacher.class); attacher.attach(np.pid) .handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Waiting for session access (again, again)..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting Processes (after attach)..."); model.fetchObjectElements(List.of("Sessions", "[0]", "Processes")) .handle(seq::next); }, t).then((elements, seq) -> { Msg.debug(this, "Processes after: " + elements); assertEquals(1, elements.size()); Msg.debug(this, "Killing..."); TargetObject attached = elements.get("0"); assertTrue(attached.getInterfaceNames().contains("Killable")); TargetKillable killable = (TargetKillable) attached; killable.kill().handle(seq::next); }).finish()); } } @Test public void testProcessAttachContKill() throws Throwable { try (DummyProc np = runProc("notepad"); ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference obj = new AtomicReference<>(); TypeSpec> t = TypeSpec.auto(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(TargetAttacher.class, root.get()) .handle( seq::next); }, obj).then(seq -> { Msg.debug(this, "Attaching..."); TargetAttacher attacher = obj.get().as(TargetAttacher.class); attacher.attach(np.pid) .handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Waiting for session access (again, again)..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting Process 1..."); model.fetchModelObject("Sessions", "[0]", "Processes", "[0]").handle(seq::next); }, obj).then(seq -> { Msg.debug(this, "Resuming..."); TargetResumable resumable = obj.get().as(TargetResumable.class); resumable.resume().handle(seq::next); }).then(seq -> { Msg.debug(this, "Waiting for session access (after resume)..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting Processes (after attach)..."); model.fetchObjectElements(List.of("Sessions", "[0]", "Processes")) .handle(seq::next); }, t).then((elements, seq) -> { Msg.debug(this, "Processes after: " + elements); assertEquals(1, elements.size()); Msg.debug(this, "Killing..."); TargetObject attached = elements.get("0"); assertTrue(attached.getInterfaceNames().contains("Killable")); TargetKillable killable = (TargetKillable) attached; killable.kill().handle(seq::nextIgnore); }).finish()); } } @Test public void testLaunchContExit() throws Throwable { try (ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference obj = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(DbgModelTargetLauncher.class, root.get()) .handle(seq::next); }, obj).then(seq -> { Msg.debug(this, "Launching..."); TargetLauncher launcher = obj.get().as(TargetLauncher.class); launcher.launch(Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, "notepad junk.txt")) .handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Waiting for session access (again)..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting Process 1..."); model.fetchModelObject("Sessions", "[0]", "Processes", "[0]").handle(seq::next); }, obj).then(seq -> { Msg.debug(this, "Resuming..."); TargetResumable resumable = obj.get().as(TargetResumable.class); resumable.resume().handle(seq::next); }).then(seq -> { Msg.debug(this, "Waiting for session access (after resume)..."); access.get().waitValue(true).handle(seq::next); }).finish()); } } @Test(expected = DebuggerModelNoSuchPathException.class) public void testAttachNoObjectErr() throws Throwable { try (ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference proc = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(TargetAttacher.class, root.get()) .handle( seq::next); }, proc).then(seq -> { Msg.debug(this, "Attaching to bogus path..."); TargetAttacher attacher = proc.get().as(TargetAttacher.class); TODO(); seq.next(null, null); }).finish()); } } @Test(expected = DebuggerModelTypeException.class) public void testAttachNonAttachableErr() throws Throwable { try (ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference proc = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(TargetAttacher.class, root.get()) .handle( seq::next); }, proc).then(seq -> { Msg.debug(this, "Attaching to bogus path..."); TargetAttacher attacher = proc.get().as(TargetAttacher.class); TODO(); seq.next(null, null); }).finish()); 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(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); 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); lastOut.set(str, null); } }; waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object..."); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { root.get().addListener(l); Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Running command..."); TargetInterpreter interpreter = root.get().as(TargetInterpreter.class); interpreter.execute(".echo xyzzy").handle(seq::next); }).then(seq -> { Msg.debug(this, "Waiting for expected output..."); lastOut.waitValue("xyzzy\n").handle(seq::next); }).finish()); } } @Test public void testExecuteCapture() throws Throwable { try (ModelHost m = modelHost(); CatchOffThread offThread = new CatchOffThread()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); DebuggerModelListener l = new DebuggerModelListener() { @Override public void consoleOutput(TargetObject interpreter, Channel channel, String out) { String str = new String(out); Msg.debug(this, "Got " + channel + " output: " + str); if (!str.contains("test")) { return; } offThread.catching(() -> fail("Unexpected output:" + str)); } }; waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object..."); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { root.get().addListener(l); Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Running command with capture..."); TargetInterpreter interpreter = root.get().as(TargetInterpreter.class); interpreter.executeCapture(".echo xyzzy").handle(seq::next); }, TypeSpec.STRING).then((out, seq) -> { Msg.debug(this, "Captured: " + out); assertTrue(out.contains("xyzzy")); seq.exit(); }).finish()); } } @Test public void testGetBreakKinds() throws Throwable { try (ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference breaks = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object..."); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding breakpoint container..."); DebugModelConventions.findSuitable(TargetBreakpointSpecContainer.class, root.get()) .handle(seq::next); }, breaks).then(seq -> { Msg.debug(this, "Got: " + breaks); TargetBreakpointKindSet kinds = breaks.get().getSupportedBreakpointKinds(); Msg.debug(this, "Supports: " + kinds); assertEquals(4, kinds.size()); seq.exit(); }).finish()); } } public static final TypeSpec> BL_COL_SPEC = null; @Test public void testPlaceBreakpoint() throws Throwable { try (ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference launcher = new AtomicReference<>(); AtomicReference breaks = new AtomicReference<>(); AtomicReference loc = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object..."); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(DbgModelTargetLauncher.class, root.get()) .handle( seq::next); }, launcher).then(seq -> { Msg.debug(this, "Launching..."); launcher.get() .launch(Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, "notepad junk.txt")) .handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Finding breakpoint container..."); DebugModelConventions.findSuitable(TargetBreakpointSpecContainer.class, root.get()) .handle(seq::next); }, breaks).then(seq -> { Msg.debug(this, "Placing breakpoint..."); breaks.get() .placeBreakpoint("0x7ff7d52c8987", Set.of(TargetBreakpointKind.SW_EXECUTE)) .handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting breakpoint specs..."); breaks.get() .fetchElements() .handle(seq::next); }, DebuggerObjectModel.ELEMENT_MAP_TYPE).then((specs, seq) -> { Msg.debug(this, "Got specs: " + specs); assertEquals(1, specs.size()); TargetBreakpointSpec spec = specs.get("0").as(TargetBreakpointSpec.class); spec.getLocations().handle(seq::next); }, BL_COL_SPEC).then((es, seq) -> { Msg.debug(this, "Got effectives: " + es); assertEquals(1, es.size()); loc.set(es.iterator().next()); Address addr = loc.get().getAddress(); Msg.debug(this, "Got address: " + addr); seq.exit(); }).finish()); } } @Test public void testStack() throws Throwable { try (ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference breaks = new AtomicReference<>(); AtomicReference launcher = new AtomicReference<>(); AtomicReference obj = new AtomicReference<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(TargetLauncher.class, root.get()) .handle( seq::next); }, launcher).then(seq -> { Msg.debug(this, "Launching..."); launcher.get() .launch(Map.of("args", List.of("notepad", "junk.txt"))) .handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Waiting for session access (again)..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding breakpoint container..."); DebugModelConventions.findSuitable(TargetBreakpointSpecContainer.class, root.get()) .handle(seq::next); }, breaks).then(seq -> { Msg.debug(this, "Placing breakpoint..."); breaks.get() .placeBreakpoint("0x7ff7d52c8987", Set.of(TargetBreakpointKind.SW_EXECUTE)) .handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting Process 1..."); model.fetchModelObject("Sessions", "[0]", "Processes", "[0]").handle(seq::next); }, obj).then(seq -> { Msg.debug(this, "Resuming..."); TargetResumable resumable = obj.get().as(TargetResumable.class); resumable.resume().handle(seq::next); }).then(seq -> { Msg.debug(this, "Waiting for session access (after resume)..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { obj.get().fetchSubElements("Threads", "[0]", "Stack").handle(seq::next); }, DebuggerObjectModel.ELEMENT_MAP_TYPE).then((frames, seq) -> { 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()); } long offset = frames.get("0") .getTypedAttributeNowByName("pc", Address.class, null) .getOffset(); assertEquals(0x7fffadd6e890L, offset); //assertEquals("main", frames.get("" + (frames.size() - 1)) // .getTypedAttributeNowByName("function", String.class, null)); seq.exit(); }).finish()); } } @Test public void testRegisters() throws Throwable { try (ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference launcher = new AtomicReference<>(); AtomicReference proc = new AtomicReference<>(); AtomicReference bank = new AtomicReference<>(); Set descs = new LinkedHashSet<>(); waitOn(AsyncUtils.sequence(TypeSpec.VOID).then(seq -> { m.init().handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(DbgModelTargetLauncher.class, root.get()) .handle( seq::next); }, launcher).then(seq -> { Msg.debug(this, "Launching..."); launcher.get() .launch(Map.of("args", List.of("notepad", "junk.txt"))) .handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Waiting for session access (again)..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Getting Process 1..."); model.fetchModelObject(List.of("Sessions", "[0]", "Processes", "[0]")) .handle( seq::next); }, proc).then(seq -> { proc.get().fetchSuccessor("Threads", "[0]", "Stack", "[0]").thenAccept(top -> { bank.set(top.as(TargetRegisterBank.class)); }).handle(seq::next); }).then(seq -> { Msg.debug(this, "Got bank: " + bank.get()); Msg.debug(this, "Descriptions ref: " + bank.get().getDescriptions()); TargetRegisterContainer cont = bank.get().getDescriptions(); Msg.debug(this, "Register descriptions: " + cont); cont.getRegisters().thenAccept(descs::addAll).handle(seq::next); }).then(seq -> { Msg.debug(this, "Elements: "); for (TargetRegister reg : descs) { Msg.debug(this, " " + reg.getIndex() + ": " + reg.getBitLength()); } bank.get().readRegisters(descs).handle(seq::next); }, TypeSpec.map(String.class, byte[].class)).then((data, seq) -> { 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"); bank.get().writeRegistersNamed(AMD64_TEST_REG_VALUES).handle(seq::next); }).then(seq -> { Msg.debug(this, "Flushing cache"); bank.get().invalidateCaches().handle(seq::next); }).then(seq -> { Msg.debug(this, "Re-reading values"); // NOTE: Can't reliably step here since rax may be referenced by the instruction bank.get().readRegistersNamed(AMD64_TEST_REG_VALUES.keySet()).handle(seq::next); }, TypeSpec.map(String.class, byte[].class)).then((data, seq) -> { assertEquals(hexlify(AMD64_TEST_REG_VALUES), hexlify(data)); seq.exit(); }).finish()); } } @Test public void testFocusProcesses() throws Throwable { try (DummyProc np = runProc("notepad"); ModelHost m = modelHost()) { DebuggerObjectModel model = m.getModel(); AtomicReference root = new AtomicReference<>(); AtomicReference scope = new AtomicReference<>(); AtomicReference access = new AtomicReference<>(); AtomicReference processes = new AtomicReference<>(); AtomicReference obj1 = new AtomicReference<>(); AtomicReference obj2 = new AtomicReference<>(); AsyncReference, Void> focusProcPath = new AsyncReference<>(); AsyncReference processCount = new AsyncReference<>(); DebuggerModelListener procListener = new DebuggerModelListener() { @Override public void elementsChanged(TargetObject parent, Collection removed, Map added) { processCount.set(processes.get().getCachedElements().size(), 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); }).then(seq -> { Msg.debug(this, "Getting session root object"); model.fetchModelObject("Sessions", "[0]").handle(seq::next); }, root).then(seq -> { scope.set(root.get().as(TargetFocusScope.class)); scope.get().addListener(focusListener); Msg.debug(this, "Tracking session access..."); DebugModelConventions.trackAccessibility(root.get()).handle(seq::next); }, access).then(seq -> { Msg.debug(this, "Waiting for session access..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(DbgModelTargetLauncher.class, root.get()) .handle( seq::next); }, obj1).then(seq -> { Msg.debug(this, "Attaching..."); TargetAttacher attacher = obj1.get().as(TargetAttacher.class); attacher.attach(np.pid).handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Getting processes container"); model.fetchModelObject("Sessions", "[0]", "Processes").handle(seq::next); }, processes).then(seq -> { processes.get().addListener(procListener); Msg.debug(this, "Finding TargetLauncher..."); DebugModelConventions.findSuitable(DbgModelTargetLauncher.class, root.get()) .handle( seq::next); }, obj2).then(seq -> { Msg.debug(this, "Creating another process"); TargetLauncher launcher = obj2.get().as(TargetLauncher.class); launcher.launch(Map.of(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, "notepad junk.txt")) .handle(seq::nextIgnore); }).then(seq -> { Msg.debug(this, "Waiting for session access (again)..."); access.get().waitValue(true).handle(seq::next); }).then(seq -> { assertTrue(PathUtils.isAncestor(List.of("Sessions", "[0]", "Processes", "[1]"), scope.get().getFocus().getPath())); // Redundant, but verifies the listener is keeping up assertEquals(List.of("Sessions", "[0]", "Processes", "[1]"), focusProcPath.get()); Msg.debug(this, "Requesting focus on process 0"); AsyncFence fence = new AsyncFence(); TargetObject p2 = model.getModelObject("Sessions", "[0]", "Processes", "[0]"); fence.include(focusProcPath.waitValue(p2.getPath())); fence.include(scope.get().requestFocus(p2)); fence.ready().handle(seq::next); }).then(seq -> { assertTrue(PathUtils.isAncestor(List.of("Sessions", "[0]", "Processes", "[0]"), scope.get().getFocus().getPath())); // Redundant, but verifies the listener is keeping up assertEquals(List.of("Sessions", "[0]", "Processes", "[0]"), focusProcPath.get()); seq.exit(); }).finish()); } } protected void init(ModelHost m) throws Throwable { waitOn(m.init()); } @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("Debugger", rootSchema.getName().toString()); } } }