Merge remote-tracking branch 'origin/GP-3997_Dan_lessObtrusiveCommandFailures--SQUASHED'

This commit is contained in:
Ryan Kurtz 2024-01-31 08:26:40 -05:00
commit 68d209347c
15 changed files with 337 additions and 120 deletions

View file

@ -16,12 +16,15 @@
package ghidra.app.services;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.*;
import org.apache.commons.lang3.exception.ExceptionUtils;
import ghidra.debug.api.progress.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.ServiceInfo;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
/**
@ -99,4 +102,33 @@ public interface ProgressService {
* @param listener the listener
*/
void removeProgressListener(ProgressListener listener);
/**
* A drop-in replacement for {@link PluginTool#execute(Task)} that publishes progress via the
* service rather than displaying a dialog.
*
* <p>
* In addition to changing how progress is displayed, this also returns a future so that task
* completion can be detected by the caller.
*
* @param task task to run in a new thread
* @return a future which completes when the task is finished
*/
default CompletableFuture<Void> execute(Task task) {
return CompletableFuture.supplyAsync(() -> {
try (CloseableTaskMonitor monitor = publishTask()) {
try {
task.run(monitor);
}
catch (CancelledException e) {
throw new CancellationException("User cancelled");
}
catch (Throwable e) {
monitor.reportError(e);
return ExceptionUtils.rethrow(e);
}
return null;
}
});
}
}

View file

@ -17,7 +17,17 @@ package ghidra.debug.api.progress;
import ghidra.util.task.TaskMonitor;
/**
* A task monitor that can be used in a try-with-resources block.
*/
public interface CloseableTaskMonitor extends TaskMonitor, AutoCloseable {
@Override
void close();
/**
* Report an error while working on this task
*
* @param error the error
*/
void reportError(Throwable error);
}

View file

