GP-4399: Display progress for memory reads in Debug Console

This commit is contained in:
Dan 2024-03-29 13:35:53 -04:00
parent fa0cb8d881
commit 5ae08742c3
6 changed files with 115 additions and 16 deletions

View file

@ -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<Void> 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 <T> 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 <T> CompletableFuture<T> execute(boolean canCancel, boolean hasProgress,
boolean isModal, Function<TaskMonitor, CompletableFuture<T>> futureSupplier) {
CloseableTaskMonitor monitor = publishTask();
monitor.setCancelEnabled(canCancel);
CompletableFuture<T> future = futureSupplier.apply(monitor);
future.handle((t, ex) -> {
monitor.close();
return null;
});
monitor.addCancelledListener(() -> future.cancel(true));
return future;
}
}

View file

@ -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.
*
* <p>
* <b>NOTE:</b> 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<Boolean> 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<Boolean> doRead(PluginTool tool,
Function<TaskMonitor, CompletableFuture<Void>> reader) {
return TargetActionTask
.executeTask(tool, getMenuName(), true, true, false, m -> reader.apply(m))
.thenApply(__ -> true);
}
}

View file

@ -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);
target.readMemoryAsync(sel, monitor).get();
}
catch (InterruptedException | ExecutionException e) {
throw new RuntimeException("Failed to read memory", e);
}
memoryWasRead(sel);
}
catch (CancelledException e) {
return false;
}
return true;
}
}, trace);
});
}
@Override

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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<T> extends Task {
final CompletableFuture<T> future = new CompletableFuture<>();
private final Function<TaskMonitor, CompletableFuture<T>> futureSupplier;
public FutureAsTask(String title, boolean canCancel, boolean hasProgress, boolean isModal,
Function<TaskMonitor, CompletableFuture<T>> futureSupplier) {
super(title, canCancel, hasProgress, isModal);
this.futureSupplier = futureSupplier;
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
CompletableFuture<T> 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 <T> CompletableFuture<T> executeTask(PluginTool tool, String title,
boolean canCancel, boolean hasProgress, boolean isModal,
Function<TaskMonitor, CompletableFuture<T>> futureSupplier) {
ProgressService progressService = tool.getService(ProgressService.class);
if (progressService != null) {
return progressService.execute(canCancel, hasProgress, isModal, futureSupplier);
}
FutureAsTask<T> task =
new FutureAsTask<>(title, canCancel, hasProgress, isModal, futureSupplier);
tool.execute(task);
return task.future;
}
/**
* Execute an {@link ActionEntry}
*