GP-445: Relieving GDB agent of its need for new-ui.

This commit is contained in:
Dan 2020-12-21 17:05:58 -05:00
parent 18ef51669a
commit 77894a0268
10 changed files with 239 additions and 63 deletions

View file

@ -22,6 +22,7 @@ import java.util.List;
import agent.gdb.manager.GdbManager;
import ghidra.dbg.gadp.server.AbstractGadpLocalDebuggerModelFactory;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.dbg.util.ShellUtils;
import ghidra.util.classfinder.ExtensionPointProperties;
@FactoryDescription( //
@ -31,8 +32,12 @@ import ghidra.util.classfinder.ExtensionPointProperties;
@ExtensionPointProperties(priority = 100)
public class GdbLocalDebuggerModelFactory extends AbstractGadpLocalDebuggerModelFactory {
public static boolean checkGdbPresent(String gdbCmd) {
List<String> args = ShellUtils.parseArgs(gdbCmd);
if (args.isEmpty()) {
return false;
}
try {
ProcessBuilder builder = new ProcessBuilder(gdbCmd, "--version");
ProcessBuilder builder = new ProcessBuilder(args.get(0), "--version");
builder.redirectError(Redirect.INHERIT);
builder.redirectOutput(Redirect.INHERIT);
@SuppressWarnings("unused")
@ -92,13 +97,17 @@ public class GdbLocalDebuggerModelFactory extends AbstractGadpLocalDebuggerModel
@Override
protected void completeCommandLine(List<String> cmd) {
List<String> gdbCmdLine = ShellUtils.parseArgs(gdbCmd);
cmd.add(GdbGadpServer.class.getCanonicalName());
// TODO: Option for additional GDB command-line parameters
if (!existing && gdbCmdLine.size() >= 2) {
cmd.addAll(gdbCmdLine.subList(1, gdbCmdLine.size()));
}
cmd.add("--gadp-args");
cmd.addAll(List.of("-H", host));
cmd.addAll(List.of("-p", Integer.toString(port))); // Available ephemeral port
if (!existing) {
cmd.addAll(List.of("-g", gdbCmd));
if (!existing && gdbCmdLine.size() >= 1) {
cmd.add("-g");
cmd.add(gdbCmdLine.get(0));
}
else {
cmd.add("-x");

View file

@ -72,6 +72,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Just a vanilla demo of the manager
*
* <p>
* This presents the usual GDB CLI, using GDB/MI on the back end. The manager is keeps track of
* events; however, in this vanilla front end, nothing consumes them. This also provides a quick
* test to ensure the console loop operates correctly, or at least closely enough to actual GDB.
@ -100,8 +101,6 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
* @return the manager
*/
public static GdbManager newInstance() {
// TODO: Add parameter and test both?
// TODO: Eventually the 'true' variant will be deprecated
return new GdbManagerImpl();
}
@ -143,6 +142,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Execute a console loop in this thread
*
* <p>
* Note this does not follow the asynchronous pattern.
*
* @throws IOException if an I/O error occurs
@ -157,6 +157,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Check if GDB is alive
*
* <p>
* Note this is not about the state of inferiors in GDB. If the GDB controlling process is
* alive, GDB is alive.
*
@ -197,6 +198,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Add a listener for target output
*
* <p>
* Note: depending on the target, its output may not be communicated via this listener. Local
* targets, e.g., tend to just print output to GDB's controlling TTY. See
* {@link GdbInferior#setTty(String)} for a means to more reliably interact with a target's
@ -231,6 +233,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Get a thread by its GDB-assigned ID
*
* <p>
* GDB numbers its threads using a global counter. These IDs are unrelated to the OS-assigned
* TID. This method can retrieve a thread by its ID no matter which inferior it belongs to.
*
@ -242,6 +245,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Get an inferior by its GDB-assigned ID
*
* <p>
* GDB numbers inferiors incrementally. All inferiors and created and destroyed by the user. See
* {@link #addInferior()}.
*
@ -261,6 +265,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Get all inferiors known to the manager
*
* <p>
* This does not ask GDB to list its inferiors. Rather it returns a read-only view of the
* manager's understanding of the current inferiors based on its tracking of GDB events.
*
@ -271,6 +276,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Get all threads known to the manager
*
* <p>
* This does not ask GDB to lists its known threads. Rather it returns a read-only view of the
* manager's understanding of the current threads based on its tracking of GDB events.
*
@ -281,6 +287,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Get all breakpoints known to the manager
*
* <p>
* This does not ask GDB to list its breakpoints. Rather it returns a read-only view of the
* manager's understanding of the current breakpoints based on its tracking of GDB events.
*
@ -291,6 +298,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Send an interrupt to GDB regardless of other queued commands
*
* <p>
* This may be useful if the manager's command queue is stalled because an inferior is running.
*
* @throws IOException if an I/O error occurs
@ -301,6 +309,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Get the state of the GDB session
*
* <p>
* In all-stop mode, if any thread is running, GDB is said to be in the running state and is
* unable to process commands. Otherwise, if all threads are stopped, then GDB is said to be in
* the stopped state and can accept and process commands. This manager has not been tested in
@ -321,6 +330,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Wait for GDB to present a prompt
*
* <p>
* This waits for a prompt from GDB unless the last line printed is already a prompt. This is
* generally not necessary following normal commands, but may be necessary after interrupting a
* running inferior, or after waiting for an inferior to reach a stopped state.
@ -332,6 +342,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* A dummy command which claims as cause a stopped event and waits for the next prompt
*
* <p>
* This is used to squelch normal processing of a stopped event until the next prompt
*
* @return a future which completes when the "command" has finished execution
@ -341,6 +352,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Add an inferior
*
* <p>
* This is equivalent to the CLI command: {@code add-inferior}.
*
* @return a future which completes with the handle to the new inferior
@ -350,8 +362,10 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Remove an inferior
*
* <p>
* This is equivalent to the CLI command: {@code remove-inferior}.
*
* <p>
* Note that unlike the CLI, it is possible to remove the current inferior, in which case, the
* lowest-id inferior is selected. Like the CLI, it is not possible to remove an active inferior
* or the last inferior.
@ -364,6 +378,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Execute an arbitrary CLI command, printing output to the CLI console
*
* <p>
* Note: to ensure a certain thread or inferior has focus for a console command, see
* {@link GdbThread#console(String)} and {@link GdbInferior#console(String)}.
*
@ -375,6 +390,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Execute an arbitrary CLI command, capturing its console output
*
* <p>
* The output will not be printed to the CLI console. To ensure a certain thread or inferior has
* focus for a console command, see {@link GdbThread#consoleCapture(String)} and
* {@link GdbInferior#consoleCapture(String)}.
@ -387,10 +403,12 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Interrupt the GDB session
*
* <p>
* This is equivalent to typing Ctrl-C in the CLI. This typically results in the target being
* interrupted, either because GDB and the target have the same controlling TTY, or because GDB
* will "forward" the interrupt to the target.
*
* <p>
* For whatever reason, interrupting the session does not always reliably interrupt the target.
* The manager will send Ctrl-C to the pseudo-terminal up to three times, waiting about 10ms
* between each, until GDB issues a stopped event and presents a new prompt.
@ -402,6 +420,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* List GDB's inferiors
*
* <p>
* This is equivalent to the CLI command: {@code inferiors}.
*
* @return a future that completes with a map of inferior IDs to inferior handles
@ -411,6 +430,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* List information for all breakpoints
*
* <p>
* This is equivalent to the CLI command {@code info break}.
*
* @return a future that completes with a list of information for all breakpoints
@ -420,6 +440,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Disable the given breakpoints
*
* <p>
* This is equivalent to the CLI command {@code disable breakpoint [NUMBER]}.
*
* @param numbers the GDB-assigned breakpoint numbers
@ -430,6 +451,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Enable the given breakpoints
*
* <p>
* This is equivalent to the CLI command {@code enable breakpoint [NUMBER]}.
*
* @param numbers the GDB-assigned breakpoint numbers
@ -440,6 +462,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Delete a breakpoint
*
* <p>
* This is equivalent to the CLI command {@code delete breakpoint [NUMBER]}.
*
* @param numbers the GDB-assigned breakpoint numbers
@ -457,6 +480,7 @@ public interface GdbManager extends AutoCloseable, GdbBreakpointInsertions {
/**
* Gather information about the host OS
*
* <p>
* This is equivalent to the CLI command: {@code info os [TYPE]}.
*
* @param type the type of OS information to gather

View file

@ -27,7 +27,6 @@ import org.python.core.PyDictionary;
import org.python.util.InteractiveConsole;
import agent.gdb.ffi.linux.Pty;
import agent.gdb.ffi.linux.PtyMaster;
import agent.gdb.manager.*;
import agent.gdb.manager.GdbCause.Causes;
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
@ -87,6 +86,60 @@ public class GdbManagerImpl implements GdbManager {
public static final int INTERRUPT_MAX_RETRIES = 3;
public static final int INTERRUPT_RETRY_PERIOD_MILLIS = 100;
class PtyThread extends Thread {
final Pty pty;
final BufferedReader reader;
final Channel channel;
Interpreter interpreter;
PrintWriter writer;
CompletableFuture<Void> hasWriter;
PtyThread(Pty pty, Channel channel, Interpreter interpreter) {
this.pty = pty;
this.channel = channel;
this.reader =
new BufferedReader(new InputStreamReader(pty.getMaster().getInputStream()));
this.interpreter = interpreter;
hasWriter = new CompletableFuture<>();
}
@Override
public void run() {
try {
String line;
while (isAlive() && null != (line = reader.readLine())) {
String l = line;
if (interpreter == null) {
if (l.startsWith("=") || l.startsWith("~")) {
interpreter = Interpreter.MI2;
}
else {
interpreter = Interpreter.CLI;
}
}
if (writer == null) {
writer = new PrintWriter(pty.getMaster().getOutputStream());
hasWriter.complete(null);
}
//Msg.debug(this, channel + ": " + line);
submit(() -> {
if (LOG_IO) {
DBG_LOG.println("<" + interpreter + ": " + l);
DBG_LOG.flush();
}
processLine(l, channel, interpreter);
});
}
}
catch (Throwable e) {
terminate();
Msg.debug(this, channel + "," + interpreter + " reader exiting because " + e);
//throw new AssertionError(e);
}
}
}
private final AsyncReference<GdbState, GdbCause> state =
new AsyncReference<>(GdbState.NOT_STARTED);
// A copy of state, which is updated on the eventThread.
@ -98,15 +151,12 @@ public class GdbManagerImpl implements GdbManager {
private final HandlerMap<GdbEvent<?>, Void, Void> handlerMap = new HandlerMap<>();
private final AtomicBoolean exited = new AtomicBoolean(false);
private Pty cliPty;
private Pty mi2Pty;
private Process gdb;
private Thread gdbWaiter;
private Thread cliReader;
private Thread mi2Reader;
private PrintWriter cliWriter;
private PrintWriter mi2Writer;
private PtyThread iniThread;
private PtyThread cliThread;
private PtyThread mi2Thread;
private final AsyncLock cmdLock = new AsyncLock();
private final AtomicReference<AsyncLock.Hold> cmdLockHold = new AtomicReference<>(null);
@ -500,41 +550,50 @@ public class GdbManagerImpl implements GdbManager {
state.set(GdbState.STARTING, Causes.UNCLAIMED);
executor = Executors.newSingleThreadExecutor();
mi2Pty = Pty.openpty();
if (gdbCmd != null) {
cliPty = Pty.openpty();
iniThread = new PtyThread(Pty.openpty(), Channel.STDOUT, null);
gdb = iniThread.pty.getSlave().session(fullargs.toArray(new String[] {}), null);
iniThread.start();
try {
gdb = cliPty.getSlave().session(fullargs.toArray(new String[] {}), null);
iniThread.hasWriter.get(10, TimeUnit.SECONDS);
}
catch (IOException e) {
// TODO: Seems I should declare this, but it makes client code ugly :(
throw new RuntimeException(e);
catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException("Could not detect GDB's interpreter mode");
}
switch (iniThread.interpreter) {
case CLI:
cliThread = iniThread;
cliThread.setName("GDB Read CLI");
PtyMaster cliMaster = cliPty.getMaster();
cliReader = new Thread(
() -> readStream(cliMaster.getInputStream(), Channel.STDOUT, Interpreter.CLI),
"GDB Read CLI");
cliReader.start();
cliWriter = new PrintWriter(cliMaster.getOutputStream());
mi2Thread = new PtyThread(Pty.openpty(), Channel.STDOUT, Interpreter.MI2);
mi2Thread.setName("GDB Read MI2");
mi2Thread.start();
cliThread.writer.println("new-ui mi2 " + mi2Thread.pty.getSlave().getFile());
cliThread.writer.flush();
try {
mi2Thread.hasWriter.get(2, TimeUnit.SECONDS);
}
catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException(
"Could not obtain GDB/MI2 interpreter. Try " + gdbCmd + " -i mi2");
}
break;
case MI2:
mi2Thread = iniThread;
mi2Thread.setName("GDB Read MI2");
break;
}
gdbWaiter = new Thread(this::waitGdbExit, "GDB WaitExit");
gdbWaiter.start();
cliWriter.println("new-ui mi2 " + mi2Pty.getSlave().getFile());
cliWriter.flush();
}
else {
System.out.println(
"Agent is waiting for GDB/MI v2 interpreter at " + mi2Pty.getSlave().getFile());
mi2Thread = new PtyThread(Pty.openpty(), Channel.STDOUT, Interpreter.MI2);
mi2Thread.setName("GDB Read MI2");
Msg.info(this, "Agent is waiting for GDB/MI v2 interpreter at " +
mi2Thread.pty.getSlave().getFile());
mi2Thread.start();
}
PtyMaster mi2Master = mi2Pty.getMaster();
mi2Reader = new Thread(
() -> readStream(mi2Master.getInputStream(), Channel.STDOUT, Interpreter.MI2),
"GDB Read MI2");
mi2Reader.start();
mi2Writer = new PrintWriter(mi2Master.getOutputStream());
}
@Override
@ -597,22 +656,24 @@ public class GdbManagerImpl implements GdbManager {
checkStarted();
exited.set(true);
executor.shutdownNow();
if (cliPty != null) {
if (gdbWaiter != null) {
gdbWaiter.interrupt();
cliReader.interrupt();
}
mi2Reader.interrupt();
if (gdb != null) {
gdb.destroyForcibly();
}
try {
if (cliPty != null) {
cliPty.close();
if (cliThread != null) {
cliThread.interrupt();
cliThread.pty.close();
}
if (mi2Thread != null) {
mi2Thread.interrupt();
mi2Thread.pty.close();
}
mi2Pty.close();
}
catch (IOException e) {
throw new AssertionError(e);
Msg.error(this, "Problem closing PTYs to GDB.");
}
if (gdb != null) {
gdb.destroyForcibly();
}
cmdLock.dispose("GDB is terminating");
state.dispose("GDB is terminating");
@ -679,9 +740,9 @@ public class GdbManagerImpl implements GdbManager {
protected PrintWriter getWriter(Interpreter interpreter) {
switch (interpreter) {
case CLI:
return cliWriter;
return cliThread == null ? null : cliThread.writer;
case MI2:
return mi2Writer;
return mi2Thread == null ? null : mi2Thread.writer;
default:
throw new AssertionError();
}
@ -790,7 +851,7 @@ public class GdbManagerImpl implements GdbManager {
*/
protected void processStdOut(GdbConsoleOutputEvent evt, Void v) {
String out = evt.getOutput();
System.out.print(out);
//System.out.print(out);
if (!evt.isStolen()) {
listenersConsoleOutput.fire.output(Channel.STDOUT, out);
}
@ -820,7 +881,7 @@ public class GdbManagerImpl implements GdbManager {
*/
protected void processStdErr(GdbDebugOutputEvent evt, Void v) {
String out = evt.getOutput();
System.err.print(out);
//System.err.print(out);
if (!evt.isStolen()) {
listenersConsoleOutput.fire.output(Channel.STDERR, out);
}
@ -1379,14 +1440,16 @@ public class GdbManagerImpl implements GdbManager {
public void sendInterruptNow() throws IOException {
checkStarted();
Msg.info(this, "Interrupting");
if (hasCli()) {
OutputStream os = cliPty.getMaster().getOutputStream();
if (cliThread != null) {
OutputStream os = cliThread.pty.getMaster().getOutputStream();
os.write(3);
os.flush();
}
if (mi2Thread != null) {
OutputStream os = mi2Thread.pty.getMaster().getOutputStream();
os.write(3);
os.flush();
}
OutputStream os = mi2Pty.getMaster().getOutputStream();
os.write(3);
os.flush();
}
@Override
@ -1488,11 +1551,11 @@ public class GdbManagerImpl implements GdbManager {
@Override
public String getMi2PtyName() {
return mi2Pty.getSlave().getFile().getAbsolutePath();
return mi2Thread.pty.getSlave().getFile().getAbsolutePath();
}
public boolean hasCli() {
return cliWriter != null;
return cliThread != null && cliThread.pty != null;
}
public Interpreter getRunningInterpreter() {

View file

@ -23,6 +23,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import agent.gdb.manager.*;
import agent.gdb.manager.impl.cmd.GdbCommandError;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerModelClosedReason;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
import ghidra.dbg.error.DebuggerUserException;
import ghidra.dbg.target.TargetAccessConditioned.TargetAccessibility;
@ -74,6 +75,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
}
// TODO: Place make this a model method?
@Override
public AddressFactory getAddressFactory() {
return addressFactory;
}
@ -122,6 +124,7 @@ public class GdbModelImpl extends AbstractDebuggerObjectModel {
}
public void terminate() throws IOException {
listeners.fire.modelClosed(DebuggerModelClosedReason.NORMAL);
session.invalidateSubtree("GDB is terminating");
gdb.terminate();
}

View file

@ -20,7 +20,7 @@ import java.util.concurrent.CompletableFuture;
import agent.gdb.manager.GdbManager;
public class SpawnedGdbManagerTest extends AbstractGdbManagerTest {
public class SpawnedCliGdbManagerTest extends AbstractGdbManagerTest {
@Override
protected CompletableFuture<Void> startManager(GdbManager manager) {
try {

View file

@ -0,0 +1,34 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.manager.impl;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import agent.gdb.manager.GdbManager;
public class SpawnedMi2Gdb7Dot6Dot1ManagerTest extends AbstractGdbManagerTest {
@Override
protected CompletableFuture<Void> startManager(GdbManager manager) {
try {
manager.start("/opt/gdb-7.6.1/bin/gdb", "-i", "mi2");
return manager.runRC();
}
catch (IOException e) {
throw new AssertionError(e);
}
}
}

View file

@ -0,0 +1,34 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.manager.impl;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import agent.gdb.manager.GdbManager;
public class SpawnedMi2GdbManagerTest2 extends AbstractGdbManagerTest {
@Override
protected CompletableFuture<Void> startManager(GdbManager manager) {
try {
manager.start(GdbManager.DEFAULT_GDB_CMD, "-i", "mi2");
return manager.runRC();
}
catch (IOException e) {
throw new AssertionError(e);
}
}
}

View file

@ -20,14 +20,15 @@ import java.net.SocketAddress;
import java.nio.channels.AsynchronousSocketChannel;
import ghidra.comm.service.AbstractAsyncServer;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.*;
import ghidra.dbg.gadp.error.GadpErrorException;
import ghidra.dbg.gadp.protocol.Gadp;
import ghidra.dbg.gadp.protocol.Gadp.ErrorCode;
import ghidra.program.model.address.*;
public abstract class AbstractGadpServer
extends AbstractAsyncServer<AbstractGadpServer, GadpClientHandler> {
extends AbstractAsyncServer<AbstractGadpServer, GadpClientHandler>
implements DebuggerModelListener {
public static final String LISTENING_ON = "GADP Server listening on ";
protected final DebuggerObjectModel model;
@ -36,6 +37,8 @@ public abstract class AbstractGadpServer
super(addr);
this.model = model;
System.out.println(LISTENING_ON + getLocalAddress());
model.addModelListener(this);
}
public DebuggerObjectModel getModel() {
@ -63,4 +66,10 @@ public abstract class AbstractGadpServer
// Note, +1 accounted for in how Ghidra AddressRanges work (inclusive of end)
return new AddressRangeImpl(min, min.add(Integer.toUnsignedLong(range.getExtend())));
}
@Override
public void modelClosed(DebuggerModelClosedReason reason) {
System.err.println("Model closed: " + reason);
System.exit(0);
}
}

View file

@ -19,7 +19,7 @@ package ghidra.dbg;
* A reason given for a closed connection
*/
public interface DebuggerModelClosedReason {
DebuggerModelClosedReason NORMAL = DebuggerNormalModelClosedReason.INSTANCE;
DebuggerModelClosedReason NORMAL = DebuggerNormalModelClosedReason.NORMAL;
static DebuggerModelClosedReason normal() {
return NORMAL;

View file

@ -16,7 +16,7 @@
package ghidra.dbg;
enum DebuggerNormalModelClosedReason implements DebuggerModelClosedReason {
INSTANCE;
NORMAL;
@Override
public boolean hasException() {