@ -15,7 +15,13 @@
*/
package ghidra.debug.api.progress;
/**
* A listener for events on the progress service, including updates to task progress
*/
public interface ProgressListener {
/**
* Describes how or why a task monitor was disposed
*/
enum Disposal {
/**
* The monitor was properly closed
@ -27,12 +33,52 @@ public interface ProgressListener {
CLEANED;
}
/**
* A new task monitor has been created
*
* <p>
* The subscriber ought to display the monitor as soon as is reasonable. Optionally, a
* subscriber may apply a grace period, e.g., half a second, before displaying it, in case it is
* quickly disposed.
*
* @param monitor a means of retrieving messages and progress about the task
*/
void monitorCreated(MonitorReceiver monitor);
/**
* A task monitor has been disposed
*
* @param monitor the receiver for the disposed monitor
* @param disposal why it was disposed
*/
void monitorDisposed(MonitorReceiver monitor, Disposal disposal);
/**
* A task has updated a monitor's message
*
* @param monitor the receiver whose monitor's message changed
* @param message the new message
*/
void messageUpdated(MonitorReceiver monitor, String message);
/**
* A task has reported an error
*
* @param monitor the receiver for the task reporting the error
* @param error the exception representing the error
*/
void errorReported(MonitorReceiver monitor, Throwable error);
/**
* A task's progress has updated
*
* <p>
* Note the subscriber may need to use {@link MonitorReceiver#getMaximum()} to properly update
* the display.
*
* @param monitor the receiver whose monitor's progress changed
* @param progress the new progress value
*/
void progressUpdated(MonitorReceiver monitor, long progress);
/**
@ -46,7 +92,7 @@ public interface ProgressListener {
* <li>show progress value in percent string</li>
* </ul>
*
* @param monitor the monitor
* @param monitor the receiver whose monitor's attribute(s) changed
*/
void attributeUpdated(MonitorReceiver monitor);
}

View file

@ -394,11 +394,11 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
* Obtain the launcher args
*
* <p>
* This should either call {@link #promptLauncherArgs(Map))} or
* {@link #loadLastLauncherArgs(Map, boolean))}. Note if choosing the latter, the user will not
* be prompted to confirm.
* This should either call {@link #promptLauncherArgs(LaunchConfigurator, Throwable)} or
* {@link #loadLastLauncherArgs(boolean)}. Note if choosing the latter, the user will not be
* prompted to confirm.
*
* @param params the parameters of the model's launcher
* @param prompt true to prompt the user, false to use saved arguments
* @param configurator the rules for configuring the launcher
* @param lastExc if retrying, the last exception to display as an error message
* @return the chosen arguments, or null if the user cancels at the prompt
@ -543,19 +543,24 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
try {
monitor.setMessage("Listening for connection");
monitor.increment();
acceptor = service.acceptOne(new InetSocketAddress("127.0.0.1", 0));
monitor.setMessage("Launching back-end");
monitor.increment();
launchBackEnd(monitor, sessions, args, acceptor.getAddress());
monitor.setMessage("Waiting for connection");
monitor.increment();
acceptor.setTimeout(getTimeoutMillis());
connection = acceptor.accept();
connection.registerTerminals(sessions.values());
monitor.setMessage("Waiting for trace");
monitor.increment();
trace = connection.waitForTrace(getTimeoutMillis());
traceManager.openTrace(trace);
traceManager.activate(traceManager.resolveTrace(trace),
ActivationCause.START_RECORDING);
monitor.setMessage("Waiting for module mapping");
monitor.increment();
try {
listenForMapping(mappingService, connection, trace).get(getTimeoutMillis(),
TimeUnit.MILLISECONDS);

View file

@ -183,12 +183,22 @@ public class TraceRmiLauncherServicePlugin extends Plugin
.toList();
}
protected void executeTask(Task task) {
ProgressService progressService = tool.getService(ProgressService.class);
if (progressService != null) {
progressService.execute(task);
}
else {
tool.execute(task);
}
}
protected void relaunch(TraceRmiLaunchOffer offer) {
tool.execute(new ReLaunchTask(offer));
executeTask(new ReLaunchTask(offer));
}
protected void configureAndLaunch(TraceRmiLaunchOffer offer) {
tool.execute(new ConfigureAndLaunchTask(offer));
executeTask(new ConfigureAndLaunchTask(offer));
}
protected String[] constructLaunchMenuPrefix() {

View file

@ -31,6 +31,7 @@ import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.AutoService.Wiring;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.datastruct.ListenerSet;
import ghidra.util.task.ConsoleTaskMonitor;
@ -63,6 +64,11 @@ public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
public void close() {
// Nothing
}
@Override
public void reportError(Throwable e) {
Msg.error(e.getMessage(), e);
}
}
@AutoServiceConsumed

View file

@ -187,6 +187,8 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
*
* <p>
* This class is public for access by test cases only.
*
* @param <T> the type of the message
*/
public interface LogRow<T> {
Icon getIcon();
@ -319,6 +321,11 @@ public class DebuggerConsoleProvider extends ComponentProviderAdapter
logTableModel.updateItem(logRow);
}
@Override
public void errorReported(MonitorReceiver monitor, Throwable error) {
log(DebuggerResources.ICON_LOG_ERROR, error.getMessage());
}
@Override
public void progressUpdated(MonitorReceiver monitor, long progress) {
LogRow<?> logRow = logTableModel.getMap().get(contextFor(monitor));

View file

@ -146,6 +146,8 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
private DebuggerControlService controlService;
// @AutoServiceConsumed // via method
private DebuggerEmulationService emulationService;
@AutoServiceConsumed
private ProgressService progressService;
public DebuggerControlPlugin(PluginTool tool) {
super(tool);
@ -282,8 +284,17 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
updateActions();
}
protected void executeTask(Task task) {
if (progressService != null) {
progressService.execute(task);
}
else {
tool.execute(task);
}
}
protected void runTask(String title, ActionEntry entry) {
tool.execute(new TargetActionTask(title, entry));
executeTask(new TargetActionTask(tool, title, entry));
}
protected void addTargetStepExtActions(Target target) {
@ -359,7 +370,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
if (target == null) {
return;
}
tool.execute(new Task("Disconnect", false, false, false) {
executeTask(new Task("Disconnect", false, false, false) {
@Override
public void run(TaskMonitor monitor) throws CancelledException {
try {

View file

@ -64,7 +64,7 @@ public class DebuggerMethodActionsPlugin extends Plugin implements PopupActionPr
@Override
public void actionPerformed(ActionContext context) {
tool.execute(new TargetActionTask(entry.display(), entry));
tool.execute(new TargetActionTask(tool, entry.display(), entry));
}
}

View file

@ -15,21 +15,42 @@
*/
package ghidra.app.plugin.core.debug.gui.control;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.services.DebuggerConsoleService;
import ghidra.debug.api.target.Target.ActionEntry;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
class TargetActionTask extends Task {
private ActionEntry entry;
private final PluginTool tool;
private final ActionEntry entry;
public TargetActionTask(String title, ActionEntry entry) {
public TargetActionTask(PluginTool tool, String title, ActionEntry entry) {
super(title, false, false, false);
this.tool = tool;
this.entry = entry;
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
entry.run(false);
try {
entry.run(false);
}
catch (Throwable e) {
reportError(e);
}
}
private void reportError(Throwable error) {
DebuggerConsoleService consoleService = tool.getService(DebuggerConsoleService.class);
if (consoleService != null) {
consoleService.log(DebuggerResources.ICON_LOG_ERROR, error.getMessage());
}
else {
Msg.showError(this, null, "Control Error", error.getMessage(), error);
}
}
}

View file

@ -52,6 +52,7 @@ import ghidra.dbg.target.TargetThread;
import ghidra.debug.api.action.ActionSource;
import ghidra.debug.api.model.*;
import ghidra.debug.api.model.DebuggerProgramLaunchOffer.PromptMode;
import ghidra.debug.api.progress.CloseableTaskMonitor;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.main.AppInfo;
import ghidra.framework.main.FrontEndTool;
@ -230,6 +231,8 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
private DebuggerTraceManagerService traceManager;
@AutoServiceConsumed
private DebuggerTargetService targetService;
@AutoServiceConsumed
private ProgressService progressService;
@SuppressWarnings("unused")
private final AutoService.Wiring autoServiceWiring;
@ -371,27 +374,41 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
private void debugProgram(DebuggerProgramLaunchOffer offer, Program program,
PromptMode prompt) {
BackgroundUtils.asyncModal(tool, offer.getButtonTitle(), true, true, m -> {
List<String> recent = new ArrayList<>(readMostRecentLaunches(program));
recent.remove(offer.getConfigName());
recent.add(offer.getConfigName());
writeMostRecentLaunches(program, recent);
CompletableFuture.runAsync(() -> {
updateActionDebugProgram();
}, AsyncUtils.SWING_EXECUTOR).exceptionally(ex -> {
Msg.error(this, "Trouble writing recent launches to program user data");
return null;
List<String> recent = new ArrayList<>(readMostRecentLaunches(program));
recent.remove(offer.getConfigName());
recent.add(offer.getConfigName());
writeMostRecentLaunches(program, recent);
updateActionDebugProgram();
if (progressService == null) {
BackgroundUtils.asyncModal(tool, offer.getButtonTitle(), true, true, m -> {
return offer.launchProgram(m, prompt).exceptionally(ex -> {
Throwable t = AsyncUtils.unwrapThrowable(ex);
if (t instanceof CancellationException || t instanceof CancelledException) {
return null;
}
return ExceptionUtils.rethrow(ex);
}).whenCompleteAsync((v, e) -> {
updateActionDebugProgram();
}, AsyncUtils.SWING_EXECUTOR);
});
return offer.launchProgram(m, prompt).exceptionally(ex -> {
}
else {
@SuppressWarnings("resource")
CloseableTaskMonitor monitor = progressService.publishTask();
offer.launchProgram(monitor, prompt).exceptionally(ex -> {
Throwable t = AsyncUtils.unwrapThrowable(ex);
if (t instanceof CancellationException || t instanceof CancelledException) {
return null;
}
monitor.reportError(t);
return ExceptionUtils.rethrow(ex);
}).whenCompleteAsync((v, e) -> {
monitor.close();
updateActionDebugProgram();
}, AsyncUtils.SWING_EXECUTOR);
});
}
}
private void debugProgramButtonActivated(ActionContext ctx) {

View file

@ -105,6 +105,44 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
return 10000;
}
protected static class TargetResult extends CompletableFuture<TargetObject>
implements DebuggerModelListener {
private final DebuggerObjectModel model;
public TargetResult(DebuggerObjectModel model) {
this.model = model;
exceptionally(this::onError);
model.addModelListener(this);
}
protected void checkObject(TargetObject object) {
if (DebugModelConventions.liveProcessOrNull(object) == null) {
return;
}
complete(object);
model.removeModelListener(this);
}
protected TargetObject onError(Throwable ex) {
model.removeModelListener(this);
return null;
}
@Override
public void created(TargetObject object) {
checkObject(object);
}
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
if (!added.containsKey(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)) {
return;
}
checkObject(object);
}
}
/**
* Listen for the launched target in the model
*
@ -118,37 +156,33 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
* @return a future that completes with the target object
*/
protected CompletableFuture<TargetObject> listenForTarget(DebuggerObjectModel model) {
var result = new CompletableFuture<TargetObject>() {
DebuggerModelListener listener = new DebuggerModelListener() {
protected void checkObject(TargetObject object) {
if (DebugModelConventions.liveProcessOrNull(object) == null) {
return;
}
complete(object);
model.removeModelListener(this);
}
return new TargetResult(model);
}
@Override
public void created(TargetObject object) {
checkObject(object);
}
protected static class RecorderResult extends CompletableFuture<TraceRecorder>
implements CollectionChangeListener<TraceRecorder> {
private final DebuggerModelService service;
private final TargetObject target;
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
if (!added.containsKey(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)) {
return;
}
checkObject(object);
}
};
};
model.addModelListener(result.listener);
result.exceptionally(ex -> {
model.removeModelListener(result.listener);
public RecorderResult(DebuggerModelService service, TargetObject target) {
this.service = service;
this.target = target;
exceptionally(this::onError);
service.addTraceRecordersChangedListener(this);
}
protected TraceRecorder onError(Throwable ex) {
service.removeTraceRecordersChangedListener(this);
return null;
});
return result;
}
@Override
public void elementAdded(TraceRecorder element) {
if (element.getTarget() == target) {
complete(element);
service.removeTraceRecordersChangedListener(this);
}
}
}
/**
@ -160,74 +194,79 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
*/
protected CompletableFuture<TraceRecorder> listenForRecorder(DebuggerModelService service,
TargetObject target) {
var result = new CompletableFuture<TraceRecorder>() {
CollectionChangeListener<TraceRecorder> listener = new CollectionChangeListener<>() {
@Override
public void elementAdded(TraceRecorder element) {
if (element.getTarget() == target) {
complete(element);
service.removeTraceRecordersChangedListener(this);
}
}
};
};
service.addTraceRecordersChangedListener(result.listener);
result.exceptionally(ex -> {
service.removeTraceRecordersChangedListener(result.listener);
return null;
});
return result;
return new RecorderResult(service, target);
}
protected Address getMappingProbeAddress() {
AddressIterator eepi = program.getSymbolTable().getExternalEntryPointIterator();
if (eepi.hasNext()) {
return eepi.next();
protected static class MappingResult extends CompletableFuture<Void>
implements DebuggerStaticMappingChangeListener {
private final DebuggerStaticMappingService mappingService;
private final TraceRecorder recorder;
private final Program program;
private final Trace trace;
private final ProgramLocation probe;
public MappingResult(DebuggerStaticMappingService mappingService, TraceRecorder recorder,
Program program) {
this.mappingService = mappingService;
this.recorder = recorder;
this.program = program;
this.probe = new ProgramLocation(program, getMappingProbeAddress());
this.trace = recorder.getTrace();
exceptionally(this::onError);
mappingService.addChangeListener(this);
check();
}
InstructionIterator ii = program.getListing().getInstructions(true);
if (ii.hasNext()) {
return ii.next().getAddress();
protected Void onError(Throwable ex) {
mappingService.removeChangeListener(this);
return null;
}
AddressSetView es = program.getMemory().getExecuteSet();
if (!es.isEmpty()) {
return es.getMinAddress();
protected Address getMappingProbeAddress() {
AddressIterator eepi = program.getSymbolTable().getExternalEntryPointIterator();
if (eepi.hasNext()) {
return eepi.next();
}
InstructionIterator ii = program.getListing().getInstructions(true);
if (ii.hasNext()) {
return ii.next().getAddress();
}
AddressSetView es = program.getMemory().getExecuteSet();
if (!es.isEmpty()) {
return es.getMinAddress();
}
if (!program.getMemory().isEmpty()) {
return program.getMinAddress();
}
return null; // There's no hope
}
if (!program.getMemory().isEmpty()) {
return program.getMinAddress();
@Override
public void mappingsChanged(Set<Trace> affectedTraces, Set<Program> affectedPrograms) {
if (!affectedPrograms.contains(program) &&
!affectedTraces.contains(trace)) {
return;
}
check();
}
protected void check() {
TraceLocation result =
mappingService.getOpenMappedLocation(trace, probe, recorder.getSnap());
if (result == null) {
return;
}
complete(null);
mappingService.removeChangeListener(this);
}
return null; // There's no hope
}
protected CompletableFuture<Void> listenForMapping(
DebuggerStaticMappingService mappingService, TraceRecorder recorder) {
ProgramLocation probe = new ProgramLocation(program, getMappingProbeAddress());
Trace trace = recorder.getTrace();
var result = new CompletableFuture<Void>() {
DebuggerStaticMappingChangeListener listener = (affectedTraces, affectedPrograms) -> {
if (!affectedPrograms.contains(program) &&
!affectedTraces.contains(trace)) {
return;
}
check();
};
protected void check() {
TraceLocation result =
mappingService.getOpenMappedLocation(trace, probe, recorder.getSnap());
if (result == null) {
return;
}
complete(null);
mappingService.removeChangeListener(listener);
}
};
mappingService.addChangeListener(result.listener);
result.check();
result.exceptionally(ex -> {
mappingService.removeChangeListener(result.listener);
return null;
});
return result;
return new MappingResult(mappingService, recorder, program);
}
protected Collection<ModuleMapEntry> invokeMapper(TaskMonitor monitor,
@ -396,15 +435,17 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
* Obtain the launcher args
*
* <p>
* This should either call {@link #promptLauncherArgs(Map))} or
* {@link #loadLastLauncherArgs(Map, boolean))}. Note if choosing the latter, the user will not
* be prompted to confirm.
* This should either call {@link #promptLauncherArgs(TargetLauncher,LaunchConfigurator)} or
* {@link #loadLastLauncherArgs(TargetLauncher, boolean)}. Note if choosing the latter, the user
* will not be prompted to confirm.
*
* @param params the parameters of the model's launcher
* @param launcher the model's launcher
* @param prompt true to prompt the user, false to use saved arguments
* @param configurator a means of configuring the launcher
* @return the chosen arguments, or null if the user cancels at the prompt
*/
public Map<String, ?> getLauncherArgs(TargetLauncher launcher,
boolean prompt, LaunchConfigurator configurator) {
public Map<String, ?> getLauncherArgs(TargetLauncher launcher, boolean prompt,
LaunchConfigurator configurator) {
return prompt
? configurator.configureLauncher(launcher,
promptLauncherArgs(launcher, configurator), RelPrompt.AFTER)
@ -668,6 +709,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
}).thenApply(__ -> {
if (locals.exception != null) {
monitor.setMessage("Launch error: " + locals.exception);
Msg.error(this, "Launch error", locals.exception);
return locals.getResult();
}
monitor.setMessage("Launch successful");

View file

@ -69,6 +69,11 @@ public class DefaultCloseableTaskMonitor implements CloseableTaskMonitor {
receiver.setMessage(message);
}
@Override
public void reportError(Throwable error) {
receiver.reportError(error);
}
@Override
public String getMessage() {
return receiver.getMessage();

View file

@ -75,6 +75,10 @@ public class DefaultMonitorReceiver implements MonitorReceiver {
plugin.listeners.invoke().messageUpdated(this, message);
}
void reportError(Throwable error) {
plugin.listeners.invoke().errorReported(this, error);
}
@Override
public String getMessage() {
synchronized (lock) {
@ -158,6 +162,7 @@ public class DefaultMonitorReceiver implements MonitorReceiver {
return cancelEnabled;
}
@Override
public boolean isShowProgressValue() {
return showProgressValue;
}

View file

@ -36,7 +36,7 @@ import ghidra.util.datastruct.ListenerSet;
""",
servicesProvided = { ProgressService.class },
packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.STABLE)
status = PluginStatus.RELEASED)
public class ProgressServicePlugin extends Plugin implements ProgressService {
ListenerSet<ProgressListener> listeners = new ListenerSet<>(ProgressListener.class, true);