mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-24 13:11:47 +00:00
GP-2036: Confirm module map after launch
This commit is contained in:
parent
0f3d941115
commit
f426a878d5
@ -1248,7 +1248,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
}
|
||||
DomainFileFilter filter = df -> Program.class.isAssignableFrom(df.getDomainObjectClass());
|
||||
|
||||
// TODO regarding the hack note below, I believe this issue ahs been fixed, but not sure how to test
|
||||
// TODO regarding the hack note below, I believe it's fixed, but not sure how to test
|
||||
return programChooserDialog =
|
||||
new DataTreeDialog(null, "Map Module to Program", DataTreeDialog.OPEN, filter) {
|
||||
{ // TODO/HACK: I get an NPE setting the default selection if I don't fake this.
|
||||
|
@ -25,7 +25,6 @@ import ghidra.dbg.DebuggerModelFactory;
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.framework.plugintool.PluginEvent;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.lifecycle.Internal;
|
||||
import ghidra.util.Swing;
|
||||
|
||||
@ -127,13 +126,4 @@ public interface DebuggerModelServiceInternal extends DebuggerModelService {
|
||||
fireModelActivatedEvent(model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement {@link #recordTargetPromptOffers(TargetObject)} using the given plugin tool
|
||||
*
|
||||
* @param t the plugin tool (front-end or tool containing proxy)
|
||||
* @param target the target to record
|
||||
* @return a future which completes with the resulting recorder, unless cancelled
|
||||
*/
|
||||
TraceRecorder doRecordTargetPromptOffers(PluginTool t, TargetObject target);
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ public class DebuggerModelServicePlugin extends Plugin
|
||||
|
||||
private static final String PREFIX_FACTORY = "Factory_";
|
||||
|
||||
// Since used for naming, no : allowed.
|
||||
// Since used for naming, no ':' allowed.
|
||||
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy.MM.dd-HH.mm.ss-z");
|
||||
|
||||
protected class ListenersForRemovalAndFocus {
|
||||
@ -368,9 +368,8 @@ public class DebuggerModelServicePlugin extends Plugin
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Internal
|
||||
public TraceRecorder doRecordTargetPromptOffers(PluginTool t, TargetObject target) {
|
||||
protected TraceRecorder doRecordTargetPromptOffers(PluginTool t, TargetObject target) {
|
||||
synchronized (recordersByTarget) {
|
||||
TraceRecorder recorder = recordersByTarget.get(target);
|
||||
if (recorder != null) {
|
||||
@ -671,13 +670,18 @@ public class DebuggerModelServicePlugin extends Plugin
|
||||
connectDialog.readConfigState(saveState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<DebuggerProgramLaunchOffer> getProgramLaunchOffers(Program program) {
|
||||
protected Stream<DebuggerProgramLaunchOffer> doGetProgramLaunchOffers(PluginTool tool,
|
||||
Program program) {
|
||||
return ClassSearcher.getInstances(DebuggerProgramLaunchOpinion.class)
|
||||
.stream()
|
||||
.flatMap(opinion -> opinion.getOffers(program, tool, this).stream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<DebuggerProgramLaunchOffer> getProgramLaunchOffers(Program program) {
|
||||
return doGetProgramLaunchOffers(tool, program);
|
||||
}
|
||||
|
||||
protected CompletableFuture<DebuggerObjectModel> doShowConnectDialog(PluginTool tool,
|
||||
DebuggerModelFactory factory) {
|
||||
CompletableFuture<DebuggerObjectModel> future = connectDialog.reset(factory);
|
||||
|
@ -274,7 +274,7 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
|
||||
|
||||
@Override
|
||||
public Stream<DebuggerProgramLaunchOffer> getProgramLaunchOffers(Program program) {
|
||||
return orderOffers(delegate.getProgramLaunchOffers(program), program);
|
||||
return orderOffers(delegate.doGetProgramLaunchOffers(tool, program), program);
|
||||
}
|
||||
|
||||
protected List<String> readMostRecentLaunches(Program program) {
|
||||
@ -330,11 +330,11 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
|
||||
}
|
||||
|
||||
private void debugProgram(DebuggerProgramLaunchOffer offer, Program program, boolean prompt) {
|
||||
BackgroundUtils.async(tool, program, offer.getButtonTitle(), true, true, true, (p, m) -> {
|
||||
List<String> mrl = new ArrayList<>(readMostRecentLaunches(program));
|
||||
mrl.remove(offer.getConfigName());
|
||||
mrl.add(offer.getConfigName());
|
||||
writeMostRecentLaunches(program, mrl);
|
||||
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 -> {
|
||||
@ -520,14 +520,9 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
|
||||
return delegate.recordTargetBestOffer(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TraceRecorder doRecordTargetPromptOffers(PluginTool t, TargetObject target) {
|
||||
return delegate.doRecordTargetPromptOffers(t, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TraceRecorder recordTargetPromptOffers(TargetObject target) {
|
||||
return doRecordTargetPromptOffers(tool, target);
|
||||
return delegate.doRecordTargetPromptOffers(tool, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -17,31 +17,38 @@ package ghidra.app.plugin.core.debug.service.model.launch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.jdom.Element;
|
||||
import org.jdom.JDOMException;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
|
||||
import ghidra.app.services.DebuggerModelService;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.async.SwingExecutorService;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.app.services.ModuleMapProposal.ModuleMapEntry;
|
||||
import ghidra.async.*;
|
||||
import ghidra.dbg.*;
|
||||
import ghidra.dbg.target.TargetLauncher;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.listing.ProgramUserData;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.TraceLocation;
|
||||
import ghidra.trace.model.modules.TraceModule;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.database.UndoableTransaction;
|
||||
import ghidra.util.datastruct.CollectionChangeListener;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import ghidra.util.xml.XmlUtilities;
|
||||
|
||||
@ -71,6 +78,146 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
return PathUtils.parse("");
|
||||
}
|
||||
|
||||
protected long getTimeoutMillis() {
|
||||
return 10000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for the launched target in the model
|
||||
*
|
||||
* <p>
|
||||
* The abstract offer will invoke this before invoking the launch command, so there should be no
|
||||
* need to replay. Once the target has been found, the listener must remove itself. The default
|
||||
* is to just listen for the first live {@link TargetProcess} that appears. See
|
||||
* {@link DebugModelConventions#isProcessAlive(TargetProcess)}.
|
||||
*
|
||||
* @param model the model
|
||||
* @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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
};
|
||||
};
|
||||
model.addModelListener(result.listener);
|
||||
result.exceptionally(ex -> {
|
||||
model.removeModelListener(result.listener);
|
||||
return null;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for the recording of a given target
|
||||
*
|
||||
* @param service the model service
|
||||
* @param target the expected target
|
||||
* @return a future that completes with the recorder
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
protected Collection<ModuleMapEntry> invokeMapper(TaskMonitor monitor,
|
||||
DebuggerStaticMappingService mappingService, TraceRecorder recorder)
|
||||
throws CancelledException {
|
||||
Map<TraceModule, ModuleMapProposal> map =
|
||||
mappingService.proposeModuleMaps(recorder.getTrace().getModuleManager().getAllModules(),
|
||||
List.of(program));
|
||||
Collection<ModuleMapEntry> proposal = MapProposal.flatten(map.values());
|
||||
mappingService.addModuleMappings(proposal, monitor, true);
|
||||
return proposal;
|
||||
}
|
||||
|
||||
private void saveLauncherArgs(Map<String, ?> args,
|
||||
Map<String, ParameterDescription<?>> params) {
|
||||
SaveState state = new SaveState();
|
||||
@ -259,8 +406,8 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
}
|
||||
}
|
||||
|
||||
protected CompletableFuture<DebuggerObjectModel> connect(boolean prompt) {
|
||||
DebuggerModelService service = tool.getService(DebuggerModelService.class);
|
||||
protected CompletableFuture<DebuggerObjectModel> connect(DebuggerModelService service,
|
||||
boolean prompt) {
|
||||
DebuggerModelFactory factory = getModelFactory();
|
||||
if (prompt) {
|
||||
return service.showConnectDialog(factory);
|
||||
@ -271,30 +418,114 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
});
|
||||
}
|
||||
|
||||
protected CompletableFuture<TargetLauncher> findLauncher(DebuggerObjectModel m) {
|
||||
List<String> launcherPath = getLauncherPath();
|
||||
TargetObjectSchema schema = m.getRootSchema().getSuccessorSchema(launcherPath);
|
||||
if (!schema.getInterfaces().contains(TargetLauncher.class)) {
|
||||
throw new AssertionError("LaunchOffer / model implementation error: " +
|
||||
"The given launcher path is not a TargetLauncher, according to its schema");
|
||||
}
|
||||
return new ValueExpecter(m, launcherPath).thenApply(o -> (TargetLauncher) o);
|
||||
}
|
||||
|
||||
// Eww.
|
||||
protected CompletableFuture<Void> launch(TargetLauncher launcher,
|
||||
boolean prompt) {
|
||||
Map<String, ?> args = getLauncherArgs(launcher.getParameters(), prompt);
|
||||
if (args == null) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
return launcher.launch(args);
|
||||
}
|
||||
|
||||
protected TargetObject onTimedOutTarget(TaskMonitor monitor) {
|
||||
monitor.setMessage("Timed out waiting for target. Aborting.");
|
||||
Msg.showError(this, null, getButtonTitle(), "Timed out waiting for target.");
|
||||
throw new CancellationException("Timed out");
|
||||
}
|
||||
|
||||
protected CompletableFuture<TraceRecorder> waitRecorder(DebuggerModelService service,
|
||||
TargetObject target) {
|
||||
CompletableFuture<TraceRecorder> futureRecorder = listenForRecorder(service, target);
|
||||
TraceRecorder recorder = service.getRecorder(target);
|
||||
if (recorder != null) {
|
||||
futureRecorder.cancel(true);
|
||||
return CompletableFuture.completedFuture(recorder);
|
||||
}
|
||||
return futureRecorder;
|
||||
}
|
||||
|
||||
protected TraceRecorder onTimedOutRecorder(TaskMonitor monitor, DebuggerModelService service,
|
||||
TargetObject target) {
|
||||
monitor.setMessage("Timed out waiting for recording. Invoking the recorder.");
|
||||
return service.recordTargetPromptOffers(target);
|
||||
}
|
||||
|
||||
protected Void onTimedOutMapping(TaskMonitor monitor,
|
||||
DebuggerStaticMappingService mappingService, TraceRecorder recorder) {
|
||||
monitor.setMessage("Timed out waiting for module map. Invoking the mapper.");
|
||||
Collection<ModuleMapEntry> mapped;
|
||||
try {
|
||||
mapped = invokeMapper(monitor, mappingService, recorder);
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
throw new CancellationException(e.getMessage());
|
||||
}
|
||||
if (mapped.isEmpty()) {
|
||||
monitor.setMessage(
|
||||
"Could not formulate a mapping with the target program. " +
|
||||
"Continuing without one.");
|
||||
Msg.showWarn(this, null, "Launch " + program,
|
||||
"The resulting target process has no mapping to the static image " +
|
||||
program + ". Intervention is required before static and dynamic " +
|
||||
"addresses can be translated. Check the target's module list.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> launchProgram(TaskMonitor monitor, boolean prompt) {
|
||||
monitor.initialize(2);
|
||||
DebuggerModelService service = tool.getService(DebuggerModelService.class);
|
||||
DebuggerStaticMappingService mappingService =
|
||||
tool.getService(DebuggerStaticMappingService.class);
|
||||
monitor.initialize(4);
|
||||
monitor.setMessage("Connecting");
|
||||
return connect(prompt).thenComposeAsync(m -> {
|
||||
List<String> launcherPath = getLauncherPath();
|
||||
TargetObjectSchema schema = m.getRootSchema().getSuccessorSchema(launcherPath);
|
||||
if (!schema.getInterfaces().contains(TargetLauncher.class)) {
|
||||
throw new AssertionError("LaunchOffer / model implementation error: " +
|
||||
"The given launcher path is not a TargetLauncher, according to its schema");
|
||||
}
|
||||
return new ValueExpecter(m, launcherPath);
|
||||
var locals = new Object() {
|
||||
CompletableFuture<TargetObject> futureTarget;
|
||||
};
|
||||
return connect(service, prompt).thenComposeAsync(m -> {
|
||||
monitor.incrementProgress(1);
|
||||
monitor.setMessage("Finding Launcher");
|
||||
return findLauncher(m);
|
||||
}, SwingExecutorService.LATER).thenCompose(l -> {
|
||||
monitor.incrementProgress(1);
|
||||
monitor.setMessage("Launching");
|
||||
TargetLauncher launcher = (TargetLauncher) l;
|
||||
Map<String, ?> args = getLauncherArgs(launcher.getParameters(), prompt);
|
||||
if (args == null) {
|
||||
// Cancelled
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
return launcher.launch(args);
|
||||
}).thenRun(() -> {
|
||||
locals.futureTarget = listenForTarget(l.getModel());
|
||||
return launch(l, prompt);
|
||||
}).thenCompose(__ -> {
|
||||
monitor.incrementProgress(1);
|
||||
monitor.setMessage("Waiting for target");
|
||||
return AsyncTimer.DEFAULT_TIMER.mark()
|
||||
.timeOut(locals.futureTarget, getTimeoutMillis(),
|
||||
() -> onTimedOutTarget(monitor));
|
||||
}).thenCompose(t -> {
|
||||
monitor.incrementProgress(1);
|
||||
monitor.setMessage("Waiting for recorder");
|
||||
return AsyncTimer.DEFAULT_TIMER.mark()
|
||||
.timeOut(waitRecorder(service, t), getTimeoutMillis(),
|
||||
() -> onTimedOutRecorder(monitor, service, t));
|
||||
}).thenCompose(r -> {
|
||||
monitor.incrementProgress(1);
|
||||
monitor.setMessage("Confirming program is mapped to target");
|
||||
CompletableFuture<Void> futureMapped = listenForMapping(mappingService, r);
|
||||
return AsyncTimer.DEFAULT_TIMER.mark()
|
||||
.timeOut(futureMapped, getTimeoutMillis(),
|
||||
() -> onTimedOutMapping(monitor, mappingService, r));
|
||||
}).exceptionally(ex -> {
|
||||
if (AsyncUtils.unwrapThrowable(ex) instanceof CancellationException) {
|
||||
return null;
|
||||
}
|
||||
return ExceptionUtils.rethrow(ex);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -18,13 +18,17 @@ package ghidra.app.plugin.core.debug.utils;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.framework.cmd.BackgroundCommand;
|
||||
import ghidra.framework.model.DomainObject;
|
||||
import ghidra.framework.model.UndoableDomainObject;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.Swing;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.*;
|
||||
|
||||
@ -75,6 +79,30 @@ public enum BackgroundUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a task with an attached monitor dialog
|
||||
*
|
||||
* <p>
|
||||
* The returned future includes error handling, so even if the task completes in error, the
|
||||
* returned future will just complete with null. If further error handling is required, then the
|
||||
* {@code futureProducer} should make the future available. Because this uses the tool's task
|
||||
* scheduler, only one task can be pending at a time, even if the current stage is running on a
|
||||
* separate executor, because the tool's task execution thread will wait on the future result.
|
||||
* You may run stages in parallel, or include stages on which the final stage does not depend;
|
||||
* however, once the final stage completes, the dialog will disappear, even though other stages
|
||||
* may remain executing in the background. See
|
||||
* {@link #asyncModal(PluginTool, String, boolean, boolean, Function)}.
|
||||
*
|
||||
* @param <T> the type of the result
|
||||
* @param tool the tool for displaying the dialog and scheduling the task
|
||||
* @param obj an object on which to open a transaction
|
||||
* @param name a name / title for the task
|
||||
* @param hasProgress true if the task has progress
|
||||
* @param canCancel true if the task can be cancelled
|
||||
* @param isModal true to display a modal dialog, false to use the tool's background monitor
|
||||
* @param futureProducer a function to start the task
|
||||
* @return a future which completes when the task is finished.
|
||||
*/
|
||||
public static <T extends UndoableDomainObject> AsyncBackgroundCommand<T> async(PluginTool tool,
|
||||
T obj, String name, boolean hasProgress, boolean canCancel, boolean isModal,
|
||||
BiFunction<T, TaskMonitor, CompletableFuture<?>> futureProducer) {
|
||||
@ -84,6 +112,54 @@ public enum BackgroundUtils {
|
||||
return cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a task with an attached monitor dialog
|
||||
*
|
||||
* <p>
|
||||
* The returned future includes error handling, so even if the task completes in error, the
|
||||
* returned future will just complete with null. If further error handling is required, then the
|
||||
* {@code futureProducer} should make the future available. This differs from
|
||||
* {@link #async(PluginTool, UndoableDomainObject, String, boolean, boolean, boolean, BiFunction)}
|
||||
* in that it doesn't use the tool's task manager, so it can run in parallel with other tasks.
|
||||
* There is not currently a supported method to run multiple non-modal tasks concurrently, since
|
||||
* they would have to share a single task monitor component.
|
||||
*
|
||||
* @param <T> the type of the result
|
||||
* @param tool the tool for displaying the dialog
|
||||
* @param name a name / title for the task
|
||||
* @param hasProgress true if the dialog should include a progress bar
|
||||
* @param canCancel true if the dialog should include a cancel button
|
||||
* @param futureProducer a function to start the task
|
||||
* @return a future which completes when the task is finished.
|
||||
*/
|
||||
public static <T> CompletableFuture<T> asyncModal(PluginTool tool, String name,
|
||||
boolean hasProgress, boolean canCancel,
|
||||
Function<TaskMonitor, CompletableFuture<T>> futureProducer) {
|
||||
var dialog = new TaskDialog(name, canCancel, true, hasProgress) {
|
||||
CompletableFuture<T> orig = futureProducer.apply(this);
|
||||
CompletableFuture<T> future = orig.exceptionally(ex -> {
|
||||
if (AsyncUtils.unwrapThrowable(ex) instanceof CancellationException) {
|
||||
return null;
|
||||
}
|
||||
Msg.showError(this, null, name, "Error running asynchronous background task", ex);
|
||||
return null;
|
||||
}).thenApply(v -> {
|
||||
Swing.runIfSwingOrRunLater(() -> close());
|
||||
return v;
|
||||
});
|
||||
|
||||
@Override
|
||||
protected void cancelCallback() {
|
||||
future.cancel(true);
|
||||
close();
|
||||
}
|
||||
};
|
||||
if (!dialog.orig.isDone()) {
|
||||
tool.showDialog(dialog);
|
||||
}
|
||||
return dialog.future;
|
||||
}
|
||||
|
||||
public static class PluginToolExecutorService extends AbstractExecutorService {
|
||||
private final PluginTool tool;
|
||||
private String name;
|
||||
|
@ -453,7 +453,7 @@ public interface DebuggerStaticMappingService {
|
||||
*
|
||||
* <p>
|
||||
* Note, this method will first examine module and program names in order to cull unlikely
|
||||
* pairs. If then takes the best-scored proposal for each module. If a module has no likely
|
||||
* pairs. It then takes the best-scored proposal for each module. If a module has no likely
|
||||
* paired program, then it is omitted from the result, i.e.., the returned map will have no
|
||||
* {@code null} values.
|
||||
*
|
||||
|
@ -15,15 +15,14 @@
|
||||
*/
|
||||
package ghidra.async;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Timer;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import ghidra.util.Msg;
|
||||
|
||||
/**
|
||||
* A timer for asynchronous scheduled tasks
|
||||
*
|
||||
* <p>
|
||||
* This object provides a futures which complete at specified times. This is useful for pausing amid
|
||||
* a chain of callback actions, i.e., between iterations of a loop. A critical tenant of
|
||||
* asynchronous reactive programming is to never block a thread, at least not for an indefinite
|
||||
@ -34,13 +33,15 @@ import ghidra.util.Msg;
|
||||
* {@link Timer}, but its {@link Future}s are not {@link CompletableFuture}s. The same is true of
|
||||
* {@link ScheduledThreadPoolExecutor}.
|
||||
*
|
||||
* <p>
|
||||
* A delay is achieved using {@link #mark()}, then {@link #after(long)}. For example, within a
|
||||
* {@link AsyncUtils#sequence(TypeSpec)}:
|
||||
*
|
||||
* <pre>
|
||||
* timer.mark().afterMark(1000).handle(seq::next);
|
||||
* timer.mark().after(1000).handle(seq::next);
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* {@link #mark()} marks the current system time; all subsequent calls to {@link #after(long)}
|
||||
* schedule futures relative to this mark. Using {@link #after(long)} before {@link #mark()} gives
|
||||
* undefined behavior. Scheduling a timed sequence of actions is best accomplished using times
|
||||
@ -48,31 +49,33 @@ import ghidra.util.Msg;
|
||||
*
|
||||
* <pre>
|
||||
* sequence(TypeSpec.VOID).then((seq) -> {
|
||||
* timer.mark().afterMark(1000).handle(seq::next);
|
||||
* timer.mark().after(1000).handle(seq::next);
|
||||
* }).then((seq) -> {
|
||||
* doTaskAtOneSecond().handle(seq::next);
|
||||
* }).then((seq) -> {
|
||||
* timer.afterMark(2000).handle(seq::next);
|
||||
* timer.after(2000).handle(seq::next);
|
||||
* }).then((seq) -> {
|
||||
* doTaskAtTwoSeconds().handle(seq::next);
|
||||
* }).asCompletableFuture();
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* This provides slightly more precise scheduling than delaying for a fixed period between tasks.
|
||||
* Consider a second example:
|
||||
*
|
||||
* <pre>
|
||||
* sequence(TypeSpec.VOID).then((seq) -> {
|
||||
* timer.mark().afterMark(1000).handle(seq::next);
|
||||
* timer.mark().after(1000).handle(seq::next);
|
||||
* }).then((seq) -> {
|
||||
* doTaskAtOneSecond().handle(seq::next);
|
||||
* }).then((seq) -> {
|
||||
* timer.mark().afterMark(1000).handle(seq::next);
|
||||
* timer.mark().after(1000).handle(seq::next);
|
||||
* }).then((seq) -> {
|
||||
* doTaskAtTwoSeconds().handle(seq::next);
|
||||
* }).asCompletableFuture();
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* In the first example, {@code doTaskAtTwoSeconds} executes at 2000ms from the mark + some
|
||||
* scheduling overhead. In the second example, {@code doTaskAtTwoSeconds} executes at 1000ms + some
|
||||
* scheduling overhead + the time to execute {@code doTaskAtOneSecond} + 1000ms + some more
|
||||
@ -82,6 +85,7 @@ import ghidra.util.Msg;
|
||||
* from the mark + some scheduling overhead. The scheduling overhead is generally bounded to a small
|
||||
* constant and depends on the accuracy of the host OS and JVM.
|
||||
*
|
||||
* <p>
|
||||
* Like {@link Timer}, each {@link AsyncTimer} is backed by a single thread which uses
|
||||
* {@link Object#wait()} to implement its timing. Thus, this is not suitable for real-time
|
||||
* applications. Unlike {@link Timer}, the backing thread is always a daemon. It will not prevent
|
||||
@ -92,10 +96,7 @@ import ghidra.util.Msg;
|
||||
public class AsyncTimer {
|
||||
public static final AsyncTimer DEFAULT_TIMER = new AsyncTimer();
|
||||
|
||||
protected Thread thread = new Thread(this::run);
|
||||
protected SortedMap<Long, Set<TimerPromise>> promises = new TreeMap<>();
|
||||
protected long nextWake = Long.MAX_VALUE;
|
||||
protected boolean alive = true;
|
||||
protected ExecutorService thread = Executors.newSingleThreadExecutor();
|
||||
|
||||
public class Mark {
|
||||
protected final long mark;
|
||||
@ -118,86 +119,42 @@ public class AsyncTimer {
|
||||
public CompletableFuture<Void> after(long intervalMillis) {
|
||||
return atSystemTime(mark + intervalMillis);
|
||||
}
|
||||
}
|
||||
|
||||
private class TimerPromise extends CompletableFuture<Void> {
|
||||
private final long time;
|
||||
|
||||
TimerPromise(long time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel(boolean mayInterruptIfRunning) {
|
||||
synchronized (AsyncTimer.this) {
|
||||
Set<TimerPromise> sameTime = promises.get(time);
|
||||
if (sameTime != null) {
|
||||
sameTime.remove(this);
|
||||
if (sameTime.isEmpty()) {
|
||||
promises.remove(time);
|
||||
}
|
||||
/**
|
||||
* Time a future out after the given interval
|
||||
*
|
||||
* @param <T> the type of the future
|
||||
* @param future the future whose value is expected in the given interval
|
||||
* @param millis the time interval in milliseconds
|
||||
* @param valueIfLate a supplier for the value if the future doesn't complete in time
|
||||
* @return a future which completes with the given futures value, or the late value if it
|
||||
* times out.
|
||||
*/
|
||||
public <T> CompletableFuture<T> timeOut(CompletableFuture<T> future, long millis,
|
||||
Supplier<T> valueIfLate) {
|
||||
return CompletableFuture.anyOf(future, after(millis)).thenApply(v -> {
|
||||
if (future.isDone()) {
|
||||
return future.getNow(null);
|
||||
}
|
||||
}
|
||||
return super.cancel(mayInterruptIfRunning);
|
||||
// Don't worry about interrupting and re-sleeping the thread
|
||||
// It costs the same, maybe less, to let it wake itself.
|
||||
return valueIfLate.get();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new timer
|
||||
*
|
||||
* <p>
|
||||
* Except to reduce contention among threads, most applications need only create one timer
|
||||
* instance.
|
||||
* instance. See {@link AsyncTimer#DEFAULT_TIMER}.
|
||||
*/
|
||||
public AsyncTimer() {
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private void run() {
|
||||
/*
|
||||
* The general idea is to keep track of the time until the next promise is to be completed,
|
||||
* and to sleep until that time. Once awake, all tasks whose scheduled time has passed are
|
||||
* completed. The actual completion calls must take place outside of the sychronized block.
|
||||
*/
|
||||
while (alive) {
|
||||
try {
|
||||
Set<TimerPromise> toComplete = new HashSet<>();
|
||||
synchronized (this) {
|
||||
long delta = nextWake - System.currentTimeMillis();
|
||||
if (delta > 0) {
|
||||
wait(delta);
|
||||
if (!alive) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
long key = Long.MAX_VALUE;
|
||||
while (!promises.isEmpty() &&
|
||||
(key = promises.firstKey()) <= System.currentTimeMillis()) {
|
||||
toComplete.addAll(promises.remove(key));
|
||||
}
|
||||
nextWake = key;
|
||||
}
|
||||
for (TimerPromise promise : toComplete) {
|
||||
promise.complete(null);
|
||||
}
|
||||
}
|
||||
catch (Throwable e) {
|
||||
Msg.warn(this, "Exception in timer thread", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
alive = false;
|
||||
thread.interrupt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a task to run when {@link System#currentTimeMillis()} has passed a given time
|
||||
*
|
||||
* <p>
|
||||
* This method returns immediately, giving a future result. The future completes "soon after"
|
||||
* the current system time passes the given time in milliseconds. There is some minimal
|
||||
* overhead, but the scheduler endeavors to complete the future as close to the given time as
|
||||
@ -207,20 +164,15 @@ public class AsyncTimer {
|
||||
* @return a future that completes soon after the given time
|
||||
*/
|
||||
public CompletableFuture<Void> atSystemTime(long timeMillis) {
|
||||
if (timeMillis <= System.currentTimeMillis()) {
|
||||
if (timeMillis - System.currentTimeMillis() <= 0) {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
synchronized (this) {
|
||||
Set<TimerPromise> sameTime =
|
||||
promises.computeIfAbsent(timeMillis, (k) -> new HashSet<>());
|
||||
TimerPromise promise = new TimerPromise(timeMillis);
|
||||
sameTime.add(promise);
|
||||
if (timeMillis < nextWake) {
|
||||
nextWake = timeMillis; // In case it hasn't started waiting yet
|
||||
notify();
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
long delta = timeMillis - System.currentTimeMillis();
|
||||
Executor executor =
|
||||
delta <= 0 ? thread : CompletableFuture.delayedExecutor(delta, TimeUnit.MILLISECONDS);
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
}, executor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -437,7 +437,7 @@ public enum DebugModelConventions {
|
||||
TargetExecutionStateful exe = (TargetExecutionStateful) process;
|
||||
TargetExecutionState state = exe.getExecutionState();
|
||||
if (state == null) {
|
||||
Msg.error(null, "null state for " + exe);
|
||||
Msg.trace(null, "null state for " + exe);
|
||||
return false;
|
||||
}
|
||||
return state.isAlive();
|
||||
|
Loading…
Reference in New Issue
Block a user