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; }