From a238c4e86bc7bd2497cf6965aa804a00b0f04ac4 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Tue, 15 Dec 2015 16:26:41 +0300 Subject: [PATCH] breakpoint support with GDB --- src/ddebug/common/debugger.d | 22 +- src/ddebug/gdb/gdbinterface.d | 270 ++++++++++++++++++++- src/dlangide/ui/debuggerui.d | 7 + src/dlangide/workspace/workspacesettings.d | 4 + 4 files changed, 296 insertions(+), 7 deletions(-) diff --git a/src/ddebug/common/debugger.d b/src/ddebug/common/debugger.d index b2e211d..3f5a268 100644 --- a/src/ddebug/common/debugger.d +++ b/src/ddebug/common/debugger.d @@ -21,11 +21,22 @@ class Breakpoint { bool enabled = true; string projectName; this() { - id = nextBreakpointId++; + id = _nextBreakpointId++; + } + Breakpoint clone() { + Breakpoint v = new Breakpoint(); + id = v.id; + file = v.file; + fullFilePath = v.fullFilePath; + projectFilePath = v.projectFilePath; + line = v.line; + enabled = v.enabled; + projectName = v.projectName; + return v; } } -private static __gshared nextBreakpointId = 1; +static __gshared _nextBreakpointId = 1; interface Debugger : ProgramExecution { void setDebuggerCallback(DebuggerCallback callback); @@ -192,8 +203,11 @@ class DebuggerProxy : Debugger, DebuggerCallback { _debugger.postRequest(delegate() { _debugger.execStepOut(); }); } /// update list of breakpoints - void setBreakpoints(Breakpoint[] bp) { - _debugger.postRequest(delegate() { _debugger.setBreakpoints(bp); }); + void setBreakpoints(Breakpoint[] breakpoints) { + Breakpoint[] cloned; + foreach(bp; breakpoints) + cloned ~= bp.clone; + _debugger.postRequest(delegate() { _debugger.setBreakpoints(cloned); }); } } diff --git a/src/ddebug/gdb/gdbinterface.d b/src/ddebug/gdb/gdbinterface.d index e7940da..7b33002 100644 --- a/src/ddebug/gdb/gdbinterface.d +++ b/src/ddebug/gdb/gdbinterface.d @@ -79,7 +79,6 @@ class GDBInterface : ConsoleDebuggerInterface { protected int commandId; - int sendCommand(string text) { commandId++; string cmd = to!string(commandId) ~ text; @@ -280,9 +279,86 @@ class GDBInterface : ConsoleDebuggerInterface { _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[] bp) { - // TODO + 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); + } } @@ -315,6 +391,7 @@ class GDBInterface : ConsoleDebuggerInterface { if (msgId == AsyncClass.running) { _callback.onDebugState(DebuggingState.running, s, 0); } else if (msgId == AsyncClass.stopped) { + _callback.onDebugState(DebuggingState.stopped, s, 0); } } @@ -479,8 +556,195 @@ enum AsyncClass { } 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; +} + +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; +} + diff --git a/src/dlangide/ui/debuggerui.d b/src/dlangide/ui/debuggerui.d index b002ae1..4b53db7 100644 --- a/src/dlangide/ui/debuggerui.d +++ b/src/dlangide/ui/debuggerui.d @@ -36,6 +36,8 @@ class DebuggerUIHandler : DebuggerCallback { void onProgramLoaded(bool successful, bool debugInfoLoaded) { _ide.logPanel.logLine("Program is loaded"); // TODO: check succes status and debug info + if (_breakpoints.length) + _debugger.setBreakpoints(_breakpoints); _debugger.execStart(); } @@ -52,7 +54,12 @@ class DebuggerUIHandler : DebuggerCallback { } } + private Breakpoint[] _breakpoints; void onBreakpointListUpdated(Breakpoint[] breakpoints) { + _breakpoints = breakpoints; + if (_state == DebuggingState.running || _state == DebuggingState.paused) { + _debugger.setBreakpoints(_breakpoints); + } } void run() { diff --git a/src/dlangide/workspace/workspacesettings.d b/src/dlangide/workspace/workspacesettings.d index c8bd580..cba892d 100644 --- a/src/dlangide/workspace/workspacesettings.d +++ b/src/dlangide/workspace/workspacesettings.d @@ -151,6 +151,7 @@ class WorkspaceSettings : SettingsFile { _startupProjectName = _setting.getString("startupProject"); Setting obj = _setting.settingByPath("breakpoints", SettingType.ARRAY); _breakpoints = null; + int maxBreakpointId = 0; for (int i = 0; i < obj.length; i++) { Breakpoint bp = new Breakpoint(); Setting item = obj[i]; @@ -160,8 +161,11 @@ class WorkspaceSettings : SettingsFile { bp.projectFilePath = item.getString("projectFilePath"); bp.line = cast(int)item.getInteger("line"); bp.enabled = item.getBoolean("enabled"); + if (bp.id > maxBreakpointId) + maxBreakpointId = bp.id; _breakpoints ~= bp; } + _nextBreakpointId = maxBreakpointId + 1; obj = _setting.settingByPath("bookmarks", SettingType.ARRAY); _bookmarks = null; for (int i = 0; i < obj.length; i++) {