debugger - error handling, ui rework

This commit is contained in:
kepler155c@gmail.com
2020-05-25 21:49:17 -06:00
parent 26b693d609
commit 70001196cb
3 changed files with 319 additions and 282 deletions

View File

@@ -1,35 +1,36 @@
--[[ -- this code is loaded into the code being debugged
some portions from https://github.com/slembcke/debugger.lua -- some portions from https://github.com/slembcke/debugger.lua
]]
local fs = _G.fs local fs = _G.fs
local dbg = { } local dbg = { }
local function hookBreakpoint(info) local function breakpointHook(info)
if dbg.breakpoints then if dbg.breakpoints then
for _,v in pairs(dbg.breakpoints) do for _,v in pairs(dbg.breakpoints) do
if v.line == info.currentline and v.file == info.short_src and not v.disabled then if v.line == info.currentline and v.file == info.short_src then
return true print(v.line, not v.disabled)
return not v.disabled
end end
end end
end end
end end
local function hookFunction(fn) local function functionHook(fn)
return function(info) return function(info)
return info.func == fn return info.func == fn
end end
end end
local function hookStep() local function stepHook()
local co = coroutine.running() local co = coroutine.running()
return function(info) return function(info)
return co == coroutine.running() or hookBreakpoint(info) return co == coroutine.running()
or breakpointHook(info)
end end
end end
local function hookStepStacksize(n) local function stackSizeHook(n)
local co = coroutine.running() local co = coroutine.running()
local i = 2 local i = 2
while true do while true do
@@ -40,27 +41,23 @@ local function hookStepStacksize(n)
i = i + 1 i = i + 1
end end
return function(info) return function(info)
if co == coroutine.running() then return co == coroutine.running()
if not debug.getinfo(i - n) then and not debug.getinfo(i - n)
return true or breakpointHook(info)
end
end
return hookBreakpoint(info)
end end
end end
local function hookStepOut() local function stepOutHook()
return hookStepStacksize(1) return stackSizeHook(1)
end end
local function hookStepOver() local function stepOverHook()
return hookStepStacksize(0) return stackSizeHook(0)
end end
local hookEval = function() end local hookEval = function() end
-- Create a table of all the locally accessible variables. -- 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) local function local_bindings(offset, stack_inspect_offset)
offset = offset + 1 + stack_inspect_offset -- add this function to the offset offset = offset + 1 + stack_inspect_offset -- add this function to the offset
local func = debug.getinfo(offset).func local func = debug.getinfo(offset).func
@@ -96,7 +93,7 @@ local function local_bindings(offset, stack_inspect_offset)
local t = { } local t = { }
for k,v in pairs(bindings) do for k,v in pairs(bindings) do
if v.raw ~= nil then if k ~= '(*temporary)' then
v.name = k v.name = k
v.value = tostring(v.raw) v.value = tostring(v.raw)
table.insert(t, v) table.insert(t, v)
@@ -138,45 +135,44 @@ local inHook = false
local function hook() local function hook()
local info = debug.getinfo(2) local info = debug.getinfo(2)
if info.currentline < 0 then
return
end
if not inHook and hookEval(info) then if not inHook and hookEval(info) then
inHook = true inHook = true
local offset = 2 -- the offset from this function to the code being debugged
local inspectOffset = 0 local inspectOffset = 0
repeat repeat
local done = true 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 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 if cmd == 's' then
hookEval = hookStep() hookEval = stepHook()
elseif cmd == 'n' then elseif cmd == 'n' then
hookEval = hookStepOver() hookEval = stepOverHook()
elseif cmd == 'f' then elseif cmd == 'f' then
hookEval = hookStepOut() hookEval = stepOutHook()
elseif cmd == 'c' then elseif cmd == 'c' then
hookEval = hookBreakpoint hookEval = breakpointHook
elseif cmd == 'd' then -- detach
debug.sethook()
elseif cmd == 'q' then
os.exit(0)
elseif cmd == 'b' then
dbg.breakpoints = param
done = false
elseif cmd == 'i' then elseif cmd == 'i' then
-- inspect stack at this offset -- get snapshot of stack at this offset
inspectOffset = param inspectOffset = param
done = false done = false
else
os.sleep(1)
done = false
end end
until done until done
@@ -184,41 +180,51 @@ local function hook()
end end
end end
local cocreate = coroutine.create function dbg.call(f, ...)
_ENV.coroutine = { } local args = { ... }
for k,v in pairs(_G.coroutine) do return xpcall(
_ENV.coroutine[k] = v function()
end f(table.unpack(args))
_ENV.coroutine.create = function(f, ...) end,
local co = cocreate(f, ...) function(err)
debug.sethook(co, dbg.hook, "l") hookEval = stepHook()
return co
-- An error has occurred
return err
end)
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') debug.sethook(hook, 'l')
-- Expose the debugger's functions -- Expose the debugger's functions
dbg.hook = hook
dbg.exit = function(err) os.exit(err) end
dbg.stopIn = function(fn) dbg.stopIn = function(fn)
hookEval = hookFunction(fn) hookEval = functionHook(fn)
end end
dbg.debugger = nil 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 return dbg

