diff --git a/README.md b/README.md index 4ceb0122..c3f0cacd 100644 --- a/README.md +++ b/README.md @@ -73,13 +73,13 @@ Despite this list, gdbgui is quite usable in its current form * ~~allow argument passing to the inferior process being debugged~~ * ~~add links back to github, etc~~ -* escape brackets on system so they don't disappear +* ~~escape brackets on system so they don't disappear~~ +* ~~only make gutter create/delete breakpoints, not anywhere in source file~~ +* ~~add ability to view/inspect variables~~ +* ~~add ability to view/inspect memory~~ * add button to widen windows * add clear button to windows * improve toolbar styling, change color when error occurs * add preference ui elements (auto-refresh various windows after command is sent; show/hide windows as desired) * make flash of color fade out when snapping to source code lines or restoring old history * add autocompletion and documentation of all commands -* add ability to view/inspect variables -* add ability to view/inspect memory -* only make gutter create/delete breakpoints, not anywhere in source file diff --git a/gdbgui/backend.py b/gdbgui/backend.py index 82f0815b..df5ba60e 100755 --- a/gdbgui/backend.py +++ b/gdbgui/backend.py @@ -30,12 +30,14 @@ def client_error(obj): def get_extra_files(): + extra_dirs = [STATIC_DIR, TEMPLATE_DIR] extra_files = [] - for dirname, dirs, files in os.walk(TEMPLATE_DIR): - for filename in files: - filename = os.path.join(dirname, filename) - if os.path.isfile(filename): - extra_files.append(filename) + for extra_dir in extra_dirs: + for dirname, dirs, files in os.walk(extra_dir): + for filename in files: + filename = os.path.join(dirname, filename) + if os.path.isfile(filename): + extra_files.append(filename) return extra_files diff --git a/gdbgui/static/css/gdbgui.css b/gdbgui/static/css/gdbgui.css index 532557d7..90a33a20 100644 --- a/gdbgui/static/css/gdbgui.css +++ b/gdbgui/static/css/gdbgui.css @@ -1,3 +1,4 @@ +/* styling for all html tags */ body{ background: #ccc; color: grey; @@ -6,6 +7,17 @@ body{ table{ font-size: 0.9em; } +pre{ + overflow: visible !important; +} + +/* styling for generic classes */ +.pre{ + white-space: pre; +} +.monospace{ + font-family: monospace; +} .lighttext{ color: #4a4a4a; } @@ -13,31 +25,67 @@ table{ /* glyphicons are too bold, make them lighter*/ color: #848484; } +.flex{ + display: flex; +} +/* components get their own titlebars */ .titlebar{ width: 100%; color: black; background-color: white; } -#console, .gdb_console{ +.pointer{ + cursor: pointer; +} +.gdb_content_div{ + overflow: auto; + height: 150px; + background-color: #f7f7f7; + border-color: grey; + border-style: solid; + border-width: 1px; + border-radius: 2px; +} + +/* specific styling for ids */ +#always_on_top{ + position: fixed; + top: 0; + margin-top: 0; + height: 102px; + width: 100%; + border-bottom: black; + border-style: solid; + border-width: 0px; + border-bottom-width: 1px; + z-index: 1000; + background: #f1f1f1; + border-bottom-color: #ccc; +} +#console{ font-size: 0.9em !important; background-color: #292929 !important; color: #f9f9f9 !important; font-family: monospace !important; } -#stdout { - color: black; +#gdb_command{ + padding-left: 45px; + border-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + font-size: 0.9em !important; + background-color: #3c3c3c !important; + color: #f9f9f9 !important; + font-family: monospace !important; } -#console, #stdout, #gdb_mi_output{ +#gdb_mi_output{ font-family: monospace; overflow: auto; font-size: 0.9em; } -.pointer{ - cursor: pointer; -} .sent_command:hover{ /* lighten background */ - background-color:rgba(255,255,255,0.5) + background-color:rgba(255,255,255,0.1) } .disabled { z-index: 1000; @@ -45,16 +93,12 @@ table{ opacity: 0.6; pointer-events: none; } -.no_margin{ +.margin_sm{ margin: 2px; } .no_padding{ padding: 2px !important; } -.code{ - overflow: auto; - font-size: 0.9em; -} .code pre, table.code { margin: 0px; padding: 0px; @@ -64,40 +108,39 @@ table{ .highlight{ background: rgba(255, 255, 0, 0.5); } -.breakpoint td.gutter div{ - background: blue; - width: 0.9em; - height: 0.9em; - border-radius: 50%; - border-color: black; -} -.no_breakpoint td.gutter div{ - /*background: blue;*/ - width: 0.9em; - height: 0.9em; - border-radius: 50%; - border-color: black; +.line_num_container{ + width: 50px; + border-width: 0; + border-right: 1px; + border-style: solid; + border-color: #c7c7c7; } -.active_breakpoint{ - background: red; +.line_num{ + padding-left: 10px; + padding-right: 10px; + font-family: monospace; + font-size: 0.9em; + color: #ababab; + cursor: pointer; } -.gutter{ - width: 0.9em; - padding-right: 5px; - background: #d4ffab; +/* the line number has its style changed if it has a breakpoint */ +.line_num.breakpoint{ + background: #33cdff; + color: black; + border-style: solid; + border-color: #000000; + border-width: 1px; } -.pre{ - white-space: pre; +/* set background color over souce code only */ +.source_code_row.line_of_code td{ + background-color: #f7f7f7; } -pre{ - overflow: visible !important; +.line_of_code pre{ + margin-left: 10px; } -.source_code:hover td, .source_code:hover td pre{ +/* when hovering, set background color of entire row */ +.source_code_row:hover td, .source_code_row:hover td pre{ background-color: lightblue; - cursor: pointer; -} -.source_code td{ - background-color: #f7f7f7 } .padding_left{ padding-left: 5px; @@ -105,26 +148,20 @@ pre{ .line_of_code{ height: 18px } -.gdb_content_div{ - overflow: auto; - height: 150px; - background-color: #f7f7f7; - border-color: grey; - border-style: solid; - border-width: 1px; - border-radius: 2px; -} .dropdown-btn { vertical-align: top; height: 30px; border-top-left-radius: 0; border-bottom-left-radius: 0; } +/* auto-complete librarie's dropdown should only be 200px high, and should +have scrollbar, so it doesn't take over the page */ .awesomplete ul { overflow: auto; max-height: 200px; } +/* show a flash of color */ .flash { -webkit-animation-name: flash-animation; -webkit-animation-duration: 1.0s; @@ -142,3 +179,18 @@ pre{ from { background: yellow; } to { background: default; } } + +#variables li{ + list-style: none; +} + +#variables ul.variable{ + padding-left: 5px; + border: 0px; +} +#variables li:hover{ + background-color: #c0eeff; +} +.toggle_children_visibility{ + font-weight: bold; +} diff --git a/gdbgui/static/js/gdbgui.js b/gdbgui/static/js/gdbgui.js index ad657687..8b680a7a 100644 --- a/gdbgui/static/js/gdbgui.js +++ b/gdbgui/static/js/gdbgui.js @@ -87,7 +87,19 @@ const GdbApi = { error: Status.render_ajax_error_msg }) }, - run_gdb_command: function(cmd){ + /** + * runs a gdb cmd (or commands) directly in gdb on the backend + * validates command before sending, and updates the gdb console and status bar + * @param cmd: a string or array of strings, that are directly evaluated by gdb + * @param success_callback: function to be called upon successful completion. The data returned + * is an object. See pygdbmi for a description of the format. + * The default callback works in most cases, but in some cases a the response is stateful and + * requires a specific callback. For example, when creating a variable in gdb + * to watch, gdb returns generic looking data that a generic callback could not + * figure out how to handle. + * @return nothing + */ + run_gdb_command: function(cmd, success_callback=process_gdb_response){ if(_.trim(cmd) === ''){ return } @@ -113,7 +125,7 @@ const GdbApi = { cache: false, method: 'POST', data: {'cmd': cmd}, - success: process_gdb_response, + success: success_callback, error: Status.render_ajax_error_msg, }) }, @@ -195,10 +207,10 @@ const GdbConsoleComponent = { if(_.isString(s)){ strings = [s] } - strings.map(string => GdbConsoleComponent.el.append(`

${Util.escape(string)}`)) + strings.map(string => GdbConsoleComponent.el.append(`

${Util.escape(string)}

`)) }, add_sent_commands(cmds){ - cmds.map(cmd => GdbConsoleComponent.el.append(`

${Util.escape(cmd)}`)) + cmds.map(cmd => GdbConsoleComponent.el.append(`

${Util.escape(cmd)}

`)) GdbConsoleComponent.scroll_to_bottom() }, scroll_to_bottom: function(){ @@ -244,6 +256,9 @@ const History = { const GdbMiOutput = { el: $('#gdb_mi_output'), + init: function(){ + $('.clear_mi_output').click(GdbMiOutput.clear) + }, clear: function(){ GdbMiOutput.el.html('') }, @@ -255,7 +270,9 @@ const GdbMiOutput = { 'status': "text-danger", 'console': "text-info", } - GdbMiOutput.el.append(`

${mi_obj.type}:\n${JSON.stringify(mi_obj, null, 4).replace(/[^(\\)]\\n/g)}`) + let mi_obj_dump = JSON.stringify(mi_obj, null, 4) + mi_obj_dump = mi_obj_dump.replace(/[^(\\)]\\n/g).replace("<", "<").replace(">", ">") + GdbMiOutput.el.append(`

${mi_obj.type}:
${mi_obj_dump}`) }, scroll_to_bottom: function(){ GdbMiOutput.el.animate({'scrollTop': GdbMiOutput.el.prop('scrollHeight')}) @@ -288,8 +305,13 @@ const Breakpoint = { links.push(`View`) } links.push(`remove`) - bkpt[' '] = links.join(' | ') + + // turn address into link + if (bkpt['addr']){ + bkpt['addr'] = Memory.make_addr_into_link(bkpt['addr']) + } + // add the breakpoint if it's not stored already if(Breakpoint.breakpoints.indexOf(bkpt) === -1){ Breakpoint.breakpoints.push(bkpt) @@ -316,20 +338,22 @@ const SourceCode = { rendered_source_file_fullname: null, rendered_source_file_line: null, init: function(){ - $("body").on("click", ".breakpoint", SourceCode.click_breakpoint) - $("body").on("click", ".no_breakpoint", SourceCode.click_source_file_gutter_with_no_breakpoint) + $("body").on("click", ".source_code_row td .line_num", SourceCode.click_gutter) $("body").on("click", ".view_file", SourceCode.click_view_file) }, cached_source_files: [], // list with keys fullname, source_code - click_breakpoint: function(e){ - let line = e.currentTarget.dataset.line - // todo: embed fullname in the dom instead of depending on state - Breakpoint.remove_breakpoint_if_present(SourceCode.rendered_source_file_fullname, line) - }, - click_source_file_gutter_with_no_breakpoint: function(e){ + click_gutter: function(e){ let line = e.currentTarget.dataset.line - let cmd = [`-break-insert ${SourceCode.rendered_source_file_fullname}:${line}`, '-break-list'] - GdbApi.run_gdb_command(cmd) + let has_breakpoint = (e.currentTarget.dataset.has_breakpoint === 'true') + if(has_breakpoint){ + // clicked gutter with a breakpoint, remove it + Breakpoint.remove_breakpoint_if_present(SourceCode.rendered_source_file_fullname, line) + + }else{ + // clicked with no breakpoint, add it, and list all breakpoints to make sure breakpoint table is up to date + let cmd = [`-break-insert ${SourceCode.rendered_source_file_fullname}:${line}`, '-break-list'] + GdbApi.run_gdb_command(cmd) + } }, render_cached_source_file: function(){ SourceCode.fetch_and_render_file(SourceCode.rendered_source_file_fullname, SourceCode.rendered_source_file_line, {'highlight': false, 'scroll': true}) @@ -344,15 +368,23 @@ const SourceCode = { bkpt_lines = Breakpoint.get_breakpoint_lines_For_file(fullname) for (let line of source_code){ - let breakpoint_class = (bkpt_lines.indexOf(line_num) !== -1) ? 'breakpoint': 'no_breakpoint'; + let has_breakpoint = bkpt_lines.indexOf(line_num) !== -1 + let breakpoint_class = has_breakpoint ? 'breakpoint' : '' let tags = '' if (line_num === current_line){ tags = `id=current_line ${options.highlight ? 'class=highlight' : ''}` } - tbody.push(` -

- ${line_num} -
${line}
+ line = line.replace("<", "<") + line = line.replace(">", ">") + tbody.push(` + + +
${line_num}
+ + + +
${line}
+ `) line_num++; @@ -508,7 +540,7 @@ const Disassembly = { } } for(let i of asm_insns){ - let assembly = i['line_asm_insn'].map(el => `${el['func-name']}+${el['offset']} ${el.address} ${el.inst}`) + let assembly = i['line_asm_insn'].map(el => `${el['func-name']}+${el['offset']} ${Memory.make_addr_into_link(el.address)} ${el.inst}`) let line_link = `${i.line} view` let source_line = '(file not loaded)' if(i.line <= source_code.length){ @@ -523,16 +555,36 @@ const Disassembly = { const Stack = { el: $('#stack'), + init: function(){ + $("body").on("click", ".select_frame", Stack.click_select_frame) + }, render_stack: function(stack){ for (let s of stack){ if ('fullname' in s){ - s[' '] = `View` + s[' '] = `View` + } + if ('addr' in s){ + s.addr = Memory.make_addr_into_link(s.addr) } } let [columns, data] = Util.get_table_data_from_objs(stack) Stack.el.html(Util.get_table(columns, data)) }, + /** + * select a frame and jump to the line in source code + * triggered when clicking on an object with the "select_frame" class + * must have data attributes: framenum, fullname, line + * + */ + click_select_frame: function(e){ + Stack.select_frame(e.currentTarget.dataset.framenum) + SourceCode.click_view_file(e) + AllLocalVariables.clear() + }, + select_frame: function(framenum){ + GdbApi.run_gdb_command(`-stack-select-frame ${framenum}`) + } } const Registers = { @@ -572,21 +624,24 @@ const Prefs = { const BinaryLoader = { el: $('#binary'), + el_past_binaries: $('#past_binaries'), init: function(){ // events $('#set_target_app').click(BinaryLoader.click_set_target_app) BinaryLoader.el.keydown(BinaryLoader.keydown_on_binary_input) try{ - BinaryLoader.last_binary = localStorage.getItem('last_binary') || '' - BinaryLoader.render(BinaryLoader.last_binary) + BinaryLoader.past_binaries = _.uniq(JSON.parse(localStorage.getItem('past_binaries'))) + BinaryLoader.render(BinaryLoader.past_binaries[0]) } catch(err){ - BinaryLoader.last_binary = '' + BinaryLoader.past_binaries = [] } - + // update list of old binarys + BinaryLoader.render_past_binary_options_datalist() }, + past_binaries: [], onclose: function(){ - localStorage.setItem('last_binary', BinaryLoader.el.val()) + localStorage.setItem('past_binaries', JSON.stringify(BinaryLoader.past_binaries) || []) return null }, click_set_target_app: function(e){ @@ -594,10 +649,18 @@ const BinaryLoader = { }, set_target_app: function(){ var binary_and_args = _.trim(BinaryLoader.el.val()) + if (binary_and_args === ''){ Status.render('enter a binary path before attempting to load') return } + + // save to list of binaries used that autopopulates the input dropdown + _.remove(BinaryLoader.past_binaries, i => i === binary_and_args) + BinaryLoader.past_binaries.unshift(binary_and_args) + BinaryLoader.render_past_binary_options_datalist() + + // find the binary and arguments so gdb can be told which is which let binary, args, cmds let index_of_first_space = binary_and_args.indexOf(' ') if( index_of_first_space === -1){ @@ -607,8 +670,11 @@ const BinaryLoader = { binary = binary_and_args.slice(0, index_of_first_space) args = binary_and_args.slice(index_of_first_space + 1, binary_and_args.length) } - cmds = [`-exec-arguments ${args}`, `-file-exec-and-symbols ${binary}`] + // tell gdb which arguments to use when calling the binary, before loading the binary + cmds = [`-exec-arguments ${args}`, `-file-exec-and-symbols ${binary}`, `-break-insert main`, `-break-list`] + + // reload breakpoints after sending to make sure they're up to date if (Prefs.auto_reload_breakpoints()){ cmds.push('-break-list') } @@ -623,6 +689,9 @@ const BinaryLoader = { BinaryLoader.set_target_app() } }, + render_past_binary_options_datalist: function(){ + BinaryLoader.el_past_binaries.html(BinaryLoader.past_binaries.map(b => `