From 5ae08742c3c261c0c8d26574440977e7a735df42 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:35:53 -0400 Subject: [PATCH] GP-4399: Display progress for memory reads in Debug Console --- .../ghidra/app/services/ProgressService.java | 25 ++++++++++ .../debug/gui/action/AutoReadMemorySpec.java | 23 +++++++++ .../gui/action/DebuggerReadsMemoryTrait.java | 27 +++++----- .../gui/action/VisibleAutoReadMemorySpec.java | 3 +- .../VisibleROOnceAutoReadMemorySpec.java | 3 +- .../debug/gui/control/TargetActionTask.java | 50 +++++++++++++++++++ 6 files changed, 115 insertions(+), 16 deletions(-) diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java index 7a1b1ebafc..2a57f15a5e 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/ProgressService.java @@ -17,6 +17,7 @@ package ghidra.app.services; import java.util.Collection; import java.util.concurrent.*; +import java.util.function.Function; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -117,6 +118,7 @@ public interface ProgressService { default CompletableFuture execute(Task task) { return CompletableFuture.supplyAsync(() -> { try (CloseableTaskMonitor monitor = publishTask()) { + monitor.setCancelEnabled(task.canCancel()); try { task.run(monitor); } @@ -131,4 +133,27 @@ public interface ProgressService { } }); } + + /** + * Similar to {@link #execute(Task)}, but for asynchronous methods + * + * @param the type of future result + * @param canCancel true if the task can be cancelled + * @param hasProgress true if the task displays progress + * @param isModal true if the task is modal (ignored) + * @param futureSupplier the task which returns a future, given the task monitor + * @return the future returned by the supplier + */ + default CompletableFuture execute(boolean canCancel, boolean hasProgress, + boolean isModal, Function> futureSupplier) { + CloseableTaskMonitor monitor = publishTask(); + monitor.setCancelEnabled(canCancel); + CompletableFuture future = futureSupplier.apply(monitor); + future.handle((t, ex) -> { + monitor.close(); + return null; + }); + monitor.addCancelledListener(() -> future.cancel(true)); + return future; + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoReadMemorySpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoReadMemorySpec.java index 94521c5d39..4af67b7ac8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoReadMemorySpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoReadMemorySpec.java @@ -18,11 +18,13 @@ package ghidra.app.plugin.core.debug.gui.action; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import javax.swing.Icon; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import ghidra.app.plugin.core.debug.gui.control.TargetActionTask; import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils; import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.framework.options.SaveState; @@ -31,6 +33,7 @@ import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.AddressSetView; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.classfinder.ExtensionPoint; +import ghidra.util.task.TaskMonitor; /** * An interface for specifying how to automatically read target memory. @@ -93,6 +96,12 @@ public interface AutoReadMemorySpec extends ExtensionPoint { * follow-up purposes only, and should always complete normally. It should complete with true if * any memory was actually loaded. Otherwise, it should complete with false. * + *

+ * NOTE: This returns the future, rather than being synchronous, because not all specs + * will actually need to create a background task. If this were synchronous, the caller would + * have to invoke it from a background thread, requiring it to create that thread whether or not + * this method actually does anything. + * * @param tool the tool containing the provider * @param coordinates the provider's current coordinates * @param visible the provider's visible addresses @@ -100,4 +109,18 @@ public interface AutoReadMemorySpec extends ExtensionPoint { */ CompletableFuture readMemory(PluginTool tool, DebuggerCoordinates coordinates, AddressSetView visible); + + /** + * A convenience for performing target memory reads with progress displayed + * + * @param tool the tool for displaying progress + * @param reader the method to perform the read, asynchronously + * @return a future which returns true if the read completes + */ + default CompletableFuture doRead(PluginTool tool, + Function> reader) { + return TargetActionTask + .executeTask(tool, getMenuName(), true, true, false, m -> reader.apply(m)) + .thenApply(__ -> true); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerReadsMemoryTrait.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerReadsMemoryTrait.java index 7354d6182d..004c4cc108 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerReadsMemoryTrait.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/DebuggerReadsMemoryTrait.java @@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.gui.action; import java.lang.invoke.MethodHandles; import java.util.Objects; +import java.util.concurrent.ExecutionException; import docking.ActionContext; import docking.ComponentProvider; @@ -28,22 +29,25 @@ import docking.widgets.EventTrigger; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources.AbstractRefreshSelectedMemoryAction; import ghidra.app.plugin.core.debug.gui.action.AutoReadMemorySpec.AutoReadMemorySpecConfigFieldCodec; +import ghidra.app.plugin.core.debug.gui.control.TargetActionTask; import ghidra.app.util.viewer.listingpanel.AddressSetDisplayListener; import ghidra.debug.api.target.Target; import ghidra.debug.api.tracemgr.DebuggerCoordinates; -import ghidra.framework.cmd.BackgroundCommand; -import ghidra.framework.model.*; +import ghidra.framework.model.DomainObjectChangeRecord; +import ghidra.framework.model.DomainObjectEvent; import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoConfigStateField; import ghidra.program.model.address.*; -import ghidra.trace.model.*; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.TraceDomainObjectListener; import ghidra.trace.model.memory.TraceMemoryState; import ghidra.trace.model.program.TraceProgramView; import ghidra.trace.model.time.TraceSnapshot; import ghidra.trace.util.TraceEvents; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; +import ghidra.util.task.Task; import ghidra.util.task.TaskMonitor; public abstract class DebuggerReadsMemoryTrait { @@ -69,22 +73,21 @@ public abstract class DebuggerReadsMemoryTrait { selection = visible; } final AddressSetView sel = selection; - Trace trace = current.getTrace(); Target target = current.getTarget(); - tool.executeBackgroundCommand(new BackgroundCommand(NAME, true, true, false) { + + TargetActionTask.executeTask(tool, new Task(NAME, true, true, false) { @Override - public boolean applyTo(DomainObject obj, TaskMonitor monitor) { + public void run(TaskMonitor monitor) throws CancelledException { target.invalidateMemoryCaches(); try { - target.readMemory(sel, monitor); - memoryWasRead(sel); + target.readMemoryAsync(sel, monitor).get(); } - catch (CancelledException e) { - return false; + catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Failed to read memory", e); } - return true; + memoryWasRead(sel); } - }, trace); + }); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleAutoReadMemorySpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleAutoReadMemorySpec.java index 0c5b29f110..55cfd0072b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleAutoReadMemorySpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleAutoReadMemorySpec.java @@ -27,7 +27,6 @@ import ghidra.program.model.address.AddressSet; import ghidra.program.model.address.AddressSetView; import ghidra.trace.model.memory.TraceMemoryManager; import ghidra.trace.model.memory.TraceMemoryState; -import ghidra.util.task.TaskMonitor; public class VisibleAutoReadMemorySpec implements AutoReadMemorySpec { public static final String CONFIG_NAME = "1_READ_VISIBLE"; @@ -68,6 +67,6 @@ public class VisibleAutoReadMemorySpec implements AutoReadMemorySpec { return CompletableFuture.completedFuture(false); } - return target.readMemoryAsync(toRead, TaskMonitor.DUMMY).thenApply(__ -> true); + return doRead(tool, monitor -> target.readMemoryAsync(toRead, monitor)); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleROOnceAutoReadMemorySpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleROOnceAutoReadMemorySpec.java index dd481f886c..b2d50b66f2 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleROOnceAutoReadMemorySpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/VisibleROOnceAutoReadMemorySpec.java @@ -28,7 +28,6 @@ import ghidra.program.model.address.*; import ghidra.trace.model.Lifespan; import ghidra.trace.model.TraceAddressSnapRange; import ghidra.trace.model.memory.*; -import ghidra.util.task.TaskMonitor; public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec { public static final String CONFIG_NAME = "1_READ_VIS_RO_ONCE"; @@ -92,6 +91,6 @@ public class VisibleROOnceAutoReadMemorySpec implements AutoReadMemorySpec { return CompletableFuture.completedFuture(false); } - return target.readMemoryAsync(toRead, TaskMonitor.DUMMY).thenApply(__ -> true); + return doRead(tool, monitor -> target.readMemoryAsync(toRead, monitor)); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/TargetActionTask.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/TargetActionTask.java index 63818985ea..07d73ef958 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/TargetActionTask.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/TargetActionTask.java @@ -16,10 +16,13 @@ package ghidra.app.plugin.core.debug.gui.control; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.services.DebuggerConsoleService; import ghidra.app.services.ProgressService; +import ghidra.async.AsyncUtils; import ghidra.debug.api.target.Target.ActionEntry; import ghidra.framework.plugintool.PluginTool; import ghidra.util.Msg; @@ -61,6 +64,29 @@ public class TargetActionTask extends Task { } } + static class FutureAsTask extends Task { + final CompletableFuture future = new CompletableFuture<>(); + private final Function> futureSupplier; + + public FutureAsTask(String title, boolean canCancel, boolean hasProgress, boolean isModal, + Function> futureSupplier) { + super(title, canCancel, hasProgress, isModal); + this.futureSupplier = futureSupplier; + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + CompletableFuture future = futureSupplier.apply(monitor); + future.handle(AsyncUtils.copyTo(this.future)); + try { + future.get(); + } + catch (InterruptedException | ExecutionException e) { + // Client should get it via the copyTo + } + } + } + /** * Execute a task * @@ -83,6 +109,30 @@ public class TargetActionTask extends Task { return wrapper.future; } + /** + * Execute an asynchronous task + * + * @param tool the tool in which to execute + * @param title the title of the task + * @param canCancel if the task can be cancelled + * @param hasProgress if the task displays progress + * @param isModal if the task is modal + * @param futureSupplier the task, a function of the monitor returning the future + * @return a future which completes in the same way as the one returned by the supplier + */ + public static CompletableFuture executeTask(PluginTool tool, String title, + boolean canCancel, boolean hasProgress, boolean isModal, + Function> futureSupplier) { + ProgressService progressService = tool.getService(ProgressService.class); + if (progressService != null) { + return progressService.execute(canCancel, hasProgress, isModal, futureSupplier); + } + FutureAsTask task = + new FutureAsTask<>(title, canCancel, hasProgress, isModal, futureSupplier); + tool.execute(task); + return task.future; + } + /** * Execute an {@link ActionEntry} *