View File

@@ -32,12 +32,13 @@ local client
local function startClient() local function startClient()
local env = kernel.makeEnv(_ENV) local env = kernel.makeEnv(_ENV)
currentFile = nil
local clientId = multishell.openTab(nil, { local clientId = multishell.openTab(nil, {
env = env, env = env,
title = fs.getName(filename):match('([^%.]+)'), title = fs.getName(filename):match('([^%.]+)'),
args = args, args = args,
fn = function(...) fn = function()
local dbg = require('debugger') local dbg = require('debugger')
local fn, msg = loadfile(filename, env) local fn, msg = loadfile(filename, env)
@@ -45,10 +46,14 @@ local function startClient()
error(msg, -1) error(msg, -1)
end end
dbg.debugger = debugger -- breakpoint table is shared across processes
dbg.breakpoints = breakpoints dbg.breakpoints = breakpoints
dbg.debugger = debugger
dbg.stopIn(fn) dbg.stopIn(fn)
fn(...) local s, m = dbg.call(fn, table.unpack(args))
if not s then
error(m, -1)
end
end, end,
}) })
client = kernel.find(clientId) client = kernel.find(clientId)
@@ -79,7 +84,7 @@ romFiles:load()
local function loadSource(file) local function loadSource(file)
currentFile = romFiles:get(file) or file:match('@?(.*)') currentFile = romFiles:get(file) or file:match('@?(.*)')
local src = { } local src = { }
local lines = Util.readLines(currentFile) local lines = Util.readLines(currentFile) or type(file) == 'string' and Util.split(file)
if lines then if lines then
for i = 1, #lines do for i = 1, #lines do
@@ -95,199 +100,210 @@ local function message(...)
end end
local page = UI.Page { local page = UI.Page {
menuBar = UI.MenuBar { backgroundColor = 'black',
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 },
},
},
container = UI.Window { container = UI.Window {
y = 2, ey = '50%', y = 1, ey = '50%',
locals = UI.ScrollingGrid { tabs = UI.Tabs {
ey = -2, ey = -2,
disableHeader = true, unselectedBackgroundColor = '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,
},
statusBar = UI.StatusBar {
ex = -7, y = -1,
backgroundColor = 'primary',
textColor = 'white',
},
UI.Button {
y = -1, x = -6,
event = 'open',
text = 'Open',
}
},
tabs = UI.Tabs { locals = UI.Tab {
y = '50%', title = 'Locals',
source = UI.Tab { index = 1,
title = 'Source', grid = UI.ScrollingGrid {
index = 1, disableHeader = true,
grid = UI.ScrollingGrid { unfocusedBackgroundSelectedColor = 'black',
disableHeader = true, columns = {
columns = { { heading = 'Key', key = 'name' },
{ key = 'marker', width = 1 }, { heading = 'Value', key = 'value', textColor = 'yellow' },
{ key = 'line', textColor = 'cyan', width = 4 }, },
{ heading = 'heading', key = 'source' }, 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 stack = UI.Tab {
return { title = 'Stack',
marker = v.disabled and 'x' or '!', index = 3,
line = row.line, grid = UI.ScrollingGrid {
source = row.source, 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
end end,
return row },
end, },
accelerators = {
t = 'toggle_enabled' 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) 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({ self:emit({
type = 'toggle_breakpoint', type = 'open_file',
file = currentFile, file = event.selected.file,
line = event.selected.line, line = event.selected.line,
}) })
elseif event.type == 'toggle_enabled' then
local line = self:getSelected() and self:getSelected().line elseif event.type == 'remove' then
if line then local bp = self.grid:getSelected()
for _,v in pairs(breakpoints) do if bp then
if v.file == currentFile and v.line == line then Util.removeByValue(self.grid.values, bp)
v.disabled = not v.disabled self:emit({ type = 'update_breakpoints' })
self:emit({ type = 'update_breakpoints' }) end
break
end 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
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 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 { quick_open = UI.QuickSelect {
y = '50%',
modal = true, modal = true,
enable = function() end, enable = function() end,
show = function(self) show = function(self)
@@ -310,26 +326,25 @@ local page = UI.Page {
openFile = function(self, file, line) openFile = function(self, file, line)
if file ~= currentFile then if file ~= currentFile then
local src = loadSource(file) local src = loadSource(file)
self.tabs.source.grid:setValues(src) self.source:setValues(src)
end end
if line then if line then
self.tabs.source.grid:setIndex(#self.tabs.source.grid.values) self.source:setIndex(#self.source.values)
self.tabs.source.grid:setIndex(math.max(1, line - 4)) self.source:setIndex(math.max(1, line - 4))
end end
self.tabs.source.grid:setIndex(line or 1) self.source:setIndex(line or 1)
self.tabs:selectTab(self.tabs.source)
if currentFile == debugFile then if currentFile == debugFile then
self.container.statusBar:setStatus( self.statusBar:setStatus(
string.format('%s : %d', fs.getName(file), debugLine)) string.format('%s : %d', fs.getName(file), debugLine))
else else
self.container.statusBar:setStatus(fs.getName(file)) self.statusBar:setStatus(fs.getName(file))
end end
self:draw() self:draw()
end, end,
eventHandler = function(self, event) eventHandler = function(self, event)
if event.type == 'cmd' then if event.type == 'cmd' then
self.container.statusBar:setStatus('Running...') self.statusBar:setStatus('Running...')
message(event.element.cmd) message(event.element.cmd)
elseif event.type == 'restart' then elseif event.type == 'restart' then
@@ -345,10 +360,9 @@ local page = UI.Page {
self:openFile(event.file, event.line) self:openFile(event.file, event.line)
elseif event.type == 'update_breakpoints' then elseif event.type == 'update_breakpoints' then
self.tabs.breaks.grid:update() self.container.tabs.breaks.grid:update()
self.tabs.breaks.grid:draw() self.container.tabs.breaks.grid:draw()
self.tabs.source.grid:draw() self.source:draw()
message('b', breakpoints)
Config.update('debugger', config) Config.update('debugger', config)
elseif event.type == 'toggle_breakpoint' then elseif event.type == 'toggle_breakpoint' then
@@ -405,26 +419,22 @@ local page = UI.Page {
end, end,
} }
Event.on('debugger', function(_, cmd, data) Event.on('debuggerX', function(_, uid, data)
if cmd == 'info' then if uid == debugger.uid then
kernel.raise(debugger.uid) kernel.raise(debugger.uid)
-- local tab -- local tab
local t = data.locals table.sort(data.locals, function(a, b) return a.name < b.name end)
-- for k,v in pairs(data.locals or { }) do page.container.tabs.locals.grid:setValues(data.locals)
-- table.insert(t, { name = k, value = tostring(v), raw = v }) page.container.tabs.locals.grid.orig = Util.shallowCopy(data.locals)
-- 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)
-- env tab -- env tab
t = { } local t = { }
for k,v in pairs(getfenv(data.info.func)) do for k,v in pairs(getfenv(data.info.func)) do
table.insert(t, { name = k, value = tostring(v), raw = v }) table.insert(t, { name = k, value = tostring(v), raw = v })
end end
page.tabs.env.grid:setValues(t) page.container.tabs.env.grid:setValues(t)
page.tabs.env.grid.orig = Util.shallowCopy(t) page.container.tabs.env.grid.orig = Util.shallowCopy(t)
debugLine = data.info.currentline debugLine = data.info.currentline
debugFile = data.info.source:match('@?(.*)') debugFile = data.info.source:match('@?(.*)')
@@ -433,7 +443,7 @@ Event.on('debugger', function(_, cmd, data)
page:openFile(debugFile, debugLine) page:openFile(debugFile, debugLine)
-- stack -- stack
page.tabs.stack.grid:setValues(data.stack) page.container.tabs.stack.grid:setValues(data.stack)
page:draw() page:draw()
page:sync() page:sync()

View File

@@ -4,16 +4,37 @@ end
local function method(times) local function method(times)
local a = 2 local a = 2
-- use step out to return out of method
for _ = 1, times do for _ = 1, times do
a = a * a a = a * a
end end
return m2(a) return m2(a)
end end
print('before') local chunk = load([[
term.current().clear() local j = 5
print('after') 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 local i = 2
print(i) print(i)
@@ -24,4 +45,4 @@ dofile("rom/modules/main/cc/expect.lua")
print(res) print(res)
print('result: ' .. res) print('result: ' .. res)
error('f') table.insert(res, 5)