dlangide/src/ddebug/gdb/gdbinterface.d

698 lines
22 KiB
D

module ddebug.gdb.gdbinterface;
public import ddebug.common.debugger;
import ddebug.common.execution;
import dlangui.core.logger;
import ddebug.common.queue;
import dlangide.builders.extprocess;
import ddebug.gdb.gdbmiparser;
import std.utf;
import std.conv : to;
import std.array : empty;
import std.algorithm : startsWith, endsWith, equal;
import core.thread;
abstract class ConsoleDebuggerInterface : DebuggerBase, TextWriter {
protected ExternalProcess _debuggerProcess;
protected ExternalProcessState runDebuggerProcess(string executable, string[]args, string dir) {
_debuggerProcess = new ExternalProcess();
ExternalProcessState state = _debuggerProcess.run(executable, args, dir, this);
return state;
}
private string[] _stdoutLines;
private char[] _stdoutBuf;
/// return true to clear lines list
protected bool onDebuggerStdoutLines(string[] lines) {
foreach(line; lines) {
onDebuggerStdoutLine(line);
}
return true;
}
protected void onDebuggerStdoutLine(string line) {
}
private void onStdoutText(string text) {
_stdoutBuf ~= text;
// pass full lines
int startPos = 0;
bool fullLinesFound = false;
for (int i = 0; i < _stdoutBuf.length; i++) {
if (_stdoutBuf[i] == '\n' || _stdoutBuf[i] == '\r') {
if (i <= startPos)
_stdoutLines ~= "";
else
_stdoutLines ~= _stdoutBuf[startPos .. i].dup;
fullLinesFound = true;
if (i + 1 < _stdoutBuf.length) {
if ((_stdoutBuf[i] == '\n' && _stdoutBuf[i + 1] == '\r')
|| (_stdoutBuf[i] == '\r' && _stdoutBuf[i + 1] == '\n'))
i++;
}
startPos = i + 1;
}
}
if (fullLinesFound) {
for (int i = 0; i + startPos < _stdoutBuf.length; i++)
_stdoutBuf[i] = _stdoutBuf[i + startPos];
_stdoutBuf.length = _stdoutBuf.length - startPos;
if (onDebuggerStdoutLines(_stdoutLines)) {
_stdoutLines.length = 0;
}
}
}
bool sendLine(string text) {
return _debuggerProcess.write(text ~ "\n");
}
/// log lines
override void writeText(dstring text) {
string text8 = toUTF8(text);
postRequest(delegate() {
onStdoutText(text8);
});
}
}
interface TextCommandTarget {
/// send command as a text string
int sendCommand(string text);
}
import std.process;
class GDBInterface : ConsoleDebuggerInterface, TextCommandTarget {
this() {
_requests.setTarget(this);
}
protected int commandId;
int sendCommand(string text) {
ExternalProcessState state = _debuggerProcess.poll();
if (state != ExternalProcessState.Running) {
_stopRequested = true;
return 0;
}
commandId++;
string cmd = to!string(commandId) ~ text;
Log.d("GDB command[", commandId, "]> ", text);
sendLine(cmd);
return commandId;
}
Pid terminalPid;
string terminalTty;
string startTerminal() {
Log.d("Starting terminal ", _terminalExecutable);
import std.random;
import std.file;
import std.path;
import std.string;
import core.thread;
uint n = uniform(0, 0x10000000, rndGen());
terminalTty = null;
string termfile = buildPath(tempDir, format("dlangide-term-name-%07x.tmp", n));
Log.d("temp file for tty name: ", termfile);
try {
string[] args = [
_terminalExecutable,
"-title",
"DLangIDE External Console",
"-e",
"echo 'DlangIDE External Console' && tty > " ~ termfile ~ " && sleep 1000000"
];
Log.d("Terminal command line: ", args);
terminalPid = spawnProcess(args);
for (int i = 0; i < 40; i++) {
Thread.sleep(dur!"msecs"(100));
if (!isTerminalActive) {
Log.e("Failed to get terminal TTY");
return null;
}
if (exists(termfile)) {
Thread.sleep(dur!"msecs"(20));
break;
}
}
// read TTY from file
if (exists(termfile)) {
terminalTty = readText(termfile);
if (terminalTty.endsWith("\n"))
terminalTty = terminalTty[0 .. $-1];
// delete file
remove(termfile);
Log.d("Terminal tty: ", terminalTty);
}
} catch (Exception e) {
Log.e("Failed to start terminal ", e);
killTerminal();
}
if (terminalTty.length == 0) {
Log.i("Cannot start terminal");
killTerminal();
} else {
Log.i("Terminal: ", terminalTty);
}
return terminalTty;
}
bool isTerminalActive() {
if (_terminalExecutable.empty)
return true;
if (terminalPid is null)
return false;
auto res = tryWait(terminalPid);
if (res.terminated) {
Log.d("isTerminalActive: Terminal is stopped");
wait(terminalPid);
terminalPid = Pid.init;
return false;
} else {
return true;
}
}
void killTerminal() {
if (_terminalExecutable.empty)
return;
if (terminalPid is null)
return;
try {
Log.d("Trying to kill terminal");
kill(terminalPid, 9);
Log.d("Waiting for terminal process termination");
wait(terminalPid);
terminalPid = Pid.init;
Log.d("Killed");
} catch (Exception e) {
Log.d("Exception while killing terminal", e);
terminalPid = Pid.init;
}
}
override void startDebugging() {
Log.d("GDBInterface.startDebugging()");
string[] debuggerArgs;
if (!_terminalExecutable.empty) {
terminalTty = startTerminal();
if (terminalTty.length == 0) {
//_callback.onResponse(ResponseCode.CannotRunDebugger, "Cannot start terminal");
_status = ExecutionStatus.Error;
_stopRequested = true;
return;
}
debuggerArgs ~= "-tty";
debuggerArgs ~= terminalTty;
}
debuggerArgs ~= "--interpreter=mi";
debuggerArgs ~= "--silent";
debuggerArgs ~= "--args";
debuggerArgs ~= _executableFile;
foreach(arg; _executableArgs)
debuggerArgs ~= arg;
ExternalProcessState state = runDebuggerProcess(_debuggerExecutable, debuggerArgs, _executableWorkingDir);
Log.i("Debugger process state:");
if (state == ExternalProcessState.Running) {
Thread.sleep(dur!"seconds"(1));
_callback.onProgramLoaded(true, true);
//sendCommand("-break-insert main");
} else {
_status = ExecutionStatus.Error;
_stopRequested = true;
return;
}
}
override protected void onDebuggerThreadFinished() {
Log.d("Debugger thread finished");
if (_debuggerProcess !is null) {
Log.d("Killing debugger process");
_debuggerProcess.kill();
Log.d("Waiting for debugger process finishing");
//_debuggerProcess.wait();
}
killTerminal();
Log.d("Sending execution status");
_callback.onProgramExecutionStatus(this, _status, _exitCode);
}
bool _threadJoined = false;
override void stop() {
if (_stopRequested) {
Log.w("GDBInterface.stop() - _stopRequested flag already set");
return;
}
_stopRequested = true;
Log.d("GDBInterface.stop()");
postRequest(delegate() {
Log.d("GDBInterface.stop() processing in queue");
execStop();
});
Thread.sleep(dur!"msecs"(200));
postRequest(delegate() {
});
_queue.close();
if (!_threadJoined) {
_threadJoined = true;
if (_threadStarted) {
try {
join();
} catch (Exception e) {
Log.e("Exception while trying to join debugger thread");
}
}
}
}
/// start program execution, can be called after program is loaded
int _startRequestId;
void execStart() {
_startRequestId = sendCommand("-exec-run");
}
/// start program execution, can be called after program is loaded
int _continueRequestId;
void execContinue() {
_continueRequestId = sendCommand("-exec-continue");
}
/// stop program execution
int _stopRequestId;
void execStop() {
_continueRequestId = sendCommand("-gdb-exit");
}
/// interrupt execution
int _pauseRequestId;
void execPause() {
_pauseRequestId = sendCommand("-exec-interrupt");
}
/// step over
int _stepOverRequestId;
void execStepOver() {
_stepOverRequestId = sendCommand("-exec-next");
}
/// step in
int _stepInRequestId;
void execStepIn() {
_stepInRequestId = sendCommand("-exec-step");
}
/// step out
int _stepOutRequestId;
void execStepOut() {
_stepOutRequestId = sendCommand("-exec-finish");
}
/// restart
int _restartRequestId;
void execRestart() {
//_restartRequestId = sendCommand("-exec-restart");
}
private GDBBreakpoint[] _breakpoints;
private static class GDBBreakpoint {
Breakpoint bp;
string number;
int createRequestId;
}
private GDBBreakpoint findBreakpoint(Breakpoint bp) {
foreach(gdbbp; _breakpoints) {
if (gdbbp.bp.id == bp.id)
return gdbbp;
}
return null;
}
private GDBBreakpoint findBreakpointByNumber(string number) {
if (number.empty)
return null;
foreach(gdbbp; _breakpoints) {
if (gdbbp.number.equal(number))
return gdbbp;
}
return null;
}
class AddBreakpointRequest : GDBRequest {
GDBBreakpoint gdbbp;
this(Breakpoint bp) {
gdbbp = new GDBBreakpoint();
gdbbp.bp = bp;
char[] cmd;
cmd ~= "-break-insert ";
if (!bp.enabled)
cmd ~= "-d "; // create disabled
cmd ~= bp.fullFilePath;
cmd ~= ":";
cmd ~= to!string(bp.line);
command = cmd.dup;
_breakpoints ~= gdbbp;
}
override void onResult() {
if (MIValue bkpt = params["bkpt"]) {
string number = bkpt.getString("number");
gdbbp.number = number;
Log.d("GDB number for breakpoint " ~ gdbbp.bp.id.to!string ~ " assigned is " ~ number);
}
}
}
/// update list of breakpoints
void setBreakpoints(Breakpoint[] breakpoints) {
char[] breakpointsToDelete;
char[] breakpointsToEnable;
char[] breakpointsToDisable;
// checking for removed breakpoints
for (int i = cast(int)_breakpoints.length - 1; i >= 0; i--) {
bool found = false;
foreach(bp; breakpoints)
if (bp.id == _breakpoints[i].bp.id) {
found = true;
break;
}
if (!found) {
for (int j = i; j < _breakpoints.length - 1; j++)
_breakpoints[j] = _breakpoints[j + 1];
_breakpoints.length = _breakpoints.length - 1;
if (breakpointsToDelete.length)
breakpointsToDelete ~= ",";
breakpointsToDelete ~= _breakpoints[i].number;
}
}
// checking for added or updated breakpoints
foreach(bp; breakpoints) {
GDBBreakpoint existing = findBreakpoint(bp);
if (!existing) {
submitRequest(new AddBreakpointRequest(bp));
} else {
if (bp.enabled && !existing.bp.enabled) {
if (breakpointsToEnable.length)
breakpointsToEnable ~= ",";
breakpointsToEnable ~= existing.number;
existing.bp.enabled = true;
} else if (!bp.enabled && existing.bp.enabled) {
if (breakpointsToDisable.length)
breakpointsToDisable ~= ",";
breakpointsToDisable ~= existing.number;
existing.bp.enabled = false;
}
}
}
if (breakpointsToDelete.length) {
Log.d("Deleting breakpoints: " ~ breakpointsToDelete);
sendCommand(("-break-delete " ~ breakpointsToDelete).dup);
}
if (breakpointsToEnable.length) {
Log.d("Enabling breakpoints: " ~ breakpointsToEnable);
sendCommand(("-break-enable " ~ breakpointsToEnable).dup);
}
if (breakpointsToDisable.length) {
Log.d("Disabling breakpoints: " ~ breakpointsToDisable);
sendCommand(("-break-disable " ~ breakpointsToDisable).dup);
}
}
// ~message
void handleStreamLineCLI(string s) {
Log.d("GDB CLI: ", s);
if (s.length >= 2 && s.startsWith('\"') && s.endsWith('\"'))
s = parseCString(s);
_callback.onDebuggerMessage(s);
}
// @message
void handleStreamLineProgram(string s) {
Log.d("GDB program stream: ", s);
//_callback.onDebuggerMessage(s);
}
// &message
void handleStreamLineGDBDebug(string s) {
Log.d("GDB internal debug message: ", s);
}
// *stopped,reason="exited-normally"
// *running,thread-id="all"
// *asyncclass,result
void handleExecAsyncMessage(uint token, string s) {
string msgType = parseIdentAndSkipComma(s);
AsyncClass msgId = asyncByName(msgType);
if (msgId == AsyncClass.other)
Log.d("GDB WARN unknown async class type: ", msgType);
MIValue params = parseMI(s);
if (!params) {
Log.e("Failed to parse exec state params");
return;
}
Log.v("GDB async *[", token, "] ", msgType, " params: ", params.toString);
string reason = params.getString("reason");
if (msgId == AsyncClass.running) {
_callback.onDebugState(DebuggingState.running, StateChangeReason.unknown, null, null);
} else if (msgId == AsyncClass.stopped) {
StateChangeReason reasonId = StateChangeReason.unknown;
DebugFrame location = parseFrame(params["frame"]);
string threadId = params.getString("thread-id");
string stoppedThreads = params.getString("all");
Breakpoint bp = null;
if (reason.equal("end-stepping-range")) {
updateState();
_callback.onDebugState(DebuggingState.paused, StateChangeReason.endSteppingRange, location, bp);
} else if (reason.equal("breakpoint-hit")) {
if (GDBBreakpoint gdbbp = findBreakpointByNumber(params.getString("bkptno"))) {
bp = gdbbp.bp;
if (!location && bp) {
location = new DebugFrame();
location.fillMissingFields(bp);
}
}
updateState();
_callback.onDebugState(DebuggingState.paused, StateChangeReason.breakpointHit, location, bp);
} else {
_callback.onDebugState(DebuggingState.stopped, StateChangeReason.exited, null, null);
}
}
}
int _stackListLocalsRequest;
DebugThreadList _currentState;
void updateState() {
_currentState = null;
submitRequest(new ThreadInfoRequest(), new StackListFramesRequest());
}
// +asyncclass,result
void handleStatusAsyncMessage(uint token, string s) {
string msgType = parseIdentAndSkipComma(s);
AsyncClass msgId = asyncByName(msgType);
if (msgId == AsyncClass.other)
Log.d("GDB WARN unknown async class type: ", msgType);
Log.v("GDB async +[", token, "] ", msgType, " params: ", s);
}
// =asyncclass,result
void handleNotifyAsyncMessage(uint token, string s) {
string msgType = parseIdentAndSkipComma(s);
AsyncClass msgId = asyncByName(msgType);
if (msgId == AsyncClass.other)
Log.d("GDB WARN unknown async class type: ", msgType);
Log.v("GDB async =[", token, "] ", msgType, " params: ", s);
}
// ^resultClass,result
void handleResultMessage(uint token, string s) {
Log.v("GDB result ^[", token, "] ", s);
string msgType = parseIdentAndSkipComma(s);
ResultClass msgId = resultByName(msgType);
if (msgId == ResultClass.other)
Log.d("GDB WARN unknown result class type: ", msgType);
MIValue params = parseMI(s);
Log.v("GDB result ^[", token, "] ", msgType, " params: ", (params ? params.toString : "unparsed: " ~ s));
if (_requests.handleResult(token, msgId, params)) {
// handled using result list
} else {
Log.w("received results for unknown request");
}
}
GDBRequestList _requests;
/// submit single request or request chain
void submitRequest(GDBRequest[] requests ... ) {
for (int i = 0; i + 1 < requests.length; i++)
requests[i].chain(requests[i + 1]);
_requests.submit(requests[0]);
}
class ThreadInfoRequest : GDBRequest {
this() { command = "-thread-info"; }
override void onResult() {
_currentState = parseThreadList(params);
if (_currentState) {
// TODO
Log.d("Thread list is parsed");
}
}
}
class StackListFramesRequest : GDBRequest {
this() { command = "-stack-list-frames"; }
override void onResult() {
DebugStack stack = parseStack(params);
if (stack) {
// TODO
Log.d("Stack frames list is parsed: " ~ to!string(stack));
if (_currentState) {
if (DebugThread currentThread = _currentState.currentThread) {
currentThread.stack = stack;
Log.d("Setting stack frames for current thread");
}
submitRequest(new LocalVariableListRequest(_currentState.currentThreadId, 0));
}
}
}
}
class LocalVariableListRequest : GDBRequest {
this(ulong threadId, int frameId) {
command = "-stack-list-locals --thread " ~ to!string(threadId) ~ " --frame " ~ to!string(frameId) ~ " --simple-values";
}
override void onResult() {
DebugVariableList variables = parseVariableList(params);
if (variables) {
// TODO
Log.d("Variable list is parsed: " ~ to!string(variables));
if (_currentState) {
if (DebugThread currentThread = _currentState.currentThread) {
if (currentThread.length > 0) {
currentThread[0].locals = variables;
Log.d("Setting variables for current thread top frame");
}
}
}
}
}
}
bool _firstIdle = true;
// (gdb)
void onDebuggerIdle() {
Log.d("GDB idle");
if (_firstIdle) {
_firstIdle = false;
return;
}
}
override protected void onDebuggerStdoutLine(string gdbLine) {
//Log.d("GDB stdout: '", line, "'");
string line = gdbLine;
if (line.empty)
return;
// parse token (sequence of digits at the beginning of message)
uint tokenId = 0;
int tokenLen = 0;
while (tokenLen < line.length && line[tokenLen] >= '0' && line[tokenLen] <= '9')
tokenLen++;
if (tokenLen > 0) {
tokenId = to!uint(line[0..tokenLen]);
line = line[tokenLen .. $];
}
if (line.length == 0)
return; // token only, no message!
char firstChar = line[0];
string restLine = line.length > 1 ? line[1..$] : "";
if (firstChar == '~') {
handleStreamLineCLI(restLine);
return;
} else if (firstChar == '@') {
handleStreamLineProgram(restLine);
return;
} else if (firstChar == '&') {
handleStreamLineGDBDebug(restLine);
return;
} else if (firstChar == '*') {
handleExecAsyncMessage(tokenId, restLine);
return;
} else if (firstChar == '+') {
handleStatusAsyncMessage(tokenId, restLine);
return;
} else if (firstChar == '=') {
handleNotifyAsyncMessage(tokenId, restLine);
return;
} else if (firstChar == '^') {
handleResultMessage(tokenId, restLine);
return;
} else if (line.startsWith("(gdb)")) {
onDebuggerIdle();
return;
} else {
Log.d("GDB unprocessed: ", gdbLine);
}
}
}
class GDBRequest {
int id;
string command;
ResultClass resultClass;
MIValue params;
GDBRequest next;
/// called if resultClass is done
void onResult() {
}
/// called if resultClass is error
void onError() {
}
/// called on other result types
void onOtherResult() {
}
/// chain additional request, for case when previous finished ok
GDBRequest chain(GDBRequest next) {
this.next = next;
return this;
}
}
struct GDBRequestList {
private TextCommandTarget _target;
private GDBRequest[int] _activeRequests;
void setTarget(TextCommandTarget target) {
_target = target;
}
void submit(GDBRequest request) {
request.id = _target.sendCommand(request.command);
if (request.id)
_activeRequests[request.id] = request;
}
bool handleResult(int token, ResultClass resultClass, MIValue params) {
if (token in _activeRequests) {
GDBRequest r = _activeRequests[token];
_activeRequests.remove(token);
r.resultClass = resultClass;
r.params = params;
if (resultClass == ResultClass.done) {
r.onResult();
if (r.next)
submit(r.next);
} else if (resultClass == ResultClass.error) {
r.onError();
} else {
r.onOtherResult();
}
return true;
}
return false;
}
}