mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 19:42:36 +02:00
Merge remote-tracking branch 'origin/patch'
Conflicts: Ghidra/Debug/Framework-Debugging/build.gradle
This commit is contained in:
commit
91f94b8155
17 changed files with 195 additions and 70 deletions
|
@ -608,6 +608,8 @@ public class GdbManagerImpl implements GdbManager {
|
|||
|
||||
cliThread = iniThread;
|
||||
cliThread.setName("GDB Read CLI");
|
||||
// Looks terrible, but we're already in this world
|
||||
cliThread.writer.print("set confirm off" + newLine);
|
||||
cliThread.writer
|
||||
.print("new-ui mi2 " + mi2Pty.getChild().nullSession() + newLine);
|
||||
cliThread.writer.flush();
|
||||
|
@ -1538,19 +1540,34 @@ public class GdbManagerImpl implements GdbManager {
|
|||
}
|
||||
}
|
||||
|
||||
public void sendInterruptNow(PtyThread thread, byte[] bytes) throws IOException {
|
||||
Msg.info(this, "Interrupting by Ctrl-C on " + thread + "'s pty");
|
||||
OutputStream os = thread.pty.getParent().getOutputStream();
|
||||
os.write(bytes);
|
||||
os.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendInterruptNow() throws IOException {
|
||||
checkStarted();
|
||||
Msg.info(this, "Interrupting");
|
||||
/*Msg.info(this, "Interrupting while runningInterpreter = " + runningInterpreter);
|
||||
if (runningInterpreter == Interpreter.MI2) {
|
||||
if (cliThread != null) {
|
||||
Msg.info(this, "Interrupting by 'interrupt' on CLI");
|
||||
OutputStream os = cliThread.pty.getParent().getOutputStream();
|
||||
os.write(("interrupt" + newLine).getBytes());
|
||||
os.flush();
|
||||
}
|
||||
else {
|
||||
sendInterruptNow(mi2Thread);
|
||||
}
|
||||
}
|
||||
else*/
|
||||
if (cliThread != null) {
|
||||
OutputStream os = cliThread.pty.getParent().getOutputStream();
|
||||
os.write(3);
|
||||
os.flush();
|
||||
sendInterruptNow(cliThread, (((char) 3) + "interrupt" + newLine).getBytes());
|
||||
}
|
||||
else if (mi2Thread != null) {
|
||||
OutputStream os = mi2Thread.pty.getParent().getOutputStream();
|
||||
os.write(3);
|
||||
os.flush();
|
||||
sendInterruptNow(mi2Thread, new byte[] { 3 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,38 +18,39 @@ package agent.gdb.manager.impl.cmd;
|
|||
import agent.gdb.manager.GdbThread;
|
||||
import agent.gdb.manager.evt.*;
|
||||
import agent.gdb.manager.impl.*;
|
||||
import agent.gdb.manager.impl.GdbManagerImpl.Interpreter;
|
||||
|
||||
public abstract class AbstractLaunchGdbCommand extends AbstractGdbCommand<GdbThread> {
|
||||
public abstract class AbstractLaunchGdbCommand extends AbstractGdbCommand<GdbThread>
|
||||
implements MixinResumeInCliGdbCommand<GdbThread> {
|
||||
|
||||
protected AbstractLaunchGdbCommand(GdbManagerImpl manager) {
|
||||
super(manager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Interpreter getInterpreter() {
|
||||
//return getInterpreter(manager);
|
||||
|
||||
/**
|
||||
* A lot of good event-handling logic is factored in the Mixin interface. However, errors
|
||||
* from CLI commands are catastrophically mishandled or just missed entirely, so we will
|
||||
* still use MI2 for these.
|
||||
*/
|
||||
return Interpreter.MI2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(GdbEvent<?> evt, GdbPendingCommand<?> pending) {
|
||||
evt = checkErrorViaCli(evt);
|
||||
if (evt instanceof GdbCommandRunningEvent) {
|
||||
pending.claim(evt);
|
||||
return pending.hasAny(GdbRunningEvent.class);
|
||||
}
|
||||
else if (evt instanceof AbstractGdbCompletedCommandEvent) {
|
||||
pending.claim(evt);
|
||||
return true; // Not the expected Completed event
|
||||
}
|
||||
else if (evt instanceof GdbRunningEvent) {
|
||||
// Event happens no matter which interpreter received the command
|
||||
pending.claim(evt);
|
||||
return pending.hasAny(GdbCommandRunningEvent.class);
|
||||
}
|
||||
else if (evt instanceof GdbThreadCreatedEvent) {
|
||||
if (evt instanceof GdbThreadCreatedEvent) {
|
||||
pending.claim(evt);
|
||||
}
|
||||
return false;
|
||||
return handleExpectingRunning(evt, pending);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GdbThread complete(GdbPendingCommand<?> pending) {
|
||||
pending.checkCompletion(GdbCommandRunningEvent.class);
|
||||
completeOnRunning(pending);
|
||||
|
||||
// Just take the first thread. Others are considered clones.
|
||||
GdbThreadCreatedEvent created = pending.findFirstOf(GdbThreadCreatedEvent.class);
|
||||
|
|
|
@ -23,7 +23,7 @@ import agent.gdb.manager.impl.GdbManagerImpl.Interpreter;
|
|||
* Implementation of {@link GdbInferior#cont()}
|
||||
*/
|
||||
public class GdbContinueCommand extends AbstractGdbCommandWithThreadId<Void>
|
||||
implements MixinResumeInCliGdbCommand {
|
||||
implements MixinResumeInCliGdbCommand<Void> {
|
||||
public GdbContinueCommand(GdbManagerImpl manager, Integer threadId) {
|
||||
super(manager, threadId);
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ public class GdbContinueCommand extends AbstractGdbCommandWithThreadId<Void>
|
|||
|
||||
@Override
|
||||
public Void complete(GdbPendingCommand<?> pending) {
|
||||
return completeOnRunning(pending);
|
||||
completeOnRunning(pending);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ public class GdbInterruptCommand extends AbstractGdbCommand<Void> {
|
|||
public String encode() {
|
||||
Interpreter i = getInterpreter();
|
||||
if (i == manager.getRunningInterpreter()) {
|
||||
Msg.debug(this, "Using ^C to interrupt");
|
||||
Msg.debug(this, "Using ^C to interrupt via " + i);
|
||||
return "\u0003";
|
||||
}
|
||||
switch (i) {
|
||||
|
|
|
@ -28,13 +28,16 @@ public class GdbRunCommand extends AbstractLaunchGdbCommand {
|
|||
super(manager);
|
||||
}
|
||||
|
||||
@Override
|
||||
/*@Override
|
||||
public Interpreter getInterpreter() {
|
||||
return Interpreter.MI2;
|
||||
}
|
||||
}*/
|
||||
|
||||
@Override
|
||||
public String encode() {
|
||||
if (getInterpreter() == Interpreter.CLI) {
|
||||
return "run";
|
||||
}
|
||||
return "-exec-run";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,13 +28,16 @@ public class GdbStartCommand extends AbstractLaunchGdbCommand {
|
|||
super(manager);
|
||||
}
|
||||
|
||||
@Override
|
||||
/*@Override
|
||||
public Interpreter getInterpreter() {
|
||||
return Interpreter.MI2;
|
||||
}
|
||||
}*/
|
||||
|
||||
@Override
|
||||
public String encode() {
|
||||
if (getInterpreter() == Interpreter.CLI) {
|
||||
return "start";
|
||||
}
|
||||
return "-exec-run --start";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,13 +28,16 @@ public class GdbStartInstructionCommand extends AbstractLaunchGdbCommand {
|
|||
super(manager);
|
||||
}
|
||||
|
||||
@Override
|
||||
/*@Override
|
||||
public Interpreter getInterpreter() {
|
||||
return Interpreter.MI2;
|
||||
}
|
||||
}*/
|
||||
|
||||
@Override
|
||||
public String encode() {
|
||||
if (getInterpreter() == Interpreter.CLI) {
|
||||
return "starti";
|
||||
}
|
||||
return "-interpreter-exec console starti";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import agent.gdb.manager.impl.GdbManagerImpl.Interpreter;
|
|||
* Implementation of {@link GdbThread#stepInstruction()}
|
||||
*/
|
||||
public class GdbStepCommand extends AbstractGdbCommandWithThreadId<Void>
|
||||
implements MixinResumeInCliGdbCommand {
|
||||
implements MixinResumeInCliGdbCommand<Void> {
|
||||
protected final StepCmd cmd;
|
||||
|
||||
public GdbStepCommand(GdbManagerImpl manager, Integer threadId, StepCmd cmd) {
|
||||
|
@ -53,6 +53,7 @@ public class GdbStepCommand extends AbstractGdbCommandWithThreadId<Void>
|
|||
|
||||
@Override
|
||||
public Void complete(GdbPendingCommand<?> pending) {
|
||||
return completeOnRunning(pending);
|
||||
completeOnRunning(pending);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import agent.gdb.manager.impl.GdbManagerImpl.Interpreter;
|
|||
* "interrupt" from the primary (console) interpreter. So, for these resumes, I need to issue the
|
||||
* command from the console, allowing ^C to work.
|
||||
*/
|
||||
public interface MixinResumeInCliGdbCommand extends GdbCommand<Void> {
|
||||
public interface MixinResumeInCliGdbCommand<T> extends GdbCommand<T> {
|
||||
|
||||
default Interpreter getInterpreter(GdbManagerImpl manager) {
|
||||
if (manager.hasCli()) {
|
||||
|
@ -55,8 +55,7 @@ public interface MixinResumeInCliGdbCommand extends GdbCommand<Void> {
|
|||
return false;
|
||||
}
|
||||
|
||||
default Void completeOnRunning(GdbPendingCommand<?> pending) {
|
||||
default void completeOnRunning(GdbPendingCommand<?> pending) {
|
||||
pending.checkCompletion(GdbCommandRunningEvent.class);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ package agent.gdb.model.impl;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import agent.gdb.manager.GdbInferior;
|
||||
import agent.gdb.manager.GdbThread;
|
||||
|
@ -24,14 +25,29 @@ import ghidra.dbg.util.ShellUtils;
|
|||
|
||||
public enum GdbModelImplUtils {
|
||||
;
|
||||
public static CompletableFuture<GdbThread> launch(GdbModelImpl impl, GdbInferior inferior,
|
||||
List<String> args, boolean useStarti) {
|
||||
|
||||
/**
|
||||
* Perform the file-args-start sequence to launch a target in an inferior
|
||||
*
|
||||
* @param inferior the inferior to assign the target
|
||||
* @param args the command-line arguments, including the executable at args[0]
|
||||
* @param useStarti true to use {@code starti}, false to use {@code start}
|
||||
* @param postFile a caller-specified routine to execute after the {@code file} part of the
|
||||
* sequence. This is useful to grab auto-derived environment information
|
||||
* @return a future which completes with the initial thread of the target
|
||||
*/
|
||||
public static CompletableFuture<GdbThread> launch(GdbInferior inferior,
|
||||
List<String> args, boolean useStarti, Supplier<CompletableFuture<Void>> postFile) {
|
||||
// Queue all these up to avoid other commands getting between.
|
||||
CompletableFuture<Void> feas = inferior.fileExecAndSymbols(args.get(0));
|
||||
CompletableFuture<Void> post = postFile.get();
|
||||
CompletableFuture<Void> sargs =
|
||||
inferior.setVar("args", ShellUtils.generateLine(args.subList(1, args.size())));
|
||||
CompletableFuture<GdbThread> start = useStarti ? inferior.starti() : inferior.start();
|
||||
return feas.thenCombine(sargs, (v1, v2) -> v2).thenCombine(start, (v, t) -> t);
|
||||
return feas
|
||||
.thenCombine(post, (v, t) -> t)
|
||||
.thenCombine(sargs, (v, t) -> t)
|
||||
.thenCombine(start, (v, t) -> t);
|
||||
}
|
||||
|
||||
public static <V> V noDupMerge(V first, V second) {
|
||||
|
|
|
@ -175,7 +175,9 @@ public class GdbModelTargetInferior
|
|||
CmdLineParser.tokenize(TargetCmdLineLauncher.PARAMETER_CMDLINE_ARGS.get(args));
|
||||
Boolean useStarti = PARAMETER_STARTI.get(args);
|
||||
return impl.gateFuture(
|
||||
GdbModelImplUtils.launch(impl, inferior, cmdLineArgs, useStarti).thenApply(__ -> null));
|
||||
GdbModelImplUtils.launch(inferior, cmdLineArgs, useStarti, () -> {
|
||||
return environment.refreshInternal();
|
||||
}).thenApply(__ -> null));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -249,8 +251,16 @@ public class GdbModelTargetInferior
|
|||
parent.getListeners().fire.event(parent, null, TargetEventType.PROCESS_CREATED,
|
||||
"Inferior " + inferior.getId() + " started " + inferior.getExecutable() + " pid=" + pid,
|
||||
List.of(this));
|
||||
/*System.err.println("inferiorStarted: realState = " + realState);
|
||||
changeAttributes(List.of(), Map.ofEntries(
|
||||
// This is hacky, but =inferior-started comes before ^running.
|
||||
// Is it ever not followed by ^running, except on failure?
|
||||
Map.entry(STATE_ATTRIBUTE_NAME, state = TargetExecutionState.RUNNING),
|
||||
Map.entry(PID_ATTRIBUTE_NAME, pid),
|
||||
Map.entry(DISPLAY_ATTRIBUTE_NAME, updateDisplay())),
|
||||
"Refresh on started");*/
|
||||
AsyncFence fence = new AsyncFence();
|
||||
fence.include(modules.refreshInternal());
|
||||
//fence.include(modules.refreshInternal());
|
||||
//fence.include(registers.refreshInternal());
|
||||
fence.include(environment.refreshInternal());
|
||||
fence.include(impl.gdb.listInferiors()); // HACK to update inferior.getExecutable()
|
||||
|
@ -266,14 +276,14 @@ public class GdbModelTargetInferior
|
|||
changeAttributes(List.of(), Map.ofEntries(
|
||||
Map.entry(STATE_ATTRIBUTE_NAME, state = realState),
|
||||
Map.entry(DISPLAY_ATTRIBUTE_NAME, updateDisplay())),
|
||||
"Refresh on started");
|
||||
"Refresh on initial break");
|
||||
}
|
||||
else {
|
||||
changeAttributes(List.of(), Map.ofEntries(
|
||||
Map.entry(STATE_ATTRIBUTE_NAME, state = realState),
|
||||
Map.entry(PID_ATTRIBUTE_NAME, p),
|
||||
Map.entry(DISPLAY_ATTRIBUTE_NAME, updateDisplay())),
|
||||
"Refresh on started");
|
||||
"Refresh on initial break");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -442,7 +452,8 @@ public class GdbModelTargetInferior
|
|||
List<Object> params = new ArrayList<>();
|
||||
gatherThreads(params, sco.getAffectedThreads());
|
||||
if (targetEventThread == null && !params.isEmpty()) {
|
||||
targetEventThread = threads.getTargetThread(sco.getAffectedThreads().iterator().next());
|
||||
targetEventThread =
|
||||
threads.getTargetThread(sco.getAffectedThreads().iterator().next());
|
||||
}
|
||||
if (targetEventThread != null) {
|
||||
impl.session.getListeners().fire.event(impl.session, targetEventThread,
|
||||
|
|
|
@ -107,7 +107,7 @@ public class GdbModelTargetProcessMemory
|
|||
return AsyncUtils.NIL;
|
||||
}
|
||||
return inferior.listMappings().exceptionally(ex -> {
|
||||
Msg.error(this, "Could not list regions", ex);
|
||||
Msg.error(this, "Could not list regions. Using default.");
|
||||
return Map.of(); // empty map will be replaced with default
|
||||
}).thenAccept(this::updateUsingMappings);
|
||||
}
|
||||
|
|
|
@ -209,7 +209,9 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot
|
|||
CmdLineParser.tokenize(TargetCmdLineLauncher.PARAMETER_CMDLINE_ARGS.get(args));
|
||||
Boolean useStarti = GdbModelTargetInferior.PARAMETER_STARTI.get(args);
|
||||
return impl.gateFuture(impl.gdb.availableInferior().thenCompose(inf -> {
|
||||
return GdbModelImplUtils.launch(impl, inf, cmdLineArgs, useStarti);
|
||||
return GdbModelImplUtils.launch(inf, cmdLineArgs, useStarti, () -> {
|
||||
return inferiors.getTargetInferior(inf).environment.refreshInternal();
|
||||
});
|
||||
}).thenApply(__ -> null));
|
||||
}
|
||||
|
||||
|
@ -231,7 +233,7 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot
|
|||
//return impl.gdb.interrupt();
|
||||
try {
|
||||
impl.gdb.sendInterruptNow();
|
||||
impl.gdb.cancelCurrentCommand();
|
||||
//impl.gdb.cancelCurrentCommand();
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.error(this, "Could not interrupt", e);
|
||||
|
|
|
@ -18,8 +18,11 @@ package agent.gdb.model;
|
|||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import agent.gdb.model.impl.GdbModelTargetInferior;
|
||||
import ghidra.dbg.target.TargetEnvironment;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
||||
import ghidra.dbg.test.AbstractDebuggerModelLauncherTest;
|
||||
|
||||
|
@ -43,4 +46,50 @@ public abstract class AbstractModelForGdbLauncherTest extends AbstractDebuggerMo
|
|||
assertEquals("little", environment.getEndian());
|
||||
assertTrue(environment.getDebugger().toLowerCase().contains("gdb"));
|
||||
}
|
||||
|
||||
protected DebuggerTestSpecimen getLaunchStrippedSpecimen() {
|
||||
return GdbLinuxSpecimen.SPIN_STRIPPED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a target which runs indefinitely, and for which GDB cannot get the temporary breakpoint
|
||||
* on main.
|
||||
*/
|
||||
@Test
|
||||
@Ignore
|
||||
public void testLaunchStrippedThenInterrupt() throws Throwable {
|
||||
m.build();
|
||||
|
||||
ProcessCreatedDebugModelListener listener = new ProcessCreatedDebugModelListener();
|
||||
// NB. I've intentionally omitted the reorderer here. The model should get it right.
|
||||
m.getModel().addModelListener(listener);
|
||||
|
||||
DebuggerTestSpecimen specimen = getLaunchStrippedSpecimen();
|
||||
TargetLauncher launcher = findLauncher();
|
||||
waitAcc(launcher);
|
||||
waitOn(launcher.launch(specimen.getLauncherArgs()));
|
||||
//System.err.println("Launched");
|
||||
|
||||
/**
|
||||
* For the moment, we're stuck having to wait for the initial break in GDB before we
|
||||
* announce that the target is RUNNING, because we depend on the environment being correct
|
||||
* *before* that announcement. For launch, we can resolve the issue, because we can refresh
|
||||
* the environment between "file" and "start". However, for attach, we're still hosed. I
|
||||
* don't care to try to distinguish the two cases, because that's a lot of work, and still
|
||||
* only a partial fix.
|
||||
*
|
||||
* Thus, we will not observe state=RUNNING until after we successfully interrupt the target.
|
||||
* This test still suffices to address the interrupt problem, but I don't know any way to
|
||||
* fix the state reporting problem until we fix the record-depends-on-language-mapping
|
||||
* issue, which is still some time away.
|
||||
*/
|
||||
TargetInterruptible interruptible =
|
||||
m.suitable(TargetInterruptible.class, launcher.getPath());
|
||||
Thread.sleep(1000); // HACK
|
||||
waitOn(interruptible.interrupt());
|
||||
//System.err.println("Interrupted");
|
||||
|
||||
waitOn(listener.observedCreated);
|
||||
//System.err.println("Observed");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,12 @@ public enum GdbLinuxSpecimen implements DebuggerTestSpecimen, DebuggerModelTestU
|
|||
return DummyProc.which("expRegisters");
|
||||
}
|
||||
},
|
||||
SPIN_STRIPPED {
|
||||
@Override
|
||||
String getCommandLine() {
|
||||
return DummyProc.which("expSpin.stripped");
|
||||
}
|
||||
},
|
||||
STACK {
|
||||
@Override
|
||||
String getCommandLine() {
|
||||
|
|
|
@ -54,6 +54,13 @@ task testSpecimenLinux_x86_64 {
|
|||
//dependsOn 'expTypesExecutable'//Linux_x86_64Executable'
|
||||
dependsOn 'expRegistersLinux_x86_64Executable'
|
||||
dependsOn 'expStackLinux_x86_64Executable'
|
||||
|
||||
doLast {
|
||||
exec {
|
||||
workingDir "build/os/linux_x86_64"
|
||||
commandLine "strip", "-o", "expSpin.stripped", "expSpin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: testSpecimenMac_x86_64 (Intel)
|
||||
|
|
|
@ -35,6 +35,31 @@ import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
|||
public abstract class AbstractDebuggerModelLauncherTest extends AbstractDebuggerModelTest
|
||||
implements RequiresLaunchSpecimen {
|
||||
|
||||
protected class ProcessCreatedDebugModelListener extends AnnotatedDebuggerAttributeListener {
|
||||
public final CompletableFuture<Void> observedCreated = new CompletableFuture<>();
|
||||
|
||||
public ProcessCreatedDebugModelListener() {
|
||||
super(MethodHandles.lookup());
|
||||
}
|
||||
|
||||
@AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)
|
||||
public void stateChanged(TargetObject object, TargetExecutionState state) {
|
||||
// We're only expecting one process, so this should be fine
|
||||
TargetProcess process = DebugModelConventions.liveProcessOrNull(object);
|
||||
if (process == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
TargetEnvironment env = findEnvironment(process.getPath());
|
||||
assertEnvironment(env);
|
||||
observedCreated.complete(null);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
observedCreated.completeExceptionally(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getExpectedLauncherPath() {
|
||||
return null;
|
||||
}
|
||||
|
@ -80,26 +105,7 @@ public abstract class AbstractDebuggerModelLauncherTest extends AbstractDebugger
|
|||
public void testLaunch() throws Throwable {
|
||||
m.build();
|
||||
|
||||
var listener = new AnnotatedDebuggerAttributeListener(MethodHandles.lookup()) {
|
||||
CompletableFuture<Void> observedCreated = new CompletableFuture<>();
|
||||
|
||||
@AttributeCallback(TargetExecutionStateful.STATE_ATTRIBUTE_NAME)
|
||||
public void stateChanged(TargetObject object, TargetExecutionState state) {
|
||||
// We're only expecting one process, so this should be fine
|
||||
TargetProcess process = DebugModelConventions.liveProcessOrNull(object);
|
||||
if (process == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
TargetEnvironment env = findEnvironment(process.getPath());
|
||||
assertEnvironment(env);
|
||||
observedCreated.complete(null);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
observedCreated.completeExceptionally(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
ProcessCreatedDebugModelListener listener = new ProcessCreatedDebugModelListener();
|
||||
// NB. I've intentionally omitted the reorderer here. The model should get it right.
|
||||
m.getModel().addModelListener(listener);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue