From d62f189aa0109c5bba64353a152e7e123726c08f Mon Sep 17 00:00:00 2001 From: Neil Lindquist Date: Tue, 19 Jun 2018 21:26:04 -0500 Subject: [PATCH 1/4] Add support for backtraces The argument to the debug_setup handler now has some stack trace * SWANK sends the top n stack frames (20 by default) Created a function to fetch the full stack trace --- lib/client.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index e2be3a3..4e82739 100644 --- a/lib/client.js +++ b/lib/client.js @@ -307,7 +307,13 @@ Client.prototype.debug_setup_handler = function(sexp) { description: restart_sexp.children[1].source.slice(1, -1) }); }); -// TODO: stack trace + obj.stack_frames = []; + sexp.children[5].children.forEach(function(frame_sexp){ + obj.stack_frames.push({ + frame_number: frame_sexp.children[0].source, + description: frame_sexp.children[1].source.slice(1, -1) + }); + }); this.on_handlers.debug_setup(obj); } @@ -335,6 +341,22 @@ Client.prototype.debug_escape_all = function() { return this.rex(cmd, "COMMON-LISP-USER", "1"); //# TODO - is "1" ok here? } +/* Get the entire stack trace + Returns a promise of the list of objects, each containing a stack frame*/ +Client.prototype.debug_get_stack_trace = function(thread) { + var cmd = '(SWANK:BACKTRACE 0 NIL)'; + return this.rex(cmd, "COMMON-LISP-USER", thread).then(function(sexp) { + stack_frames = []; + sexp.children.forEach(function(frame_sexp){ + stack_frames.push({ + frame_number: frame_sexp.children[0].source, + description: frame_sexp.children[1].source.slice(1, -1) + }); + }); + return stack_frames; + }); +} + /************* * Profiling * *************/ From adfa38dfb216f633e96e8b539047368fcc80f6af Mon Sep 17 00:00:00 2001 From: Neil Lindquist Date: Sun, 24 Jun 2018 20:51:33 -0500 Subject: [PATCH 2/4] Improve parsing of strings send from swank --- lib/client.js | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/client.js b/lib/client.js index 4e82739..7af53b8 100644 --- a/lib/client.js +++ b/lib/client.js @@ -31,6 +31,16 @@ function Client(host, port) { } +/***************************************************************** + General Utilities + *****************************************************************/ +/* Reads a string value from a raw lisp source */ +function readString(raw){ + return raw.slice(1, -1) //remove leading/trailing quotes + .replace(/\\(\"|\\)/g, "$1"); //resolve escapes +} + + /***************************************************************** Low-level data handling protocol @@ -133,7 +143,7 @@ Client.prototype.on_swank_message = function(msg) { if (cmd == ":return") { this.swank_message_rex_return_handler(sexp); } else if (cmd == ':write-string') { - this.on_handlers.print_string(sexp.children[1].source.slice(1,-1).replace(/\\\\/g, "\\")); + this.on_handlers.print_string(readString(sexp.children[1].source)); } else if (cmd == ':presentation-start') { var presentation_id = sexp.children[1].source; this.on_handlers.presentation_start(presentation_id); @@ -142,7 +152,7 @@ Client.prototype.on_swank_message = function(msg) { // console.log(presentation_id); this.on_handlers.presentation_end(presentation_id); } else if (cmd == ":new-package") { - this.on_handlers.new_package(sexp.children[1].source.slice(1, -1).replace(/\\\\/g, "\\")); + this.on_handlers.new_package(readString(sexp.children[1].source)); } else if (cmd == ":debug") { this.debug_setup_handler(sexp); } else if (cmd == ":debug-activate") { @@ -279,7 +289,7 @@ Client.prototype.autocomplete = function(prefix, pkg) { .then(function (ast) { try { return ast.children[0].children.map(function(competion) { - return competion.source.slice(1, -1); + return readString(competion.source); }); } catch (e) { return []; @@ -298,20 +308,20 @@ Client.prototype.debug_setup_handler = function(sexp) { var obj = {}; obj.thread = sexp.children[1].source; obj.level = sexp.children[2].source; - obj.title = sexp.children[3].children[0].source.slice(1, -1); - obj.type = sexp.children[3].children[1].source.slice(1, -1); + obj.title = readString(sexp.children[3].children[0].source); + obj.type = readString(sexp.children[3].children[1].source); obj.restarts = []; sexp.children[4].children.forEach(function(restart_sexp) { obj.restarts.push({ - cmd: restart_sexp.children[0].source.slice(1, -1), - description: restart_sexp.children[1].source.slice(1, -1) + cmd: readString(restart_sexp.children[0].source), + description: readString(restart_sexp.children[1].source) }); }); obj.stack_frames = []; sexp.children[5].children.forEach(function(frame_sexp){ obj.stack_frames.push({ frame_number: frame_sexp.children[0].source, - description: frame_sexp.children[1].source.slice(1, -1) + description: readString(frame_sexp.children[1].source) }); }); @@ -350,7 +360,7 @@ Client.prototype.debug_get_stack_trace = function(thread) { sexp.children.forEach(function(frame_sexp){ stack_frames.push({ frame_number: frame_sexp.children[0].source, - description: frame_sexp.children[1].source.slice(1, -1) + description: readString(frame_sexp.children[1].source) }); }); return stack_frames; @@ -413,14 +423,14 @@ Client.prototype.find_definitions = function(fn, pkg) { // Extract the filename depending on the response var filename = null; if (ast.children[i].children[1].children[1].children[0].source.toLowerCase() == ":file") { - filename = ast.children[i].children[1].children[1].children[1].source.slice(1, -1); + filename = readString(ast.children[i].children[1].children[1].children[1].source); } else if (ast.children[i].children[1].children[1].children[0].source.toLowerCase() == ":buffer-and-file") { - filename = ast.children[i].children[1].children[1].children[2].source.slice(1, -1); + filename = readString(ast.children[i].children[1].children[1].children[2].source); } // Push an appropriate reference! refs.push({ - label: ast.children[i].children[0].source.slice(1, -1), + label: readString(ast.children[i].children[0].source), filename: filename, index: parseInt(ast.children[i].children[1].children[2].children[1].source) }); From 944c3d820ac933a073352b2fb65390e3c41d8617 Mon Sep 17 00:00:00 2001 From: Neil Lindquist Date: Tue, 19 Jun 2018 21:26:04 -0500 Subject: [PATCH 3/4] Add functions for debugging inspection --- lib/client.js | 199 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 197 insertions(+), 2 deletions(-) diff --git a/lib/client.js b/lib/client.js index 7af53b8..0f420d2 100644 --- a/lib/client.js +++ b/lib/client.js @@ -319,9 +319,15 @@ Client.prototype.debug_setup_handler = function(sexp) { }); obj.stack_frames = []; sexp.children[5].children.forEach(function(frame_sexp){ + if (frame_sexp.children.length >= 3) { + restartable = frame_sexp.children[2].children[1].source.toLowerCase() != 'nil'; + } else { + restartable = false; + } obj.stack_frames.push({ frame_number: frame_sexp.children[0].source, - description: readString(frame_sexp.children[1].source) + description: readString(frame_sexp.children[1].source), + restartable: restartable }); }); @@ -351,6 +357,17 @@ Client.prototype.debug_escape_all = function() { return this.rex(cmd, "COMMON-LISP-USER", "1"); //# TODO - is "1" ok here? } +/* Abort the current debug level */ +Client.prototype.debug_abort_current_level = function (level, thread) { + var cmd; + if (level == 1) { + cmd = '(SWANK:THROW-TO-TOPLEVEL)'; + } else { + cmd = '(SWANK:SLDB-ABORT)'; + } + return this.rex(cmd, 'COMMON-LISP-USER', thread) +} + /* Get the entire stack trace Returns a promise of the list of objects, each containing a stack frame*/ Client.prototype.debug_get_stack_trace = function(thread) { @@ -358,15 +375,193 @@ Client.prototype.debug_get_stack_trace = function(thread) { return this.rex(cmd, "COMMON-LISP-USER", thread).then(function(sexp) { stack_frames = []; sexp.children.forEach(function(frame_sexp){ + if (frame_sexp.children.length >= 3) { + restartable = frame_sexp.children[2].children[1].source.toLowerCase() != 'nil'; + } else { + restartable = false; + } stack_frames.push({ frame_number: frame_sexp.children[0].source, - description: readString(frame_sexp.children[1].source) + description: readString(frame_sexp.children[1].source), + restartable: restartable }); }); return stack_frames; }); } +/* Retrieve the stack frame details for the specified frame. + The existing stack frame object will be updated. + Returns a promise of the updated stack frame object. */ +Client.prototype.debug_stack_frame_details = function(index, stack_frames, thread) { + frame_info = stack_frames.find(function(frame) { + return Number(frame.frame_number) === Number(index); + }); + if (frame_info.hasOwnProperty('locals')) { + //frame details have already been fetched + return Promise.resolve(frame_info); + } else { + var cmd = '(SWANK:FRAME-LOCALS-AND-CATCH-TAGS ' + index + ')'; + return this.rex(cmd, "COMMON-LISP-USER", thread).then(function(sexp) { + if (sexp.children[0].type.toLowerCase() == 'list'){ + frame_info.locals = sexp.children[0].children.map(function(local_sexp) { + return { + name: readString(local_sexp.children[1].source), + id: local_sexp.children[3].source, + value: local_sexp.children[5].source + }; + }); + } else { + frame_info.locals = []; + } + + if (sexp.children[1].type.toLowerCase() == 'list') { + frame_info.catch_tags = sexp.children[1].children.map(function(tag_sexp) { + return readString(tag_sexp.source); + }); + } else { + frame_info.catch_tags = [] + } + + return frame_info; + }); + } +} + +/* Restart the specified frame. + May not be supported on some implementations. */ +Client.prototype.debug_restart_frame = function(frame, thread) { + var cmd = '(SWANK:RESTART-FRAME ' + frame + ')'; + return this.rex(cmd, "COMMON-LISP-USER", thread); +} + +/* Return the given value from the specified frame. + May not be supported on some implementations. */ +Client.prototype.debug_return_from_frame = function(frame, value, thread) { + var cmd = '(SWANK:SLDB-RETURN-FROM-FRAME ' + frame + ' "' + value.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '")'; + return this.rex(cmd, "COMMON-LISP-USER", thread); +} + +/* Gets information to display the frame's source */ +Client.prototype.debug_frame_source = function(frame, thread) { + var cmd = '(SWANK:FRAME-SOURCE-LOCATION ' + frame + ')'; + return this.rex(cmd, "COMMON-LISP-USER", thread).then(function(location_sexp) { + //(:location ) + location = {}; + + if(location_sexp.children[0].source.toLowerCase() == ":error"){ + location.buffer_type = "error"; + location.error = readString(location_sexp.children[1].source); + } else { + var raw_buffer_sexp = location_sexp.children[1]; + location.buffer_type = raw_buffer_sexp.children[0].source.slice(1).toLowerCase(); + if (location.buffer_type == 'file') { + location.file = readString(raw_buffer_sexp.children[1].source); + } else if (location.buffer_type == 'buffer') { + location.buffer_name = readString(raw_buffer_sexp.children[1].source); + } else if (location.buffer_type == 'buffer-and-file') { + location.buffer_name = readString(raw_buffer_sexp.children[1].source); + location.file = readString(raw_buffer_sexp.children[2].source); + } else if (location.buffer_type == 'source-form') { + location.source_form = readString(raw_buffer_sexp.children[1].source); + } else if (location.buffer_type == 'zip') { + location.zip_file = readString(raw_buffer_sexp.children[1].source); + location.zip_entry = readString(raw_buffer_sexp.children[1].source); + } + + var raw_position_sexp = location_sexp.children[2]; + + location.position_type = raw_position_sexp.children[0].source.slice(1).toLowerCase(); + if (location.position_type == 'position') { + location.position_offset = Number(raw_position_sexp.children[1].source); + } else if (location.position_type == 'offset') { + location.position_type = 'position' + location.position_offset = Number(raw_position_sexp.children[1].source) + + Number(raw_position_sexp.children[2].source); + } else if (location.position_type == 'line') { + location.position_line = Number(raw_position_sexp.children[1]); + if (raw_position_sexp.children.length >= 3) { + location.position_column = Number(raw_position_sexp.children[2]); + } + } else if (location.position_type == 'function-name') { + location.position_function = raw_position_sexp.children[1].source; + } else if (location.position_type == 'source_path') { + if (raw_position_sexp.children[1].type.toLowerCase == 'list') { + location.position_source_path_list = raw_position_sexp.children[1].map(function(elt) { + return elt.source; + }); + } else { + location.position_source_path_list = []; + } + location.position_source_path_start = raw_position_sexp.children[2].source; + } else if (location.position_type == 'method') { + location.position_method_name = raw_position_sexp.children[1].source; + if (raw_position_sexp.children[2].type.toLowerCase == 'list') { + location.position_specializers = raw_position_sexp.children[2].map(function(elt) { + return elt.source; + }); + } else { + location.position_specializers = []; + } + location.position_qualifiers = raw_position_sexp.children.slice(3).map(function(elt) { + return elt.source; + }); + } + } + + return location; + }); +} + +/* Disassembles the specified frame. */ +Client.prototype.debug_disassemble_frame = function(frame, thread) { + var cmd = '(SWANK:SLDB-DISASSEMBLE ' + frame + ')'; + return this.rex(cmd, "COMMON-LISP-USER", thread).then(function(sexp) { + return readString(sexp.source); + }); +} + +/* Evaluate the given string in the specified frame. + Returns a promise of the results of the evaluation as a string. */ +Client.prototype.debug_eval_in_frame = function(frame, expr, thread) { + var cmd = '(SWANK:EVAL-STRING-IN-FRAME "' + expr.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '" ' + + frame + ' "COMMON-LISP-USER")'; + return this.rex(cmd, "COMMON-LISP-USER", thread).then(function(result) { + return readString(result.source); + }); +} + +/* Steps the debugger to the next expression in the frame. */ +Client.prototype.debug_step = function(frame, thread) { + var cmd = '(SWANK:SLDB-STEP ' + frame + ')'; + return this.rex(cmd, "COMMON-LISP-USER", thread); +} + +/* Steps the debugger to the next form in the function. */ +Client.prototype.debug_next = function(frame, thread) { + var cmd = '(SWANK:SLDB-NEXT ' + frame + ')'; + return this.rex(cmd, "COMMON-LISP-USER", thread); +} + +/* Complete the current function then resume stepping. */ +Client.prototype.debug_step_out = function(frame, thread) { + var cmd = '(SWANK:SLDB-OUT ' + frame + ')'; + return this.rex(cmd, "COMMON-LISP-USER", thread); +} + +/* Insert a breakpoint at the end of the frame. */ +Client.prototype.debug_break_on_return = function(frame, thread) { + var cmd = '(SWANK:SLDB-BREAK-ON-RETURN ' + frame + ')'; + return this.rex(cmd, "COMMON-LISP-USER", thread); +} + +/* Insert a breakpoint at the specified function. */ +Client.prototype.debug_break = function(function_name, thread) { + var cmd = '(SWANK:SLDB-BREAK "' + function_name.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '")'; + return this.rex(cmd, "COMMON-LISP-USER", thread); +} + + /************* * Profiling * *************/ From a55d82e13ec5bc90b9c8c809fa23993b9827d939 Mon Sep 17 00:00:00 2001 From: Neil Lindquist Date: Sat, 22 Dec 2018 19:52:09 -0600 Subject: [PATCH 4/4] Provide error messages for return from frame --- lib/client.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/client.js b/lib/client.js index 0f420d2..8b68ef2 100644 --- a/lib/client.js +++ b/lib/client.js @@ -436,10 +436,13 @@ Client.prototype.debug_restart_frame = function(frame, thread) { } /* Return the given value from the specified frame. - May not be supported on some implementations. */ + May not be supported on some implementations. + Returns a function that provides the error message on an error*/ Client.prototype.debug_return_from_frame = function(frame, value, thread) { var cmd = '(SWANK:SLDB-RETURN-FROM-FRAME ' + frame + ' "' + value.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '")'; - return this.rex(cmd, "COMMON-LISP-USER", thread); + return this.rex(cmd, "COMMON-LISP-USER", thread).then(function(errorMessage) { + return readString(errorMessage.source); + }); } /* Gets information to display the frame's source */