diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java index 150f778122..4107480c19 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java @@ -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. diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceInternal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceInternal.java index 52e142aadc..91cb690cb1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceInternal.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceInternal.java @@ -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); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java index 0d1f50456b..0058277b12 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java @@ -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 getProgramLaunchOffers(Program program) { + protected Stream doGetProgramLaunchOffers(PluginTool tool, + Program program) { return ClassSearcher.getInstances(DebuggerProgramLaunchOpinion.class) .stream() .flatMap(opinion -> opinion.getOffers(program, tool, this).stream()); } + @Override + public Stream getProgramLaunchOffers(Program program) { + return doGetProgramLaunchOffers(tool, program); + } + protected CompletableFuture doShowConnectDialog(PluginTool tool, DebuggerModelFactory factory) { CompletableFuture future = connectDialog.reset(factory); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java index 4ad30c65cb..e9a72e6620 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java @@ -274,7 +274,7 @@ public class DebuggerModelServiceProxyPlugin extends Plugin @Override public Stream getProgramLaunchOffers(Program program) { - return orderOffers(delegate.getProgramLaunchOffers(program), program); + return orderOffers(delegate.doGetProgramLaunchOffers(tool, program), program); } protected List 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 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 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 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 7950f02c55..8ccbc840e3 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 @@ -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 + * + *

+ * 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 listenForTarget(DebuggerObjectModel model) { + var result = new CompletableFuture() { + 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 removed, + Map 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 listenForRecorder(DebuggerModelService service, + TargetObject target) { + var result = new CompletableFuture() { + CollectionChangeListener 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 listenForMapping( + DebuggerStaticMappingService mappingService, TraceRecorder recorder) { + ProgramLocation probe = new ProgramLocation(program, getMappingProbeAddress()); + Trace trace = recorder.getTrace(); + var result = new CompletableFuture() { + 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 invokeMapper(TaskMonitor monitor, + DebuggerStaticMappingService mappingService, TraceRecorder recorder) + throws CancelledException { + Map map = + mappingService.proposeModuleMaps(recorder.getTrace().getModuleManager().getAllModules(), + List.of(program)); + Collection proposal = MapProposal.flatten(map.values()); + mappingService.addModuleMappings(proposal, monitor, true); + return proposal; + } + private void saveLauncherArgs(Map args, Map> params) { SaveState state = new SaveState(); @@ -259,8 +406,8 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg } } - protected CompletableFuture connect(boolean prompt) { - DebuggerModelService service = tool.getService(DebuggerModelService.class); + protected CompletableFuture 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 findLauncher(DebuggerObjectModel m) { + List 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 launch(TargetLauncher launcher, + boolean prompt) { + Map 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 waitRecorder(DebuggerModelService service, + TargetObject target) { + CompletableFuture 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 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 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 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 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 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 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); }); } } 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 a35f337a0c..f30e8095b2 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 @@ -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 + * + *

+ * 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 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 AsyncBackgroundCommand async(PluginTool tool, T obj, String name, boolean hasProgress, boolean canCancel, boolean isModal, BiFunction> futureProducer) { @@ -84,6 +112,54 @@ public enum BackgroundUtils { return cmd; } + /** + * Launch a task with an attached monitor dialog + * + *

+ * 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 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 CompletableFuture asyncModal(PluginTool tool, String name, + boolean hasProgress, boolean canCancel, + Function> futureProducer) { + var dialog = new TaskDialog(name, canCancel, true, hasProgress) { + CompletableFuture orig = futureProducer.apply(this); + CompletableFuture 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; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java index 182e64402f..0a012b4b19 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerStaticMappingService.java @@ -453,7 +453,7 @@ public interface DebuggerStaticMappingService { * *

* 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. * diff --git a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncTimer.java b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncTimer.java index dd38d9ddff..6c82fcb4ae 100644 --- a/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncTimer.java +++ b/Ghidra/Debug/Framework-AsyncComm/src/main/java/ghidra/async/AsyncTimer.java @@ -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 * + *

* 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}. * + *

* A delay is achieved using {@link #mark()}, then {@link #after(long)}. For example, within a * {@link AsyncUtils#sequence(TypeSpec)}: * *

- * timer.mark().afterMark(1000).handle(seq::next);
+ * timer.mark().after(1000).handle(seq::next);
  * 
* + *

* {@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; * *

  * 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();
  * 
* + *

* This provides slightly more precise scheduling than delaying for a fixed period between tasks. * Consider a second example: * *

  * 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();
  * 
* + *

* 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. * + *

* 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> 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 after(long intervalMillis) { return atSystemTime(mark + intervalMillis); } - } - private class TimerPromise extends CompletableFuture { - private final long time; - - TimerPromise(long time) { - this.time = time; - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - synchronized (AsyncTimer.this) { - Set 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 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 CompletableFuture timeOut(CompletableFuture future, long millis, + Supplier 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 * + *

* 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 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 * + *

* 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 atSystemTime(long timeMillis) { - if (timeMillis <= System.currentTimeMillis()) { + if (timeMillis - System.currentTimeMillis() <= 0) { return AsyncUtils.NIL; } - synchronized (this) { - Set 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); } /** diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java index d5cf2286fd..9cc5f3f3c6 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/DebugModelConventions.java @@ -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();