GP-2036: Confirm module map after launch

This commit is contained in:
Dan 2022-05-23 13:11:18 -04:00
parent 0f3d941115
commit f426a878d5
9 changed files with 392 additions and 144 deletions

View File

@ -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.

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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