mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +02:00
GP-2036: Confirm module map after launch
This commit is contained in:
parent
0f3d941115
commit
f426a878d5
9 changed files with 392 additions and 144 deletions
|
@ -1248,7 +1248,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||||
}
|
}
|
||||||
DomainFileFilter filter = df -> Program.class.isAssignableFrom(df.getDomainObjectClass());
|
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 =
|
return programChooserDialog =
|
||||||
new DataTreeDialog(null, "Map Module to Program", DataTreeDialog.OPEN, filter) {
|
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.
|
{ // 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.DebuggerObjectModel;
|
||||||
import ghidra.dbg.target.TargetObject;
|
import ghidra.dbg.target.TargetObject;
|
||||||
import ghidra.framework.plugintool.PluginEvent;
|
import ghidra.framework.plugintool.PluginEvent;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
|
||||||
import ghidra.lifecycle.Internal;
|
import ghidra.lifecycle.Internal;
|
||||||
import ghidra.util.Swing;
|
import ghidra.util.Swing;
|
||||||
|
|
||||||
|
@ -127,13 +126,4 @@ public interface DebuggerModelServiceInternal extends DebuggerModelService {
|
||||||
fireModelActivatedEvent(model);
|
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_";
|
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");
|
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy.MM.dd-HH.mm.ss-z");
|
||||||
|
|
||||||
protected class ListenersForRemovalAndFocus {
|
protected class ListenersForRemovalAndFocus {
|
||||||
|
@ -368,9 +368,8 @@ public class DebuggerModelServicePlugin extends Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Internal
|
@Internal
|
||||||
public TraceRecorder doRecordTargetPromptOffers(PluginTool t, TargetObject target) {
|
protected TraceRecorder doRecordTargetPromptOffers(PluginTool t, TargetObject target) {
|
||||||
synchronized (recordersByTarget) {
|
synchronized (recordersByTarget) {
|
||||||
TraceRecorder recorder = recordersByTarget.get(target);
|
TraceRecorder recorder = recordersByTarget.get(target);
|
||||||
if (recorder != null) {
|
if (recorder != null) {
|
||||||
|
@ -671,13 +670,18 @@ public class DebuggerModelServicePlugin extends Plugin
|
||||||
connectDialog.readConfigState(saveState);
|
connectDialog.readConfigState(saveState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected Stream<DebuggerProgramLaunchOffer> doGetProgramLaunchOffers(PluginTool tool,
|
||||||
public Stream<DebuggerProgramLaunchOffer> getProgramLaunchOffers(Program program) {
|
Program program) {
|
||||||
return ClassSearcher.getInstances(DebuggerProgramLaunchOpinion.class)
|
return ClassSearcher.getInstances(DebuggerProgramLaunchOpinion.class)
|
||||||
.stream()
|
.stream()
|
||||||
.flatMap(opinion -> opinion.getOffers(program, tool, this).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,
|
protected CompletableFuture<DebuggerObjectModel> doShowConnectDialog(PluginTool tool,
|
||||||
DebuggerModelFactory factory) {
|
DebuggerModelFactory factory) {
|
||||||
CompletableFuture<DebuggerObjectModel> future = connectDialog.reset(factory);
|
CompletableFuture<DebuggerObjectModel> future = connectDialog.reset(factory);
|
||||||
|
|
|
@ -274,7 +274,7 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<DebuggerProgramLaunchOffer> getProgramLaunchOffers(Program program) {
|
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) {
|
protected List<String> readMostRecentLaunches(Program program) {
|
||||||
|
@ -330,11 +330,11 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
private void debugProgram(DebuggerProgramLaunchOffer offer, Program program, boolean prompt) {
|
private void debugProgram(DebuggerProgramLaunchOffer offer, Program program, boolean prompt) {
|
||||||
BackgroundUtils.async(tool, program, offer.getButtonTitle(), true, true, true, (p, m) -> {
|
BackgroundUtils.asyncModal(tool, offer.getButtonTitle(), true, true, m -> {
|
||||||
List<String> mrl = new ArrayList<>(readMostRecentLaunches(program));
|
List<String> recent = new ArrayList<>(readMostRecentLaunches(program));
|
||||||
mrl.remove(offer.getConfigName());
|
recent.remove(offer.getConfigName());
|
||||||
mrl.add(offer.getConfigName());
|
recent.add(offer.getConfigName());
|
||||||
writeMostRecentLaunches(program, mrl);
|
writeMostRecentLaunches(program, recent);
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
updateActionDebugProgram();
|
updateActionDebugProgram();
|
||||||
}, AsyncUtils.SWING_EXECUTOR).exceptionally(ex -> {
|
}, AsyncUtils.SWING_EXECUTOR).exceptionally(ex -> {
|
||||||
|
@ -520,14 +520,9 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
|
||||||
return delegate.recordTargetBestOffer(target);
|
return delegate.recordTargetBestOffer(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public TraceRecorder doRecordTargetPromptOffers(PluginTool t, TargetObject target) {
|
|
||||||
return delegate.doRecordTargetPromptOffers(t, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TraceRecorder recordTargetPromptOffers(TargetObject target) {
|
public TraceRecorder recordTargetPromptOffers(TargetObject target) {
|
||||||
return doRecordTargetPromptOffers(tool, target);
|
return delegate.doRecordTargetPromptOffers(tool, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,31 +17,38 @@ package ghidra.app.plugin.core.debug.service.model.launch;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.jdom.Element;
|
import org.jdom.Element;
|
||||||
import org.jdom.JDOMException;
|
import org.jdom.JDOMException;
|
||||||
|
|
||||||
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
|
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
|
||||||
import ghidra.app.services.DebuggerModelService;
|
import ghidra.app.services.*;
|
||||||
import ghidra.async.AsyncUtils;
|
import ghidra.app.services.ModuleMapProposal.ModuleMapEntry;
|
||||||
import ghidra.async.SwingExecutorService;
|
import ghidra.async.*;
|
||||||
import ghidra.dbg.*;
|
import ghidra.dbg.*;
|
||||||
import ghidra.dbg.target.TargetLauncher;
|
import ghidra.dbg.target.*;
|
||||||
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
|
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
|
||||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||||
import ghidra.dbg.target.TargetObject;
|
|
||||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||||
import ghidra.dbg.util.PathUtils;
|
import ghidra.dbg.util.PathUtils;
|
||||||
import ghidra.framework.model.DomainFile;
|
import ghidra.framework.model.DomainFile;
|
||||||
import ghidra.framework.options.SaveState;
|
import ghidra.framework.options.SaveState;
|
||||||
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
|
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
import ghidra.framework.plugintool.PluginTool;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.address.*;
|
||||||
import ghidra.program.model.listing.ProgramUserData;
|
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.Msg;
|
||||||
import ghidra.util.database.UndoableTransaction;
|
import ghidra.util.database.UndoableTransaction;
|
||||||
|
import ghidra.util.datastruct.CollectionChangeListener;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
import ghidra.util.xml.XmlUtilities;
|
import ghidra.util.xml.XmlUtilities;
|
||||||
|
|
||||||
|
@ -71,6 +78,146 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||||
return PathUtils.parse("");
|
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,
|
private void saveLauncherArgs(Map<String, ?> args,
|
||||||
Map<String, ParameterDescription<?>> params) {
|
Map<String, ParameterDescription<?>> params) {
|
||||||
SaveState state = new SaveState();
|
SaveState state = new SaveState();
|
||||||
|
@ -259,8 +406,8 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CompletableFuture<DebuggerObjectModel> connect(boolean prompt) {
|
protected CompletableFuture<DebuggerObjectModel> connect(DebuggerModelService service,
|
||||||
DebuggerModelService service = tool.getService(DebuggerModelService.class);
|
boolean prompt) {
|
||||||
DebuggerModelFactory factory = getModelFactory();
|
DebuggerModelFactory factory = getModelFactory();
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
return service.showConnectDialog(factory);
|
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
|
@Override
|
||||||
public CompletableFuture<Void> launchProgram(TaskMonitor monitor, boolean prompt) {
|
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");
|
monitor.setMessage("Connecting");
|
||||||
return connect(prompt).thenComposeAsync(m -> {
|
var locals = new Object() {
|
||||||
List<String> launcherPath = getLauncherPath();
|
CompletableFuture<TargetObject> futureTarget;
|
||||||
TargetObjectSchema schema = m.getRootSchema().getSuccessorSchema(launcherPath);
|
};
|
||||||
if (!schema.getInterfaces().contains(TargetLauncher.class)) {
|
return connect(service, prompt).thenComposeAsync(m -> {
|
||||||
throw new AssertionError("LaunchOffer / model implementation error: " +
|
monitor.incrementProgress(1);
|
||||||
"The given launcher path is not a TargetLauncher, according to its schema");
|
monitor.setMessage("Finding Launcher");
|
||||||
}
|
return findLauncher(m);
|
||||||
return new ValueExpecter(m, launcherPath);
|
|
||||||
}, SwingExecutorService.LATER).thenCompose(l -> {
|
}, SwingExecutorService.LATER).thenCompose(l -> {
|
||||||
monitor.incrementProgress(1);
|
monitor.incrementProgress(1);
|
||||||
monitor.setMessage("Launching");
|
monitor.setMessage("Launching");
|
||||||
TargetLauncher launcher = (TargetLauncher) l;
|
locals.futureTarget = listenForTarget(l.getModel());
|
||||||
Map<String, ?> args = getLauncherArgs(launcher.getParameters(), prompt);
|
return launch(l, prompt);
|
||||||
if (args == null) {
|
}).thenCompose(__ -> {
|
||||||
// Cancelled
|
|
||||||
return AsyncUtils.NIL;
|
|
||||||
}
|
|
||||||
return launcher.launch(args);
|
|
||||||
}).thenRun(() -> {
|
|
||||||
monitor.incrementProgress(1);
|
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.List;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
|
|
||||||
|
import ghidra.async.AsyncUtils;
|
||||||
import ghidra.framework.cmd.BackgroundCommand;
|
import ghidra.framework.cmd.BackgroundCommand;
|
||||||
import ghidra.framework.model.DomainObject;
|
import ghidra.framework.model.DomainObject;
|
||||||
import ghidra.framework.model.UndoableDomainObject;
|
import ghidra.framework.model.UndoableDomainObject;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
import ghidra.framework.plugintool.PluginTool;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
import ghidra.util.Swing;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.task.*;
|
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,
|
public static <T extends UndoableDomainObject> AsyncBackgroundCommand<T> async(PluginTool tool,
|
||||||
T obj, String name, boolean hasProgress, boolean canCancel, boolean isModal,
|
T obj, String name, boolean hasProgress, boolean canCancel, boolean isModal,
|
||||||
BiFunction<T, TaskMonitor, CompletableFuture<?>> futureProducer) {
|
BiFunction<T, TaskMonitor, CompletableFuture<?>> futureProducer) {
|
||||||
|
@ -84,6 +112,54 @@ public enum BackgroundUtils {
|
||||||
return cmd;
|
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 {
|
public static class PluginToolExecutorService extends AbstractExecutorService {
|
||||||
private final PluginTool tool;
|
private final PluginTool tool;
|
||||||
private String name;
|
private String name;
|
||||||
|
|
|
@ -453,7 +453,7 @@ public interface DebuggerStaticMappingService {
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Note, this method will first examine module and program names in order to cull unlikely
|
* 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
|
* paired program, then it is omitted from the result, i.e.., the returned map will have no
|
||||||
* {@code null} values.
|
* {@code null} values.
|
||||||
*
|
*
|
||||||
|
|
|
@ -15,15 +15,14 @@
|
||||||
*/
|
*/
|
||||||
package ghidra.async;
|
package ghidra.async;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.Timer;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import ghidra.util.Msg;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A timer for asynchronous scheduled tasks
|
* A timer for asynchronous scheduled tasks
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* This object provides a futures which complete at specified times. This is useful for pausing amid
|
* 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
|
* 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
|
* 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 Timer}, but its {@link Future}s are not {@link CompletableFuture}s. The same is true of
|
||||||
* {@link ScheduledThreadPoolExecutor}.
|
* {@link ScheduledThreadPoolExecutor}.
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* A delay is achieved using {@link #mark()}, then {@link #after(long)}. For example, within a
|
* A delay is achieved using {@link #mark()}, then {@link #after(long)}. For example, within a
|
||||||
* {@link AsyncUtils#sequence(TypeSpec)}:
|
* {@link AsyncUtils#sequence(TypeSpec)}:
|
||||||
*
|
*
|
||||||
* <pre>
|
* <pre>
|
||||||
* timer.mark().afterMark(1000).handle(seq::next);
|
* timer.mark().after(1000).handle(seq::next);
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* {@link #mark()} marks the current system time; all subsequent calls to {@link #after(long)}
|
* {@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
|
* 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
|
* undefined behavior. Scheduling a timed sequence of actions is best accomplished using times
|
||||||
|
@ -48,31 +49,33 @@ import ghidra.util.Msg;
|
||||||
*
|
*
|
||||||
* <pre>
|
* <pre>
|
||||||
* sequence(TypeSpec.VOID).then((seq) -> {
|
* sequence(TypeSpec.VOID).then((seq) -> {
|
||||||
* timer.mark().afterMark(1000).handle(seq::next);
|
* timer.mark().after(1000).handle(seq::next);
|
||||||
* }).then((seq) -> {
|
* }).then((seq) -> {
|
||||||
* doTaskAtOneSecond().handle(seq::next);
|
* doTaskAtOneSecond().handle(seq::next);
|
||||||
* }).then((seq) -> {
|
* }).then((seq) -> {
|
||||||
* timer.afterMark(2000).handle(seq::next);
|
* timer.after(2000).handle(seq::next);
|
||||||
* }).then((seq) -> {
|
* }).then((seq) -> {
|
||||||
* doTaskAtTwoSeconds().handle(seq::next);
|
* doTaskAtTwoSeconds().handle(seq::next);
|
||||||
* }).asCompletableFuture();
|
* }).asCompletableFuture();
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* This provides slightly more precise scheduling than delaying for a fixed period between tasks.
|
* This provides slightly more precise scheduling than delaying for a fixed period between tasks.
|
||||||
* Consider a second example:
|
* Consider a second example:
|
||||||
*
|
*
|
||||||
* <pre>
|
* <pre>
|
||||||
* sequence(TypeSpec.VOID).then((seq) -> {
|
* sequence(TypeSpec.VOID).then((seq) -> {
|
||||||
* timer.mark().afterMark(1000).handle(seq::next);
|
* timer.mark().after(1000).handle(seq::next);
|
||||||
* }).then((seq) -> {
|
* }).then((seq) -> {
|
||||||
* doTaskAtOneSecond().handle(seq::next);
|
* doTaskAtOneSecond().handle(seq::next);
|
||||||
* }).then((seq) -> {
|
* }).then((seq) -> {
|
||||||
* timer.mark().afterMark(1000).handle(seq::next);
|
* timer.mark().after(1000).handle(seq::next);
|
||||||
* }).then((seq) -> {
|
* }).then((seq) -> {
|
||||||
* doTaskAtTwoSeconds().handle(seq::next);
|
* doTaskAtTwoSeconds().handle(seq::next);
|
||||||
* }).asCompletableFuture();
|
* }).asCompletableFuture();
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* In the first example, {@code doTaskAtTwoSeconds} executes at 2000ms from the mark + some
|
* 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. In the second example, {@code doTaskAtTwoSeconds} executes at 1000ms + some
|
||||||
* scheduling overhead + the time to execute {@code doTaskAtOneSecond} + 1000ms + some more
|
* 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
|
* 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.
|
* 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
|
* 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
|
* {@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
|
* 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 class AsyncTimer {
|
||||||
public static final AsyncTimer DEFAULT_TIMER = new AsyncTimer();
|
public static final AsyncTimer DEFAULT_TIMER = new AsyncTimer();
|
||||||
|
|
||||||
protected Thread thread = new Thread(this::run);
|
protected ExecutorService thread = Executors.newSingleThreadExecutor();
|
||||||
protected SortedMap<Long, Set<TimerPromise>> promises = new TreeMap<>();
|
|
||||||
protected long nextWake = Long.MAX_VALUE;
|
|
||||||
protected boolean alive = true;
|
|
||||||
|
|
||||||
public class Mark {
|
public class Mark {
|
||||||
protected final long mark;
|
protected final long mark;
|
||||||
|
@ -118,86 +119,42 @@ public class AsyncTimer {
|
||||||
public CompletableFuture<Void> after(long intervalMillis) {
|
public CompletableFuture<Void> after(long intervalMillis) {
|
||||||
return atSystemTime(mark + intervalMillis);
|
return atSystemTime(mark + intervalMillis);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class TimerPromise extends CompletableFuture<Void> {
|
/**
|
||||||
private final long time;
|
* Time a future out after the given interval
|
||||||
|
*
|
||||||
TimerPromise(long time) {
|
* @param <T> the type of the future
|
||||||
this.time = time;
|
* @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
|
||||||
@Override
|
* @return a future which completes with the given futures value, or the late value if it
|
||||||
public boolean cancel(boolean mayInterruptIfRunning) {
|
* times out.
|
||||||
synchronized (AsyncTimer.this) {
|
*/
|
||||||
Set<TimerPromise> sameTime = promises.get(time);
|
public <T> CompletableFuture<T> timeOut(CompletableFuture<T> future, long millis,
|
||||||
if (sameTime != null) {
|
Supplier<T> valueIfLate) {
|
||||||
sameTime.remove(this);
|
return CompletableFuture.anyOf(future, after(millis)).thenApply(v -> {
|
||||||
if (sameTime.isEmpty()) {
|
if (future.isDone()) {
|
||||||
promises.remove(time);
|
return future.getNow(null);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return valueIfLate.get();
|
||||||
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.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new timer
|
* Create a new timer
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
* Except to reduce contention among threads, most applications need only create one timer
|
* Except to reduce contention among threads, most applications need only create one timer
|
||||||
* instance.
|
* instance. See {@link AsyncTimer#DEFAULT_TIMER}.
|
||||||
*/
|
*/
|
||||||
public AsyncTimer() {
|
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
|
* 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"
|
* 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
|
* 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
|
* 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
|
* @return a future that completes soon after the given time
|
||||||
*/
|
*/
|
||||||
public CompletableFuture<Void> atSystemTime(long timeMillis) {
|
public CompletableFuture<Void> atSystemTime(long timeMillis) {
|
||||||
if (timeMillis <= System.currentTimeMillis()) {
|
if (timeMillis - System.currentTimeMillis() <= 0) {
|
||||||
return AsyncUtils.NIL;
|
return AsyncUtils.NIL;
|
||||||
}
|
}
|
||||||
synchronized (this) {
|
|
||||||
Set<TimerPromise> sameTime =
|
long delta = timeMillis - System.currentTimeMillis();
|
||||||
promises.computeIfAbsent(timeMillis, (k) -> new HashSet<>());
|
Executor executor =
|
||||||
TimerPromise promise = new TimerPromise(timeMillis);
|
delta <= 0 ? thread : CompletableFuture.delayedExecutor(delta, TimeUnit.MILLISECONDS);
|
||||||
sameTime.add(promise);
|
return CompletableFuture.runAsync(() -> {
|
||||||
if (timeMillis < nextWake) {
|
}, executor);
|
||||||
nextWake = timeMillis; // In case it hasn't started waiting yet
|
|
||||||
notify();
|
|
||||||
}
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -437,7 +437,7 @@ public enum DebugModelConventions {
|
||||||
TargetExecutionStateful exe = (TargetExecutionStateful) process;
|
TargetExecutionStateful exe = (TargetExecutionStateful) process;
|
||||||
TargetExecutionState state = exe.getExecutionState();
|
TargetExecutionState state = exe.getExecutionState();
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
Msg.error(null, "null state for " + exe);
|
Msg.trace(null, "null state for " + exe);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return state.isAlive();
|
return state.isAlive();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue