From ad6cb5892d30d81553feb9e7f96be8c0dc9fba3b Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:20:38 -0400 Subject: [PATCH] GP-3872: Port scripting API to Trace RMI --- .../app/services/TraceRmiLauncherService.java | 9 + .../model/DebuggerObjectActionContext.java | 2 +- ...DebuggerSingleObjectPathActionContext.java | 34 + .../java/ghidra/debug/api/target/Target.java | 14 + .../ghidra/debug/flatapi/FlatDebuggerAPI.java | 883 +++-------- .../flatapi/FlatDebuggerRecorderAPI.java | 656 +++++++++ .../debug/flatapi/FlatDebuggerRmiAPI.java | 162 ++ .../launcher/AbstractTraceRmiLaunchOffer.java | 9 + .../TraceRmiLauncherServicePlugin.java | 9 + .../service/tracermi/TraceRmiTarget.java | 34 +- .../tracermi/TestTraceRmiConnection.java | 29 + .../ghidra_scripts/DemoDebuggerScript.java | 32 +- .../MonitorModelEventsScript.java | 4 +- .../gui/control/DebuggerControlPlugin.java | 2 +- .../control/DebuggerMethodActionsPlugin.java | 2 +- .../gui/memory/DebuggerRegionsPanel.java | 1 + .../gui/memory/DebuggerRegionsProvider.java | 2 +- .../model/AbstractObjectsTableBasedPanel.java | 1 + .../gui/model/DebuggerModelProvider.java | 1 + .../gui/modules/DebuggerModulesPanel.java | 1 + .../gui/modules/DebuggerModulesProvider.java | 2 +- .../service/model/TraceRecorderTarget.java | 22 +- .../debug/service/target/AbstractTarget.java | 5 + .../debug/service/control/MockTarget.java | 11 +- .../debug/flatapi/FlatDebuggerAPITest.java | 1301 ----------------- ...ctGhidraHeadedDebuggerIntegrationTest.java | 95 +- .../launcher/TestTraceRmiLaunchOpinion.java | 94 ++ .../flatapi/AbstractFlatDebuggerAPITest.java | 293 ++++ .../AbstractLiveFlatDebuggerAPITest.java | 101 ++ .../flatapi/DeadFlatDebuggerAPITest.java | 539 +++++++ .../flatapi/FlatDebuggerRecorderAPITest.java | 502 +++++++ .../debug/flatapi/FlatDebuggerRmiAPITest.java | 282 ++++ 32 files changed, 3103 insertions(+), 2031 deletions(-) rename Ghidra/Debug/{Debugger/src/main/java/ghidra/app/plugin/core/debug/gui => Debugger-api/src/main/java/ghidra/debug/api}/model/DebuggerObjectActionContext.java (96%) create mode 100644 Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerSingleObjectPathActionContext.java create mode 100644 Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPI.java create mode 100644 Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRmiAPI.java delete mode 100644 Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java create mode 100644 Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TestTraceRmiLaunchOpinion.java create mode 100644 Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/AbstractFlatDebuggerAPITest.java create mode 100644 Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/AbstractLiveFlatDebuggerAPITest.java create mode 100644 Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/DeadFlatDebuggerAPITest.java create mode 100644 Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPITest.java create mode 100644 Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRmiAPITest.java diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiLauncherService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiLauncherService.java index 8d769d7ad23..e88e008139a 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiLauncherService.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/TraceRmiLauncherService.java @@ -16,6 +16,7 @@ package ghidra.app.services; import java.util.Collection; +import java.util.List; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; import ghidra.framework.plugintool.ServiceInfo; @@ -35,4 +36,12 @@ public interface TraceRmiLauncherService { * @return the offers */ Collection getOffers(Program program); + + /** + * Get offers with a saved configuration, ordered by most-recently-saved + * + * @param program the program + * @return the offers + */ + List getSavedOffers(Program program); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerObjectActionContext.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerObjectActionContext.java similarity index 96% rename from Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerObjectActionContext.java rename to Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerObjectActionContext.java index 1680d695827..ec6563492d3 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerObjectActionContext.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerObjectActionContext.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.app.plugin.core.debug.gui.model; +package ghidra.debug.api.model; import java.awt.Component; import java.util.Collection; diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerSingleObjectPathActionContext.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerSingleObjectPathActionContext.java new file mode 100644 index 00000000000..251a27ad0e1 --- /dev/null +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/model/DebuggerSingleObjectPathActionContext.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.debug.api.model; + +import docking.DefaultActionContext; +import ghidra.trace.model.target.TraceObjectKeyPath; + +/** + * Really just used by scripts to get a path into an action context + */ +public class DebuggerSingleObjectPathActionContext extends DefaultActionContext { + private final TraceObjectKeyPath path; + + public DebuggerSingleObjectPathActionContext(TraceObjectKeyPath path) { + this.path = path; + } + + public TraceObjectKeyPath getPath() { + return path; + } +} diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java index 6d4cbb3c6b0..041c3aecd39 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java @@ -169,6 +169,20 @@ public boolean builtIn() { */ Map collectActions(ActionName name, ActionContext context); + /** + * Execute a command as if in the CLI + * + * @param command the command + * @param toString true to capture the output and return it, false to print to the terminal + * @return the captured output, or null if {@code toString} is false + */ + CompletableFuture executeAsync(String command, boolean toString); + + /** + * @see #executeAsync(String, boolean) + */ + String execute(String command, boolean toString); + /** * Get the trace thread that contains the given object * diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java index 7920288bbf2..1d4c840f2fa 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerAPI.java @@ -16,44 +16,42 @@ package ghidra.debug.flatapi; import java.io.IOException; -import java.lang.invoke.MethodHandles; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.*; import java.util.stream.Collectors; +import docking.ActionContext; import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraState; import ghidra.app.services.*; import ghidra.app.services.DebuggerControlService.StateEditor; -import ghidra.dbg.AnnotatedDebuggerAttributeListener; -import ghidra.dbg.DebuggerObjectModel; -import ghidra.dbg.target.*; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; -import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; -import ghidra.dbg.target.TargetSteppable.TargetStepKind; -import ghidra.dbg.util.PathUtils; import ghidra.debug.api.breakpoint.LogicalBreakpoint; import ghidra.debug.api.control.ControlMode; -import ghidra.debug.api.model.DebuggerProgramLaunchOffer; -import ghidra.debug.api.model.DebuggerProgramLaunchOffer.*; -import ghidra.debug.api.model.TraceRecorder; +import ghidra.debug.api.model.DebuggerObjectActionContext; +import ghidra.debug.api.model.DebuggerSingleObjectPathActionContext; +import ghidra.debug.api.target.ActionName; import ghidra.debug.api.target.Target; +import ghidra.debug.api.target.Target.ActionEntry; import ghidra.debug.api.tracemgr.DebuggerCoordinates; +import ghidra.framework.model.DomainObjectChangedEvent; +import ghidra.framework.model.DomainObjectListener; import ghidra.pcode.exec.trace.TraceSleighUtils; import ghidra.program.flatapi.FlatProgramAPI; import ghidra.program.model.address.*; import ghidra.program.model.lang.*; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; -import ghidra.trace.model.Trace; -import ghidra.trace.model.TraceLocation; +import ghidra.trace.model.*; import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.memory.TraceMemoryOperations; import ghidra.trace.model.memory.TraceMemorySpace; import ghidra.trace.model.program.TraceProgramView; +import ghidra.trace.model.target.*; +import ghidra.trace.model.thread.TraceObjectThread; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.util.MathUtilities; @@ -464,6 +462,8 @@ default boolean goToDynamic(ProgramLocation location) { /** * Go to the given dynamic address in the dynamic listing * + * @param address the destination address + * @return true if successful, false otherwise * @see #goToDynamic(ProgramLocation) */ default boolean goToDynamic(Address address) { @@ -473,6 +473,8 @@ default boolean goToDynamic(Address address) { /** * Go to the given dynamic address in the dynamic listing * + * @param addrString the destination address, as a string + * @return true if successful, false otherwise * @see #goToDynamic(ProgramLocation) */ default boolean goToDynamic(String addrString) { @@ -617,6 +619,7 @@ default Trace emulateLaunch(Program program, Address address) throws IOException /** * Does the same as {@link #emulateLaunch(Program, Address)}, for the current program * + * @param address the initial program counter * @return the resulting trace * @throws IOException if the trace cannot be created */ @@ -796,6 +799,15 @@ default AddressRange safeRange(Address start, int length) { } } + /** + * The the target service + * + * @return the service + */ + default DebuggerTargetService getTargetService() { + return requireService(DebuggerTargetService.class); + } + /** * Copy memory from target to trace, if applicable and not already cached * @@ -803,23 +815,16 @@ default AddressRange safeRange(Address start, int length) { * @param snap the snap the snap, to determine whether target bytes are applicable * @param start the starting address * @param length the number of bytes to make fresh - * @throws InterruptedException if the operation is interrupted - * @throws ExecutionException if an error occurs - * @throws TimeoutException if the operation times out + * @param monitor a monitor for progress + * @throws CancelledException if the operation was cancelled */ default void refreshMemoryIfLive(Trace trace, long snap, Address start, int length, - TaskMonitor monitor) throws InterruptedException, ExecutionException, TimeoutException { - TraceRecorder recorder = getModelService().getRecorder(trace); - if (recorder == null) { - return; - } - if (recorder.getSnap() != snap) { + TaskMonitor monitor) throws CancelledException { + Target target = getTargetService().getTarget(trace); + if (target == null || target.getSnap() != snap) { return; } - waitOn(recorder.readMemoryBlocks(new AddressSet(safeRange(start, length)), monitor)); - waitOn(recorder.getTarget().getModel().flushEvents()); - waitOn(recorder.flushTransactions()); - trace.flushEvents(); + target.readMemory(new AddressSet(safeRange(start, length)), monitor); } /** @@ -831,16 +836,11 @@ default void refreshMemoryIfLive(Trace trace, long snap, Address start, int leng * @param buffer the destination buffer * @param monitor a monitor for live read progress * @return the number of bytes read + * @throws CancelledException if the operation was cancelled */ default int readMemory(Trace trace, long snap, Address start, byte[] buffer, - TaskMonitor monitor) { - try { - refreshMemoryIfLive(trace, snap, start, buffer.length, monitor); - } - catch (InterruptedException | ExecutionException | TimeoutException e) { - return 0; - } - + TaskMonitor monitor) throws CancelledException { + refreshMemoryIfLive(trace, snap, start, buffer.length, monitor); return trace.getMemoryManager().getViewBytes(snap, start, ByteBuffer.wrap(buffer)); } @@ -853,9 +853,10 @@ default int readMemory(Trace trace, long snap, Address start, byte[] buffer, * @param length the desired number of bytes * @param monitor a monitor for live read progress * @return the array of bytes read, can be shorter than desired + * @throws CancelledException if the operation was cancelled */ default byte[] readMemory(Trace trace, long snap, Address start, int length, - TaskMonitor monitor) { + TaskMonitor monitor) throws CancelledException { byte[] arr = new byte[length]; int actual = readMemory(trace, snap, start, arr, monitor); if (actual == length) { @@ -870,9 +871,12 @@ default byte[] readMemory(Trace trace, long snap, Address start, int length, * * @param start the starting address * @param buffer the destination buffer + * @param monitor a monitor for live read progress * @return the number of bytes read + * @throws CancelledException if the operation was cancelled */ - default int readMemory(Address start, byte[] buffer, TaskMonitor monitor) { + default int readMemory(Address start, byte[] buffer, TaskMonitor monitor) + throws CancelledException { TraceProgramView view = requireCurrentView(); return readMemory(view.getTrace(), view.getSnap(), start, buffer, monitor); } @@ -882,9 +886,12 @@ default int readMemory(Address start, byte[] buffer, TaskMonitor monitor) { * * @param start the starting address * @param length the desired number of bytes + * @param monitor a monitor for live read progress * @return the array of bytes read, can be shorter than desired + * @throws CancelledException if the operation was cancelled */ - default byte[] readMemory(Address start, int length, TaskMonitor monitor) { + default byte[] readMemory(Address start, int length, TaskMonitor monitor) + throws CancelledException { TraceProgramView view = requireCurrentView(); return readMemory(view.getTrace(), view.getSnap(), start, length, monitor); } @@ -895,9 +902,10 @@ default byte[] readMemory(Address start, int length, TaskMonitor monitor) { *

* NOTE: This searches the trace only. It will not interrogate the live target. There are * two mechanisms for searching a live target's full memory: 1) Capture the full memory (or the - * subset to search) -- using, e.g., {@link #refreshMemoryIfLive(Trace, long, Address, int)} -- - * then search the trace. 2) If possible, invoke the target debugger's search functions -- - * using, e.g., {@link #executeCapture(String)}. + * subset to search) -- using, e.g., + * {@link #refreshMemoryIfLive(Trace, long, Address, int, TaskMonitor)} -- then search the + * trace. 2) If possible, invoke the target debugger's search functions -- using, e.g., + * {@link #executeCapture(String)}. * *

* This delegates to @@ -938,6 +946,16 @@ default Address searchMemory(Trace trace, long snap, AddressRange range, ByteBuf /** * @see #searchMemory(Trace, long, AddressRange, ByteBuffer, ByteBuffer, boolean, TaskMonitor) + * + * @param trace the trace to search + * @param snap the snapshot of the trace to search + * @param range the range within to search + * @param data the bytes to search for + * @param mask a mask on the bits to search, or null to match exactly. + * @param forward true to start at the min address going forward, false to start at the max + * address going backward + * @param monitor a monitor for search progress + * @return the minimum address of the matched bytes, or null if not found */ default Address searchMemory(Trace trace, long snap, AddressRange range, byte[] data, byte[] mask, boolean forward, TaskMonitor monitor) { @@ -953,27 +971,17 @@ default Address searchMemory(Trace trace, long snap, AddressRange range, byte[] * @param frame the frame level, 0 being the innermost * @param snap the snap, to determine whether target values are applicable * @param registers the registers to make fresh - * @throws InterruptedException if the operation is interrupted - * @throws ExecutionException if an error occurs - * @throws TimeoutException if the operation times out */ default void refreshRegistersIfLive(TracePlatform platform, TraceThread thread, int frame, - long snap, Collection registers) - throws InterruptedException, ExecutionException, TimeoutException { + long snap, Collection registers) { Trace trace = thread.getTrace(); - TraceRecorder recorder = getModelService().getRecorder(trace); - if (recorder == null) { - return; - } - if (recorder.getSnap() != snap) { + + Target target = getTargetService().getTarget(trace); + if (target == null || target.getSnap() != snap) { return; } - Set asSet = - registers instanceof Set ? (Set) registers : Set.copyOf(registers); - waitOn(recorder.captureThreadRegisters(platform, thread, frame, asSet)); - waitOn(recorder.getTarget().getModel().flushEvents()); - waitOn(recorder.flushTransactions()); - trace.flushEvents(); + Set asSet = registers instanceof Set s ? s : Set.copyOf(registers); + target.readRegisters(platform, thread, frame, asSet); } /** @@ -988,12 +996,7 @@ default void refreshRegistersIfLive(TracePlatform platform, TraceThread thread, */ default List readRegisters(TracePlatform platform, TraceThread thread, int frame, long snap, Collection registers) { - try { - refreshRegistersIfLive(platform, thread, frame, snap, registers); - } - catch (InterruptedException | ExecutionException | TimeoutException e) { - return null; - } + refreshRegistersIfLive(platform, thread, frame, snap, registers); TraceMemorySpace regs = thread.getTrace().getMemoryManager().getMemoryRegisterSpace(thread, frame, false); if (regs == null) { @@ -1005,8 +1008,13 @@ default List readRegisters(TracePlatform platform, TraceThread th /** * Read a register * - * @see #readRegisters(TraceThread, int, long, Collection) + * @param platform the platform whose language defines the registers + * @param thread the trace thread + * @param frame the source frame level, 0 being the innermost + * @param snap the source snap + * @param register the source register * @return the register's value, or null on error + * @see #readRegisters(TracePlatform, TraceThread, int, long, Collection) */ default RegisterValue readRegister(TracePlatform platform, TraceThread thread, int frame, long snap, Register register) { @@ -1017,7 +1025,9 @@ default RegisterValue readRegister(TracePlatform platform, TraceThread thread, i /** * Read several registers from the current context, refreshing from the target if needed * - * @see #readRegisters(TraceThread, int, long, Collection) + * @param registers the source registers + * @return the list of register values, or null on error + * @see #readRegisters(TracePlatform, TraceThread, int, long, Collection) */ default List readRegisters(Collection registers) { DebuggerCoordinates current = getCurrentDebuggerCoordinates(); @@ -1070,8 +1080,10 @@ default Register validateRegisterName(Language language, String name) { /** * Read several registers from the current context, refreshing from the target if needed * - * @see #readRegisters(TraceThread, int, long, Collection) + * @param names the source register names + * @return the list of register values, or null on error * @throws IllegalArgumentException if any name is invalid + * @see #readRegisters(TracePlatform, TraceThread, int, long, Collection) */ default List readRegistersNamed(Collection names) { return readRegisters(validateRegisterNames(requireCurrentTrace().getBaseLanguage(), names)); @@ -1111,8 +1123,10 @@ default RegisterValue readRegister(Register register) { /** * Read a register from the current context, refreshing from the target if needed * - * @see #readRegister(Register) + * @param name the register name + * @return the value, or null on error * @throws IllegalArgumentException if the name is invalid + * @see #readRegister(Register) */ default RegisterValue readRegister(String name) { TracePlatform platform = requireCurrentPlatform(); @@ -1225,6 +1239,7 @@ default void setControlMode(ControlMode mode) { /** * Create a state editor for the given context, adhering to its current control mode * + * @param coordinates the context * @return the editor */ default StateEditor createStateEditor(DebuggerCoordinates coordinates) { @@ -1384,8 +1399,14 @@ default boolean writeRegister(TraceThread thread, int frame, long snap, Register /** * Patch a register of the given context, according to its current control mode * - * @see #writeRegister(TraceThread, int, long, RegisterValue) + * @param thread the thread + * @param frame the frame + * @param snap the snap + * @param name the register name + * @param value the value + * @return true if successful, false otherwise * @throws IllegalArgumentException if the register name is invalid + * @see #writeRegister(TraceThread, int, long, RegisterValue) */ default boolean writeRegister(TraceThread thread, int frame, long snap, String name, BigInteger value) { @@ -1400,7 +1421,6 @@ default boolean writeRegister(TraceThread thread, int frame, long snap, String n * If you intend to apply several patches, consider using {@link #createStateEditor()} and * {@link #writeRegister(StateEditor, RegisterValue)}. * - * @param mode specifies in what way to apply the patch * @param rv the register value * @return true if successful, false otherwise */ @@ -1411,357 +1431,114 @@ default boolean writeRegister(RegisterValue rv) { /** * Patch a register of the current thread, according to the current control mode * - * @see #writeRegister(RegisterValue) + * @param name the register name + * @param value the value + * @return true if successful, false otherwise * @throws IllegalArgumentException if the register name is invalid + * @see #writeRegister(RegisterValue) */ default boolean writeRegister(String name, BigInteger value) { return writeRegister(new RegisterValue( validateRegisterName(requireCurrentTrace().getBaseLanguage(), name), value)); } - /** - * Get the current target - * - *

- * If the current trace is not live, this returns null. - * - * @return the target, or null - */ - default Target getCurrentTarget() { - return getTraceManager().getCurrent().getTarget(); - } - - /** - * Get the model (target) service - * - * @return the service - */ - default DebuggerModelService getModelService() { - return requireService(DebuggerModelService.class); - } - - /** - * Get offers for launching the given program - * - * @param program the program - * @return the offers - */ - @Deprecated - default List getLaunchOffers(Program program) { - return getModelService().getProgramLaunchOffers(program).collect(Collectors.toList()); + default ActionContext createContext(TraceObject object) { + TraceObjectValue value = object.getCanonicalParents(Lifespan.ALL).findAny().orElseThrow(); + return new DebuggerObjectActionContext(List.of(value), null, null); } - /** - * Get offers for launching the current program - * - * @return the offers - */ - default List getLaunchOffers() { - return getLaunchOffers(requireCurrentProgram()); - } - - /** - * Get the best launch offer for a program, throwing an exception if there is no offer - * - * @param program the program - * @return the offer - * @throws NoSuchElementException if there is no offer - */ - default DebuggerProgramLaunchOffer requireLaunchOffer(Program program) { - Optional offer = - getModelService().getProgramLaunchOffers(program).findFirst(); - if (offer.isEmpty()) { - throw new NoSuchElementException("No offers to launch " + program); + default ActionContext createContext(TraceThread thread) { + if (thread instanceof TraceObjectThread objThread) { + return createContext(objThread.getObject()); } - return offer.get(); + return new DebuggerSingleObjectPathActionContext( + TraceObjectKeyPath.parse(thread.getPath())); } - /** - * Launch the given offer, overriding its command line - * - *

- * NOTE: Most offers take a command line, but not all do. If this is used for an offer - * that does not, it's behavior is undefined. - * - *

- * Launches are not always successful, and may in fact fail frequently, usually because of - * configuration errors or missing components on the target platform. This may leave stale - * connections and/or target debuggers, processes, etc., in strange states. Furthermore, even if - * launching the target is successful, starting the recorder may not succeed, typically because - * Ghidra cannot identify and map the target platform to a Sleigh language. This method makes no - * attempt at cleaning up partial pieces. Instead it returns those pieces in the launch result. - * If the result includes a recorder, the launch was successful. If not, the script can decide - * what to do with the other pieces. That choice depends on what is expected of the user. Can - * the user reasonable be expected to intervene and complete the launch manually? How many - * targets does the script intend to launch? How big is the mess if left partially completed? - * - * @param offer the offer (this includes the program given when asking for offers) - * @param commandLine the command-line override. If this doesn't refer to the same program as - * the offer, there may be unexpected results - * @param monitor the monitor for the launch stages - * @return the result, possibly partial - */ - default LaunchResult launch(DebuggerProgramLaunchOffer offer, String commandLine, - TaskMonitor monitor) { - try { - return waitOn(offer.launchProgram(monitor, PromptMode.NEVER, new LaunchConfigurator() { - @Override - public Map configureLauncher(TargetLauncher launcher, - Map arguments, RelPrompt relPrompt) { - Map adjusted = new HashMap<>(arguments); - adjusted.put(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, commandLine); - return adjusted; - } - })); - } - catch (InterruptedException | ExecutionException | TimeoutException e) { - // TODO: This is not ideal, since it's likely partially completed - return LaunchResult.totalFailure(e); - } - } - - /** - * Launch the given offer with the default/saved arguments - * - * @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor) - */ - default LaunchResult launch(DebuggerProgramLaunchOffer offer, TaskMonitor monitor) { - try { - return waitOn(offer.launchProgram(monitor, PromptMode.NEVER)); + default ActionContext createContext(Trace trace) { + DebuggerCoordinates coords = getTraceManager().getCurrentFor(trace); + if (coords == null) { + return new DebuggerSingleObjectPathActionContext(TraceObjectKeyPath.of()); } - catch (InterruptedException | ExecutionException | TimeoutException e) { - // TODO: This is not ideal, since it's likely partially completed - return LaunchResult.totalFailure(e); + if (coords.getObject() != null) { + return createContext(coords.getObject()); } - } - - /** - * Launch the given program, overriding its command line - * - *

- * This takes the best offer for the given program. The command line should invoke the given - * program. If it does not, there may be unexpected results. - * - * @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor) - */ - default LaunchResult launch(Program program, String commandLine, TaskMonitor monitor) - throws InterruptedException, ExecutionException, TimeoutException { - return launch(requireLaunchOffer(program), commandLine, monitor); - } - - /** - * Launch the given program with the default/saved arguments - * - *

- * This takes the best offer for the given program. - * - * @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor) - */ - default LaunchResult launch(Program program, TaskMonitor monitor) - throws InterruptedException, ExecutionException, TimeoutException { - return launch(requireLaunchOffer(program), monitor); - } - - /** - * Launch the current program, overriding its command line - * - * @see #launch(Program, String, TaskMonitor) - */ - default LaunchResult launch(String commandLine, TaskMonitor monitor) - throws InterruptedException, ExecutionException, TimeoutException { - return launch(requireCurrentProgram(), commandLine, monitor); - } - - /** - * Launch the current program with the default/saved arguments - * - * @see #launch(Program, TaskMonitor) - */ - default LaunchResult launch(TaskMonitor monitor) - throws InterruptedException, ExecutionException, TimeoutException { - return launch(requireCurrentProgram(), monitor); - } - - /** - * Get the target for a given trace - * - *

- * WARNING: This method will likely change or be removed in the future. - * - * @param trace the trace - * @return the target, or null if not alive - */ - default TargetObject getTarget(Trace trace) { - TraceRecorder recorder = getModelService().getRecorder(trace); - if (recorder == null) { - return null; + if (coords.getPath() != null) { + return new DebuggerSingleObjectPathActionContext(coords.getPath()); } - return recorder.getTarget(); + return new DebuggerSingleObjectPathActionContext(TraceObjectKeyPath.of()); } - /** - * Get the target thread for a given trace thread - * - *

- * WARNING: This method will likely change or be removed in the future. - * - * @param thread the trace thread - * @return the target thread, or null if not alive - */ - default TargetThread getTargetThread(TraceThread thread) { - TraceRecorder recorder = getModelService().getRecorder(thread.getTrace()); - if (recorder == null) { - return null; - } - return recorder.getTargetThread(thread); + default ActionEntry findAction(Target target, ActionName action, ActionContext context) { + return target.collectActions(action, context) + .values() + .stream() + .filter(e -> !e.requiresPrompt()) + .sorted(Comparator.comparing(e -> -e.specificity())) + .findFirst() + .orElseThrow(); } - /** - * Get the user focus for a given trace - * - *

- * WARNING: This method will likely change or be removed in the future. - * - * @param trace the trace - * @return the target, or null if not alive - */ - default TargetObject getTargetFocus(Trace trace) { - TraceRecorder recorder = getModelService().getRecorder(trace); - if (recorder == null) { - return null; - } - TargetObject focus = recorder.getFocus(); - return focus != null ? focus : recorder.getTarget(); + default Object doAction(Target target, ActionName name, ActionContext context) { + ActionEntry action = findAction(target, name, context); + return action.get(false); } - /** - * Find the most suitable object related to the given object implementing the given interface - * - *

- * WARNING: This method will likely change or be removed in the future. - * - * @param the interface type - * @param seed the seed object - * @param iface the interface class - * @return the related interface, or null - * @throws ClassCastException if the model violated its schema wrt. the requested interface - */ - @SuppressWarnings("unchecked") - default T findInterface(TargetObject seed, Class iface) { - DebuggerObjectModel model = seed.getModel(); - List found = model - .getRootSchema() - .searchForSuitable(iface, seed.getPath()); - if (found == null) { - return null; + default boolean doThreadAction(TraceThread thread, ActionName name) { + if (thread == null) { + return false; } + Target target = getTargetService().getTarget(thread.getTrace()); try { - Object value = waitOn(model.fetchModelValue(found)); - return (T) value; + doAction(target, name, createContext(thread)); + return true; } - catch (InterruptedException | ExecutionException | TimeoutException e) { - return null; + catch (Exception e) { + return false; } } - /** - * Find the most suitable object related to the given thread implementing the given interface - * - * @param the interface type - * @param thread the thread - * @param iface the interface class - * @return the related interface, or null - * @throws ClassCastException if the model violated its schema wrt. the requested interface - */ - default T findInterface(TraceThread thread, Class iface) { - TargetThread targetThread = getTargetThread(thread); - if (targetThread == null) { - return null; + default boolean doTraceAction(Trace trace, ActionName name) { + if (trace == null) { + return false; } - return findInterface(targetThread, iface); - } - - /** - * Find the most suitable object related to the given trace's focus implementing the given - * interface - * - * @param the interface type - * @param thread the thread - * @param iface the interface class - * @return the related interface, or null - * @throws ClassCastException if the model violated its schema wrt. the requested interface - */ - default T findInterface(Trace trace, Class iface) { - TargetObject focus = getTargetFocus(trace); - if (focus == null) { - return null; + Target target = getTargetService().getTarget(trace); + try { + doAction(target, name, createContext(trace)); + return true; } - return findInterface(focus, iface); - } - - /** - * Find the interface related to the current thread or trace - * - *

- * This first attempts to find the most suitable object related to the current trace thread. If - * that fails, or if there is no current thread, it tries to find the one related to the current - * trace (or its focus). If there is no current trace, this throws an exception. - * - * @param the interface type - * @param iface the interface class - * @return the related interface, or null - * @throws IllegalStateException if there is no current trace - */ - default T findInterface(Class iface) { - TraceThread thread = getCurrentThread(); - T t = thread == null ? null : findInterface(thread, iface); - if (t != null) { - return t; + catch (Exception e) { + return false; } - return findInterface(requireCurrentTrace(), iface); } /** - * Step the given target object + * Step the given thread, stepping into subroutines * - * @param steppable the steppable target object - * @param kind the kind of step to take + * @param thread the thread to step * @return true if successful, false otherwise */ - default boolean step(TargetSteppable steppable, TargetStepKind kind) { - if (steppable == null) { - return false; - } - try { - waitOn(steppable.step(kind)); - } - catch (InterruptedException | ExecutionException | TimeoutException e) { - return false; - } - return true; + default boolean stepInto(TraceThread thread) { + return doThreadAction(thread, ActionName.STEP_INTO); } /** - * Step the given thread on target according to the given kind + * Step the current thread, stepping into subroutines * - * @param thread the trace thread - * @param kind the kind of step to take * @return true if successful, false otherwise */ - default boolean step(TraceThread thread, TargetStepKind kind) { - if (thread == null) { - return false; - } - return step(findInterface(thread, TargetSteppable.class), kind); + default boolean stepInto() { + return stepInto(getCurrentThread()); } /** - * Step the current thread, stepping into subroutines + * Step the given thread, stepping over subroutines * + * @param thread the thread to step * @return true if successful, false otherwise */ - default boolean stepInto() { - return step(findInterface(TargetSteppable.class), TargetStepKind.INTO); + default boolean stepOver(TraceThread thread) { + return doThreadAction(thread, ActionName.STEP_OVER); } /** @@ -1770,35 +1547,26 @@ default boolean stepInto() { * @return true if successful, false otherwise */ default boolean stepOver() { - return step(findInterface(TargetSteppable.class), TargetStepKind.OVER); + return stepOver(getCurrentThread()); } /** - * Step the current thread, until it returns from the current subroutine + * Step the given thread, until it returns from the current subroutine * + * @param thread the thread to step * @return true if successful, false otherwise */ - default boolean stepOut() { - return step(findInterface(TargetSteppable.class), TargetStepKind.FINISH); + default boolean stepOut(TraceThread thread) { + return doThreadAction(thread, ActionName.STEP_OUT); } /** - * Resume execution of the given target object + * Step the current thread, until it returns from the current subroutine * - * @param resumable the resumable target object * @return true if successful, false otherwise */ - default boolean resume(TargetResumable resumable) { - if (resumable == null) { - return false; - } - try { - waitOn(resumable.resume()); - } - catch (InterruptedException | ExecutionException | TimeoutException e) { - return false; - } - return true; + default boolean stepOut() { + return stepOut(getCurrentThread()); } /** @@ -1811,7 +1579,7 @@ default boolean resume(TargetResumable resumable) { * @return true if successful, false otherwise */ default boolean resume(TraceThread thread) { - return resume(findInterface(thread, TargetResumable.class)); + return doThreadAction(thread, ActionName.RESUME); } /** @@ -1824,7 +1592,7 @@ default boolean resume(TraceThread thread) { * @return true if successful, false otherwise */ default boolean resume(Trace trace) { - return resume(findInterface(trace, TargetResumable.class)); + return doTraceAction(trace, ActionName.RESUME); } /** @@ -1833,32 +1601,10 @@ default boolean resume(Trace trace) { * @return true if successful, false otherwise */ default boolean resume() { - TraceThread thread = getCurrentThread(); - TargetResumable resumable = - thread == null ? null : findInterface(thread, TargetResumable.class); - if (resumable == null) { - resumable = findInterface(requireCurrentTrace(), TargetResumable.class); - } - return resume(resumable); - } - - /** - * Interrupt execution of the given target object - * - * @param interruptible the interruptible target object - * @return true if successful, false otherwise - */ - default boolean interrupt(TargetInterruptible interruptible) { - if (interruptible == null) { - return false; - } - try { - waitOn(interruptible.interrupt()); - } - catch (InterruptedException | ExecutionException | TimeoutException e) { - return false; + if (resume(getCurrentThread())) { + return true; } - return true; + return resume(getCurrentTrace()); } /** @@ -1867,10 +1613,11 @@ default boolean interrupt(TargetInterruptible interruptible) { *

* This is commonly called "pause" or "break," as well, but not "stop." * + * @param thread the thread to interrupt (may interrupt the whole target) * @return true if successful, false otherwise */ default boolean interrupt(TraceThread thread) { - return interrupt(findInterface(thread, TargetInterruptible.class)); + return doThreadAction(thread, ActionName.INTERRUPT); } /** @@ -1879,10 +1626,11 @@ default boolean interrupt(TraceThread thread) { *

* This is commonly called "pause" or "break," as well, but not "stop." * + * @param trace the trace whose target to interrupt * @return true if successful, false otherwise */ default boolean interrupt(Trace trace) { - return interrupt(findInterface(trace, TargetInterruptible.class)); + return doTraceAction(trace, ActionName.INTERRUPT); } /** @@ -1891,26 +1639,10 @@ default boolean interrupt(Trace trace) { * @return true if successful, false otherwise */ default boolean interrupt() { - return interrupt(findInterface(TargetInterruptible.class)); - } - - /** - * Terminate execution of the given target object - * - * @param interruptible the interruptible target object - * @return true if successful, false otherwise - */ - default boolean kill(TargetKillable killable) { - if (killable == null) { - return false; - } - try { - waitOn(killable.kill()); - } - catch (InterruptedException | ExecutionException | TimeoutException e) { - return false; + if (interrupt(getCurrentThread())) { + return true; } - return true; + return interrupt(getCurrentTrace()); } /** @@ -1919,10 +1651,11 @@ default boolean kill(TargetKillable killable) { *

* This is commonly called "stop" as well. * + * @param thread the thread to kill (may kill the whole target) * @return true if successful, false otherwise */ default boolean kill(TraceThread thread) { - return kill(findInterface(thread, TargetKillable.class)); + return doThreadAction(thread, ActionName.KILL); } /** @@ -1931,10 +1664,11 @@ default boolean kill(TraceThread thread) { *

* This is commonly called "stop" as well. * + * @param trace the trace whose target to kill * @return true if successful, false otherwise */ default boolean kill(Trace trace) { - return kill(findInterface(trace, TargetKillable.class)); + return doTraceAction(trace, ActionName.KILL); } /** @@ -1943,27 +1677,10 @@ default boolean kill(Trace trace) { * @return true if successful, false otherwise */ default boolean kill() { - return kill(findInterface(TargetKillable.class)); - } - - /** - * Get the current state of the given target - * - *

- * Any invalidated object is considered {@link TargetExecutionState#TERMINATED}. Otherwise, it's - * at least considered {@link TargetExecutionState#ALIVE}. A more specific state may be - * determined by searching the model for the conventionally-related object implementing - * {@link TargetObjectStateful}. This method applies this convention. - * - * @param target the target object - * @return the target object's execution state - */ - default TargetExecutionState getExecutionState(TargetObject target) { - if (!target.isValid()) { - return TargetExecutionState.TERMINATED; + if (kill(getCurrentThread())) { + return true; } - TargetExecutionStateful stateful = findInterface(target, TargetExecutionStateful.class); - return stateful == null ? TargetExecutionState.ALIVE : stateful.getExecutionState(); + return kill(getCurrentTrace()); } /** @@ -1976,14 +1693,20 @@ default TargetExecutionState getExecutionState(TargetObject target) { * consider the current snap. It only considers a live target in the present. * * @param trace the trace - * @return the trace's target's execution state + * @return the trace's execution state */ default TargetExecutionState getExecutionState(Trace trace) { - TargetObject target = getTarget(trace); + Target target = getTargetService().getTarget(trace); if (target == null) { return TargetExecutionState.TERMINATED; } - return getExecutionState(target); + // Use resume action's enablement as a proxy for state + // This should work for recorder or rmi targets + ActionEntry action = findAction(target, ActionName.RESUME, createContext(trace)); + if (action == null) { + return TargetExecutionState.ALIVE; + } + return action.isEnabled() ? TargetExecutionState.STOPPED : TargetExecutionState.RUNNING; } /** @@ -2000,14 +1723,14 @@ default TargetExecutionState getExecutionState(Trace trace) { * snap. * * @param thread - * @return + * @return the thread's execution state */ default TargetExecutionState getExecutionState(TraceThread thread) { - TargetObject target = getTargetThread(thread); - if (target == null) { + DebuggerCoordinates coords = getTraceManager().getCurrentFor(thread.getTrace()); + if (!coords.isAlive()) { return TargetExecutionState.TERMINATED; } - return getExecutionState(target); + return coords.getTarget().getThreadExecutionState(thread); } /** @@ -2050,46 +1773,41 @@ default boolean isThreadAlive(TraceThread thread) { * NOTE: To be the "current" target thread, the target must be recorded, and its trace * thread must be the current thread. * - * @return + * @return true if alive */ default boolean isThreadAlive() { return isThreadAlive(requireThread(getCurrentThread())); } /** - * Waits for the given target to exit the {@link TargetExecutionState#RUNNING} state + * Wait for the trace's target to break * *

- * NOTE: There may be subtleties depending on the target debugger. For the most part, if - * the connection is handling a single target, things will work as expected. However, if there - * are multiple targets on one connection, it is possible for the given target to break, but for - * the target debugger to remain unresponsive to commands. This would happen, e.g., if a second - * target on the same connection is still running. + * If the trace has no target, this method returns immediately, i.e., it assumes the target has + * terminated. * - * @param target the target + * @param trace the trace * @param timeout the maximum amount of time to wait * @param unit the units for time * @throws TimeoutException if the timeout expires */ - default void waitForBreak(TargetObject target, long timeout, TimeUnit unit) - throws TimeoutException { - TargetExecutionStateful stateful = findInterface(target, TargetExecutionStateful.class); - if (stateful == null) { - throw new IllegalArgumentException("Given target is not stateful"); + default void waitForBreak(Trace trace, long timeout, TimeUnit unit) throws TimeoutException { + if (!getExecutionState(trace).isRunning()) { + return; } - var listener = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + var listener = new DomainObjectListener() { CompletableFuture future = new CompletableFuture<>(); - @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) - private void stateChanged(TargetObject parent, TargetExecutionState state) { - if (parent == stateful && !state.isRunning()) { + @Override + public void domainObjectChanged(DomainObjectChangedEvent ev) { + if (!getExecutionState(trace).isRunning()) { future.complete(null); } } }; - target.getModel().addModelListener(listener); + trace.addListener(listener); try { - if (!stateful.getExecutionState().isRunning()) { + if (!getExecutionState(trace).isRunning()) { return; } listener.future.get(timeout, unit); @@ -2098,38 +1816,15 @@ private void stateChanged(TargetObject parent, TargetExecutionState state) { throw new RuntimeException(e); } finally { - target.getModel().removeModelListener(listener); + trace.removeListener(listener); } } - /** - * Wait for the trace's target to break - * - *

- * If the trace has no target, this method returns immediately, i.e., it assumes the target has - * terminated. - * - * @see #waitForBreak(TargetObject, long, TimeUnit) - * @param trace the trace - * @param timeout the maximum amount of time to wait - * @param unit the units for time - * @throws TimeoutException if the timeout expires - */ - default void waitForBreak(Trace trace, long timeout, TimeUnit unit) throws TimeoutException { - TargetObject target = getTarget(trace); - if (target == null || !target.isValid()) { - return; - } - waitForBreak(target, timeout, unit); - } - /** * Wait for the current target to break * * @see #waitForBreak(Trace, long, TimeUnit) * @param timeout the maximum - * @param unit - * @param timeout the maximum amount of time to wait * @param unit the units for time * @throws TimeoutException if the timeout expires * @throws IllegalStateException if there is no current trace @@ -2138,30 +1833,6 @@ default void waitForBreak(long timeout, TimeUnit unit) throws TimeoutException { waitForBreak(requireCurrentTrace(), timeout, unit); } - /** - * Execute a command in a connection's interpreter, capturing the output - * - *

- * This executes a raw command in the given interpreter. The command could have arbitrary - * effects, so it may be necessary to wait for those effects to be handled by the tool's - * services and plugins before proceeding. - * - * @param interpreter the interpreter - * @param command the command - * @return the output, or null if there is no interpreter - */ - default String executeCapture(TargetInterpreter interpreter, String command) { - if (interpreter == null) { - return null; - } - try { - return waitOn(interpreter.executeCapture(command)); - } - catch (InterruptedException | ExecutionException | TimeoutException e) { - return null; - } - } - /** * Execute a command on the live debugger for the given trace, capturing the output * @@ -2170,7 +1841,8 @@ default String executeCapture(TargetInterpreter interpreter, String command) { * @return the output, or null if there is no live interpreter */ default String executeCapture(Trace trace, String command) { - return executeCapture(findInterface(trace, TargetInterpreter.class), command); + Target target = getTargetService().getTarget(trace); + return target.execute(command, true); } /** @@ -2185,39 +1857,21 @@ default String executeCapture(String command) { } /** - * Execute a command in a connection's interpreter - * - *

- * This executes a raw command in the given interpreter. The command could have arbitrary - * effects, so it may be necessary to wait for those effects to be handled by the tool's - * services and plugins before proceeding. + * Execute a command on the live debugger for the given trace * - * @param interpreter the interpreter + * @param trace the trace * @param command the command * @return true if successful */ - default boolean execute(TargetInterpreter interpreter, String command) { - if (interpreter == null) { - return false; - } + default boolean execute(Trace trace, String command) { + Target target = getTargetService().getTarget(trace); try { - waitOn(interpreter.executeCapture(command)); + target.execute(command, false); + return true; } - catch (InterruptedException | ExecutionException | TimeoutException e) { + catch (Exception e) { return false; } - return true; - } - - /** - * Execute a command on the live debugger for the given trace - * - * @param trace the trace - * @param command the command - * @return true if successful - */ - default boolean execute(Trace trace, String command) { - return execute(findInterface(trace, TargetInterpreter.class), command); } /** @@ -2398,7 +2052,7 @@ default NavigableMap> getBreakpoints(Program pro /** * Get the breakpoints in the given trace, indexed by (dynamic) address * - * @param program the program + * @param trace the trace * @return the address-breakpoint-set map */ default NavigableMap> getBreakpoints(Trace trace) { @@ -2454,6 +2108,8 @@ public void close() throws InterruptedException, ExecutionException, TimeoutExce * *

* Use this via a try-with-resources block containing the operations causing changes. + * + * @return a closable object for a try-with-resources block */ default ExpectingBreakpointChanges expectBreakpointChanges() { return new ExpectingBreakpointChanges(this, getBreakpointService()); @@ -2655,118 +2311,26 @@ default boolean breakpointsClear(ProgramLocation location) { return true; } - /** - * Get the value at the given path for the given model - * - * @param model the model - * @param path the path - * @return the avlue, or null if the trace is not live or if the path does not exist - */ - default Object getModelValue(DebuggerObjectModel model, String path) { - try { - return waitOn(model.fetchModelValue(PathUtils.parse(path))); - } - catch (InterruptedException | ExecutionException | TimeoutException e) { - return null; - } - } - - /** - * Get the value at the given path for the current trace's model - * - * @param path the path - * @return the value, or null if the trace is not live or if the path does not exist - */ - default Object getModelValue(String path) { - TraceRecorder recorder = getModelService().getRecorder(getCurrentTrace()); - if (recorder == null) { - return null; - } - return getModelValue(recorder.getTarget().getModel(), path); - } - - /** - * Refresh the given objects children (elements and attributes) - * - * @param object the object - * @return the set of children, excluding primitive-valued attributes - */ - default Set refreshObjectChildren(TargetObject object) { - try { - // Refresh both children and memory/register values - waitOn(object.invalidateCaches()); - waitOn(object.resync()); - } - catch (InterruptedException | ExecutionException | TimeoutException e) { - return null; - } - Set result = new LinkedHashSet<>(); - result.addAll(object.getCachedElements().values()); - for (Object v : object.getCachedAttributes().values()) { - if (v instanceof TargetObject) { - result.add((TargetObject) v); - } - } - return result; - } - - /** - * Refresh the given object and its children, recursively - * - *

- * The objects are traversed in depth-first pre-order. Links are traversed, even if the object - * is not part of the specified subtree, but an object is skipped if it has already been - * visited. - * - * @param object the seed object - * @return true if the traversal completed successfully - */ - default boolean refreshSubtree(TargetObject object) { - var util = new Object() { - Set visited = new HashSet<>(); - - boolean visit(TargetObject object) { - if (!visited.add(object)) { - return true; - } - for (TargetObject child : refreshObjectChildren(object)) { - if (!visit(child)) { - return false; - } - } - return true; - } - }; - return util.visit(object); - } - /** * Flush each stage of the asynchronous processing pipelines from end to end * *

- * This method includes as many components as its author knows to flush. If the given trace is - * alive, flushing starts with the connection's event queue, followed by the recorder's event - * and transaction queues. Next, it flushes the trace's event queue. Then, it waits for various - * services' changes to settle, in dependency order. Currently, that is the static mapping - * service followed by the logical breakpoint service. Note that some stages use timeouts. It's - * also possible the target had not generated all the expected events by the time this method - * began flushing its queue. Thus, callers should still check that some expected condition is - * met and possibly repeat the flush before proceeding. + * This method includes as many components as its author knows to flush. It flushes the trace's + * event queue. Then, it waits for various services' changes to settle, in dependency order. + * Currently, that is the static mapping service followed by the logical breakpoint service. + * Note that some stages use timeouts. It's also possible the target had not generated all the + * expected events by the time this method began flushing its queue. Thus, callers should still + * check that some expected condition is met and possibly repeat the flush before proceeding. * *

- * There are additional dependents, e.g., the breakpoint listing plugin; however, scripts should - * not depend on them, so we do not wait on them. + * There are additional dependents in the GUI; however, scripts should not depend on them, so we + * do not wait on them. * * @param trace the trace whose events need to be completely processed before continuing. - * @return + * @return true if all stages were flushed, false if there were errors */ default boolean flushAsyncPipelines(Trace trace) { try { - TraceRecorder recorder = getModelService().getRecorder(trace); - if (recorder != null) { - waitOn(recorder.getTarget().getModel().flushEvents()); - waitOn(recorder.flushTransactions()); - } trace.flushEvents(); waitOn(getMappingService().changesSettled()); waitOn(getBreakpointService().changesSettled()); @@ -2779,5 +2343,4 @@ default boolean flushAsyncPipelines(Trace trace) { } // TODO: Interaction with the target process itself, e.g., via stdio. - // The DebugModel API does not currently support this. } diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPI.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPI.java new file mode 100644 index 00000000000..118732a5db1 --- /dev/null +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPI.java @@ -0,0 +1,656 @@ +/* ### + * 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.debug.flatapi; + +import java.lang.invoke.MethodHandles; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +import ghidra.app.services.DebuggerModelService; +import ghidra.dbg.AnnotatedDebuggerAttributeListener; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.target.*; +import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; +import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; +import ghidra.dbg.target.TargetSteppable.TargetStepKind; +import ghidra.dbg.util.PathUtils; +import ghidra.debug.api.model.DebuggerProgramLaunchOffer; +import ghidra.debug.api.model.DebuggerProgramLaunchOffer.*; +import ghidra.debug.api.model.TraceRecorder; +import ghidra.program.model.listing.Program; +import ghidra.trace.model.Trace; +import ghidra.trace.model.thread.TraceThread; +import ghidra.util.Swing; +import ghidra.util.task.TaskMonitor; + +@Deprecated +public interface FlatDebuggerRecorderAPI extends FlatDebuggerAPI { + + /** + * Get the model (legacy target) service + * + * @return the service + */ + default DebuggerModelService getModelService() { + return requireService(DebuggerModelService.class); + } + + /** + * Get the target for a given trace + * + *

+ * WARNING: This method will likely change or be removed in the future. + * + * @param trace the trace + * @return the target, or null if not alive + */ + default TargetObject getTarget(Trace trace) { + TraceRecorder recorder = getModelService().getRecorder(trace); + if (recorder == null) { + return null; + } + return recorder.getTarget(); + } + + /** + * Get the target thread for a given trace thread + * + *

+ * WARNING: This method will likely change or be removed in the future. + * + * @param thread the trace thread + * @return the target thread, or null if not alive + */ + default TargetThread getTargetThread(TraceThread thread) { + TraceRecorder recorder = getModelService().getRecorder(thread.getTrace()); + if (recorder == null) { + return null; + } + return recorder.getTargetThread(thread); + } + + /** + * Get the user focus for a given trace + * + *

+ * WARNING: This method will likely change or be removed in the future. + * + * @param trace the trace + * @return the target, or null if not alive + */ + default TargetObject getTargetFocus(Trace trace) { + TraceRecorder recorder = getModelService().getRecorder(trace); + if (recorder == null) { + return null; + } + TargetObject focus = recorder.getFocus(); + return focus != null ? focus : recorder.getTarget(); + } + + /** + * Find the most suitable object related to the given object implementing the given interface + * + *

+ * WARNING: This method will likely change or be removed in the future. + * + * @param the interface type + * @param seed the seed object + * @param iface the interface class + * @return the related interface, or null + * @throws ClassCastException if the model violated its schema wrt. the requested interface + */ + @SuppressWarnings("unchecked") + default T findInterface(TargetObject seed, Class iface) { + DebuggerObjectModel model = seed.getModel(); + List found = model + .getRootSchema() + .searchForSuitable(iface, seed.getPath()); + if (found == null) { + return null; + } + try { + Object value = waitOn(model.fetchModelValue(found)); + return (T) value; + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + } + + /** + * Find the most suitable object related to the given thread implementing the given interface + * + * @param the interface type + * @param thread the thread + * @param iface the interface class + * @return the related interface, or null + * @throws ClassCastException if the model violated its schema wrt. the requested interface + */ + default T findInterface(TraceThread thread, Class iface) { + TargetThread targetThread = getTargetThread(thread); + if (targetThread == null) { + return null; + } + return findInterface(targetThread, iface); + } + + /** + * Find the most suitable object related to the given trace's focus implementing the given + * interface + * + * @param the interface type + * @param trace the trace + * @param iface the interface class + * @return the related interface, or null + * @throws ClassCastException if the model violated its schema wrt. the requested interface + */ + default T findInterface(Trace trace, Class iface) { + TargetObject focus = getTargetFocus(trace); + if (focus == null) { + return null; + } + return findInterface(focus, iface); + } + + /** + * Find the interface related to the current thread or trace + * + *

+ * This first attempts to find the most suitable object related to the current trace thread. If + * that fails, or if there is no current thread, it tries to find the one related to the current + * trace (or its focus). If there is no current trace, this throws an exception. + * + * @param the interface type + * @param iface the interface class + * @return the related interface, or null + * @throws IllegalStateException if there is no current trace + */ + default T findInterface(Class iface) { + TraceThread thread = getCurrentThread(); + T t = thread == null ? null : findInterface(thread, iface); + if (t != null) { + return t; + } + return findInterface(requireCurrentTrace(), iface); + } + + /** + * Step the given target object + * + * @param steppable the steppable target object + * @param kind the kind of step to take + * @return true if successful, false otherwise + */ + default boolean step(TargetSteppable steppable, TargetStepKind kind) { + if (steppable == null) { + return false; + } + try { + waitOn(steppable.step(kind)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Step the given thread on target according to the given kind + * + * @param thread the trace thread + * @param kind the kind of step to take + * @return true if successful, false otherwise + */ + default boolean step(TraceThread thread, TargetStepKind kind) { + if (thread == null) { + return false; + } + return step(findInterface(thread, TargetSteppable.class), kind); + } + + /** + * Resume execution of the given target object + * + * @param resumable the resumable target object + * @return true if successful, false otherwise + */ + default boolean resume(TargetResumable resumable) { + if (resumable == null) { + return false; + } + try { + waitOn(resumable.resume()); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Interrupt execution of the given target object + * + * @param interruptible the interruptible target object + * @return true if successful, false otherwise + */ + default boolean interrupt(TargetInterruptible interruptible) { + if (interruptible == null) { + return false; + } + try { + waitOn(interruptible.interrupt()); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Terminate execution of the given target object + * + * @param interruptible the interruptible target object + * @return true if successful, false otherwise + */ + default boolean kill(TargetKillable killable) { + if (killable == null) { + return false; + } + try { + waitOn(killable.kill()); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Get the current state of the given target + * + *

+ * Any invalidated object is considered {@link TargetExecutionState#TERMINATED}. Otherwise, it's + * at least considered {@link TargetExecutionState#ALIVE}. A more specific state may be + * determined by searching the model for the conventionally-related object implementing + * {@link TargetObjectStateful}. This method applies this convention. + * + * @param target the target object + * @return the target object's execution state + */ + default TargetExecutionState getExecutionState(TargetObject target) { + if (!target.isValid()) { + return TargetExecutionState.TERMINATED; + } + TargetExecutionStateful stateful = findInterface(target, TargetExecutionStateful.class); + return stateful == null ? TargetExecutionState.ALIVE : stateful.getExecutionState(); + } + + /** + * Waits for the given target to exit the {@link TargetExecutionState#RUNNING} state + * + *

+ * NOTE: There may be subtleties depending on the target debugger. For the most part, if + * the connection is handling a single target, things will work as expected. However, if there + * are multiple targets on one connection, it is possible for the given target to break, but for + * the target debugger to remain unresponsive to commands. This would happen, e.g., if a second + * target on the same connection is still running. + * + * @param target the target + * @param timeout the maximum amount of time to wait + * @param unit the units for time + * @throws TimeoutException if the timeout expires + */ + default void waitForBreak(TargetObject target, long timeout, TimeUnit unit) + throws TimeoutException { + TargetExecutionStateful stateful = findInterface(target, TargetExecutionStateful.class); + if (stateful == null) { + throw new IllegalArgumentException("Given target is not stateful"); + } + var listener = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) { + CompletableFuture future = new CompletableFuture<>(); + + @AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME) + private void stateChanged(TargetObject parent, TargetExecutionState state) { + if (parent == stateful && !state.isRunning()) { + future.complete(null); + } + } + }; + target.getModel().addModelListener(listener); + try { + if (!stateful.getExecutionState().isRunning()) { + return; + } + listener.future.get(timeout, unit); + } + catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + finally { + target.getModel().removeModelListener(listener); + } + } + + @Override + default void waitForBreak(Trace trace, long timeout, TimeUnit unit) throws TimeoutException { + TargetObject target = getTarget(trace); + if (target == null || !target.isValid()) { + return; + } + waitForBreak(target, timeout, unit); + } + + /** + * Execute a command in a connection's interpreter, capturing the output + * + *

+ * This executes a raw command in the given interpreter. The command could have arbitrary + * effects, so it may be necessary to wait for those effects to be handled by the tool's + * services and plugins before proceeding. + * + * @param interpreter the interpreter + * @param command the command + * @return the output, or null if there is no interpreter + */ + default String executeCapture(TargetInterpreter interpreter, String command) { + if (interpreter == null) { + return null; + } + try { + return waitOn(interpreter.executeCapture(command)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + } + + /** + * Execute a command in a connection's interpreter + * + *

+ * This executes a raw command in the given interpreter. The command could have arbitrary + * effects, so it may be necessary to wait for those effects to be handled by the tool's + * services and plugins before proceeding. + * + * @param interpreter the interpreter + * @param command the command + * @return true if successful + */ + default boolean execute(TargetInterpreter interpreter, String command) { + if (interpreter == null) { + return false; + } + try { + waitOn(interpreter.executeCapture(command)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + return true; + } + + /** + * Get the value at the given path for the given model + * + * @param model the model + * @param path the path + * @return the avlue, or null if the trace is not live or if the path does not exist + */ + default Object getModelValue(DebuggerObjectModel model, String path) { + try { + return waitOn(model.fetchModelValue(PathUtils.parse(path))); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + } + + /** + * Get the value at the given path for the current trace's model + * + * @param path the path + * @return the value, or null if the trace is not live or if the path does not exist + */ + default Object getModelValue(String path) { + TraceRecorder recorder = getModelService().getRecorder(getCurrentTrace()); + if (recorder == null) { + return null; + } + return getModelValue(recorder.getTarget().getModel(), path); + } + + /** + * Refresh the given objects children (elements and attributes) + * + * @param object the object + * @return the set of children, excluding primitive-valued attributes + */ + default Set refreshObjectChildren(TargetObject object) { + try { + // Refresh both children and memory/register values + waitOn(object.invalidateCaches()); + waitOn(object.resync()); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + Set result = new LinkedHashSet<>(); + result.addAll(object.getCachedElements().values()); + for (Object v : object.getCachedAttributes().values()) { + if (v instanceof TargetObject) { + result.add((TargetObject) v); + } + } + return result; + } + + /** + * Refresh the given object and its children, recursively + * + *

+ * The objects are traversed in depth-first pre-order. Links are traversed, even if the object + * is not part of the specified subtree, but an object is skipped if it has already been + * visited. + * + * @param object the seed object + * @return true if the traversal completed successfully + */ + default boolean refreshSubtree(TargetObject object) { + var util = new Object() { + Set visited = new HashSet<>(); + + boolean visit(TargetObject object) { + if (!visited.add(object)) { + return true; + } + for (TargetObject child : refreshObjectChildren(object)) { + if (!visit(child)) { + return false; + } + } + return true; + } + }; + return util.visit(object); + } + + /** + * {@inheritDoc} + * + *

+ * This override includes flushing the recorder's event and transaction queues. + */ + @Override + default boolean flushAsyncPipelines(Trace trace) { + try { + TraceRecorder recorder = getModelService().getRecorder(trace); + if (recorder != null) { + waitOn(recorder.getTarget().getModel().flushEvents()); + waitOn(recorder.flushTransactions()); + } + trace.flushEvents(); + waitOn(getMappingService().changesSettled()); + waitOn(getBreakpointService().changesSettled()); + Swing.allowSwingToProcessEvents(); + return true; + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + return false; + } + } + + /** + * Get offers for launching the given program + * + * @param program the program + * @return the offers + */ + default List getLaunchOffers(Program program) { + return getModelService().getProgramLaunchOffers(program).collect(Collectors.toList()); + } + + /** + * Get offers for launching the current program + * + * @return the offers + */ + default List getLaunchOffers() { + return getLaunchOffers(requireCurrentProgram()); + } + + /** + * Get the best launch offer for a program, throwing an exception if there is no offer + * + * @param program the program + * @return the offer + * @throws NoSuchElementException if there is no offer + */ + default DebuggerProgramLaunchOffer requireLaunchOffer(Program program) { + Optional offer = + getModelService().getProgramLaunchOffers(program).findFirst(); + if (offer.isEmpty()) { + throw new NoSuchElementException("No offers to launch " + program); + } + return offer.get(); + } + + /** + * Launch the given offer, overriding its command line + * + *

+ * NOTE: Most offers take a command line, but not all do. If this is used for an offer + * that does not, it's behavior is undefined. + * + *

+ * Launches are not always successful, and may in fact fail frequently, usually because of + * configuration errors or missing components on the target platform. This may leave stale + * connections and/or target debuggers, processes, etc., in strange states. Furthermore, even if + * launching the target is successful, starting the recorder may not succeed, typically because + * Ghidra cannot identify and map the target platform to a Sleigh language. This method makes no + * attempt at cleaning up partial pieces. Instead it returns those pieces in the launch result. + * If the result includes a recorder, the launch was successful. If not, the script can decide + * what to do with the other pieces. That choice depends on what is expected of the user. Can + * the user reasonable be expected to intervene and complete the launch manually? How many + * targets does the script intend to launch? How big is the mess if left partially completed? + * + * @param offer the offer (this includes the program given when asking for offers) + * @param commandLine the command-line override. If this doesn't refer to the same program as + * the offer, there may be unexpected results + * @param monitor the monitor for the launch stages + * @return the result, possibly partial + */ + default LaunchResult launch(DebuggerProgramLaunchOffer offer, String commandLine, + TaskMonitor monitor) { + try { + return waitOn(offer.launchProgram(monitor, PromptMode.NEVER, new LaunchConfigurator() { + @Override + public Map configureLauncher(TargetLauncher launcher, + Map arguments, RelPrompt relPrompt) { + Map adjusted = new HashMap<>(arguments); + adjusted.put(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, commandLine); + return adjusted; + } + })); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + // TODO: This is not ideal, since it's likely partially completed + return LaunchResult.totalFailure(e); + } + } + + /** + * Launch the given offer with the default/saved arguments + * + * @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor) + */ + default LaunchResult launch(DebuggerProgramLaunchOffer offer, TaskMonitor monitor) { + try { + return waitOn(offer.launchProgram(monitor, PromptMode.NEVER)); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + // TODO: This is not ideal, since it's likely partially completed + return LaunchResult.totalFailure(e); + } + } + + /** + * Launch the given program, overriding its command line + * + *

+ * This takes the best offer for the given program. The command line should invoke the given + * program. If it does not, there may be unexpected results. + * + * @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor) + */ + default LaunchResult launch(Program program, String commandLine, TaskMonitor monitor) + throws InterruptedException, ExecutionException, TimeoutException { + return launch(requireLaunchOffer(program), commandLine, monitor); + } + + /** + * Launch the given program with the default/saved arguments + * + *

+ * This takes the best offer for the given program. + * + * @see #launch(DebuggerProgramLaunchOffer, String, TaskMonitor) + */ + default LaunchResult launch(Program program, TaskMonitor monitor) + throws InterruptedException, ExecutionException, TimeoutException { + return launch(requireLaunchOffer(program), monitor); + } + + /** + * Launch the current program, overriding its command line + * + * @see #launch(Program, String, TaskMonitor) + */ + default LaunchResult launch(String commandLine, TaskMonitor monitor) + throws InterruptedException, ExecutionException, TimeoutException { + return launch(requireCurrentProgram(), commandLine, monitor); + } + + /** + * Launch the current program with the default/saved arguments + * + * @see #launch(Program, TaskMonitor) + */ + default LaunchResult launch(TaskMonitor monitor) + throws InterruptedException, ExecutionException, TimeoutException { + return launch(requireCurrentProgram(), monitor); + } +} diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRmiAPI.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRmiAPI.java new file mode 100644 index 00000000000..e4f71f1ad34 --- /dev/null +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/flatapi/FlatDebuggerRmiAPI.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.debug.flatapi; + +import java.util.*; + +import ghidra.app.services.TraceRmiLauncherService; +import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; +import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*; +import ghidra.program.model.listing.Program; +import ghidra.util.task.TaskMonitor; + +public interface FlatDebuggerRmiAPI extends FlatDebuggerAPI { + + /** + * Get the trace-rmi launcher service + * + * @return the service + */ + default TraceRmiLauncherService getTraceRmiLauncherService() { + return requireService(TraceRmiLauncherService.class); + } + + /** + * Get offers for launching the given program + * + * @param program the program, or null for no image + * @return the offers + */ + default Collection getLaunchOffers(Program program) { + return getTraceRmiLauncherService().getOffers(program); + } + + /** + * Get offers for launching the current program + * + * @return the offers + */ + default Collection getLaunchOffers() { + return getLaunchOffers(getCurrentProgram()); + } + + /** + * Get saved offers for launching the given program, ordered by most-recently-saved + * + * @param program the program, or null for no image + * @return the offers + */ + default List getSavedLaunchOffers(Program program) { + return getTraceRmiLauncherService().getSavedOffers(program); + } + + /** + * Get saved offers for launching the current program, ordered by most-recently-saved + * + * @return the offers + */ + default List getSavedLaunchOffers() { + return getSavedLaunchOffers(getCurrentProgram()); + } + + /** + * Get the most-recently-saved launch offer for the given program + * + * @param program the program, or null for no image + * @return the offer + * @throws NoSuchElementException if no offer's configuration has been saved + */ + default TraceRmiLaunchOffer requireLastLaunchOffer(Program program) { + List offers = getSavedLaunchOffers(program); + if (offers.isEmpty()) { + throw new NoSuchElementException("No saved offers to launch " + program); + } + return offers.get(0); + } + + /** + * Get the most-recently-saved launch offer for the current program + * + * @return the offer + * @throws NoSuchElementException if no offer's configuration has been saved + */ + default TraceRmiLaunchOffer requireLastLaunchOffer() { + return requireLastLaunchOffer(getCurrentProgram()); + } + + /** + * Launch the given offer with the default, saved, and/or overridden arguments + * + *

+ * If the offer has saved arguments, those will be loaded. Otherwise, the default arguments will + * be used. If given, specific arguments can be overridden by the caller. The caller may need to + * examine the offer's parameters before overriding any arguments. Conventionally, the argument + * displayed as "Image" gives the path to the executable, and "Args" gives the command-line + * arguments to pass to the target. + * + * @param offer the offer to launch + * @param monitor a monitor for the launch stages + * @param overrideArgs overridden arguments, which may be empty + * @return the launch result, which may indicate errors + */ + default LaunchResult launch(TraceRmiLaunchOffer offer, Map overrideArgs, + TaskMonitor monitor) { + return offer.launchProgram(monitor, new LaunchConfigurator() { + @Override + public Map configureLauncher(TraceRmiLaunchOffer offer, + Map arguments, RelPrompt relPrompt) { + if (arguments.isEmpty()) { + return arguments; + } + Map args = new HashMap<>(arguments); + args.putAll(overrideArgs); + return args; + } + }); + } + + /** + * Launch the given offer with the default or saved arguments + * + * @param offer the offer to launch + * @param monitor a monitor for the launch stages + * @return the launch result, which may indicate errors + */ + default LaunchResult launch(TraceRmiLaunchOffer offer, TaskMonitor monitor) { + return launch(offer, Map.of(), monitor); + } + + /** + * Launch the given program with the most-recently-saved offer + * + * @param program the program to launch + * @param monitor a monitor for the launch stages + * @return the launch result, which may indicate errors + */ + default LaunchResult launch(Program program, TaskMonitor monitor) { + return launch(requireLastLaunchOffer(program), monitor); + } + + /** + * Launch the current program with the most-recently-saved offer + * + * @param monitor a monitor for the launch stages + * @return the launch result, which may indicate errors + */ + default LaunchResult launch(TaskMonitor monitor) { + return launch(requireLastLaunchOffer(), monitor); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java index 6aceb0290a7..93460640108 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java @@ -146,6 +146,15 @@ public AbstractTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program this.terminalService = Objects.requireNonNull(tool.getService(TerminalService.class)); } + @Override + public boolean equals(Object obj) { + if (this.getClass() != obj.getClass()) { + return false; + } + AbstractTraceRmiLaunchOffer other = (AbstractTraceRmiLaunchOffer) obj; + return this.getConfigName().equals(other.getConfigName()); + } + protected int getTimeoutMillis() { return DEFAULT_TIMEOUT_MILLIS; } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePlugin.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePlugin.java index 738732e2707..5cd4b71272a 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePlugin.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePlugin.java @@ -192,6 +192,15 @@ public Collection getOffers(Program program) { .toList(); } + @Override + public List getSavedOffers(Program program) { + Map savedConfigs = loadSavedConfigs(program); + return getOffers(program).stream() + .filter(o -> savedConfigs.containsKey(o.getConfigName())) + .sorted(Comparator.comparing(o -> -savedConfigs.get(o.getConfigName()))) + .toList(); + } + protected void executeTask(Task task) { ProgressService progressService = tool.getService(ProgressService.class); if (progressService != null) { diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java index 65f9cc08f3f..cf79ddc2698 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java @@ -25,7 +25,6 @@ import docking.ActionContext; import ghidra.app.context.ProgramLocationActionContext; -import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext; import ghidra.app.plugin.core.debug.gui.tracermi.RemoteMethodInvocationDialog; import ghidra.app.plugin.core.debug.service.target.AbstractTarget; import ghidra.app.services.DebuggerConsoleService; @@ -38,6 +37,7 @@ import ghidra.dbg.util.PathMatcher; import ghidra.dbg.util.PathPredicates; import ghidra.dbg.util.PathPredicates.Align; +import ghidra.debug.api.model.DebuggerObjectActionContext; import ghidra.debug.api.target.ActionName; import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.debug.api.tracermi.*; @@ -51,6 +51,7 @@ import ghidra.trace.model.breakpoint.*; import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.trace.model.memory.TraceObjectMemoryRegion; import ghidra.trace.model.stack.*; import ghidra.trace.model.target.*; @@ -611,6 +612,13 @@ static List makeBySpecificity(TargetObjectSchema rootSchema, } } + record ExecuteMatcher(int score, List spec) implements MethodMatcher { + static final ExecuteMatcher HAS_CMD_TOSTRING = new ExecuteMatcher(2, List.of( + new TypeParamSpec("command", String.class), + new TypeParamSpec("toString", Boolean.class))); + static final List ALL = matchers(HAS_CMD_TOSTRING); + } + record ReadMemMatcher(int score, List spec) implements MethodMatcher { static final ReadMemMatcher HAS_PROC_RANGE = new ReadMemMatcher(2, List.of( new TypeParamSpec("process", TargetProcess.class), @@ -841,6 +849,18 @@ public CompletableFuture readRegs(TraceObject obj, RemoteMethod method, } } + @Override + public CompletableFuture executeAsync(String command, boolean toString) { + MatchedMethod execute = matches.getBest("execute", ActionName.EXECUTE, ExecuteMatcher.ALL); + if (execute == null) { + return CompletableFuture.failedFuture(new NoSuchElementException()); + } + Map args = new HashMap<>(); + args.put(execute.params.get("command").name(), command); + args.put(execute.params.get("toString").name(), toString); + return execute.method.invokeAsync(args).toCompletableFuture().thenApply(v -> (String) v); + } + @Override public CompletableFuture activateAsync(DebuggerCoordinates prev, DebuggerCoordinates coords) { @@ -906,15 +926,11 @@ protected SchemaContext getSchemaContext() { } protected TraceObject getProcessForSpace(AddressSpace space) { - for (TraceObjectValue objVal : trace.getObjectManager() - .getValuesIntersecting( + for (TraceMemoryRegion region : trace.getMemoryManager() + .getRegionsIntersecting( Lifespan.at(getSnap()), - new AddressRangeImpl(space.getMinAddress(), space.getMaxAddress()), - TargetMemoryRegion.RANGE_ATTRIBUTE_NAME)) { - TraceObject obj = objVal.getParent(); - if (!obj.getInterfaces().contains(TraceObjectMemoryRegion.class)) { - continue; - } + new AddressRangeImpl(space.getMinAddress(), space.getMaxAddress()))) { + TraceObject obj = ((TraceObjectMemoryRegion) region).getObject(); return obj.queryCanonicalAncestorsTargetInterface(TargetProcess.class) .findFirst() .orElse(null); diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java index baddfb52faa..deac96838d5 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/TestTraceRmiConnection.java @@ -20,12 +20,14 @@ import java.net.SocketAddress; import java.util.*; import java.util.concurrent.*; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import ghidra.app.services.DebuggerTargetService; import ghidra.async.AsyncPairingQueue; import ghidra.async.AsyncUtils; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; import ghidra.debug.api.target.ActionName; import ghidra.debug.api.target.Target; @@ -65,6 +67,19 @@ public TestRemoteMethod(String name, ActionName action, String display, String d retType); } + public TestRemoteMethod(String name, ActionName action, String display, String description, + Map parameters, TargetObjectSchema retType) { + this(name, action, display, description, parameters, retType.getName(), + new AsyncPairingQueue<>(), new AsyncPairingQueue<>()); + } + + public TestRemoteMethod(String name, ActionName action, String display, String description, + TargetObjectSchema retType, RemoteParameter... parameters) { + this(name, action, display, description, Stream.of(parameters) + .collect(Collectors.toMap(RemoteParameter::name, p -> p)), + retType); + } + @Override public RemoteAsyncResult invokeAsync(Map arguments) { argQueue.give().complete(arguments); @@ -80,10 +95,24 @@ public Map expect() throws InterruptedException, ExecutionExcept public void result(Object ret) { retQueue.give().complete(ret); } + + public CompletableFuture> expect( + Function, Object> impl) { + record ArgsRet(Map args, Object ret) { + } + var result = argQueue().take().thenApply(a -> new ArgsRet(a, impl.apply(a))); + result.thenApply(ar -> ar.ret).handle(AsyncUtils.copyTo(retQueue().give())); + return result.thenApply(ar -> ar.args); + } } public record TestRemoteParameter(String name, SchemaName type, boolean required, Object defaultValue, String display, String description) implements RemoteParameter { + public TestRemoteParameter(String name, TargetObjectSchema type, boolean required, + Object defaultValue, String display, String description) { + this(name, type.getName(), required, defaultValue, display, description); + } + @Override public Object getDefaultValue() { return defaultValue; diff --git a/Ghidra/Debug/Debugger/ghidra_scripts/DemoDebuggerScript.java b/Ghidra/Debug/Debugger/ghidra_scripts/DemoDebuggerScript.java index f38cbc83393..2f98daf0a46 100644 --- a/Ghidra/Debug/Debugger/ghidra_scripts/DemoDebuggerScript.java +++ b/Ghidra/Debug/Debugger/ghidra_scripts/DemoDebuggerScript.java @@ -27,12 +27,12 @@ import ghidra.app.script.GhidraScript; import ghidra.debug.api.breakpoint.LogicalBreakpoint; -import ghidra.debug.api.model.DebuggerProgramLaunchOffer.LaunchResult; -import ghidra.debug.flatapi.FlatDebuggerAPI; +import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchResult; +import ghidra.debug.flatapi.FlatDebuggerRmiAPI; import ghidra.program.model.address.Address; import ghidra.trace.model.Trace; -public class DemoDebuggerScript extends GhidraScript implements FlatDebuggerAPI { +public class DemoDebuggerScript extends GhidraScript implements FlatDebuggerRmiAPI { @Override protected void run() throws Exception { @@ -55,25 +55,19 @@ protected void run() throws Exception { if (result.exception() != null) { printerr("Failed to launch " + currentProgram + ": " + result.exception()); - if (result.model() != null) { - result.model().close(); - } - - if (result.recorder() != null) { - closeTrace(result.recorder().getTrace()); - } + result.close(); return; } - Trace trace = result.recorder().getTrace(); + Trace trace = result.trace(); println("Successfully launched in trace " + trace); /** - * Breakpoints are highly dependent on the module map. To work correctly: 1) The target - * debugger must provide the module map. 2) Ghidra must have recorded that module map into - * the trace. 3) Ghidra must recognize the module names and map them to programs open in the - * tool. These events all occur asynchronously, usually immediately after launch. Most - * launchers will wait for the target program module to be mapped to its Ghidra program - * database, but the breakpoint service may still be processing the new mapping. + * Breakpoints are highly dependent on the module map. To work correctly, the target + * debugger must record the module map into the trace, and Ghidra must recognize the module + * names and map them to programs open in the tool. These events all occur asynchronously, + * usually immediately after launch. Most launchers will wait for the target program module + * to be mapped to its Ghidra program database, but the breakpoint service may still be + * processing the new mapping. */ flushAsyncPipelines(trace); @@ -104,8 +98,8 @@ protected void run() throws Exception { while (isTargetAlive()) { waitForBreak(10, TimeUnit.SECONDS); /** - * The recorder is going to schedule some reads upon break, so let's allow them to - * settle. + * The target is going to perform some reads upon break, so let's allow them to + * complete. */ flushAsyncPipelines(trace); diff --git a/Ghidra/Debug/Debugger/ghidra_scripts/MonitorModelEventsScript.java b/Ghidra/Debug/Debugger/ghidra_scripts/MonitorModelEventsScript.java index 0e2923aadac..f7659707074 100644 --- a/Ghidra/Debug/Debugger/ghidra_scripts/MonitorModelEventsScript.java +++ b/Ghidra/Debug/Debugger/ghidra_scripts/MonitorModelEventsScript.java @@ -20,9 +20,9 @@ import ghidra.dbg.target.TargetEventScope.TargetEventType; import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetThread; -import ghidra.debug.flatapi.FlatDebuggerAPI; +import ghidra.debug.flatapi.FlatDebuggerRecorderAPI;; -public class MonitorModelEventsScript extends GhidraScript implements FlatDebuggerAPI { +public class MonitorModelEventsScript extends GhidraScript implements FlatDebuggerRecorderAPI { static DebuggerModelListener listener = new DebuggerModelListener() { @Override public void attributesChanged(TargetObject object, Collection removed, diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java index 3ffc558938d..216097b4a71 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java @@ -30,7 +30,6 @@ import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent; import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; -import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext; import ghidra.app.services.*; import ghidra.app.services.DebuggerControlService.ControlModeChangeListener; import ghidra.app.services.DebuggerEmulationService.CachedEmulator; @@ -39,6 +38,7 @@ import ghidra.async.AsyncUtils; import ghidra.debug.api.control.ControlMode; import ghidra.debug.api.emulation.DebuggerPcodeMachine; +import ghidra.debug.api.model.DebuggerObjectActionContext; import ghidra.debug.api.target.ActionName; import ghidra.debug.api.target.Target; import ghidra.debug.api.target.Target.ActionEntry; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerMethodActionsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerMethodActionsPlugin.java index ccac7eae1fd..03cc35ec34c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerMethodActionsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerMethodActionsPlugin.java @@ -29,9 +29,9 @@ 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.model.DebuggerObjectActionContext; import ghidra.app.services.*; import ghidra.debug.api.control.ControlMode; +import ghidra.debug.api.model.DebuggerObjectActionContext; import ghidra.debug.api.target.ActionName; import ghidra.debug.api.target.Target; import ghidra.debug.api.target.Target.ActionEntry; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPanel.java index f0ac6ebd593..9c2a698cd51 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsPanel.java @@ -25,6 +25,7 @@ import ghidra.app.plugin.core.debug.gui.model.columns.*; import ghidra.dbg.target.*; import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.debug.api.model.DebuggerObjectActionContext; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.ServiceProvider; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java index 0f0691b7de7..503fc6a9409 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerRegionsProvider.java @@ -35,10 +35,10 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.AbstractSelectAddressesAction; import ghidra.app.plugin.core.debug.gui.DebuggerResources.SelectRowsAction; -import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext; import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider; import ghidra.app.plugin.core.debug.service.modules.MapRegionsBackgroundCommand; import ghidra.app.services.*; +import ghidra.debug.api.model.DebuggerObjectActionContext; import ghidra.debug.api.modules.MapProposal; import ghidra.debug.api.modules.RegionMapProposal; import ghidra.debug.api.modules.RegionMapProposal.RegionMapEntry; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractObjectsTableBasedPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractObjectsTableBasedPanel.java index d09189ec82d..d452314ab7d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractObjectsTableBasedPanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractObjectsTableBasedPanel.java @@ -29,6 +29,7 @@ import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueProperty; import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow; import ghidra.app.services.DebuggerListingService; +import ghidra.debug.api.model.DebuggerObjectActionContext; import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.framework.plugintool.AutoService; import ghidra.framework.plugintool.Plugin; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java index 9486f0d05d1..e1e8617ad54 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java @@ -51,6 +51,7 @@ import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.RootNode; import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow; import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.debug.api.model.DebuggerObjectActionContext; import ghidra.debug.api.target.ActionName; import ghidra.debug.api.target.Target; import ghidra.debug.api.target.Target.ActionEntry; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPanel.java index 41680e5cc60..b305f80f7f5 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesPanel.java @@ -28,6 +28,7 @@ import ghidra.dbg.target.TargetModule; import ghidra.dbg.target.TargetProcess; import ghidra.dbg.target.schema.TargetObjectSchema; +import ghidra.debug.api.model.DebuggerObjectActionContext; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.ServiceProvider; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java index d43aa899a44..04050091681 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java @@ -41,11 +41,11 @@ import ghidra.app.plugin.core.debug.gui.action.AutoMapSpec; import ghidra.app.plugin.core.debug.gui.action.AutoMapSpec.AutoMapSpecConfigFieldCodec; import ghidra.app.plugin.core.debug.gui.action.ByModuleAutoMapSpec; -import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext; import ghidra.app.plugin.core.debug.service.model.TraceRecorderTarget; import ghidra.app.plugin.core.debug.service.modules.MapModulesBackgroundCommand; import ghidra.app.plugin.core.debug.service.modules.MapSectionsBackgroundCommand; import ghidra.app.services.*; +import ghidra.debug.api.model.DebuggerObjectActionContext; import ghidra.debug.api.modules.*; import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry; import ghidra.debug.api.modules.SectionMapProposal.SectionMapEntry; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java index 91934b60efb..2f94f2df870 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java @@ -23,7 +23,6 @@ import docking.ActionContext; import ghidra.app.context.ProgramLocationActionContext; -import ghidra.app.plugin.core.debug.gui.model.DebuggerObjectActionContext; import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog; import ghidra.app.plugin.core.debug.service.target.AbstractTarget; import ghidra.app.services.DebuggerConsoleService; @@ -39,7 +38,7 @@ import ghidra.dbg.target.TargetSteppable.TargetStepKind; import ghidra.dbg.util.PathMatcher; import ghidra.dbg.util.PathPredicates; -import ghidra.debug.api.model.TraceRecorder; +import ghidra.debug.api.model.*; import ghidra.debug.api.target.ActionName; import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.framework.plugintool.PluginTool; @@ -113,6 +112,13 @@ protected T findObjectInContext(ActionContext context, } return iface.cast(recorder.getTargetObject(suitable)); } + else if (context instanceof DebuggerSingleObjectPathActionContext ctx) { + TargetObject targetObject = recorder.getTargetObject(ctx.getPath()); + if (targetObject == null) { + return null; + } + return targetObject.getCachedSuitable(iface); + } return null; } @@ -469,6 +475,18 @@ public CompletableFuture invalidateMemoryCachesAsync() { return CompletableFuture.allOf(requests); } + @Override + public CompletableFuture executeAsync(String command, boolean toString) { + TargetInterpreter interpreter = findObjectInRecorder(null, TargetInterpreter.class); + if (interpreter == null) { + return AsyncUtils.nil(); + } + if (toString) { + return interpreter.executeCapture(command); + } + return interpreter.execute(command).thenApply(r -> null); + } + @Override public CompletableFuture readMemoryAsync(AddressSetView set, TaskMonitor monitor) { return recorder.readMemoryBlocks(set, monitor) diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/target/AbstractTarget.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/target/AbstractTarget.java index 29307c8b530..8117ffae85d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/target/AbstractTarget.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/target/AbstractTarget.java @@ -307,6 +307,11 @@ protected static void runSyncMonitored(TaskMonitor monitor, String name, getSyncMonitored(monitor, name, supplier); } + @Override + public String execute(String command, boolean toString) { + return getSync("execute", () -> executeAsync(command, toString)); + } + @Override public void activate(DebuggerCoordinates prev, DebuggerCoordinates coords) { runSync("activate", () -> activateAsync(prev, coords)); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/control/MockTarget.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/control/MockTarget.java index c2dd3952db7..4c43781d7d7 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/control/MockTarget.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/control/MockTarget.java @@ -24,7 +24,6 @@ import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.debug.api.target.ActionName; import ghidra.debug.api.target.Target; -import ghidra.debug.api.target.Target.ActionEntry; import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.program.model.address.*; import ghidra.program.model.lang.Register; @@ -115,6 +114,16 @@ public CompletableFuture invalidateMemoryCachesAsync() { public void invalidateMemoryCaches() { } + @Override + public CompletableFuture executeAsync(String command, boolean toString) { + return AsyncUtils.nil(); + } + + @Override + public String execute(String command, boolean toString) { + return null; + } + @Override public CompletableFuture readMemoryAsync(AddressSetView set, TaskMonitor monitor) { return AsyncUtils.nil(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java deleted file mode 100644 index 4898e6ed2ed..00000000000 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/debug/flatapi/FlatDebuggerAPITest.java +++ /dev/null @@ -1,1301 +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.debug.flatapi; - -import static org.junit.Assert.*; - -import java.io.IOException; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.util.*; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.function.BiFunction; -import java.util.function.Function; - -import org.junit.Before; -import org.junit.Test; - -import db.Transaction; -import generic.Unique; -import ghidra.app.plugin.assembler.Assembler; -import ghidra.app.plugin.assembler.Assemblers; -import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; -import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; -import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingProvider; -import ghidra.app.plugin.core.debug.service.breakpoint.DebuggerLogicalBreakpointServicePlugin; -import ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin; -import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin; -import ghidra.app.plugin.core.debug.service.model.TestDebuggerProgramLaunchOpinion.TestDebuggerProgramLaunchOffer; -import ghidra.app.plugin.core.debug.service.model.launch.AbstractDebuggerProgramLaunchOffer; -import ghidra.app.script.GhidraState; -import ghidra.app.services.*; -import ghidra.dbg.DebuggerModelFactory; -import ghidra.dbg.DebuggerObjectModel; -import ghidra.dbg.model.*; -import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; -import ghidra.dbg.target.TargetObject; -import ghidra.dbg.target.TargetSteppable.TargetStepKind; -import ghidra.debug.api.breakpoint.LogicalBreakpoint; -import ghidra.debug.api.breakpoint.LogicalBreakpoint.State; -import ghidra.debug.api.control.ControlMode; -import ghidra.debug.api.model.DebuggerProgramLaunchOffer; -import ghidra.debug.api.model.DebuggerProgramLaunchOffer.LaunchResult; -import ghidra.debug.api.model.TraceRecorder; -import ghidra.debug.api.tracemgr.DebuggerCoordinates; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressSpace; -import ghidra.program.model.lang.*; -import ghidra.program.model.listing.Program; -import ghidra.trace.database.memory.DBTraceMemoryManager; -import ghidra.trace.database.memory.DBTraceMemorySpace; -import ghidra.trace.model.Lifespan; -import ghidra.trace.model.Trace; -import ghidra.trace.model.breakpoint.TraceBreakpointKind; -import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; -import ghidra.trace.model.memory.TraceMemoryFlag; -import ghidra.trace.model.stack.TraceStack; -import ghidra.trace.model.thread.TraceThread; -import ghidra.trace.model.time.schedule.TraceSchedule; - -public class FlatDebuggerAPITest extends AbstractGhidraHeadedDebuggerTest { - - protected static class TestFactory implements DebuggerModelFactory { - private final DebuggerObjectModel model; - - public TestFactory(DebuggerObjectModel model) { - this.model = model; - } - - @Override - public CompletableFuture build() { - return CompletableFuture.completedFuture(model); - } - } - - protected class TestOffer extends AbstractDebuggerProgramLaunchOffer { - public TestOffer(Program program, DebuggerModelFactory factory) { - super(program, env.getTool(), factory); - } - - public TestOffer(Program program, DebuggerObjectModel model) { - this(program, new TestFactory(model)); - } - - @Override - public String getConfigName() { - return "TEST"; - } - - @Override - public String getMenuTitle() { - return "in Test Debugger"; - } - } - - protected static class TestModelBuilder extends TestDebuggerModelBuilder { - private final TestDebuggerObjectModel model; - - public TestModelBuilder(TestDebuggerObjectModel model) { - this.model = model; - } - - @Override - protected TestDebuggerObjectModel newModel(String typeHint) { - return model; - } - } - - protected class TestFlatAPI implements FlatDebuggerAPI { - protected final GhidraState state = - new GhidraState(env.getTool(), env.getProject(), program, null, null, null); - - @Override - public GhidraState getState() { - return state; - } - } - - protected DebuggerLogicalBreakpointService breakpointService; - protected DebuggerStaticMappingService mappingService; - protected DebuggerEmulationService emulationService; - protected DebuggerListingService listingService; - protected DebuggerControlService editingService; - protected FlatDebuggerAPI flat; - - @Before - public void setUpFlatAPITest() throws Throwable { - breakpointService = addPlugin(tool, DebuggerLogicalBreakpointServicePlugin.class); - mappingService = tool.getService(DebuggerStaticMappingService.class); - emulationService = addPlugin(tool, DebuggerEmulationServicePlugin.class); - listingService = addPlugin(tool, DebuggerListingPlugin.class); - editingService = addPlugin(tool, DebuggerControlServicePlugin.class); - flat = new TestFlatAPI(); - - // TODO: This seems to hold up the task manager. - waitForComponentProvider(DebuggerListingProvider.class).setAutoDisassemble(false); - } - - @Test - public void testRequireService() throws Throwable { - assertEquals(modelService, flat.requireService(DebuggerModelService.class)); - } - - interface NoSuchService { - } - - @Test(expected = IllegalStateException.class) - public void testRequireServiceAbsentErr() { - flat.requireService(NoSuchService.class); - } - - @Test - public void testGetCurrentDebuggerCoordinates() throws Throwable { - assertSame(DebuggerCoordinates.NOWHERE, flat.getCurrentDebuggerCoordinates()); - - createAndOpenTrace(); - traceManager.activateTrace(tb.trace); - - assertEquals(DebuggerCoordinates.NOWHERE.trace(tb.trace), - flat.getCurrentDebuggerCoordinates()); - } - - @Test - public void testGetCurrentTrace() throws Throwable { - assertNull(flat.getCurrentTrace()); - - createAndOpenTrace(); - traceManager.activateTrace(tb.trace); - - assertEquals(tb.trace, flat.getCurrentTrace()); - } - - @Test(expected = IllegalStateException.class) - public void testRequireCurrentTraceAbsentErr() { - flat.requireCurrentTrace(); - } - - @Test - public void testGetCurrentThread() throws Throwable { - assertNull(flat.getCurrentThread()); - - createAndOpenTrace(); - TraceThread thread; - try (Transaction tx = tb.startTransaction()) { - thread = tb.getOrAddThread("Threads[0]", 0); - } - waitForSwing(); - traceManager.activateTrace(tb.trace); - - assertEquals(thread, flat.getCurrentThread()); - } - - @Test - public void testGetCurrentView() throws Throwable { - assertNull(flat.getCurrentView()); - - createAndOpenTrace(); - traceManager.activateTrace(tb.trace); - - assertEquals(tb.trace.getProgramView(), flat.getCurrentView()); - } - - @Test(expected = IllegalStateException.class) - public void testRequireCurrentViewAbsentErr() { - flat.requireCurrentView(); - } - - @Test - public void testGetCurrentFrame() throws Throwable { - assertEquals(0, flat.getCurrentFrame()); - - createAndOpenTrace(); - TraceThread thread; - try (Transaction tx = tb.startTransaction()) { - thread = tb.getOrAddThread("Threads[0]", 0); - TraceStack stack = tb.trace.getStackManager().getStack(thread, 0, true); - stack.setDepth(3, true); - } - waitForSwing(); - traceManager.activateThread(thread); - traceManager.activateFrame(1); - - assertEquals(1, flat.getCurrentFrame()); - } - - @Test - public void testGetCurrentSnap() throws Throwable { - assertEquals(0L, flat.getCurrentSnap()); - - createAndOpenTrace(); - traceManager.activateTrace(tb.trace); - traceManager.activateSnap(1); - - assertEquals(1L, flat.getCurrentSnap()); - } - - @Test - public void testGetCurrentEmulationSchedule() throws Throwable { - assertEquals(TraceSchedule.parse("0"), flat.getCurrentEmulationSchedule()); - - createAndOpenTrace(); - traceManager.activateTrace(tb.trace); - traceManager.activateSnap(1); - - assertEquals(TraceSchedule.parse("1"), flat.getCurrentEmulationSchedule()); - } - - @Test - public void testActivateTrace() throws Throwable { - createAndOpenTrace(); - flat.activateTrace(tb.trace); - - assertEquals(tb.trace, traceManager.getCurrentTrace()); - } - - @Test - public void testActivateTraceNull() throws Throwable { - createAndOpenTrace(); - traceManager.activateTrace(tb.trace); - waitForSwing(); - assertEquals(tb.trace, traceManager.getCurrentTrace()); - - flat.activateTrace(null); - assertEquals(null, traceManager.getCurrentTrace()); - } - - @Test - public void testActivateTraceNotOpen() throws Throwable { - createTrace(); - assertFalse(traceManager.getOpenTraces().contains(tb.trace)); - - flat.activateTrace(tb.trace); - - assertTrue(traceManager.getOpenTraces().contains(tb.trace)); - assertEquals(tb.trace, traceManager.getCurrentTrace()); - } - - protected TraceThread createTraceWithThreadAndStack(boolean open) throws Throwable { - if (open) { - createAndOpenTrace(); - } - else { - createTrace(); - } - TraceThread thread; - try (Transaction tx = tb.startTransaction()) { - thread = tb.getOrAddThread("Threads[0]", 0); - TraceStack stack = tb.trace.getStackManager().getStack(thread, 0, true); - stack.setDepth(3, true); - } - waitForSwing(); - return thread; - } - - @Test - public void testActivateThread() throws Throwable { - TraceThread thread = createTraceWithThreadAndStack(true); - flat.activateThread(thread); - - assertEquals(thread, traceManager.getCurrentThread()); - } - - @Test - public void testActivateThreadNull() throws Throwable { - flat.activateThread(null); - assertEquals(null, traceManager.getCurrentThread()); - - TraceThread thread = createTraceWithThreadAndStack(true); - traceManager.activateThread(thread); - waitForSwing(); - assertEquals(thread, traceManager.getCurrentThread()); - - flat.activateThread(null); - assertNull(traceManager.getCurrentThread()); - } - - @Test - public void testActivateThreadNotOpen() throws Throwable { - TraceThread thread = createTraceWithThreadAndStack(false); - assertFalse(traceManager.getOpenTraces().contains(tb.trace)); - - flat.activateThread(thread); - - assertTrue(traceManager.getOpenTraces().contains(tb.trace)); - assertEquals(thread, traceManager.getCurrentThread()); - } - - @Test - public void testActivateFrame() throws Throwable { - TraceThread thread = createTraceWithThreadAndStack(true); - traceManager.activateThread(thread); - waitForSwing(); - flat.activateFrame(1); - - assertEquals(1, traceManager.getCurrentFrame()); - } - - @Test - public void testActivateSnap() throws Throwable { - createAndOpenTrace(); - traceManager.activateTrace(tb.trace); - waitForSwing(); - flat.activateSnap(1); - - assertEquals(1L, traceManager.getCurrentSnap()); - } - - protected void createTraceWithBinText() throws Throwable { - createAndOpenTrace(); - - try (Transaction tx = tb.startTransaction()) { - DBTraceMemoryManager mm = tb.trace.getMemoryManager(); - mm.createRegion("Memory[bin.text]", 0, tb.range(0x00400000, 0x0040ffff), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); - - mm.putBytes(0, tb.addr(0x00400000), tb.buf(1, 2, 3, 4, 5, 6, 7, 8)); - } - traceManager.activateTrace(tb.trace); - waitForSwing(); - } - - @Test - public void testGetCurrentDebuggerAddress() throws Throwable { - assertEquals(null, flat.getCurrentDebuggerAddress()); - - createTraceWithBinText(); - - assertEquals(tb.addr(0x00400000), flat.getCurrentDebuggerAddress()); - } - - @Test - public void testGoToDynamic() throws Throwable { - createTraceWithBinText(); - - assertTrue(flat.goToDynamic("00400123")); - assertEquals(tb.addr(0x00400123), listingService.getCurrentLocation().getAddress()); - - assertTrue(flat.goToDynamic(tb.addr(0x00400321))); - assertEquals(tb.addr(0x00400321), listingService.getCurrentLocation().getAddress()); - } - - @Override - protected void createProgram(Language lang, CompilerSpec cSpec) throws IOException { - super.createProgram(lang, cSpec); - flat.getState().setCurrentProgram(program); - } - - protected void createMappedTraceAndProgram() throws Throwable { - createAndOpenTrace(); - createProgramFromTrace(); - - intoProject(program); - intoProject(tb.trace); - - programManager.openProgram(program); - traceManager.activateTrace(tb.trace); - - try (Transaction tx = program.openTransaction("add block")) { - program.getMemory() - .createInitializedBlock(".text", addr(program, 0x00400000), 4096, (byte) 0, - monitor, false); - } - - CompletableFuture changesSettled; - try (Transaction tx = tb.startTransaction()) { - tb.trace.getMemoryManager() - .createRegion("Memory[bin.text]", 0, tb.range(0x00400000, 0x00400fff), - Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); - changesSettled = mappingService.changesSettled(); - mappingService.addIdentityMapping(tb.trace, program, Lifespan.nowOn(0), true); - } - waitForSwing(); - waitOn(changesSettled); - } - - @Test - public void testGetCurrentProgram() throws Throwable { - assertEquals(null, flat.getCurrentProgram()); - - createProgram(); - programManager.openProgram(program); - - assertEquals(program, flat.getCurrentProgram()); - } - - @Test(expected = IllegalStateException.class) - public void testRequireCurrentProgramAbsentErr() throws Throwable { - flat.requireCurrentProgram(); - } - - @Test - public void testTranslateStaticToDynamic() throws Throwable { - createMappedTraceAndProgram(); - - assertEquals(flat.dynamicLocation("00400123"), - flat.translateStaticToDynamic(flat.staticLocation("00400123"))); - assertNull(flat.translateStaticToDynamic(flat.staticLocation("00600123"))); - - assertEquals(tb.addr(0x00400123), flat.translateStaticToDynamic(addr(program, 0x00400123))); - assertNull(flat.translateStaticToDynamic(addr(program, 0x00600123))); - } - - @Test - public void testTranslateDynamicToStatic() throws Throwable { - createMappedTraceAndProgram(); - - assertEquals(flat.staticLocation("00400123"), - flat.translateDynamicToStatic(flat.dynamicLocation("00400123"))); - assertNull(flat.translateDynamicToStatic(flat.dynamicLocation("00600123"))); - - assertEquals(addr(program, 0x00400123), flat.translateDynamicToStatic(tb.addr(0x00400123))); - assertNull(flat.translateDynamicToStatic(tb.addr(0x00600123))); - } - - protected Address createEmulatableProgram() throws Throwable { - createProgram(); - programManager.openProgram(program); - - Address entry = addr(program, 0x00400000); - try (Transaction start = program.openTransaction("init")) { - program.getMemory() - .createInitializedBlock(".text", entry, 4096, (byte) 0, - monitor, false); - Assembler asm = Assemblers.getAssembler(program); - asm.assemble(entry, "imm r0,#123"); - } - - // Emulate launch will create a static mapping - intoProject(program); - - return entry; - } - - @Test - public void testEmulateLaunch() throws Throwable { - Address entry = createEmulatableProgram(); - - Trace trace = flat.emulateLaunch(entry); - assertEquals(trace, traceManager.getCurrentTrace()); - } - - @Test - public void testEmulate() throws Throwable { - Address entry = createEmulatableProgram(); - - flat.emulateLaunch(entry); - TraceSchedule schedule = - traceManager.getCurrent().getTime().steppedForward(traceManager.getCurrentThread(), 1); - flat.emulate(schedule, monitor); - - assertEquals(schedule, traceManager.getCurrent().getTime()); - } - - @Test - public void testStepEmuInstruction() throws Throwable { - Address entry = createEmulatableProgram(); - - flat.emulateLaunch(entry); - TraceSchedule schedule = - traceManager.getCurrent().getTime().steppedForward(traceManager.getCurrentThread(), 1); - - flat.stepEmuInstruction(1, monitor); - assertEquals(schedule, traceManager.getCurrent().getTime()); - - flat.stepEmuInstruction(-1, monitor); - assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime()); - } - - @Test - public void testStepEmuPcodeOp() throws Throwable { - Address entry = createEmulatableProgram(); - - flat.emulateLaunch(entry); - TraceSchedule schedule = traceManager.getCurrent() - .getTime() - .steppedPcodeForward(traceManager.getCurrentThread(), 1); - - flat.stepEmuPcodeOp(1, monitor); - assertEquals(schedule, traceManager.getCurrent().getTime()); - - flat.stepEmuPcodeOp(-1, monitor); - assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime()); - } - - @Test - public void testSkipEmuInstruction() throws Throwable { - Address entry = createEmulatableProgram(); - - flat.emulateLaunch(entry); - TraceSchedule schedule = - traceManager.getCurrent().getTime().skippedForward(traceManager.getCurrentThread(), 1); - - flat.skipEmuInstruction(1, monitor); - assertEquals(schedule, traceManager.getCurrent().getTime()); - - flat.skipEmuInstruction(-1, monitor); - assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime()); - } - - @Test - public void testSkipEmuPcodeOp() throws Throwable { - Address entry = createEmulatableProgram(); - - flat.emulateLaunch(entry); - TraceSchedule schedule = traceManager.getCurrent() - .getTime() - .skippedPcodeForward(traceManager.getCurrentThread(), 1); - - flat.skipEmuPcodeOp(1, monitor); - assertEquals(schedule, traceManager.getCurrent().getTime()); - - flat.skipEmuPcodeOp(-1, monitor); - assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime()); - } - - @Test - public void testPatchEmu() throws Throwable { - Address entry = createEmulatableProgram(); - - flat.emulateLaunch(entry); - TraceSchedule schedule = traceManager.getCurrent() - .getTime() - .patched(traceManager.getCurrentThread(), - traceManager.getCurrentPlatform().getLanguage(), "r0=0x321"); - - flat.patchEmu("r0=0x321", monitor); - assertEquals(schedule, traceManager.getCurrent().getTime()); - - flat.stepEmuInstruction(-1, monitor); - assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime()); - } - - @Test - public void testReadMemoryBuffer() throws Throwable { - createAndOpenTrace(); - traceManager.activateTrace(tb.trace); - - byte[] data = new byte[1024]; - assertEquals(1024, flat.readMemory(tb.addr(0x00400000), data, monitor)); - assertArrayEquals(new byte[1024], data); - } - - @Test - public void testReadMemoryLength() throws Throwable { - createAndOpenTrace(); - traceManager.activateTrace(tb.trace); - - byte[] data = flat.readMemory(tb.addr(0x00400000), 1024, monitor); - assertArrayEquals(new byte[1024], data); - } - - @Test - public void testReadLiveMemory() throws Throwable { - createTestModel(); - mb.createTestProcessesAndThreads(); - mb.testProcess1.memory.writeMemory(mb.addr(0x00400000), mb.arr(1, 2, 3, 4, 5, 6, 7, 8)); - waitOn(mb.testModel.flushEvents()); - TraceRecorder recorder = record(mb.testProcess1); - waitRecorder(recorder); - useTrace(recorder.getTrace()); - waitForSwing(); - - byte[] data = flat.readMemory(tb.addr(0x00400000), 8, monitor); - assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8), data); - } - - @Test - public void testSearchMemory() throws Throwable { - createTraceWithBinText(); - traceManager.activateTrace(tb.trace); - waitForSwing(); - - assertEquals(tb.addr(0x00400003), flat.searchMemory(tb.trace, 2, tb.range(0L, -1L), - tb.arr(4, 5, 6, 7), null, true, monitor)); - assertEquals(tb.addr(0x00400003), flat.searchMemory(tb.trace, 2, tb.range(0L, -1L), - tb.arr(4, 5, 6, 7), tb.arr(-1, -1, -1, -1), true, monitor)); - } - - @Test - public void testReadRegister() throws Throwable { - TraceThread thread = createTraceWithThreadAndStack(true); - traceManager.activateThread(thread); - - Register r0 = tb.language.getRegister("r0"); - assertEquals(new RegisterValue(r0), flat.readRegister("r0")); - } - - @Test(expected = IllegalArgumentException.class) - public void testReadRegisterInvalidNameErr() throws Throwable { - TraceThread thread = createTraceWithThreadAndStack(true); - traceManager.activateThread(thread); - - flat.readRegister("THERE_IS_NO_SUCH_REGISTER"); - } - - @Test - public void testReadRegisters() throws Throwable { - TraceThread thread = createTraceWithThreadAndStack(true); - traceManager.activateThread(thread); - waitForSwing(); - - Register r0 = tb.language.getRegister("r0"); - Register r1 = tb.language.getRegister("r1"); - assertEquals(List.of( - new RegisterValue(r0), - new RegisterValue(r1)), - flat.readRegistersNamed(List.of("r0", "r1"))); - } - - @Test(expected = IllegalArgumentException.class) - public void testReadRegistersInvalidNameErr() throws Throwable { - TraceThread thread = createTraceWithThreadAndStack(true); - traceManager.activateThread(thread); - - flat.readRegistersNamed(Set.of("THERE_IS_NO_SUCH_REGISTER")); - } - - @Test - public void testReadLiveRegister() throws Throwable { - createTestModel(); - mb.createTestProcessesAndThreads(); - mb.createTestThreadRegisterBanks(); - mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), r -> true); - mb.testBank1.writeRegister("r0", mb.arr(1, 2, 3, 4, 5, 6, 7, 8)); - waitOn(mb.testModel.flushEvents()); - TraceRecorder recorder = record(mb.testProcess1); - waitRecorder(recorder); - useTrace(recorder.getTrace()); - traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); - waitForSwing(); - - RegisterValue rv = flat.readRegister("r0"); - assertEquals(BigInteger.valueOf(0x0102030405060708L), rv.getUnsignedValue()); - } - - @Test - public void testReadLiveRegisters() throws Throwable { - createTestModel(); - mb.createTestProcessesAndThreads(); - mb.createTestThreadRegisterBanks(); - mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), r -> true); - mb.testBank1.writeRegister("r0", mb.arr(1, 2, 3, 4, 5, 6, 7, 8)); - mb.testBank1.writeRegister("r1", mb.arr(8, 7, 6, 5, 4, 3, 2, 1)); - waitOn(mb.testModel.flushEvents()); - TraceRecorder recorder = record(mb.testProcess1); - waitRecorder(recorder); - useTrace(recorder.getTrace()); - traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); - waitForSwing(); - - Register r0 = tb.language.getRegister("r0"); - Register r1 = tb.language.getRegister("r1"); - assertEquals(List.of( - new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)), - new RegisterValue(r1, BigInteger.valueOf(0x0807060504030201L))), - flat.readRegistersNamed(List.of("r0", "r1"))); - } - - @Test - public void testWriteMemoryGivenContext() throws Throwable { - createTraceWithBinText(); - editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE); - - assertTrue(flat.writeMemory(tb.trace, 0, tb.addr(0x00400123), tb.arr(3, 2, 1))); - ByteBuffer buf = ByteBuffer.allocate(3); - assertEquals(3, tb.trace.getMemoryManager().getBytes(0, tb.addr(0x00400123), buf)); - assertArrayEquals(tb.arr(3, 2, 1), buf.array()); - } - - @Test - public void testWriteMemoryCurrentContext() throws Throwable { - createTraceWithBinText(); - editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE); - - assertTrue(flat.writeMemory(tb.addr(0x00400123), tb.arr(3, 2, 1))); - ByteBuffer buf = ByteBuffer.allocate(3); - assertEquals(3, tb.trace.getMemoryManager().getBytes(0, tb.addr(0x00400123), buf)); - assertArrayEquals(tb.arr(3, 2, 1), buf.array()); - } - - @Test - public void testWriteRegisterGivenContext() throws Throwable { - TraceThread thread = createTraceWithThreadAndStack(true); - editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE); - traceManager.activateThread(thread); - waitForSwing(); - - assertTrue(flat.writeRegister(thread, 0, 0, "r0", BigInteger.valueOf(0x0102030405060708L))); - DBTraceMemorySpace regs = - tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false); - assertNotNull(regs); - Register r0 = tb.language.getRegister("r0"); - assertEquals(new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)), - regs.getValue(0, r0)); - } - - @Test - public void testWriteRegisterCurrentContext() throws Throwable { - TraceThread thread = createTraceWithThreadAndStack(true); - editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE); - traceManager.activateThread(thread); - waitForSwing(); - - assertTrue(flat.writeRegister("r0", BigInteger.valueOf(0x0102030405060708L))); - DBTraceMemorySpace regs = - tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false); - assertNotNull(regs); - Register r0 = tb.language.getRegister("r0"); - assertEquals(new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)), - regs.getValue(0, r0)); - } - - @Test - public void testGetLaunchOffers() throws Throwable { - createProgram(); - programManager.openProgram(program); - waitForSwing(); - - DebuggerProgramLaunchOffer offer = Unique.assertOne(flat.getLaunchOffers()); - assertEquals(TestDebuggerProgramLaunchOffer.class, offer.getClass()); - } - - @Test - public void testLaunchCustomCommandLine() throws Throwable { - createProgram(); - programManager.openProgram(program); - waitForSwing(); - - var model = new TestDebuggerObjectModel() { - Map observedParams; - - @Override - protected TestTargetSession newTestTargetSession(String rootHint) { - return new TestTargetSession(this, "Session", ROOT_SCHEMA) { - @Override - public CompletableFuture launch(Map params) { - observedParams = params; - throw new CancellationException(); - } - }; - } - }; - DebuggerProgramLaunchOffer offer = new TestOffer(program, model); - - LaunchResult result = flat.launch(offer, "custom command line", monitor); - - assertEquals("custom command line", - model.observedParams.get(TargetCmdLineLauncher.CMDLINE_ARGS_NAME)); - assertNotNull(result.model()); - assertNull(result.target()); - assertEquals(CancellationException.class, result.exception().getClass()); - } - - protected TraceRecorder record(TargetObject target) - throws LanguageNotFoundException, CompilerSpecNotFoundException, IOException { - return modelService.recordTargetAndActivateTrace(target, - new TestDebuggerTargetTraceMapper(target)); - } - - @Test - public void testGetTarget() throws Exception { - createTestModel(); - mb.createTestProcessesAndThreads(); - TraceRecorder recorder = record(mb.testProcess1); - - assertEquals(mb.testProcess1, flat.getTarget(recorder.getTrace())); - } - - @Test - public void testGetTargetThread() throws Throwable { - createTestModel(); - mb.createTestProcessesAndThreads(); - TraceRecorder recorder = record(mb.testProcess1); - waitRecorder(recorder); - - Trace trace = recorder.getTrace(); - TraceThread thread = - trace.getThreadManager() - .getLiveThreadByPath(recorder.getSnap(), "Processes[1].Threads[1]"); - assertNotNull(thread); - assertEquals(mb.testThread1, flat.getTargetThread(thread)); - } - - @Test - public void testGetTargetFocus() throws Throwable { - createTestModel(); - mb.createTestProcessesAndThreads(); - TraceRecorder recorder = record(mb.testProcess1); - waitRecorder(recorder); - - waitOn(mb.testModel.requestFocus(mb.testThread2)); - waitRecorder(recorder); - - assertEquals(mb.testThread2, flat.getTargetFocus(recorder.getTrace())); - } - - protected void runTestStep(Function step, TargetStepKind kind) - throws Throwable { - var model = new TestDebuggerObjectModel() { - TestTargetThread observedThread; - TargetStepKind observedKind; - - @Override - protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, - int tid) { - return new TestTargetThread(container, tid) { - @Override - public CompletableFuture step(TargetStepKind kind) { - observedThread = this; - observedKind = kind; - return super.step(kind); - } - }; - } - }; - mb = new TestModelBuilder(model); - createTestModel(); - mb.createTestProcessesAndThreads(); - TraceRecorder recorder = record(mb.testProcess1); - waitRecorder(recorder); - assertTrue(waitOn(recorder.requestFocus(mb.testThread2))); - waitRecorder(recorder); - - assertTrue(step.apply(flat)); - waitRecorder(recorder); - assertEquals(mb.testThread2, model.observedThread); - assertEquals(kind, model.observedKind); - } - - @Test - public void testStepGivenThread() throws Throwable { - runTestStep(flat -> flat.step(flat.getCurrentThread(), TargetStepKind.INTO), - TargetStepKind.INTO); - } - - @Test - public void testStepInto() throws Throwable { - runTestStep(FlatDebuggerAPI::stepInto, TargetStepKind.INTO); - } - - @Test - public void testStepOver() throws Throwable { - runTestStep(FlatDebuggerAPI::stepOver, TargetStepKind.OVER); - } - - @Test - public void testStepOut() throws Throwable { - runTestStep(FlatDebuggerAPI::stepOut, TargetStepKind.FINISH); - } - - protected void runTestResume(Function resume) throws Throwable { - var model = new TestDebuggerObjectModel() { - TestTargetThread observedThread; - - @Override - protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, - int tid) { - return new TestTargetThread(container, tid) { - @Override - public CompletableFuture resume() { - observedThread = this; - return super.resume(); - } - }; - } - }; - mb = new TestModelBuilder(model); - createTestModel(); - mb.createTestProcessesAndThreads(); - TraceRecorder recorder = record(mb.testProcess1); - waitRecorder(recorder); - assertTrue(waitOn(recorder.requestFocus(mb.testThread2))); - waitRecorder(recorder); - - assertTrue(resume.apply(flat)); - waitRecorder(recorder); - assertEquals(mb.testThread2, model.observedThread); - } - - @Test - public void testResumeGivenThread() throws Throwable { - runTestResume(flat -> flat.resume(flat.getCurrentThread())); - } - - @Test - public void testResumeGivenTrace() throws Throwable { - runTestResume(flat -> flat.resume(flat.getCurrentTrace())); - } - - @Test - public void testResume() throws Throwable { - runTestResume(FlatDebuggerAPI::resume); - } - - protected void runTestInterrupt(Function interrupt) throws Throwable { - var model = new TestDebuggerObjectModel() { - TestTargetThread observedThread; - - @Override - protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, - int tid) { - return new TestTargetThread(container, tid) { - @Override - public CompletableFuture interrupt() { - observedThread = this; - return super.interrupt(); - } - }; - } - }; - mb = new TestModelBuilder(model); - createTestModel(); - mb.createTestProcessesAndThreads(); - TraceRecorder recorder = record(mb.testProcess1); - waitRecorder(recorder); - assertTrue(waitOn(recorder.requestFocus(mb.testThread2))); - waitRecorder(recorder); - - assertTrue(interrupt.apply(flat)); - waitRecorder(recorder); - assertEquals(mb.testThread2, model.observedThread); - } - - @Test - public void testInterruptGivenThread() throws Throwable { - runTestInterrupt(flat -> flat.interrupt(flat.getCurrentThread())); - } - - @Test - public void testInterruptGivenTrace() throws Throwable { - runTestInterrupt(flat -> flat.interrupt(flat.getCurrentTrace())); - } - - @Test - public void testInterrupt() throws Throwable { - runTestInterrupt(FlatDebuggerAPI::interrupt); - } - - protected void runTestKill(Function kill) throws Throwable { - var model = new TestDebuggerObjectModel() { - TestTargetThread observedThread; - - @Override - protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, - int tid) { - return new TestTargetThread(container, tid) { - @Override - public CompletableFuture kill() { - observedThread = this; - return super.kill(); - } - }; - } - }; - mb = new TestModelBuilder(model); - createTestModel(); - mb.createTestProcessesAndThreads(); - TraceRecorder recorder = record(mb.testProcess1); - waitRecorder(recorder); - assertTrue(waitOn(recorder.requestFocus(mb.testThread2))); - waitRecorder(recorder); - - assertTrue(kill.apply(flat)); - waitRecorder(recorder); - assertEquals(mb.testThread2, model.observedThread); - } - - @Test - public void testKillGivenThread() throws Throwable { - runTestKill(flat -> flat.kill(flat.getCurrentThread())); - } - - @Test - public void testKillGivenTrace() throws Throwable { - runTestKill(flat -> flat.kill(flat.getCurrentTrace())); - } - - @Test - public void testKill() throws Throwable { - runTestKill(FlatDebuggerAPI::kill); - } - - protected void runTestExecuteCapture(BiFunction executeCapture) - throws Throwable { - // NOTE: Can't use TestTargetInterpreter.queueExecute stuff, since flat API waits - var model = new TestDebuggerObjectModel() { - @Override - protected TestTargetInterpreter newTestTargetInterpreter(TestTargetSession session) { - return new TestTargetInterpreter(session) { - @Override - public CompletableFuture executeCapture(String cmd) { - return CompletableFuture.completedFuture("Response to " + cmd); - } - }; - } - }; - mb = new TestModelBuilder(model); - createTestModel(); - mb.createTestProcessesAndThreads(); - TraceRecorder recorder = record(mb.testProcess1); - waitRecorder(recorder); - assertTrue(waitOn(recorder.requestFocus(mb.testThread2))); - waitRecorder(recorder); - - assertEquals("Response to cmd", executeCapture.apply(flat, "cmd")); - } - - @Test - public void testExecuteCaptureGivenTrace() throws Throwable { - runTestExecuteCapture((flat, cmd) -> flat.executeCapture(flat.getCurrentTrace(), cmd)); - } - - @Test - public void testExecuteCapture() throws Throwable { - runTestExecuteCapture(FlatDebuggerAPI::executeCapture); - } - - protected void createProgramWithText() throws Throwable { - createProgram(); - programManager.openProgram(program); - waitForSwing(); - - try (Transaction tx = program.openTransaction("Add block")) { - program.getMemory() - .createInitializedBlock( - ".text", addr(program, 0x00400000), 1024, (byte) 0, monitor, false); - } - } - - protected void createProgramWithBreakpoint() throws Throwable { - createProgramWithText(); - - CompletableFuture changesSettled = breakpointService.changesSettled(); - waitOn(breakpointService.placeBreakpointAt(program, addr(program, 0x00400000), 1, - Set.of(TraceBreakpointKind.SW_EXECUTE), "name")); - waitForSwing(); - waitOn(changesSettled); - } - - @Test - public void testGetAllBreakpoints() throws Throwable { - createProgramWithBreakpoint(); - - assertEquals(1, flat.getAllBreakpoints().size()); - } - - @Test - public void testGetBreakpointsAt() throws Throwable { - createProgramWithBreakpoint(); - - assertEquals(1, flat.getBreakpointsAt(flat.staticLocation("00400000")).size()); - assertEquals(0, flat.getBreakpointsAt(flat.staticLocation("00400001")).size()); - } - - @Test - public void testGetBreakpointsNamed() throws Throwable { - createProgramWithBreakpoint(); - - assertEquals(1, flat.getBreakpointsNamed("name").size()); - assertEquals(0, flat.getBreakpointsNamed("miss").size()); - } - - @Test - public void testBreakpointsToggle() throws Throwable { - createProgramWithBreakpoint(); - LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); - - assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState()); - assertEquals(Set.of(lb), flat.breakpointsToggle(flat.staticLocation("00400000"))); - assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState()); - } - - @Test - public void testBreakpointSetSoftwareExecute() throws Throwable { - createProgramWithText(); - - LogicalBreakpoint lb = Unique.assertOne( - flat.breakpointSetSoftwareExecute(flat.staticLocation("00400000"), "name")); - assertEquals(addr(program, 0x00400000), lb.getAddress()); - assertEquals(TraceBreakpointKindSet.SW_EXECUTE, lb.getKinds()); - assertEquals(1, lb.getLength()); - } - - @Test - public void testBreakpointSetHardwareExecute() throws Throwable { - createProgramWithText(); - - LogicalBreakpoint lb = Unique.assertOne( - flat.breakpointSetHardwareExecute(flat.staticLocation("00400000"), "name")); - assertEquals(addr(program, 0x00400000), lb.getAddress()); - assertEquals(TraceBreakpointKindSet.HW_EXECUTE, lb.getKinds()); - assertEquals(1, lb.getLength()); - } - - @Test - public void testBreakpointSetRead() throws Throwable { - createProgramWithText(); - - LogicalBreakpoint lb = Unique.assertOne( - flat.breakpointSetRead(flat.staticLocation("00400000"), 4, "name")); - assertEquals(addr(program, 0x00400000), lb.getAddress()); - assertEquals(TraceBreakpointKindSet.READ, lb.getKinds()); - assertEquals(4, lb.getLength()); - } - - @Test - public void testBreakpointSetWrite() throws Throwable { - createProgramWithText(); - - LogicalBreakpoint lb = Unique.assertOne( - flat.breakpointSetWrite(flat.staticLocation("00400000"), 4, "name")); - assertEquals(addr(program, 0x00400000), lb.getAddress()); - assertEquals(TraceBreakpointKindSet.WRITE, lb.getKinds()); - assertEquals(4, lb.getLength()); - } - - @Test - public void testBreakpointSetAccess() throws Throwable { - createProgramWithText(); - - LogicalBreakpoint lb = Unique.assertOne( - flat.breakpointSetAccess(flat.staticLocation("00400000"), 4, "name")); - assertEquals(addr(program, 0x00400000), lb.getAddress()); - assertEquals(TraceBreakpointKindSet.ACCESS, lb.getKinds()); - assertEquals(4, lb.getLength()); - } - - @Test - public void testBreakpointsEnable() throws Throwable { - createProgramWithBreakpoint(); - LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); - CompletableFuture changesSettled = breakpointService.changesSettled(); - waitOn(lb.disable()); - waitForSwing(); - waitOn(changesSettled); - - assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState()); - assertEquals(Set.of(lb), flat.breakpointsEnable(flat.staticLocation("00400000"))); - assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState()); - } - - @Test - public void testBreakpointsDisable() throws Throwable { - createProgramWithBreakpoint(); - LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); - - assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState()); - assertEquals(Set.of(lb), flat.breakpointsDisable(flat.staticLocation("00400000"))); - assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState()); - } - - @Test - public void testBreakpointsClear() throws Throwable { - createProgramWithBreakpoint(); - LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); - - assertTrue(flat.breakpointsClear(flat.staticLocation("00400000"))); - assertTrue(lb.isEmpty()); - assertEquals(0, breakpointService.getAllBreakpoints().size()); - } - - @Test - public void testGetModelValue() throws Throwable { - createTestModel(); - mb.createTestProcessesAndThreads(); - record(mb.testProcess1); - - assertEquals(mb.testThread2, flat.getModelValue("Processes[1].Threads[2]")); - } - - @Test - public void testRefreshObjectChildren() throws Throwable { - var model = new TestDebuggerObjectModel() { - Set observed = new HashSet<>(); - - @Override - protected TestTargetProcess newTestTargetProcess(TestTargetProcessContainer container, - int pid, AddressSpace space) { - return new TestTargetProcess(container, pid, space) { - @Override - public CompletableFuture resync(RefreshBehavior refreshAttributes, - RefreshBehavior refreshElements) { - observed.add(this); - return super.resync(refreshAttributes, refreshElements); - } - }; - } - }; - mb = new TestModelBuilder(model); - createTestModel(); - mb.createTestProcessesAndThreads(); - - flat.refreshObjectChildren(mb.testProcess1); - assertEquals(Set.of(mb.testProcess1), model.observed); - } - - @Test - public void testRefreshSubtree() throws Throwable { - var model = new TestDebuggerObjectModel() { - Set observed = new HashSet<>(); - - @Override - protected TestTargetProcess newTestTargetProcess(TestTargetProcessContainer container, - int pid, AddressSpace space) { - return new TestTargetProcess(container, pid, space) { - @Override - public CompletableFuture resync(RefreshBehavior refreshAttributes, - RefreshBehavior refreshElements) { - observed.add(this); - return super.resync(refreshAttributes, refreshElements); - } - }; - } - - @Override - protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, - int tid) { - return new TestTargetThread(container, tid) { - @Override - public CompletableFuture resync(RefreshBehavior refreshAttributes, - RefreshBehavior refreshElements) { - observed.add(this); - return super.resync(refreshAttributes, refreshElements); - } - }; - } - }; - mb = new TestModelBuilder(model); - createTestModel(); - mb.createTestProcessesAndThreads(); - - flat.refreshSubtree(mb.testModel.session); - assertEquals(Set.of(mb.testProcess1, mb.testProcess3, mb.testThread1, mb.testThread2, - mb.testThread3, mb.testThread4), model.observed); - } - - @Test - public void testFlushAsyncPipelines() throws Throwable { - createTestModel(); - mb.createTestProcessesAndThreads(); - TraceRecorder recorder = record(mb.testProcess1); - - // Ensure it works whether or not there are pending events - for (int i = 0; i < 10; i++) { - flat.flushAsyncPipelines(recorder.getTrace()); - } - } -} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java index 13dc3ea13e6..bf58413e203 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java @@ -127,6 +127,8 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest protected TestTraceRmiConnection rmiCx; + protected TestRemoteMethod rmiMethodExecute; + protected TestRemoteMethod rmiMethodResume; protected TestRemoteMethod rmiMethodInterrupt; protected TestRemoteMethod rmiMethodKill; @@ -145,41 +147,53 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest protected TestRemoteMethod rmiMethodReadRegs; protected TestRemoteMethod rmiMethodWriteReg; + protected TestRemoteMethod rmiMethodReadMem; + protected TestRemoteMethod rmiMethodWriteMem; + protected void createRmiConnection() { rmiCx = new TestTraceRmiConnection(); } + protected void addExecuteMethod() { + rmiMethodExecute = new TestRemoteMethod("execute", ActionName.EXECUTE, "Execute", + "Execut a CLI command", EnumerableTargetObjectSchema.STRING, + new TestRemoteParameter("cmd", EnumerableTargetObjectSchema.STRING, true, null, + "Command", "The command to execute"), + new TestRemoteParameter("to_string", EnumerableTargetObjectSchema.BOOL, true, false, + "To String", "Capture output to string")); + + rmiCx.getMethods().add(rmiMethodExecute); + } + protected void addControlMethods() { rmiMethodResume = new TestRemoteMethod("resume", ActionName.RESUME, "Resume", - "Resume the target", EnumerableTargetObjectSchema.VOID.getName(), + "Resume the target", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process", "The process to resume")); rmiMethodInterrupt = new TestRemoteMethod("interrupt", ActionName.INTERRUPT, "Interrupt", - "Interrupt the target", EnumerableTargetObjectSchema.VOID.getName(), + "Interrupt the target", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process", "The process to interrupt")); rmiMethodKill = new TestRemoteMethod("kill", ActionName.KILL, "Kill", - "Kill the target", EnumerableTargetObjectSchema.VOID.getName(), + "Kill the target", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process", "The process to kill")); rmiMethodStepInto = new TestRemoteMethod("step_into", ActionName.STEP_INTO, "Step Into", - "Step the thread, descending into subroutines", - EnumerableTargetObjectSchema.VOID.getName(), + "Step the thread, descending into subroutines", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("thread", new SchemaName("Thread"), true, null, "Thread", "The thread to step")); rmiMethodStepOver = new TestRemoteMethod("step_over", ActionName.STEP_OVER, "Step Over", "Step the thread, without descending into subroutines", - EnumerableTargetObjectSchema.VOID.getName(), + EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("thread", new SchemaName("Thread"), true, null, "Thread", "The thread to step")); rmiMethodStepOut = new TestRemoteMethod("step_out", ActionName.STEP_OUT, "Step Out", - "Allow the thread to finish the current subroutine", - EnumerableTargetObjectSchema.VOID.getName(), + "Allow the thread to finish the current subroutine", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("thread", new SchemaName("Thread"), true, null, "Thread", "The thread to step")); @@ -194,56 +208,51 @@ protected void addControlMethods() { protected void addBreakpointMethods() { rmiMethodSetHwBreak = new TestRemoteMethod("set_hw_break", ActionName.BREAK_HW_EXECUTE, - "Hardware Breakpoint", - "Place a hardware execution breakpoint", EnumerableTargetObjectSchema.VOID.getName(), + "Hardware Breakpoint", "Place a hardware execution breakpoint", + EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process", "The process in which to place the breakpoint"), - new TestRemoteParameter("address", EnumerableTargetObjectSchema.ADDRESS.getName(), true, + new TestRemoteParameter("address", EnumerableTargetObjectSchema.ADDRESS, true, null, "Address", "The desired address")); rmiMethodSetSwBreak = new TestRemoteMethod("set_sw_break", ActionName.BREAK_SW_EXECUTE, - "Software Breakpoint", - "Place a software execution breakpoint", EnumerableTargetObjectSchema.VOID.getName(), + "Software Breakpoint", "Place a software execution breakpoint", + EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process", "The process in which to place the breakpoint"), - new TestRemoteParameter("address", EnumerableTargetObjectSchema.ADDRESS.getName(), true, + new TestRemoteParameter("address", EnumerableTargetObjectSchema.ADDRESS, true, null, "Address", "The desired address")); rmiMethodSetReadBreak = new TestRemoteMethod("set_read_break", ActionName.BREAK_READ, - "Read Breakpoint", - "Place a read breakpoint", EnumerableTargetObjectSchema.VOID.getName(), + "Read Breakpoint", "Place a read breakpoint", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process", "The process in which to place the breakpoint"), - new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE.getName(), true, + new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE, true, null, "Range", "The desired address range")); rmiMethodSetWriteBreak = new TestRemoteMethod("set_write_break", ActionName.BREAK_WRITE, - "Write Breakpoint", - "Place a write breakpoint", EnumerableTargetObjectSchema.VOID.getName(), + "Write Breakpoint", "Place a write breakpoint", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process", "The process in which to place the breakpoint"), - new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE.getName(), true, + new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE, true, null, "Range", "The desired address range")); rmiMethodSetAccessBreak = new TestRemoteMethod("set_acc_break", ActionName.BREAK_ACCESS, - "Access Breakpoint", - "Place an access breakpoint", EnumerableTargetObjectSchema.VOID.getName(), + "Access Breakpoint", "Place an access breakpoint", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("process", new SchemaName("Process"), true, null, "Process", "The process in which to place the breakpoint"), - new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE.getName(), true, + new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE, true, null, "Range", "The desired address range")); rmiMethodToggleBreak = new TestRemoteMethod("toggle_break", ActionName.TOGGLE, - "Toggle Breakpoint", - "Toggle a breakpoint", EnumerableTargetObjectSchema.VOID.getName(), + "Toggle Breakpoint", "Toggle a breakpoint", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("breakpoint", new SchemaName("BreakpointSpec"), true, null, "Breakpoint", "The breakpoint to toggle"), - new TestRemoteParameter("enabled", EnumerableTargetObjectSchema.BOOL.getName(), true, + new TestRemoteParameter("enabled", EnumerableTargetObjectSchema.BOOL, true, null, "Enable", "True to enable. False to disable")); rmiMethodDeleteBreak = new TestRemoteMethod("delete_break", ActionName.DELETE, - "Delete Breakpoint", - "Delete a breakpoint", EnumerableTargetObjectSchema.VOID.getName(), + "Delete Breakpoint", "Delete a breakpoint", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("breakpoint", new SchemaName("BreakpointSpec"), true, null, "Breakpoint", "The breakpoint to delete")); @@ -259,17 +268,17 @@ protected void addBreakpointMethods() { protected void addRegisterMethods() { rmiMethodReadRegs = new TestRemoteMethod("read_regs", ActionName.REFRESH, "Read Registers", - "Read registers", EnumerableTargetObjectSchema.VOID.getName(), + "Read registers", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("container", new SchemaName("RegisterContainer"), true, null, "Registers", "The registers node to read")); rmiMethodWriteReg = new TestRemoteMethod("write_reg", ActionName.WRITE_REG, - "Write Register", "Write a register", EnumerableTargetObjectSchema.VOID.getName(), + "Write Register", "Write a register", EnumerableTargetObjectSchema.VOID, new TestRemoteParameter("frame", new SchemaName("Frame"), false, 0, "Frame", "The frame to write to"), - new TestRemoteParameter("name", EnumerableTargetObjectSchema.STRING.getName(), true, + new TestRemoteParameter("name", EnumerableTargetObjectSchema.STRING, true, null, "Register", "The name of the register to write"), - new TestRemoteParameter("value", EnumerableTargetObjectSchema.BYTE_ARR.getName(), true, + new TestRemoteParameter("value", EnumerableTargetObjectSchema.BYTE_ARR, true, null, "Value", "The desired value")); TestRemoteMethodRegistry reg = rmiCx.getMethods(); @@ -277,6 +286,28 @@ protected void addRegisterMethods() { reg.add(rmiMethodWriteReg); } + protected void addMemoryMethods() { + rmiMethodReadMem = new TestRemoteMethod("read_mem", ActionName.READ_MEM, "Read Memory", + "Read memory", EnumerableTargetObjectSchema.VOID, + new TestRemoteParameter("process", new SchemaName("Process"), true, null, + "Process", "The process whose memory to read"), + new TestRemoteParameter("range", EnumerableTargetObjectSchema.RANGE, true, null, + "Range", "The address range to read")); + + rmiMethodWriteMem = new TestRemoteMethod("write_mem", ActionName.WRITE_MEM, "Write Memory", + "Write memory", EnumerableTargetObjectSchema.VOID, + new TestRemoteParameter("process", new SchemaName("Process"), true, null, + "Process", "The process whose memory to read"), + new TestRemoteParameter("start", EnumerableTargetObjectSchema.ADDRESS, true, null, + "Start", "The address to start writing"), + new TestRemoteParameter("data", EnumerableTargetObjectSchema.BYTE_ARR, true, null, + "Data", "The data to write")); + + TestRemoteMethodRegistry reg = rmiCx.getMethods(); + reg.add(rmiMethodReadMem); + reg.add(rmiMethodWriteMem); + } + protected TraceObject addMemoryRegion(TraceObjectManager objs, Lifespan lifespan, AddressRange range, String name, String flags) { String pathStr = diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TestTraceRmiLaunchOpinion.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TestTraceRmiLaunchOpinion.java new file mode 100644 index 00000000000..c467376c511 --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TestTraceRmiLaunchOpinion.java @@ -0,0 +1,94 @@ +/* ### + * 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.gui.tracermi.launcher; + +import static org.junit.Assert.assertEquals; + +import java.net.SocketAddress; +import java.util.*; + +import ghidra.dbg.target.TargetMethod.ParameterDescription; +import ghidra.debug.api.tracermi.TerminalSession; +import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; +import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion; +import ghidra.program.model.listing.Program; +import ghidra.util.task.TaskMonitor; + +public class TestTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion { + + public static class TestTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer { + private static final ParameterDescription PARAM_DESC_IMAGE = + ParameterDescription.create(String.class, "image", true, "", + PARAM_DISPLAY_IMAGE, "Image to execute"); + + public TestTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program) { + super(plugin, program); + } + + public Program getProgram() { + return program; + } + + @Override + public String getConfigName() { + return "TEST"; + } + + @Override + public String getTitle() { + return "Test"; + } + + @Override + public String getDescription() { + return "Test launch offer"; + } + + @Override + public Map> getParameters() { + return Map.ofEntries(Map.entry(PARAM_DESC_IMAGE.name, PARAM_DESC_IMAGE)); + } + + @Override + public boolean requiresImage() { + return false; + } + + @Override + protected void launchBackEnd(TaskMonitor monitor, Map sessions, + Map args, SocketAddress address) throws Exception { + } + + @Override + public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) { + assertEquals(PromptMode.NEVER, configurator.getPromptMode()); + Map args = + configurator.configureLauncher(this, loadLastLauncherArgs(false), RelPrompt.NONE); + return new LaunchResult(program, null, null, null, null, + new RuntimeException("Test launcher cannot launch " + args.get("image"))); + } + + public void saveLauncherArgs(Map args) { + super.saveLauncherArgs(args, getParameters()); + } + } + + @Override + public Collection getOffers(TraceRmiLauncherServicePlugin plugin, + Program program) { + return List.of(new TestTraceRmiLaunchOffer(plugin, program)); + } +} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/AbstractFlatDebuggerAPITest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/AbstractFlatDebuggerAPITest.java new file mode 100644 index 00000000000..d4fd8f0142e --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/AbstractFlatDebuggerAPITest.java @@ -0,0 +1,293 @@ +/* ### + * 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.debug.flatapi; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import org.junit.Before; +import org.junit.Test; + +import db.Transaction; +import ghidra.app.plugin.assembler.Assembler; +import ghidra.app.plugin.assembler.Assemblers; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerIntegrationTest; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin; +import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingProvider; +import ghidra.app.plugin.core.debug.service.breakpoint.DebuggerLogicalBreakpointServicePlugin; +import ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin; +import ghidra.app.plugin.core.debug.service.emulation.DebuggerEmulationServicePlugin; +import ghidra.app.services.*; +import ghidra.debug.api.control.ControlMode; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.*; +import ghidra.trace.database.memory.DBTraceMemoryManager; +import ghidra.trace.database.memory.DBTraceMemorySpace; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.memory.TraceMemoryFlag; +import ghidra.trace.model.stack.TraceStack; +import ghidra.trace.model.thread.TraceThread; + +public abstract class AbstractFlatDebuggerAPITest + extends AbstractGhidraHeadedDebuggerIntegrationTest { + + protected DebuggerLogicalBreakpointService breakpointService; + protected DebuggerStaticMappingService mappingService; + protected DebuggerEmulationService emulationService; + protected DebuggerListingService listingService; + protected DebuggerControlService editingService; + protected API api; + + protected abstract API newFlatAPI(); + + @Before + public void setUpFlatAPITest() throws Throwable { + breakpointService = addPlugin(tool, DebuggerLogicalBreakpointServicePlugin.class); + mappingService = tool.getService(DebuggerStaticMappingService.class); + emulationService = addPlugin(tool, DebuggerEmulationServicePlugin.class); + listingService = addPlugin(tool, DebuggerListingPlugin.class); + editingService = addPlugin(tool, DebuggerControlServicePlugin.class); + api = newFlatAPI(); + + // TODO: This seems to hold up the task manager. + waitForComponentProvider(DebuggerListingProvider.class).setAutoDisassemble(false); + } + + @Override + protected void createProgram(Language lang, CompilerSpec cSpec) throws IOException { + super.createProgram(lang, cSpec); + api.getState().setCurrentProgram(program); + } + + protected TraceThread createTraceWithThreadAndStack(boolean open) throws Throwable { + if (open) { + createAndOpenTrace(); + } + else { + createTrace(); + } + TraceThread thread; + try (Transaction tx = tb.startTransaction()) { + thread = tb.getOrAddThread("Threads[0]", 0); + TraceStack stack = tb.trace.getStackManager().getStack(thread, 0, true); + stack.setDepth(3, true); + } + waitForSwing(); + return thread; + } + + protected void createTraceWithBinText() throws Throwable { + createAndOpenTrace(); + + try (Transaction tx = tb.startTransaction()) { + DBTraceMemoryManager mm = tb.trace.getMemoryManager(); + mm.createRegion("Memory[bin.text]", 0, tb.range(0x00400000, 0x0040ffff), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); + + mm.putBytes(0, tb.addr(0x00400000), tb.buf(1, 2, 3, 4, 5, 6, 7, 8)); + } + traceManager.activateTrace(tb.trace); + waitForSwing(); + } + + protected void createMappedTraceAndProgram() throws Throwable { + createAndOpenTrace(); + createProgramFromTrace(); + + intoProject(program); + intoProject(tb.trace); + + programManager.openProgram(program); + traceManager.activateTrace(tb.trace); + + try (Transaction tx = program.openTransaction("add block")) { + program.getMemory() + .createInitializedBlock(".text", addr(program, 0x00400000), 4096, (byte) 0, + monitor, false); + } + + CompletableFuture changesSettled; + try (Transaction tx = tb.startTransaction()) { + tb.trace.getMemoryManager() + .createRegion("Memory[bin.text]", 0, tb.range(0x00400000, 0x00400fff), + Set.of(TraceMemoryFlag.READ, TraceMemoryFlag.EXECUTE)); + changesSettled = mappingService.changesSettled(); + mappingService.addIdentityMapping(tb.trace, program, Lifespan.nowOn(0), true); + } + waitForSwing(); + waitOn(changesSettled); + } + + protected Address createEmulatableProgram() throws Throwable { + createProgram(); + programManager.openProgram(program); + + Address entry = addr(program, 0x00400000); + try (Transaction start = program.openTransaction("init")) { + program.getMemory() + .createInitializedBlock(".text", entry, 4096, (byte) 0, + monitor, false); + Assembler asm = Assemblers.getAssembler(program); + asm.assemble(entry, "imm r0,#123"); + } + + // Emulate launch will create a static mapping + intoProject(program); + + return entry; + } + + @Test + public void testReadMemoryBuffer() throws Throwable { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + + byte[] data = new byte[1024]; + assertEquals(1024, api.readMemory(tb.addr(0x00400000), data, monitor)); + assertArrayEquals(new byte[1024], data); + } + + @Test + public void testReadMemoryLength() throws Throwable { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + + byte[] data = api.readMemory(tb.addr(0x00400000), 1024, monitor); + assertArrayEquals(new byte[1024], data); + } + + @Test + public void testReadRegister() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + + Register r0 = tb.language.getRegister("r0"); + assertEquals(new RegisterValue(r0), api.readRegister("r0")); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadRegisterInvalidNameErr() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + + api.readRegister("THERE_IS_NO_SUCH_REGISTER"); + } + + @Test + public void testReadRegisters() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + waitForSwing(); + + Register r0 = tb.language.getRegister("r0"); + Register r1 = tb.language.getRegister("r1"); + assertEquals(List.of( + new RegisterValue(r0), + new RegisterValue(r1)), + api.readRegistersNamed(List.of("r0", "r1"))); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadRegistersInvalidNameErr() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + + api.readRegistersNamed(Set.of("THERE_IS_NO_SUCH_REGISTER")); + } + + @Test + public void testWriteMemoryGivenContext() throws Throwable { + createTraceWithBinText(); + editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE); + + assertTrue(api.writeMemory(tb.trace, 0, tb.addr(0x00400123), tb.arr(3, 2, 1))); + ByteBuffer buf = ByteBuffer.allocate(3); + assertEquals(3, tb.trace.getMemoryManager().getBytes(0, tb.addr(0x00400123), buf)); + assertArrayEquals(tb.arr(3, 2, 1), buf.array()); + } + + @Test + public void testWriteMemoryCurrentContext() throws Throwable { + createTraceWithBinText(); + editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE); + + assertTrue(api.writeMemory(tb.addr(0x00400123), tb.arr(3, 2, 1))); + ByteBuffer buf = ByteBuffer.allocate(3); + assertEquals(3, tb.trace.getMemoryManager().getBytes(0, tb.addr(0x00400123), buf)); + assertArrayEquals(tb.arr(3, 2, 1), buf.array()); + } + + @Test + public void testWriteRegisterGivenContext() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE); + traceManager.activateThread(thread); + waitForSwing(); + + assertTrue(api.writeRegister(thread, 0, 0, "r0", BigInteger.valueOf(0x0102030405060708L))); + DBTraceMemorySpace regs = + tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false); + assertNotNull(regs); + Register r0 = tb.language.getRegister("r0"); + assertEquals(new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)), + regs.getValue(0, r0)); + } + + @Test + public void testWriteRegisterCurrentContext() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + editingService.setCurrentMode(tb.trace, ControlMode.RW_TRACE); + traceManager.activateThread(thread); + waitForSwing(); + + assertTrue(api.writeRegister("r0", BigInteger.valueOf(0x0102030405060708L))); + DBTraceMemorySpace regs = + tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, false); + assertNotNull(regs); + Register r0 = tb.language.getRegister("r0"); + assertEquals(new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)), + regs.getValue(0, r0)); + } + + protected void createProgramWithText() throws Throwable { + createProgram(); + programManager.openProgram(program); + waitForSwing(); + + try (Transaction tx = program.openTransaction("Add block")) { + program.getMemory() + .createInitializedBlock( + ".text", addr(program, 0x00400000), 1024, (byte) 0, monitor, false); + } + } + + protected void createProgramWithBreakpoint() throws Throwable { + createProgramWithText(); + + CompletableFuture changesSettled = breakpointService.changesSettled(); + waitOn(breakpointService.placeBreakpointAt(program, addr(program, 0x00400000), 1, + Set.of(TraceBreakpointKind.SW_EXECUTE), "name")); + waitForSwing(); + waitOn(changesSettled); + } +} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/AbstractLiveFlatDebuggerAPITest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/AbstractLiveFlatDebuggerAPITest.java new file mode 100644 index 00000000000..f0486a408fb --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/AbstractLiveFlatDebuggerAPITest.java @@ -0,0 +1,101 @@ +/* ### + * 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.debug.flatapi; + +import java.util.function.BooleanSupplier; +import java.util.function.Function; + +import org.junit.Test; + +import ghidra.app.script.GhidraState; + +public abstract class AbstractLiveFlatDebuggerAPITest + extends AbstractFlatDebuggerAPITest { + + protected class TestFlatAPI implements FlatDebuggerAPI { + protected final GhidraState state = + new GhidraState(env.getTool(), env.getProject(), program, null, null, null); + + @Override + public GhidraState getState() { + return state; + } + } + + protected abstract void runTestResume(BooleanSupplier resume) throws Throwable; + + @Test + public void testResumeGivenThread() throws Throwable { + runTestResume(() -> api.resume(api.getCurrentThread())); + } + + @Test + public void testResumeGivenTrace() throws Throwable { + runTestResume(() -> api.resume(api.getCurrentTrace())); + } + + @Test + public void testResume() throws Throwable { + runTestResume(api::resume); + } + + protected abstract void runTestInterrupt(BooleanSupplier interrupt) throws Throwable; + + @Test + public void testInterruptGivenThread() throws Throwable { + runTestInterrupt(() -> api.interrupt(api.getCurrentThread())); + } + + @Test + public void testInterruptGivenTrace() throws Throwable { + runTestInterrupt(() -> api.interrupt(api.getCurrentTrace())); + } + + @Test + public void testInterrupt() throws Throwable { + runTestInterrupt(api::interrupt); + } + + protected abstract void runTestKill(BooleanSupplier kill) throws Throwable; + + @Test + public void testKillGivenThread() throws Throwable { + runTestKill(() -> api.kill(api.getCurrentThread())); + } + + @Test + public void testKillGivenTrace() throws Throwable { + runTestKill(() -> api.kill(api.getCurrentTrace())); + } + + @Test + public void testKill() throws Throwable { + runTestKill(api::kill); + } + + protected abstract void runTestExecuteCapture(Function executeCapture) + throws Throwable; + + @Test + public void testExecuteCaptureGivenTrace() throws Throwable { + runTestExecuteCapture(cmd -> api.executeCapture(api.getCurrentTrace(), cmd)); + } + + @Test + public void testExecuteCapture() throws Throwable { + runTestExecuteCapture(api::executeCapture); + } +} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/DeadFlatDebuggerAPITest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/DeadFlatDebuggerAPITest.java new file mode 100644 index 00000000000..55df0404a0e --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/DeadFlatDebuggerAPITest.java @@ -0,0 +1,539 @@ +/* ### + * 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.debug.flatapi; + +import static org.junit.Assert.*; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import db.Transaction; +import generic.Unique; +import ghidra.app.script.GhidraState; +import ghidra.app.services.DebuggerTraceManagerService; +import ghidra.debug.api.breakpoint.LogicalBreakpoint; +import ghidra.debug.api.breakpoint.LogicalBreakpoint.State; +import ghidra.debug.api.tracemgr.DebuggerCoordinates; +import ghidra.program.model.address.Address; +import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; +import ghidra.trace.model.stack.TraceStack; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.schedule.TraceSchedule; + +public class DeadFlatDebuggerAPITest extends AbstractFlatDebuggerAPITest { + + protected class TestFlatAPI implements FlatDebuggerAPI { + protected final GhidraState state = + new GhidraState(env.getTool(), env.getProject(), program, null, null, null); + + @Override + public GhidraState getState() { + return state; + } + } + + @Override + protected FlatDebuggerAPI newFlatAPI() { + return new TestFlatAPI(); + } + + @Test + public void testRequireService() throws Throwable { + assertEquals(traceManager, api.requireService(DebuggerTraceManagerService.class)); + } + + interface NoSuchService { + } + + @Test(expected = IllegalStateException.class) + public void testRequireServiceAbsentErr() { + api.requireService(NoSuchService.class); + } + + @Test + public void testGetCurrentDebuggerCoordinates() throws Throwable { + assertSame(DebuggerCoordinates.NOWHERE, api.getCurrentDebuggerCoordinates()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + + assertEquals(DebuggerCoordinates.NOWHERE.trace(tb.trace), + api.getCurrentDebuggerCoordinates()); + } + + @Test + public void testGetCurrentTrace() throws Throwable { + assertNull(api.getCurrentTrace()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + + assertEquals(tb.trace, api.getCurrentTrace()); + } + + @Test(expected = IllegalStateException.class) + public void testRequireCurrentTraceAbsentErr() { + api.requireCurrentTrace(); + } + + @Test + public void testGetCurrentThread() throws Throwable { + assertNull(api.getCurrentThread()); + + createAndOpenTrace(); + TraceThread thread; + try (Transaction tx = tb.startTransaction()) { + thread = tb.getOrAddThread("Threads[0]", 0); + } + waitForSwing(); + traceManager.activateTrace(tb.trace); + + assertEquals(thread, api.getCurrentThread()); + } + + @Test + public void testGetCurrentView() throws Throwable { + assertNull(api.getCurrentView()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + + assertEquals(tb.trace.getProgramView(), api.getCurrentView()); + } + + @Test(expected = IllegalStateException.class) + public void testRequireCurrentViewAbsentErr() { + api.requireCurrentView(); + } + + @Test + public void testGetCurrentFrame() throws Throwable { + assertEquals(0, api.getCurrentFrame()); + + createAndOpenTrace(); + TraceThread thread; + try (Transaction tx = tb.startTransaction()) { + thread = tb.getOrAddThread("Threads[0]", 0); + TraceStack stack = tb.trace.getStackManager().getStack(thread, 0, true); + stack.setDepth(3, true); + } + waitForSwing(); + traceManager.activateThread(thread); + traceManager.activateFrame(1); + + assertEquals(1, api.getCurrentFrame()); + } + + @Test + public void testGetCurrentSnap() throws Throwable { + assertEquals(0L, api.getCurrentSnap()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + traceManager.activateSnap(1); + + assertEquals(1L, api.getCurrentSnap()); + } + + @Test + public void testGetCurrentEmulationSchedule() throws Throwable { + assertEquals(TraceSchedule.parse("0"), api.getCurrentEmulationSchedule()); + + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + traceManager.activateSnap(1); + + assertEquals(TraceSchedule.parse("1"), api.getCurrentEmulationSchedule()); + } + + @Test + public void testActivateTrace() throws Throwable { + createAndOpenTrace(); + api.activateTrace(tb.trace); + + assertEquals(tb.trace, traceManager.getCurrentTrace()); + } + + @Test + public void testActivateTraceNull() throws Throwable { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + assertEquals(tb.trace, traceManager.getCurrentTrace()); + + api.activateTrace(null); + assertEquals(null, traceManager.getCurrentTrace()); + } + + @Test + public void testActivateTraceNotOpen() throws Throwable { + createTrace(); + assertFalse(traceManager.getOpenTraces().contains(tb.trace)); + + api.activateTrace(tb.trace); + + assertTrue(traceManager.getOpenTraces().contains(tb.trace)); + assertEquals(tb.trace, traceManager.getCurrentTrace()); + } + + @Test + public void testGetCurrentProgram() throws Throwable { + assertEquals(null, api.getCurrentProgram()); + + createProgram(); + programManager.openProgram(program); + + assertEquals(program, api.getCurrentProgram()); + } + + @Test(expected = IllegalStateException.class) + public void testRequireCurrentProgramAbsentErr() throws Throwable { + api.requireCurrentProgram(); + } + + @Test + public void testActivateThread() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + api.activateThread(thread); + + assertEquals(thread, traceManager.getCurrentThread()); + } + + @Test + public void testActivateThreadNull() throws Throwable { + api.activateThread(null); + assertEquals(null, traceManager.getCurrentThread()); + + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + waitForSwing(); + assertEquals(thread, traceManager.getCurrentThread()); + + api.activateThread(null); + assertNull(traceManager.getCurrentThread()); + } + + @Test + public void testActivateThreadNotOpen() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(false); + assertFalse(traceManager.getOpenTraces().contains(tb.trace)); + + api.activateThread(thread); + + assertTrue(traceManager.getOpenTraces().contains(tb.trace)); + assertEquals(thread, traceManager.getCurrentThread()); + } + + @Test + public void testActivateFrame() throws Throwable { + TraceThread thread = createTraceWithThreadAndStack(true); + traceManager.activateThread(thread); + waitForSwing(); + api.activateFrame(1); + + assertEquals(1, traceManager.getCurrentFrame()); + } + + @Test + public void testActivateSnap() throws Throwable { + createAndOpenTrace(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + api.activateSnap(1); + + assertEquals(1L, traceManager.getCurrentSnap()); + } + + @Test + public void testGetCurrentDebuggerAddress() throws Throwable { + assertEquals(null, api.getCurrentDebuggerAddress()); + + createTraceWithBinText(); + + assertEquals(tb.addr(0x00400000), api.getCurrentDebuggerAddress()); + } + + @Test + public void testGoToDynamic() throws Throwable { + createTraceWithBinText(); + + assertTrue(api.goToDynamic("00400123")); + assertEquals(tb.addr(0x00400123), listingService.getCurrentLocation().getAddress()); + + assertTrue(api.goToDynamic(tb.addr(0x00400321))); + assertEquals(tb.addr(0x00400321), listingService.getCurrentLocation().getAddress()); + } + + @Test + public void testTranslateStaticToDynamic() throws Throwable { + createMappedTraceAndProgram(); + + assertEquals(api.dynamicLocation("00400123"), + api.translateStaticToDynamic(api.staticLocation("00400123"))); + assertNull(api.translateStaticToDynamic(api.staticLocation("00600123"))); + + assertEquals(tb.addr(0x00400123), api.translateStaticToDynamic(addr(program, 0x00400123))); + assertNull(api.translateStaticToDynamic(addr(program, 0x00600123))); + } + + @Test + public void testTranslateDynamicToStatic() throws Throwable { + createMappedTraceAndProgram(); + + assertEquals(api.staticLocation("00400123"), + api.translateDynamicToStatic(api.dynamicLocation("00400123"))); + assertNull(api.translateDynamicToStatic(api.dynamicLocation("00600123"))); + + assertEquals(addr(program, 0x00400123), api.translateDynamicToStatic(tb.addr(0x00400123))); + assertNull(api.translateDynamicToStatic(tb.addr(0x00600123))); + } + + @Test + public void testEmulateLaunch() throws Throwable { + Address entry = createEmulatableProgram(); + + Trace trace = api.emulateLaunch(entry); + assertEquals(trace, traceManager.getCurrentTrace()); + } + + @Test + public void testEmulate() throws Throwable { + Address entry = createEmulatableProgram(); + + api.emulateLaunch(entry); + TraceSchedule schedule = + traceManager.getCurrent().getTime().steppedForward(traceManager.getCurrentThread(), 1); + api.emulate(schedule, monitor); + + assertEquals(schedule, traceManager.getCurrent().getTime()); + } + + @Test + public void testStepEmuInstruction() throws Throwable { + Address entry = createEmulatableProgram(); + + api.emulateLaunch(entry); + TraceSchedule schedule = + traceManager.getCurrent().getTime().steppedForward(traceManager.getCurrentThread(), 1); + + api.stepEmuInstruction(1, monitor); + assertEquals(schedule, traceManager.getCurrent().getTime()); + + api.stepEmuInstruction(-1, monitor); + assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime()); + } + + @Test + public void testStepEmuPcodeOp() throws Throwable { + Address entry = createEmulatableProgram(); + + api.emulateLaunch(entry); + TraceSchedule schedule = traceManager.getCurrent() + .getTime() + .steppedPcodeForward(traceManager.getCurrentThread(), 1); + + api.stepEmuPcodeOp(1, monitor); + assertEquals(schedule, traceManager.getCurrent().getTime()); + + api.stepEmuPcodeOp(-1, monitor); + assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime()); + } + + @Test + public void testSkipEmuInstruction() throws Throwable { + Address entry = createEmulatableProgram(); + + api.emulateLaunch(entry); + TraceSchedule schedule = + traceManager.getCurrent().getTime().skippedForward(traceManager.getCurrentThread(), 1); + + api.skipEmuInstruction(1, monitor); + assertEquals(schedule, traceManager.getCurrent().getTime()); + + api.skipEmuInstruction(-1, monitor); + assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime()); + } + + @Test + public void testSkipEmuPcodeOp() throws Throwable { + Address entry = createEmulatableProgram(); + + api.emulateLaunch(entry); + TraceSchedule schedule = traceManager.getCurrent() + .getTime() + .skippedPcodeForward(traceManager.getCurrentThread(), 1); + + api.skipEmuPcodeOp(1, monitor); + assertEquals(schedule, traceManager.getCurrent().getTime()); + + api.skipEmuPcodeOp(-1, monitor); + assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime()); + } + + @Test + public void testPatchEmu() throws Throwable { + Address entry = createEmulatableProgram(); + + api.emulateLaunch(entry); + TraceSchedule schedule = traceManager.getCurrent() + .getTime() + .patched(traceManager.getCurrentThread(), + traceManager.getCurrentPlatform().getLanguage(), "r0=0x321"); + + api.patchEmu("r0=0x321", monitor); + assertEquals(schedule, traceManager.getCurrent().getTime()); + + api.stepEmuInstruction(-1, monitor); + assertEquals(TraceSchedule.ZERO, traceManager.getCurrent().getTime()); + } + + @Test + public void testSearchMemory() throws Throwable { + createTraceWithBinText(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + assertEquals(tb.addr(0x00400003), api.searchMemory(tb.trace, 2, tb.range(0L, -1L), + tb.arr(4, 5, 6, 7), null, true, monitor)); + assertEquals(tb.addr(0x00400003), api.searchMemory(tb.trace, 2, tb.range(0L, -1L), + tb.arr(4, 5, 6, 7), tb.arr(-1, -1, -1, -1), true, monitor)); + } + + @Test + public void testGetAllBreakpoints() throws Throwable { + createProgramWithBreakpoint(); + + assertEquals(1, api.getAllBreakpoints().size()); + } + + @Test + public void testGetBreakpointsAt() throws Throwable { + createProgramWithBreakpoint(); + + assertEquals(1, api.getBreakpointsAt(api.staticLocation("00400000")).size()); + assertEquals(0, api.getBreakpointsAt(api.staticLocation("00400001")).size()); + } + + @Test + public void testGetBreakpointsNamed() throws Throwable { + createProgramWithBreakpoint(); + + assertEquals(1, api.getBreakpointsNamed("name").size()); + assertEquals(0, api.getBreakpointsNamed("miss").size()); + } + + @Test + public void testBreakpointsToggle() throws Throwable { + createProgramWithBreakpoint(); + LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); + + assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState()); + assertEquals(Set.of(lb), api.breakpointsToggle(api.staticLocation("00400000"))); + assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState()); + } + + @Test + public void testBreakpointSetSoftwareExecute() throws Throwable { + createProgramWithText(); + + LogicalBreakpoint lb = Unique.assertOne( + api.breakpointSetSoftwareExecute(api.staticLocation("00400000"), "name")); + assertEquals(addr(program, 0x00400000), lb.getAddress()); + assertEquals(TraceBreakpointKindSet.SW_EXECUTE, lb.getKinds()); + assertEquals(1, lb.getLength()); + } + + @Test + public void testBreakpointSetHardwareExecute() throws Throwable { + createProgramWithText(); + + LogicalBreakpoint lb = Unique.assertOne( + api.breakpointSetHardwareExecute(api.staticLocation("00400000"), "name")); + assertEquals(addr(program, 0x00400000), lb.getAddress()); + assertEquals(TraceBreakpointKindSet.HW_EXECUTE, lb.getKinds()); + assertEquals(1, lb.getLength()); + } + + @Test + public void testBreakpointSetRead() throws Throwable { + createProgramWithText(); + + LogicalBreakpoint lb = Unique.assertOne( + api.breakpointSetRead(api.staticLocation("00400000"), 4, "name")); + assertEquals(addr(program, 0x00400000), lb.getAddress()); + assertEquals(TraceBreakpointKindSet.READ, lb.getKinds()); + assertEquals(4, lb.getLength()); + } + + @Test + public void testBreakpointSetWrite() throws Throwable { + createProgramWithText(); + + LogicalBreakpoint lb = Unique.assertOne( + api.breakpointSetWrite(api.staticLocation("00400000"), 4, "name")); + assertEquals(addr(program, 0x00400000), lb.getAddress()); + assertEquals(TraceBreakpointKindSet.WRITE, lb.getKinds()); + assertEquals(4, lb.getLength()); + } + + @Test + public void testBreakpointSetAccess() throws Throwable { + createProgramWithText(); + + LogicalBreakpoint lb = Unique.assertOne( + api.breakpointSetAccess(api.staticLocation("00400000"), 4, "name")); + assertEquals(addr(program, 0x00400000), lb.getAddress()); + assertEquals(TraceBreakpointKindSet.ACCESS, lb.getKinds()); + assertEquals(4, lb.getLength()); + } + + @Test + public void testBreakpointsEnable() throws Throwable { + createProgramWithBreakpoint(); + LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); + CompletableFuture changesSettled = breakpointService.changesSettled(); + waitOn(lb.disable()); + waitForSwing(); + waitOn(changesSettled); + + assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState()); + assertEquals(Set.of(lb), api.breakpointsEnable(api.staticLocation("00400000"))); + assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState()); + } + + @Test + public void testBreakpointsDisable() throws Throwable { + createProgramWithBreakpoint(); + LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); + + assertEquals(State.INEFFECTIVE_ENABLED, lb.computeState()); + assertEquals(Set.of(lb), api.breakpointsDisable(api.staticLocation("00400000"))); + assertEquals(State.INEFFECTIVE_DISABLED, lb.computeState()); + } + + @Test + public void testBreakpointsClear() throws Throwable { + createProgramWithBreakpoint(); + LogicalBreakpoint lb = Unique.assertOne(breakpointService.getAllBreakpoints()); + + assertTrue(api.breakpointsClear(api.staticLocation("00400000"))); + assertTrue(lb.isEmpty()); + assertEquals(0, breakpointService.getAllBreakpoints().size()); + } +} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPITest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPITest.java new file mode 100644 index 00000000000..c049fb9c8ad --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRecorderAPITest.java @@ -0,0 +1,502 @@ +/* ### + * 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.debug.flatapi; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.*; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.function.BooleanSupplier; +import java.util.function.Function; + +import org.junit.Test; + +import generic.Unique; +import ghidra.app.plugin.core.debug.service.model.TestDebuggerProgramLaunchOpinion.TestDebuggerProgramLaunchOffer; +import ghidra.app.plugin.core.debug.service.model.launch.AbstractDebuggerProgramLaunchOffer; +import ghidra.dbg.DebuggerModelFactory; +import ghidra.dbg.DebuggerObjectModel; +import ghidra.dbg.model.*; +import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher; +import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.TargetSteppable.TargetStepKind; +import ghidra.debug.api.model.DebuggerProgramLaunchOffer; +import ghidra.debug.api.model.DebuggerProgramLaunchOffer.LaunchResult; +import ghidra.debug.api.model.TraceRecorder; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.Program; +import ghidra.trace.model.Trace; +import ghidra.trace.model.thread.TraceThread; + +public class FlatDebuggerRecorderAPITest + extends AbstractLiveFlatDebuggerAPITest { + + protected static class TestFactory implements DebuggerModelFactory { + private final DebuggerObjectModel model; + + public TestFactory(DebuggerObjectModel model) { + this.model = model; + } + + @Override + public CompletableFuture build() { + return CompletableFuture.completedFuture(model); + } + } + + protected class TestOffer extends AbstractDebuggerProgramLaunchOffer { + public TestOffer(Program program, DebuggerModelFactory factory) { + super(program, env.getTool(), factory); + } + + public TestOffer(Program program, DebuggerObjectModel model) { + this(program, new TestFactory(model)); + } + + @Override + public String getConfigName() { + return "TEST"; + } + + @Override + public String getMenuTitle() { + return "in Test Debugger"; + } + } + + protected static class TestModelBuilder extends TestDebuggerModelBuilder { + private final TestDebuggerObjectModel model; + + public TestModelBuilder(TestDebuggerObjectModel model) { + this.model = model; + } + + @Override + protected TestDebuggerObjectModel newModel(String typeHint) { + return model; + } + } + + protected class TestFlatRecorderAPI extends TestFlatAPI implements FlatDebuggerRecorderAPI { + } + + @Override + protected FlatDebuggerRecorderAPI newFlatAPI() { + return new TestFlatRecorderAPI(); + } + + protected TraceRecorder recordTarget(TargetObject target) + throws LanguageNotFoundException, CompilerSpecNotFoundException, IOException { + return modelService.recordTargetAndActivateTrace(target, + new TestDebuggerTargetTraceMapper(target)); + } + + @Test + public void testReadLiveMemory() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + mb.testProcess1.memory.writeMemory(mb.addr(0x00400000), mb.arr(1, 2, 3, 4, 5, 6, 7, 8)); + waitOn(mb.testModel.flushEvents()); + TraceRecorder recorder = recordTarget(mb.testProcess1); + waitRecorder(recorder); + useTrace(recorder.getTrace()); + waitForSwing(); + + byte[] data = api.readMemory(tb.addr(0x00400000), 8, monitor); + assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8), data); + } + + @Test + public void testReadLiveRegister() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + mb.createTestThreadRegisterBanks(); + mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), r -> true); + mb.testBank1.writeRegister("r0", mb.arr(1, 2, 3, 4, 5, 6, 7, 8)); + waitOn(mb.testModel.flushEvents()); + TraceRecorder recorder = recordTarget(mb.testProcess1); + waitRecorder(recorder); + useTrace(recorder.getTrace()); + traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); + waitForSwing(); + + RegisterValue rv = api.readRegister("r0"); + assertEquals(BigInteger.valueOf(0x0102030405060708L), rv.getUnsignedValue()); + } + + @Test + public void testReadLiveRegisters() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + mb.createTestThreadRegisterBanks(); + mb.testProcess1.regs.addRegistersFromLanguage(getToyBE64Language(), r -> true); + mb.testBank1.writeRegister("r0", mb.arr(1, 2, 3, 4, 5, 6, 7, 8)); + mb.testBank1.writeRegister("r1", mb.arr(8, 7, 6, 5, 4, 3, 2, 1)); + waitOn(mb.testModel.flushEvents()); + TraceRecorder recorder = recordTarget(mb.testProcess1); + waitRecorder(recorder); + useTrace(recorder.getTrace()); + traceManager.activateThread(recorder.getTraceThread(mb.testThread1)); + waitForSwing(); + + Register r0 = tb.language.getRegister("r0"); + Register r1 = tb.language.getRegister("r1"); + assertEquals(List.of( + new RegisterValue(r0, BigInteger.valueOf(0x0102030405060708L)), + new RegisterValue(r1, BigInteger.valueOf(0x0807060504030201L))), + api.readRegistersNamed(List.of("r0", "r1"))); + } + + @Test + public void testGetLaunchOffers() throws Throwable { + createProgram(); + programManager.openProgram(program); + waitForSwing(); + + Unique.assertOne(api.getLaunchOffers().stream().mapMulti((o, m) -> { + if (o instanceof TestDebuggerProgramLaunchOffer to) { + m.accept(to); + } + })); + } + + @Test + public void testLaunchCustomCommandLine() throws Throwable { + createProgram(); + programManager.openProgram(program); + waitForSwing(); + + var model = new TestDebuggerObjectModel() { + Map observedParams; + + @Override + protected TestTargetSession newTestTargetSession(String rootHint) { + return new TestTargetSession(this, "Session", ROOT_SCHEMA) { + @Override + public CompletableFuture launch(Map params) { + observedParams = params; + throw new CancellationException(); + } + }; + } + }; + DebuggerProgramLaunchOffer offer = new TestOffer(program, model); + + LaunchResult result = api.launch(offer, "custom command line", monitor); + + assertEquals("custom command line", + model.observedParams.get(TargetCmdLineLauncher.CMDLINE_ARGS_NAME)); + assertNotNull(result.model()); + assertNull(result.target()); + assertEquals(CancellationException.class, result.exception().getClass()); + } + + @Test + public void testGetTarget() throws Exception { + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = recordTarget(mb.testProcess1); + + assertEquals(mb.testProcess1, api.getTarget(recorder.getTrace())); + } + + @Test + public void testGetTargetThread() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = recordTarget(mb.testProcess1); + waitRecorder(recorder); + + Trace trace = recorder.getTrace(); + TraceThread thread = + trace.getThreadManager() + .getLiveThreadByPath(recorder.getSnap(), "Processes[1].Threads[1]"); + assertNotNull(thread); + assertEquals(mb.testThread1, api.getTargetThread(thread)); + } + + @Test + public void testGetTargetFocus() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = recordTarget(mb.testProcess1); + waitRecorder(recorder); + + waitOn(mb.testModel.requestFocus(mb.testThread2)); + waitRecorder(recorder); + + assertEquals(mb.testThread2, api.getTargetFocus(recorder.getTrace())); + } + + protected void runTestStep(BooleanSupplier step, TargetStepKind kind) throws Throwable { + var model = new TestDebuggerObjectModel() { + TestTargetThread observedThread; + TargetStepKind observedKind; + + @Override + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, + int tid) { + return new TestTargetThread(container, tid) { + @Override + public CompletableFuture step(TargetStepKind kind) { + observedThread = this; + observedKind = kind; + return super.step(kind); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = recordTarget(mb.testProcess1); + waitRecorder(recorder); + assertTrue(waitOn(recorder.requestFocus(mb.testThread2))); + waitRecorder(recorder); + + assertTrue(step.getAsBoolean()); + waitRecorder(recorder); + assertEquals(mb.testThread2, model.observedThread); + assertEquals(kind, model.observedKind); + } + + @Test + public void testStepGivenThread() throws Throwable { + runTestStep(() -> api.step(api.getCurrentThread(), TargetStepKind.INTO), + TargetStepKind.INTO); + } + + @Test + public void testStepInto() throws Throwable { + runTestStep(api::stepInto, TargetStepKind.INTO); + } + + @Test + public void testStepOver() throws Throwable { + runTestStep(api::stepOver, TargetStepKind.OVER); + } + + @Test + public void testStepOut() throws Throwable { + runTestStep(api::stepOut, TargetStepKind.FINISH); + } + + @Override + protected void runTestResume(BooleanSupplier resume) throws Throwable { + var model = new TestDebuggerObjectModel() { + TestTargetThread observedThread; + + @Override + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, + int tid) { + return new TestTargetThread(container, tid) { + @Override + public CompletableFuture resume() { + observedThread = this; + return super.resume(); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = recordTarget(mb.testProcess1); + waitRecorder(recorder); + assertTrue(waitOn(recorder.requestFocus(mb.testThread2))); + waitRecorder(recorder); + + assertTrue(resume.getAsBoolean()); + waitRecorder(recorder); + assertEquals(mb.testThread2, model.observedThread); + } + + @Override + protected void runTestInterrupt(BooleanSupplier interrupt) throws Throwable { + var model = new TestDebuggerObjectModel() { + TestTargetThread observedThread; + + @Override + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, + int tid) { + return new TestTargetThread(container, tid) { + @Override + public CompletableFuture interrupt() { + observedThread = this; + return super.interrupt(); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = recordTarget(mb.testProcess1); + waitRecorder(recorder); + assertTrue(waitOn(recorder.requestFocus(mb.testThread2))); + waitRecorder(recorder); + + assertTrue(interrupt.getAsBoolean()); + waitRecorder(recorder); + assertEquals(mb.testThread2, model.observedThread); + } + + @Override + protected void runTestKill(BooleanSupplier kill) throws Throwable { + var model = new TestDebuggerObjectModel() { + TestTargetThread observedThread; + + @Override + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, + int tid) { + return new TestTargetThread(container, tid) { + @Override + public CompletableFuture kill() { + observedThread = this; + return super.kill(); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = recordTarget(mb.testProcess1); + waitRecorder(recorder); + assertTrue(waitOn(recorder.requestFocus(mb.testThread2))); + waitRecorder(recorder); + + assertTrue(kill.getAsBoolean()); + waitRecorder(recorder); + assertEquals(mb.testThread2, model.observedThread); + } + + @Override + protected void runTestExecuteCapture(Function executeCapture) throws Throwable { + // NOTE: Can't use TestTargetInterpreter.queueExecute stuff, since flat API waits + var model = new TestDebuggerObjectModel() { + @Override + protected TestTargetInterpreter newTestTargetInterpreter(TestTargetSession session) { + return new TestTargetInterpreter(session) { + @Override + public CompletableFuture executeCapture(String cmd) { + return CompletableFuture.completedFuture("Response to " + cmd); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = recordTarget(mb.testProcess1); + waitRecorder(recorder); + assertTrue(waitOn(recorder.requestFocus(mb.testThread2))); + waitRecorder(recorder); + + assertEquals("Response to cmd", executeCapture.apply("cmd")); + } + + @Test + public void testGetModelValue() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + recordTarget(mb.testProcess1); + + assertEquals(mb.testThread2, api.getModelValue("Processes[1].Threads[2]")); + } + + @Test + public void testRefreshObjectChildren() throws Throwable { + var model = new TestDebuggerObjectModel() { + Set observed = new HashSet<>(); + + @Override + protected TestTargetProcess newTestTargetProcess(TestTargetProcessContainer container, + int pid, AddressSpace space) { + return new TestTargetProcess(container, pid, space) { + @Override + public CompletableFuture resync(RefreshBehavior refreshAttributes, + RefreshBehavior refreshElements) { + observed.add(this); + return super.resync(refreshAttributes, refreshElements); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + + api.refreshObjectChildren(mb.testProcess1); + assertEquals(Set.of(mb.testProcess1), model.observed); + } + + @Test + public void testRefreshSubtree() throws Throwable { + var model = new TestDebuggerObjectModel() { + Set observed = new HashSet<>(); + + @Override + protected TestTargetProcess newTestTargetProcess(TestTargetProcessContainer container, + int pid, AddressSpace space) { + return new TestTargetProcess(container, pid, space) { + @Override + public CompletableFuture resync(RefreshBehavior refreshAttributes, + RefreshBehavior refreshElements) { + observed.add(this); + return super.resync(refreshAttributes, refreshElements); + } + }; + } + + @Override + protected TestTargetThread newTestTargetThread(TestTargetThreadContainer container, + int tid) { + return new TestTargetThread(container, tid) { + @Override + public CompletableFuture resync(RefreshBehavior refreshAttributes, + RefreshBehavior refreshElements) { + observed.add(this); + return super.resync(refreshAttributes, refreshElements); + } + }; + } + }; + mb = new TestModelBuilder(model); + createTestModel(); + mb.createTestProcessesAndThreads(); + + api.refreshSubtree(mb.testModel.session); + assertEquals(Set.of(mb.testProcess1, mb.testProcess3, mb.testThread1, mb.testThread2, + mb.testThread3, mb.testThread4), model.observed); + } + + @Test + public void testFlushAsyncPipelines() throws Throwable { + createTestModel(); + mb.createTestProcessesAndThreads(); + TraceRecorder recorder = recordTarget(mb.testProcess1); + + // Ensure it works whether or not there are pending events + for (int i = 0; i < 10; i++) { + api.flushAsyncPipelines(recorder.getTrace()); + } + } +} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRmiAPITest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRmiAPITest.java new file mode 100644 index 00000000000..4d61f52f0f7 --- /dev/null +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/FlatDebuggerRmiAPITest.java @@ -0,0 +1,282 @@ +/* ### + * 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.debug.flatapi; + +import static org.junit.Assert.*; + +import java.math.BigInteger; +import java.util.*; +import java.util.function.*; + +import org.junit.Before; +import org.junit.Test; + +import db.Transaction; +import generic.Unique; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TestTraceRmiLaunchOpinion.TestTraceRmiLaunchOffer; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin; +import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiConnection.TestRemoteMethod; +import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget; +import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchResult; +import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; +import ghidra.trace.database.memory.DBTraceMemorySpace; +import ghidra.trace.database.target.DBTraceObjectManager; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.thread.TraceObjectThread; +import ghidra.trace.model.thread.TraceThread; + +public class FlatDebuggerRmiAPITest extends AbstractLiveFlatDebuggerAPITest { + + protected TraceRmiLauncherServicePlugin rmiLaunchPlugin; + + protected class TestFlatRmiAPI extends TestFlatAPI implements FlatDebuggerRmiAPI { + } + + @Before + public void setUpRmiTest() throws Throwable { + rmiLaunchPlugin = addPlugin(tool, TraceRmiLauncherServicePlugin.class); + } + + @Override + protected FlatDebuggerRmiAPI newFlatAPI() { + return new TestFlatRmiAPI(); + } + + protected TraceRmiTarget createTarget() throws Throwable { + createRmiConnection(); + addExecuteMethod(); + addControlMethods(); + addMemoryMethods(); + addRegisterMethods(); + createTrace(); + try (Transaction tx = tb.startTransaction()) { + DBTraceObjectManager objs = tb.trace.getObjectManager(); + objs.createRootObject(SCHEMA_SESSION); + tb.createObjectsProcessAndThreads(); + tb.createObjectsFramesAndRegs( + tb.obj("Processes[1].Threads[1]").queryInterface(TraceObjectThread.class), + Lifespan.nowOn(0), tb.host, 2); + addMemoryRegion(objs, Lifespan.nowOn(0), tb.range(0x00400000, 0x00400fff), ".text", + "rx"); + } + TraceRmiTarget target = rmiCx.publishTarget(tool, tb.trace); + traceManager.openTrace(tb.trace); + // Do not activate, as this pollutes the method invocation queues + waitForSwing(); + return target; + } + + @Test + public void testReadLiveMemory() throws Throwable { + TraceRmiTarget target = createTarget(); + var args = rmiMethodReadMem.expect(a -> { + try (Transaction tx = tb.startTransaction()) { + tb.trace.getMemoryManager() + .putBytes(target.getSnap(), tb.addr(0x00400000), + tb.buf(1, 2, 3, 4, 5, 6, 7, 8)); + } + return null; + }); + byte[] data = api.readMemory(tb.trace, target.getSnap(), tb.addr(0x00400000), 8, monitor); + assertEquals(Map.ofEntries( + Map.entry("process", tb.obj("Processes[1]")), + // Framework quantizes to page + Map.entry("range", tb.range(0x00400000, 0x00400fff))), + waitOn(args)); + assertArrayEquals(tb.arr(1, 2, 3, 4, 5, 6, 7, 8), data); + } + + @Test + public void testReadLiveRegister() throws Throwable { + TraceRmiTarget target = createTarget(); + TraceThread thread = + tb.trace.getThreadManager().getLiveThreadByPath(0, "Processes[1].Threads[1]"); + Register r0 = tb.reg("r0"); + var args = rmiMethodReadRegs.expect(a -> { + try (Transaction tx = tb.startTransaction()) { + DBTraceMemorySpace regs = + tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, true); + regs.setValue(target.getSnap(), new RegisterValue(r0, new BigInteger("1234"))); + } + return null; + }); + RegisterValue value = api.readRegister(tb.host, thread, 0, target.getSnap(), r0); + assertEquals(Map.ofEntries( + Map.entry("container", tb.obj("Processes[1].Threads[1].Stack[0].Registers"))), + waitOn(args)); + assertEquals(new RegisterValue(r0, new BigInteger("1234")), value); + } + + @Test + public void testReadLiveRegisters() throws Throwable { + TraceRmiTarget target = createTarget(); + TraceThread thread = + tb.trace.getThreadManager().getLiveThreadByPath(0, "Processes[1].Threads[1]"); + Register r0 = tb.reg("r0"); + Register r1 = tb.reg("r1"); + var args = rmiMethodReadRegs.expect(a -> { + try (Transaction tx = tb.startTransaction()) { + DBTraceMemorySpace regs = + tb.trace.getMemoryManager().getMemoryRegisterSpace(thread, true); + regs.setValue(target.getSnap(), new RegisterValue(r0, new BigInteger("1234"))); + regs.setValue(target.getSnap(), new RegisterValue(r1, new BigInteger("5678"))); + } + return null; + }); + List values = + api.readRegisters(tb.host, thread, 0, target.getSnap(), List.of(r0, r1)); + assertEquals(Map.ofEntries( + Map.entry("container", tb.obj("Processes[1].Threads[1].Stack[0].Registers"))), + waitOn(args)); + assertEquals(List.of( + new RegisterValue(r0, new BigInteger("1234")), + new RegisterValue(r1, new BigInteger("5678"))), + values); + } + + protected List filter(Collection col, Class cls) { + return col.stream(). mapMulti((e, m) -> { + if (cls.isInstance(e)) { + m.accept(cls.cast(e)); + } + }).toList(); + } + + @Test + public void testGetLaunchOffers() throws Throwable { + createProgram(); + programManager.openProgram(program); + waitForSwing(); + + TestTraceRmiLaunchOffer offer = + Unique.assertOne(filter(api.getLaunchOffers(), TestTraceRmiLaunchOffer.class)); + assertEquals(program, offer.getProgram()); + } + + @Test + public void testGetSavedLaunchOffers() throws Throwable { + createProgram(); + programManager.openProgram(program); + waitForSwing(); + + assertEquals(List.of(), api.getSavedLaunchOffers()); + + TestTraceRmiLaunchOffer offer = + Unique.assertOne(filter(api.getLaunchOffers(), TestTraceRmiLaunchOffer.class)); + offer.saveLauncherArgs(Map.of("image", "/test/image")); + + assertEquals(List.of(offer), api.getSavedLaunchOffers()); + } + + @Test + public void testLaunchCustomCommandLine() throws Throwable { + TestTraceRmiLaunchOffer offer = + Unique.assertOne(filter(api.getLaunchOffers(), TestTraceRmiLaunchOffer.class)); + offer.saveLauncherArgs(Map.of("image", "/test/image")); + + LaunchResult result = api.launch(monitor); + assertEquals("Test launcher cannot launch /test/image", result.exception().getMessage()); + } + + protected void runTestStep(Predicate step, Supplier method) + throws Throwable { + createTarget(); + TraceObjectThread thread = + tb.obj("Processes[1].Threads[1]").queryInterface(TraceObjectThread.class); + traceManager.activateThread(thread); + waitForSwing(); + + var args = method.get().expect(a -> null); + assertTrue(step.test(thread)); + assertEquals(Map.ofEntries( + Map.entry("thread", thread.getObject())), + waitOn(args)); + } + + @Test + public void testStepGivenThread() throws Throwable { + runTestStep(api::stepInto, () -> rmiMethodStepInto); + } + + @Test + public void testStepInto() throws Throwable { + runTestStep(t -> api.stepInto(), () -> rmiMethodStepInto); + } + + @Test + public void testStepOver() throws Throwable { + runTestStep(t -> api.stepOver(), () -> rmiMethodStepOver); + } + + @Test + public void testStepOut() throws Throwable { + runTestStep(t -> api.stepOut(), () -> rmiMethodStepOut); + } + + @Override + protected void runTestResume(BooleanSupplier resume) throws Throwable { + createTarget(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + var args = rmiMethodResume.expect(a -> null); + assertTrue(resume.getAsBoolean()); + assertEquals(Map.ofEntries( + Map.entry("process", tb.obj("Processes[1]"))), + waitOn(args)); + } + + @Override + protected void runTestInterrupt(BooleanSupplier interrupt) throws Throwable { + createTarget(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + var args = rmiMethodInterrupt.expect(a -> null); + assertTrue(interrupt.getAsBoolean()); + assertEquals(Map.ofEntries( + Map.entry("process", tb.obj("Processes[1]"))), + waitOn(args)); + } + + @Override + protected void runTestKill(BooleanSupplier kill) throws Throwable { + createTarget(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + var args = rmiMethodKill.expect(a -> null); + assertTrue(kill.getAsBoolean()); + assertEquals(Map.ofEntries( + Map.entry("process", tb.obj("Processes[1]"))), + waitOn(args)); + } + + @Override + protected void runTestExecuteCapture(Function executeCapture) throws Throwable { + createTarget(); + traceManager.activateTrace(tb.trace); + waitForSwing(); + + var args = rmiMethodExecute.expect(a -> "result"); + assertEquals("result", api.executeCapture("some command")); + assertEquals(Map.ofEntries( + Map.entry("cmd", "some command"), + Map.entry("to_string", true)), + waitOn(args)); + } +}