From 70001196cb9202a445a582915eb756debc269659 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 25 May 2020 21:49:17 -0600 Subject: [PATCH] debugger - error handling, ui rework --- debugger/apis/init.lua | 150 ++++++++------- debugger/debug.lua | 420 +++++++++++++++++++++-------------------- debugger/example.lua | 31 ++- 3 files changed, 319 insertions(+), 282 deletions(-) diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index 233d71b..fe9de22 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -1,35 +1,36 @@ ---[[ -some portions from https://github.com/slembcke/debugger.lua -]] +-- this code is loaded into the code being debugged +-- some portions from https://github.com/slembcke/debugger.lua local fs = _G.fs local dbg = { } -local function hookBreakpoint(info) +local function breakpointHook(info) if dbg.breakpoints then for _,v in pairs(dbg.breakpoints) do - if v.line == info.currentline and v.file == info.short_src and not v.disabled then - return true + if v.line == info.currentline and v.file == info.short_src then +print(v.line, not v.disabled) + return not v.disabled end end end end -local function hookFunction(fn) +local function functionHook(fn) return function(info) return info.func == fn end end -local function hookStep() +local function stepHook() local co = coroutine.running() return function(info) - return co == coroutine.running() or hookBreakpoint(info) + return co == coroutine.running() + or breakpointHook(info) end end -local function hookStepStacksize(n) +local function stackSizeHook(n) local co = coroutine.running() local i = 2 while true do @@ -40,27 +41,23 @@ local function hookStepStacksize(n) i = i + 1 end return function(info) - if co == coroutine.running() then - if not debug.getinfo(i - n) then - return true - end - end - return hookBreakpoint(info) + return co == coroutine.running() + and not debug.getinfo(i - n) + or breakpointHook(info) end end -local function hookStepOut() - return hookStepStacksize(1) +local function stepOutHook() + return stackSizeHook(1) end -local function hookStepOver() - return hookStepStacksize(0) +local function stepOverHook() + return stackSizeHook(0) end local hookEval = function() end -- Create a table of all the locally accessible variables. --- Globals are not included when running the locals command local function local_bindings(offset, stack_inspect_offset) offset = offset + 1 + stack_inspect_offset -- add this function to the offset local func = debug.getinfo(offset).func @@ -96,7 +93,7 @@ local function local_bindings(offset, stack_inspect_offset) local t = { } for k,v in pairs(bindings) do - if v.raw ~= nil then + if k ~= '(*temporary)' then v.name = k v.value = tostring(v.raw) table.insert(t, v) @@ -138,45 +135,44 @@ local inHook = false local function hook() local info = debug.getinfo(2) - if info.currentline < 0 then - return - end + if not inHook and hookEval(info) then inHook = true - local offset = 2 -- the offset from this function to the code being debugged local inspectOffset = 0 repeat local done = true - local snapshot = { - info = debug.getinfo(offset + inspectOffset), - locals = local_bindings(offset, inspectOffset), - stack = get_trace(offset, inspectOffset), - } + local snapshot = { + info = debug.getinfo(2 + inspectOffset), + locals = local_bindings(2, inspectOffset), + stack = get_trace(2, inspectOffset), + } inspectOffset = 0 -- reset - local cmd, param = dbg.read(snapshot) + os.queueEvent('debuggerX', dbg.debugger.uid, snapshot) + + local e, cmd, param + repeat + e, cmd, param = os.pullEvent('debugger') + until e == 'debugger' + if cmd == 's' then - hookEval = hookStep() + hookEval = stepHook() elseif cmd == 'n' then - hookEval = hookStepOver() + hookEval = stepOverHook() elseif cmd == 'f' then - hookEval = hookStepOut() + hookEval = stepOutHook() elseif cmd == 'c' then - hookEval = hookBreakpoint - elseif cmd == 'd' then -- detach - debug.sethook() - elseif cmd == 'q' then - os.exit(0) - elseif cmd == 'b' then - dbg.breakpoints = param - done = false + hookEval = breakpointHook elseif cmd == 'i' then - -- inspect stack at this offset + -- get snapshot of stack at this offset inspectOffset = param done = false + else + os.sleep(1) + done = false end until done @@ -184,41 +180,51 @@ local function hook() end end -local cocreate = coroutine.create -_ENV.coroutine = { } -for k,v in pairs(_G.coroutine) do - _ENV.coroutine[k] = v -end -_ENV.coroutine.create = function(f, ...) - local co = cocreate(f, ...) - debug.sethook(co, dbg.hook, "l") - return co +function dbg.call(f, ...) + local args = { ... } + return xpcall( + function() + f(table.unpack(args)) + end, + function(err) + hookEval = stepHook() + + -- An error has occurred + return err + end) end +_ENV.coroutine = setmetatable({ + + create = function(f) + local co = _G.coroutine.create(function(...) + local r = { dbg.call(f, ...) } + + if not r[1] then + error(r[2], -1) + end + + return table.unpack(r, 2) + end) + + debug.sethook(co, hook, 'l') + return co + end + --[[ + create = function(f) + local co = _G.coroutine.create(f) + debug.sethook(co, hook, 'l') + return co + end + ]] +}, { __index = coroutine }) + debug.sethook(hook, 'l') -- Expose the debugger's functions -dbg.hook = hook -dbg.exit = function(err) os.exit(err) end dbg.stopIn = function(fn) - hookEval = hookFunction(fn) + hookEval = functionHook(fn) end dbg.debugger = nil -dbg.read = function(info) - _G._pinfo = info - - os.sleep(0) -- this is important ... - dbg.debugger:resume('debugger', 'info', info) - - while true do - local _, cmd, args = os.pullEvent('debugger') - if cmd == 'b' then - dbg.breakpoints = args - else - return cmd, args - end - end -end - return dbg diff --git a/debugger/debug.lua b/debugger/debug.lua index 71b71c4..5f4f2a2 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -32,12 +32,13 @@ local client local function startClient() local env = kernel.makeEnv(_ENV) + currentFile = nil local clientId = multishell.openTab(nil, { env = env, title = fs.getName(filename):match('([^%.]+)'), args = args, - fn = function(...) + fn = function() local dbg = require('debugger') local fn, msg = loadfile(filename, env) @@ -45,10 +46,14 @@ local function startClient() error(msg, -1) end - dbg.debugger = debugger + -- breakpoint table is shared across processes dbg.breakpoints = breakpoints + dbg.debugger = debugger dbg.stopIn(fn) - fn(...) + local s, m = dbg.call(fn, table.unpack(args)) + if not s then + error(m, -1) + end end, }) client = kernel.find(clientId) @@ -79,7 +84,7 @@ romFiles:load() local function loadSource(file) currentFile = romFiles:get(file) or file:match('@?(.*)') local src = { } - local lines = Util.readLines(currentFile) + local lines = Util.readLines(currentFile) or type(file) == 'string' and Util.split(file) if lines then for i = 1, #lines do @@ -95,199 +100,210 @@ local function message(...) end local page = UI.Page { - menuBar = UI.MenuBar { - buttons = { - { text = 'Continue', event = 'cmd', cmd = 'c' }, - { text = 'Step', event = 'cmd', cmd = 's' }, - { text = 'Over', event = 'cmd', cmd = 'n' }, - { text = 'Out', event = 'cmd', cmd = 'f' }, - { text = 'Restart', event = 'restart', width = 9, ex = -1 }, - }, - }, + backgroundColor = 'black', container = UI.Window { - y = 2, ey = '50%', - locals = UI.ScrollingGrid { + y = 1, ey = '50%', + tabs = UI.Tabs { ey = -2, - disableHeader = true, - columns = { - { heading = 'Key', key = 'name' }, - { heading = 'Value', key = 'value', textColor = 'yellow' }, - }, - autospace = true, - accelerators = { - grid_select = 'show_variable', - }, - getRowTextColor = function(self, row, selected) - return row.type == 'U' and 'cyan' - or row.type == 'V' and 'lime' - or UI.Grid.getRowTextColor(self, row, selected) - end, - }, - statusBar = UI.StatusBar { - ex = -7, y = -1, - backgroundColor = 'primary', - textColor = 'white', - }, - UI.Button { - y = -1, x = -6, - event = 'open', - text = 'Open', - } - }, + unselectedBackgroundColor = 'black', - tabs = UI.Tabs { - y = '50%', - source = UI.Tab { - title = 'Source', - index = 1, - grid = UI.ScrollingGrid { - disableHeader = true, - columns = { - { key = 'marker', width = 1 }, - { key = 'line', textColor = 'cyan', width = 4 }, - { heading = 'heading', key = 'source' }, + locals = UI.Tab { + title = 'Locals', + index = 1, + grid = UI.ScrollingGrid { + disableHeader = true, + unfocusedBackgroundSelectedColor = 'black', + columns = { + { heading = 'Key', key = 'name' }, + { heading = 'Value', key = 'value', textColor = 'yellow' }, + }, + autospace = true, + accelerators = { + grid_select = 'show_variable', + }, + getRowTextColor = function(self, row, selected) + return row.type == 'U' and 'cyan' + or row.type == 'V' and 'lime' + or UI.Grid.getRowTextColor(self, row, selected) + end, }, - getDisplayValues = function(_, row) - for _,v in pairs(breakpoints) do - if v.file == currentFile and v.line == row.line then - return { - marker = v.disabled and 'x' or '!', - line = row.line, - source = row.source, - } + }, + + stack = UI.Tab { + title = 'Stack', + index = 3, + grid = UI.ScrollingGrid { + disableHeader = true, + sortColumn = 'index', + unfocusedBackgroundSelectedColor = 'black', + columns = { + { key = 'index', width = 2 }, + { heading = 'heading', key = 'desc' }, + }, + getRowTextColor = function(self, row, selected) + return row.current and 'yellow' + or UI.Grid.getRowTextColor(self, row, selected) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + message('i', event.selected.index) + else + return UI.Grid.eventHandler(self, event) end - end - return row - end, - accelerators = { - t = 'toggle_enabled' + end, + }, + }, + + env = UI.Tab { + title = 'Env', + index = 4, + grid = UI.ScrollingGrid { + disableHeader = true, + autospace = true, + unfocusedBackgroundSelectedColor = 'black', + columns = { + { heading = 'Key', key = 'name' }, + { heading = 'Value', key = 'value', textColor = 'yellow' }, + }, + accelerators = { + grid_select = 'show_variable', + }, + sortCompare = function() end, + }, + }, + + breaks = UI.Tab { + title = 'Breakpoints', + index = 2, + menuBar = UI.MenuBar { + buttons = { + { text = 'Toggle', event = 'toggle' }, + { text = 'Remove', event = 'remove' }, + { text = 'Clear', event = 'clear' }, + }, + }, + grid = UI.ScrollingGrid { + y = 2, + values = breakpoints, + autospace = true, + columns = { + { heading = 'Line', key = 'line', width = 5 }, + { heading = 'Name', key = 'short' }, + { heading = 'Path', key = 'path', textColor = 'lightGray' }, + }, + getRowTextColor = function(self, row, selected) + return row.disabled and 'lightGray' + or UI.Grid.getRowTextColor(self, row, selected) + end, }, - getRowTextColor = function(self, row, selected) - return row.line == debugLine and currentFile == debugFile and 'yellow' - or UI.Grid.getRowTextColor(self, row, selected) - end, eventHandler = function(self, event) - if event.type == 'grid_select' then + if event.type == 'clear' then + Util.clear(self.grid.values) + self:emit({ type = 'update_breakpoints' }) + + elseif event.type == 'toggle' then + local bp = self.grid:getSelected() + if bp then + bp.disabled = not bp.disabled + self:emit({ type = 'update_breakpoints' }) + end + + elseif event.type == 'grid_select' then self:emit({ - type = 'toggle_breakpoint', - file = currentFile, + type = 'open_file', + file = event.selected.file, line = event.selected.line, }) - elseif event.type == 'toggle_enabled' then - local line = self:getSelected() and self:getSelected().line - if line then - for _,v in pairs(breakpoints) do - if v.file == currentFile and v.line == line then - v.disabled = not v.disabled - self:emit({ type = 'update_breakpoints' }) - break - end - end + + elseif event.type == 'remove' then + local bp = self.grid:getSelected() + if bp then + Util.removeByValue(self.grid.values, bp) + self:emit({ type = 'update_breakpoints' }) + end + + end + return UI.Tab.eventHandler(self, event) + end, + }, + }, + + menuBar = UI.MenuBar { + y = -1, + buttons = { + { text = 'Continue', event = 'cmd', cmd = 'c' }, + { text = 'Step', event = 'cmd', cmd = 's' }, + { text = 'Over', event = 'cmd', cmd = 'n' }, + { text = 'Out', event = 'cmd', cmd = 'f' }, + { text = 'Restart', event = 'restart', width = 9, ex = -1 }, + }, + }, + }, + + source = UI.ScrollingGrid { + y = '50%', ey = -2, + disableHeader = true, + columns = { + { key = 'marker', width = 1 }, + { key = 'line', textColor = 'cyan', width = 4 }, + { heading = 'heading', key = 'source' }, + }, + accelerators = { + t = 'toggle_enabled' + }, + getDisplayValues = function(_, row) + for _,v in pairs(breakpoints) do + if v.file == currentFile and v.line == row.line then + return { + marker = v.disabled and 'x' or '!', + line = row.line, + source = row.source, + } + end + end + return row + end, + getRowTextColor = function(self, row, selected) + return row.line == debugLine and currentFile == debugFile and 'yellow' + or UI.Grid.getRowTextColor(self, row, selected) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + self:emit({ + type = 'toggle_breakpoint', + file = currentFile, + line = event.selected.line, + }) + elseif event.type == 'toggle_enabled' then + local line = self:getSelected() and self:getSelected().line + if line then + for _,v in pairs(breakpoints) do + if v.file == currentFile and v.line == line then + v.disabled = not v.disabled + self:emit({ type = 'update_breakpoints' }) + break end end - return UI.Grid.eventHandler(self, event) - end, - }, - }, - - stack = UI.Tab { - title = 'Stack', - index = 3, - grid = UI.ScrollingGrid { - disableHeader = true, - columns = { - { key = 'index', width = 2 }, - { heading = 'heading', key = 'desc' }, - }, - getRowTextColor = function(self, row, selected) - return row.current and 'yellow' - or UI.Grid.getRowTextColor(self, row, selected) - end, - sortColumn = 'index', - eventHandler = function(self, event) - if event.type == 'grid_select' then - message('i', event.selected.index) - else - return UI.Grid.eventHandler(self, event) - end - end, - }, - }, - - env = UI.Tab { - title = 'Env', - index = 4, - grid = UI.ScrollingGrid { - columns = { - { heading = 'Key', key = 'name' }, - { heading = 'Value', key = 'value', textColor = 'yellow' }, - }, - autospace = true, - accelerators = { - grid_select = 'show_variable', - }, - sortCompare = function() end, - }, - }, - - breaks = UI.Tab { - title = 'Breakpoints', - index = 2, - menuBar = UI.MenuBar { - buttons = { - { text = 'Toggle', event = 'toggle' }, - { text = 'Remove', event = 'remove' }, - { text = 'Clear', event = 'clear' }, - }, - }, - grid = UI.ScrollingGrid { - y = 2, - columns = { - { heading = 'Line', key = 'line', width = 5 }, - { heading = 'Name', key = 'short' }, - { heading = 'Path', key = 'path', textColor = 'lightGray' }, - }, - values = breakpoints, - autospace = true, - getRowTextColor = function(self, row, selected) - return row.disabled and 'lightGray' - or UI.Grid.getRowTextColor(self, row, selected) - end, - }, - eventHandler = function(self, event) - if event.type == 'clear' then - Util.clear(self.grid.values) - self:emit({ type = 'update_breakpoints' }) - - elseif event.type == 'toggle' then - local bp = self.grid:getSelected() - if bp then - bp.disabled = not bp.disabled - self:emit({ type = 'update_breakpoints' }) - end - - elseif event.type == 'grid_select' then - self:emit({ - type = 'open_file', - file = event.selected.file, - line = event.selected.line, - }) - - elseif event.type == 'remove' then - local bp = self.grid:getSelected() - if bp then - Util.removeByValue(self.grid.values, bp) - self:emit({ type = 'update_breakpoints' }) - end - end - return UI.Tab.eventHandler(self, event) - end, - }, + end + return UI.Grid.eventHandler(self, event) + end, + }, + statusBar = UI.StatusBar { + ex = -7, y = -1, + backgroundColor = 'black', + textColor = 'orange', + }, + UI.FlatButton { + y = -1, x = -5, + textColor = 'orange', + event = 'open', + text = 'Open', }, quick_open = UI.QuickSelect { + y = '50%', modal = true, enable = function() end, show = function(self) @@ -310,26 +326,25 @@ local page = UI.Page { openFile = function(self, file, line) if file ~= currentFile then local src = loadSource(file) - self.tabs.source.grid:setValues(src) + self.source:setValues(src) end if line then - self.tabs.source.grid:setIndex(#self.tabs.source.grid.values) - self.tabs.source.grid:setIndex(math.max(1, line - 4)) + self.source:setIndex(#self.source.values) + self.source:setIndex(math.max(1, line - 4)) end - self.tabs.source.grid:setIndex(line or 1) - self.tabs:selectTab(self.tabs.source) + self.source:setIndex(line or 1) if currentFile == debugFile then - self.container.statusBar:setStatus( + self.statusBar:setStatus( string.format('%s : %d', fs.getName(file), debugLine)) else - self.container.statusBar:setStatus(fs.getName(file)) + self.statusBar:setStatus(fs.getName(file)) end self:draw() end, eventHandler = function(self, event) if event.type == 'cmd' then - self.container.statusBar:setStatus('Running...') + self.statusBar:setStatus('Running...') message(event.element.cmd) elseif event.type == 'restart' then @@ -345,10 +360,9 @@ local page = UI.Page { self:openFile(event.file, event.line) elseif event.type == 'update_breakpoints' then - self.tabs.breaks.grid:update() - self.tabs.breaks.grid:draw() - self.tabs.source.grid:draw() - message('b', breakpoints) + self.container.tabs.breaks.grid:update() + self.container.tabs.breaks.grid:draw() + self.source:draw() Config.update('debugger', config) elseif event.type == 'toggle_breakpoint' then @@ -405,26 +419,22 @@ local page = UI.Page { end, } -Event.on('debugger', function(_, cmd, data) - if cmd == 'info' then +Event.on('debuggerX', function(_, uid, data) + if uid == debugger.uid then kernel.raise(debugger.uid) -- local tab - local t = data.locals --- for k,v in pairs(data.locals or { }) do --- table.insert(t, { name = k, value = tostring(v), raw = v }) --- end - table.sort(t, function(a, b) return a.name < b.name end) - page.container.locals:setValues(t) - page.container.locals.orig = Util.shallowCopy(t) + table.sort(data.locals, function(a, b) return a.name < b.name end) + page.container.tabs.locals.grid:setValues(data.locals) + page.container.tabs.locals.grid.orig = Util.shallowCopy(data.locals) -- env tab - t = { } + local t = { } for k,v in pairs(getfenv(data.info.func)) do table.insert(t, { name = k, value = tostring(v), raw = v }) end - page.tabs.env.grid:setValues(t) - page.tabs.env.grid.orig = Util.shallowCopy(t) + page.container.tabs.env.grid:setValues(t) + page.container.tabs.env.grid.orig = Util.shallowCopy(t) debugLine = data.info.currentline debugFile = data.info.source:match('@?(.*)') @@ -433,7 +443,7 @@ Event.on('debugger', function(_, cmd, data) page:openFile(debugFile, debugLine) -- stack - page.tabs.stack.grid:setValues(data.stack) + page.container.tabs.stack.grid:setValues(data.stack) page:draw() page:sync() diff --git a/debugger/example.lua b/debugger/example.lua index 8df3d2b..d8093a2 100644 --- a/debugger/example.lua +++ b/debugger/example.lua @@ -4,16 +4,37 @@ end local function method(times) local a = 2 - -- use step out to return out of method for _ = 1, times do a = a * a end return m2(a) end -print('before') -term.current().clear() -print('after') +local chunk = load([[ + local j = 5 + for i = 1, 5 do + j = j * i + end + --table.insert(j, 5) + return j]], nil, nil, _ENV) + +local j = chunk() +print(j) + +require('opus.util').print(coroutine) +local co = coroutine.create(function(args) + print('in coroutine') + return 'hi' +end) + +local _, t = coroutine.resume(co, 'test') +while coroutine.status(co) ~= 'dead' do + coroutine.resume(co, os.pullEvent()) + --print('alive') +end +print(coroutine.status(co)) + +print(t) local i = 2 print(i) @@ -24,4 +45,4 @@ dofile("rom/modules/main/cc/expect.lua") print(res) print('result: ' .. res) -error('f') +table.insert(res, 5)