dlangide/src/ddebug/gdb/gdbinterface.d

919 lines
26 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 std.utf;
import std.conv : to;
import std.array : empty;
import std.algorithm : startsWith, equal;
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);
});
}
}
import std.process;
class GDBInterface : ConsoleDebuggerInterface {
protected int commandId;
int sendCommand(string text) {
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) {
_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)
return;
Log.d("GDBInterface.stop()");
postRequest(delegate() {
execStop();
});
_stopRequested = true;
postRequest(delegate() {
});
_queue.close();
if (!_threadJoined) {
_threadJoined = true;
join();
}
}
/// 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");
}
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 void addBreakpoint(Breakpoint bp) {
GDBBreakpoint 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);
gdbbp.createRequestId = sendCommand(cmd.dup);
_breakpoints ~= gdbbp;
}
/// 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) {
addBreakpoint(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);
_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);
Log.v("GDB async *[", token, "] ", msgType, " params: ", s);
if (msgId == AsyncClass.running) {
_callback.onDebugState(DebuggingState.running, s, 0);
} else if (msgId == AsyncClass.stopped) {
_callback.onDebugState(DebuggingState.stopped, s, 0);
}
}
// +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) {
string msgType = parseIdentAndSkipComma(s);
ResultClass msgId = resultByName(msgType);
if (msgId == ResultClass.other)
Log.d("GDB WARN unknown result class type: ", msgType);
Log.v("GDB result ^[", token, "] ", msgType, " params: ", s);
}
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);
}
}
}
string parseIdent(ref string s) {
string res = null;
int len = 0;
for(; len < s.length; len++) {
char ch = s[len];
if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-'))
break;
}
if (len > 0) {
res = s[0..len];
s = s[len .. $];
}
return res;
}
bool skipComma(ref string s) {
if (s.length > 0 && s[0] == ',') {
s = s[1 .. $];
return true;
}
return false;
}
string parseIdentAndSkipComma(ref string s) {
string res = parseIdent(s);
skipComma(s);
return res;
}
ResultClass resultByName(string s) {
if (s.equal("done")) return ResultClass.done;
if (s.equal("running")) return ResultClass.running;
if (s.equal("connected")) return ResultClass.connected;
if (s.equal("error")) return ResultClass.error;
if (s.equal("exit")) return ResultClass.exit;
return ResultClass.other;
}
enum ResultClass {
done,
running,
connected,
error,
exit,
other
}
AsyncClass asyncByName(string s) {
if (s.equal("stopped")) return AsyncClass.stopped;
if (s.equal("running")) return AsyncClass.running;
if (s.equal("library-loaded")) return AsyncClass.library_loaded;
if (s.equal("library-unloaded")) return AsyncClass.library_unloaded;
if (s.equal("thread-group-added")) return AsyncClass.thread_group_added;
if (s.equal("thread-group-started")) return AsyncClass.thread_group_started;
if (s.equal("thread-group-exited")) return AsyncClass.thread_group_exited;
if (s.equal("thread-created")) return AsyncClass.thread_created;
if (s.equal("thread-exited")) return AsyncClass.thread_exited;
return AsyncClass.other;
}
enum AsyncClass {
running,
stopped,
library_loaded,
library_unloaded,
thread_group_added,
thread_group_started,
thread_group_exited,
thread_created,
thread_exited,
other
}
enum MITokenType {
/// end of line
eol,
/// error
error,
/// identifier
ident,
/// C string
str,
/// = sign
eq,
/// , sign
comma,
/// { brace
curlyOpen,
/// } brace
curlyClose,
/// [ brace
squareOpen,
/// ] brace
squareClose,
}
struct MIToken {
MITokenType type;
string str;
this(MITokenType type, string str = null) {
this.type = type;
this.str = str;
}
}
MIToken parseMIToken(ref string s) {
if (s.empty)
return MIToken(MITokenType.eol);
char ch = s[0];
if (ch == ',') {
s = s[1..$];
return MIToken(MITokenType.comma, ",");
}
if (ch == '=') {
s = s[1..$];
return MIToken(MITokenType.eq, "=");
}
if (ch == '{') {
s = s[1..$];
return MIToken(MITokenType.curlyOpen, "{");
}
if (ch == '}') {
s = s[1..$];
return MIToken(MITokenType.curlyClose, "}");
}
if (ch == '[') {
s = s[1..$];
return MIToken(MITokenType.squareOpen, "[");
}
if (ch == ']') {
s = s[1..$];
return MIToken(MITokenType.squareClose, "]");
}
// C string
if (ch == '\"') {
string str = parseCString(s);
if (!str.ptr) {
return MIToken(MITokenType.error);
}
return MIToken(MITokenType.str, str);
}
// identifier
string str = parseIdent(s);
if (!str.empty)
return MIToken(MITokenType.ident, str);
return MIToken(MITokenType.error);
}
/// tokenize GDB MI output into array of tokens
MIToken[] tokenizeMI(string s, out bool error) {
error = false;
string src = s;
MIToken[] res;
for(;;) {
MIToken token = parseMIToken(s);
if (token.type == MITokenType.eol)
break;
if (token.type == MITokenType.error) {
error = true;
Log.e("Error while tokenizing GDB output ", src, " near ", s);
break;
}
res ~= token;
}
return res;
}
MIValue parseMI(string s) {
string src = s;
try {
bool err = false;
MIToken[] tokens = tokenizeMI(s, err);
if (err) {
// tokenizer error
return null;
}
MIValue[] items = parseMIList(tokens);
return new MIList(items);
} catch (Exception e) {
Log.e("Cannot parse MI from " ~ src, e);
return null;
}
}
MIValue parseMIValue(ref MIToken[] tokens) {
if (tokens.length == 0)
return null;
MITokenType tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol;
MITokenType nextTokenType = tokens.length > 1 ? tokens[1].type : MITokenType.eol;
if (tokenType == MITokenType.ident) {
string ident = tokens[0].str;
if (nextTokenType == MITokenType.eol || nextTokenType == MITokenType.comma) {
MIValue res = new MIIdent(ident);
tokens = tokens[1..$];
return res;
} else if (nextTokenType == MITokenType.eq) {
tokens = tokens[1..$];
MIValue value = parseMIValue(tokens);
MIValue res = new MIKeyValue(ident, value);
return res;
}
throw new Exception("Unexpected token " ~ to!string(tokenType));
} else if (tokenType == MITokenType.str) {
string str = tokens[0].str;
tokens = tokens[1..$];
MIValue res = new MIString(str);
} else if (tokenType == MITokenType.curlyOpen) {
tokens = tokens[1..$];
MIValue[] list = parseMIList(tokens, MITokenType.curlyClose);
return new MIMap(list);
} else if (tokenType == MITokenType.squareOpen) {
tokens = tokens[1..$];
MIValue[] list = parseMIList(tokens, MITokenType.squareClose);
return new MIList(list);
}
throw new Exception("Invalid token at end of list: " ~ tokenType.to!string);
}
MIValue[] parseMIList(ref MIToken[] tokens, MITokenType closingToken = MITokenType.eol) {
MIValue[] res;
for (;;) {
MITokenType tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol;
if (tokenType == MITokenType.eol)
return res;
if (tokenType == closingToken) {
tokens = tokens[1..$];
return res;
}
MIValue value = parseMIValue(tokens);
res ~= value;
tokenType = tokens.length > 0 ? tokens[0].type : MITokenType.eol;
if (tokenType == MITokenType.comma) {
tokens = tokens[1..$];
continue;
}
throw new Exception("Unexpected token in list " ~ to!string(tokenType));
}
}
enum MIValueType {
/// ident
empty,
/// ident
ident,
/// c-string
str,
/// key=value pair
keyValue,
/// list []
list,
/// map {key=value, ...}
map,
}
class MIValue {
MIValueType type;
this(MIValueType type) {
this.type = type;
}
@property string str() { return null; }
@property int length() { return 1; }
MIValue opIndex(int index) { return null; }
MIValue opIndex(string key) { return null; }
}
class MIKeyValue : MIValue {
private string _key;
private MIValue _value;
this(string key, MIValue value) {
super(MIValueType.keyValue);
_key = key;
_value = value;
}
@property string key() { return _key; }
@property string str() { return _key; }
@property MIValue value() { return _value; }
}
class MIIdent : MIValue {
private string _ident;
this(string ident) {
super(MIValueType.ident);
_ident = ident;
}
override @property string str() { return _ident; }
}
class MIString : MIValue {
private string _str;
this(string str) {
super(MIValueType.str);
_str = str;
}
override @property string str() { return _str; }
}
class MIList : MIValue {
private MIValue[] _items;
private MIValue[string] _map;
override @property int length() { return cast(int)_items.length; }
override MIValue opIndex(int index) {
if (index < 0 || index >= _items.length)
return null;
return _items[index];
}
override MIValue opIndex(string key) {
if (key in _map) {
MIValue res = _map[key];
return res;
}
return null;
}
this(MIValue[] items) {
super(MIValueType.list);
_items = items;
// fill map
foreach(item; _items) {
if (item.type == MIValueType.keyValue) {
if (!item.str.empty)
_map[item.str] = (cast(MIKeyValue)item).value;
}
}
}
}
class MIMap : MIList {
this(MIValue[] items) {
super(items);
type = MIValueType.map;
}
}
private char nextChar(ref string s) {
if (s.empty)
return 0;
char ch = s[0];
s = s[1 .. $];
return ch;
}
string parseCString(ref string s) {
char[] res;
// skip opening "
char ch = nextChar(s);
if (!ch)
return null;
s = s[1 .. $];
if (ch != '\"')
return null;
for (;;) {
if (s.empty) {
// unexpected end of string
return null;
}
ch = nextChar(s);
if (ch == '\"')
break;
if (ch == '\\') {
// escape sequence
ch = nextChar(s);
if (ch >= '0' && ch <= '7') {
// octal
int number = (ch - '0');
char ch2 = nextChar(s);
char ch3 = nextChar(s);
if (ch2 < '0' || ch2 > '7')
return null;
if (ch3 < '0' || ch3 > '7')
return null;
number = number * 8 + (ch2 - '0');
number = number * 8 + (ch3 - '0');
if (number > 255)
return null; // invalid octal number
res ~= cast(char)number;
} else {
switch (ch) {
case 'n':
res ~= '\n';
break;
case 'r':
res ~= '\r';
break;
case 't':
res ~= '\t';
break;
case 'a':
res ~= '\a';
break;
case 'b':
res ~= '\b';
break;
case 'f':
res ~= '\f';
break;
case 'v':
res ~= '\v';
break;
case 'x': {
// 2 digit hex number
uint ch2 = decodeHexDigit(nextChar(s));
uint ch3 = decodeHexDigit(nextChar(s));
if (ch2 > 15 || ch3 > 15)
return null;
res ~= cast(char)((ch2 << 4) | ch3);
break;
}
default:
res ~= ch;
break;
}
}
} else {
res ~= ch;
}
}
if (!res.length)
return "";
return res.dup;
}
/// decodes hex digit (0..9, a..f, A..F), returns uint.max if invalid
private uint decodeHexDigit(T)(T ch) {
if (ch >= '0' && ch <= '9')
return ch - '0';
else if (ch >= 'a' && ch <= 'f')
return ch - 'a' + 10;
else if (ch >= 'A' && ch <= 'F')
return ch - 'A' + 10;
return uint.max;
}