diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java
index b08681a407..d5fef0742e 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java
@@ -21,6 +21,8 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
+import javax.swing.JOptionPane;
+
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.jdom.Element;
import org.jdom.JDOMException;
@@ -46,6 +48,7 @@ import ghidra.trace.model.Trace;
import ghidra.trace.model.TraceLocation;
import ghidra.trace.model.modules.TraceModule;
import ghidra.util.Msg;
+import ghidra.util.Swing;
import ghidra.util.database.UndoableTransaction;
import ghidra.util.datastruct.CollectionChangeListener;
import ghidra.util.exception.CancelledException;
@@ -53,6 +56,12 @@ import ghidra.util.task.TaskMonitor;
import ghidra.util.xml.XmlUtilities;
public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProgramLaunchOffer {
+ private static final String HTML = "
";
+ private static final String NO_PAUSE_DIAGNOSTIC_MESSAGE = "" +
+ "It's possible the target launched but never paused, and so Ghidra has not been " +
+ "able to inspect it. Try interrupting the target, then inspect the process list. " +
+ "Further intervention may be required to establish the module/address mappings.";
+
protected final Program program;
protected final PluginTool tool;
protected final DebuggerModelFactory factory;
@@ -430,7 +439,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
return factory.build().thenApplyAsync(m -> {
service.addModel(m);
return m;
- });
+ }, SwingExecutorService.LATER);
}
protected CompletableFuture findLauncher(DebuggerObjectModel m) {
@@ -453,9 +462,37 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
return launcher.launch(args);
}
+ protected void checkCancelled(TaskMonitor monitor) {
+ if (monitor.isCancelled()) {
+ throw new CancellationException("User cancelled");
+ }
+ }
+
+ protected TargetLauncher onTimedOutFindLauncher(TaskMonitor monitor) {
+ checkCancelled(monitor);
+ monitor.setMessage("Timed out finding the launcher. Aborting.");
+ JOptionPane.showMessageDialog(null, HTML + "Timed out finding the launcher. " +
+ "This indicates an error in the implementation of the connector and/or the launcher " +
+ "opinion. Try again, and/or report the bug.",
+ getMenuParentTitle(), JOptionPane.ERROR_MESSAGE);
+ throw new CancellationException("Timed out");
+ }
+
+ protected Void onTimedOutLaunch(TaskMonitor monitor) {
+ checkCancelled(monitor);
+ monitor.setMessage("Timed out waiting for launch. Aborting.");
+ JOptionPane.showMessageDialog(null, HTML +
+ "Timed out waiting for launch. " + NO_PAUSE_DIAGNOSTIC_MESSAGE,
+ getMenuParentTitle(), JOptionPane.ERROR_MESSAGE);
+ throw new CancellationException("Timed out");
+ }
+
protected TargetObject onTimedOutTarget(TaskMonitor monitor) {
+ checkCancelled(monitor);
monitor.setMessage("Timed out waiting for target. Aborting.");
- Msg.showError(this, null, getButtonTitle(), "Timed out waiting for target.");
+ JOptionPane.showMessageDialog(null, HTML +
+ "Timed out waiting for target. " + NO_PAUSE_DIAGNOSTIC_MESSAGE,
+ getMenuParentTitle(), JOptionPane.ERROR_MESSAGE);
throw new CancellationException("Timed out");
}
@@ -472,12 +509,27 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
protected TraceRecorder onTimedOutRecorder(TaskMonitor monitor, DebuggerModelService service,
TargetObject target) {
+ checkCancelled(monitor);
monitor.setMessage("Timed out waiting for recording. Invoking the recorder.");
- return service.recordTargetPromptOffers(target);
+ TraceRecorder recorder = service.recordTargetPromptOffers(target);
+ if (recorder == null) {
+ throw new CancellationException("User cancelled at record dialog");
+ }
+ DebuggerTraceManagerService traceManager =
+ tool.getService(DebuggerTraceManagerService.class);
+ if (traceManager != null) {
+ Trace trace = recorder.getTrace();
+ Swing.runLater(() -> {
+ traceManager.openTrace(trace);
+ traceManager.activateTrace(trace);
+ });
+ }
+ return recorder;
}
protected Void onTimedOutMapping(TaskMonitor monitor,
DebuggerStaticMappingService mappingService, TraceRecorder recorder) {
+ checkCancelled(monitor);
monitor.setMessage("Timed out waiting for module map. Invoking the mapper.");
Collection mapped;
try {
@@ -503,41 +555,49 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
DebuggerModelService service = tool.getService(DebuggerModelService.class);
DebuggerStaticMappingService mappingService =
tool.getService(DebuggerStaticMappingService.class);
- monitor.initialize(4);
+ monitor.initialize(6);
monitor.setMessage("Connecting");
var locals = new Object() {
CompletableFuture futureTarget;
};
- return connect(service, prompt).thenComposeAsync(m -> {
+ return connect(service, prompt).thenCompose(m -> {
+ checkCancelled(monitor);
monitor.incrementProgress(1);
monitor.setMessage("Finding Launcher");
- return findLauncher(m);
- }, SwingExecutorService.LATER).thenCompose(l -> {
+ return AsyncTimer.DEFAULT_TIMER.mark()
+ .timeOut(findLauncher(m), getTimeoutMillis(),
+ () -> onTimedOutFindLauncher(monitor));
+ }).thenCompose(l -> {
+ checkCancelled(monitor);
monitor.incrementProgress(1);
monitor.setMessage("Launching");
locals.futureTarget = listenForTarget(l.getModel());
- return launch(l, prompt);
+ return AsyncTimer.DEFAULT_TIMER.mark()
+ .timeOut(launch(l, prompt), getTimeoutMillis(),
+ () -> onTimedOutLaunch(monitor));
}).thenCompose(__ -> {
+ checkCancelled(monitor);
monitor.incrementProgress(1);
monitor.setMessage("Waiting for target");
return AsyncTimer.DEFAULT_TIMER.mark()
.timeOut(locals.futureTarget, getTimeoutMillis(),
() -> onTimedOutTarget(monitor));
}).thenCompose(t -> {
+ checkCancelled(monitor);
monitor.incrementProgress(1);
monitor.setMessage("Waiting for recorder");
return AsyncTimer.DEFAULT_TIMER.mark()
.timeOut(waitRecorder(service, t), getTimeoutMillis(),
() -> onTimedOutRecorder(monitor, service, t));
}).thenCompose(r -> {
+ checkCancelled(monitor);
monitor.incrementProgress(1);
if (r == null) {
throw new CancellationException();
}
monitor.setMessage("Confirming program is mapped to target");
- CompletableFuture futureMapped = listenForMapping(mappingService, r);
return AsyncTimer.DEFAULT_TIMER.mark()
- .timeOut(futureMapped, getTimeoutMillis(),
+ .timeOut(listenForMapping(mappingService, r), getTimeoutMillis(),
() -> onTimedOutMapping(monitor, mappingService, r));
}).exceptionally(ex -> {
if (AsyncUtils.unwrapThrowable(ex) instanceof CancellationException) {
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java
index f30e8095b2..996f35ca9d 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/utils/BackgroundUtils.java
@@ -136,6 +136,7 @@ public enum BackgroundUtils {
boolean hasProgress, boolean canCancel,
Function> futureProducer) {
var dialog = new TaskDialog(name, canCancel, true, hasProgress) {
+ CancelledListener cancelledListener = this::cancelled;
CompletableFuture orig = futureProducer.apply(this);
CompletableFuture future = orig.exceptionally(ex -> {
if (AsyncUtils.unwrapThrowable(ex) instanceof CancellationException) {
@@ -148,11 +149,15 @@ public enum BackgroundUtils {
return v;
});
- @Override
- protected void cancelCallback() {
+ {
+ addCancelledListener(cancelledListener);
+ }
+
+ private void cancelled() {
future.cancel(true);
close();
}
+
};
if (!dialog.orig.isDone()) {
tool.showDialog(dialog);
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java
index e151455f0b..d34446e812 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java
@@ -139,9 +139,6 @@ public interface DebuggerModelService {
* as much. If the user cancels, the returned future completes with {@code null}. The invocation
* is assumed to come from a {@link ActionSource#MANUAL} source.
*
- *
- * TODO: Should the prompt allow the user to force an opinion which gave no offers?
- *
* @see DebuggerMappingOpinion#queryOpinions(TargetObject)
* @param target the target to record.
* @return a future which completes with the recorder, or completes exceptionally