--{{{ history --28/03/10 ORX Adjusted show() to work with the Orxonox resource system --15/03/06 DCN Created based on RemDebug --28/04/06 DCN Update for Lua 5.1 --01/06/06 DCN Fix command argument parsing -- Add step/over N facility -- Add trace lines facility --05/06/06 DCN Add trace call/return facility --06/06/06 DCN Make it behave when stepping through the creation of a coroutine --06/06/06 DCN Integrate the simple debugger into the main one --07/06/06 DCN Provide facility to step into coroutines --13/06/06 DCN Fix bug that caused the function environment to get corrupted with the global one --14/06/06 DCN Allow 'sloppy' file names when setting breakpoints --04/08/06 DCN Allow for no space after command name --11/08/06 DCN Use io.write not print --30/08/06 DCN Allow access to array elements in 'dump' --10/10/06 DCN Default to breakfile for all commands that require a filename and give '-' --06/12/06 DCN Allow for punctuation characters in DUMP variable names --03/01/07 DCN Add pause on/off facility --19/06/07 DCN Allow for duff commands being typed in the debugger (thanks to Michael.Bringmann@lsi.com) -- Allow for case sensitive file systems (thanks to Michael.Bringmann@lsi.com) --}}} --{{{ description --A simple command line debug system for Lua written by Dave Nichols of --Match-IT Limited. Its public domain software. Do with it as you wish. --This debugger was inspired by: -- RemDebug 1.0 Beta -- Copyright Kepler Project 2005 (http://www.keplerproject.org/remdebug) --Usage: -- require('debugger') --load the debug library -- pause(message) --start/resume a debug session --An assert() failure will also invoke the debugger. --}}} local IsWindows = string.find(string.lower(os.getenv('OS') or ''),'^windows') local coro_debugger local events = { BREAK = 1, WATCH = 2, STEP = 3, SET = 4 } local breakpoints = {} local watches = {} local step_into = false local step_over = false local step_lines = 0 local step_level = {main=0} local stack_level = {main=0} local trace_level = {main=0} local trace_calls = false local trace_returns = false local trace_lines = false local ret_file, ret_line, ret_name local current_thread = 'main' local started = false local pause_off = false local _g = _G local cocreate, cowrap = coroutine.create, coroutine.wrap local pausemsg = 'pause' --{{{ local hints -- command help --The format in here is name=summary|description local hints = { pause = [[ pause(msg) -- start/resume a debugger session| This can only be used in your code or from the console as a means to start/resume a debug session. If msg is given that is shown when the session starts/resumes. Useful to give a context if you've instrumented your code with pause() statements. ]], poff = [[ poff -- turn off pause() command| This causes all pause() commands to be ignored. This is useful if you have instrumented your code in a busy loop and want to continue normal execution with no further interruption. ]], pon = [[ pon -- turn on pause() command| This re-instates honouring the pause() commands you may have instrumented your code with. ]], setb = [[ setb [line file] -- set a breakpoint to line/file| If file is omitted or is "-" the breakpoint is set at the file for the currently set level (see "set"). Execution pauses when this line is about to be executed and the debugger session is re-activated. The file can be given as the fully qualified name, partially qualified or just the file name. E.g. if file is set as "myfile.lua", then whenever execution reaches any file that ends with "myfile.lua" it will pause. ]], delb = [[ delb [line file] -- removes a breakpoint| If file is omitted or is "-" the breakpoint is removed for the file of the currently set level (see "set"). ]], delallb = [[ delallb -- removes all breakpoints| ]], setw = [[ setw -- adds a new watch expression| The expression is evaluated before each line is executed. If the expression yields true then execution is paused and the debugger session re-activated. The expression is executed in the context of the line about to be executed. ]], delw = [[ delw -- removes the watch expression at index| The index is that returned when the watch expression was set by setw. ]], delallw = [[ delallw -- removes all watch expressions| ]], run = [[ run -- run until next breakpoint or watch expression| ]], step = [[ step [N] -- run next N lines, stepping into function calls| If N is omitted, use 1. ]], over = [[ over [N] -- run next N lines, stepping over function calls| If N is omitted, use 1. ]], out = [[ out [N] -- run lines until stepped out of N functions| If N is omitted, use 1. If you are inside a function, using "out 1" will run until you return from that function to the caller. ]], goto = [[ goto -- step to line number in the current file| The line and current file are those in the currently set context level. ]], listb = [[ listb -- lists breakpoints| ]], listw = [[ listw -- lists watch expressions| ]], set = [[ set [level] -- set context to stack level, omitted=show| If level is omitted it just prints the current level set. This sets the current context to the level given. This affects the context used for several other functions (e.g. vars). The possible levels are those shown by trace. ]], vars = [[ vars [depth] -- list context locals to depth, omitted=1| If depth is omitted then uses 1. Use a depth of 0 for the maximum. Lists all non-nil local variables and all non-nil upvalues in the currently set context. For variables that are tables, lists all fields to the given depth. ]], fenv = [[ fenv [depth] -- list context function env to depth, omitted=1| If depth is omitted then uses 1. Use a depth of 0 for the maximum. Lists all function environment variables in the currently set context. For variables that are tables, lists all fields to the given depth. ]], glob = [[ glob [depth] -- list globals to depth, omitted=1| If depth is omitted then uses 1. Use a depth of 0 for the maximum. Lists all global variables. For variables that are tables, lists all fields to the given depth. ]], ups = [[ ups -- list all the upvalue names| These names will also be in the "vars" list unless their value is nil. This provides a means to identify which vars are upvalues and which are locals. If a name is both an upvalue and a local, the local value takes precedance. ]], locs = [[ locs -- list all the locals names| These names will also be in the "vars" list unless their value is nil. This provides a means to identify which vars are upvalues and which are locals. If a name is both an upvalue and a local, the local value takes precedance. ]], dump = [[ dump [depth] -- dump all fields of variable to depth| If depth is omitted then uses 1. Use a depth of 0 for the maximum. Prints the value of in the currently set context level. If is a table, lists all fields to the given depth. can be just a name, or name.field or name.# to any depth, e.g. t.1.f accesses field 'f' in array element 1 in table 't'. Can also be called from a script as dump(var,depth). ]], tron = [[ tron [crl] -- turn trace on for (c)alls, (r)etuns, (l)lines| If no parameter is given then tracing is turned off. When tracing is turned on a line is printed to the console for each debug 'event' selected. c=function calls, r=function returns, l=lines. ]], trace = [[ trace -- dumps a stack trace| Format is [level] = file,line,name The level is a candidate for use by the 'set' command. ]], info = [[ info -- dumps the complete debug info captured| Only useful as a diagnostic aid for the debugger itself. This information can be HUGE as it dumps all variables to the maximum depth, so be careful. ]], show = [[ show line file X Y -- show X lines before and Y after line in file| If line is omitted or is '-' then the current set context line is used. If file is omitted or is '-' then the current set context file is used. If file is not fully qualified and cannot be opened as specified, then a search for the file in the package[path] is performed using the usual "require" searching rules. If no file extension is given, .lua is used. Prints the lines from the source file around the given line. ]], exit = [[ exit -- exits debugger, re-start it using pause()| ]], help = [[ help [command] -- show this list or help for command| ]], [""] = [[ -- execute a statement in the current context| The statement can be anything that is legal in the context, including assignments. Such assignments affect the context and will be in force immediately. Any results returned are printed. Use '=' as a short-hand for 'return', e.g. "=func(arg)" will call 'func' with 'arg' and print the results, and "=var" will just print the value of 'var'. ]], what = [[ what -- show where is defined (if known)| ]], } --}}} --{{{ local function getinfo(level,field) --like debug.getinfo but copes with no activation record at the given level --and knows how to get 'field'. 'field' can be the name of any of the --activation record fields or any of the 'what' names or nil for everything. --only valid when using the stack level to get info, not a function name. local function getinfo(level,field) level = level + 1 --to get to the same relative level as the caller if not field then return debug.getinfo(level) end local what if field == 'name' or field == 'namewhat' then what = 'n' elseif field == 'what' or field == 'source' or field == 'linedefined' or field == 'lastlinedefined' or field == 'short_src' then what = 'S' elseif field == 'currentline' then what = 'l' elseif field == 'nups' then what = 'u' elseif field == 'func' then what = 'f' else return debug.getinfo(level,field) end local ar = debug.getinfo(level,what) if ar then return ar[field] else return nil end end --}}} --{{{ local function indented( level, ... ) local function indented( level, ... ) io.write( string.rep(' ',level), table.concat({...}), '\n' ) end --}}} --{{{ local function dumpval( level, name, value, limit ) local dumpvisited local function dumpval( level, name, value, limit ) local index if type(name) == 'number' then index = string.format('[%d] = ',name) elseif type(name) == 'string' and (name == '__VARSLEVEL__' or name == '__ENVIRONMENT__' or name == '__GLOBALS__' or name == '__UPVALUES__' or name == '__LOCALS__') then --ignore these, they are debugger generated return elseif type(name) == 'string' and string.find(name,'^[_%a][_.%w]*$') then index = name ..' = ' else index = string.format('[%q] = ',tostring(name)) end if type(value) == 'table' then if dumpvisited[value] then indented( level, index, string.format('ref%q;',dumpvisited[value]) ) else dumpvisited[value] = tostring(value) if (limit or 0) > 0 and level+1 >= limit then indented( level, index, dumpvisited[value] ) else indented( level, index, '{ -- ', dumpvisited[value] ) for n,v in pairs(value) do dumpval( level+1, n, v, limit ) end indented( level, '};' ) end end else if type(value) == 'string' then if string.len(value) > 40 then indented( level, index, '[[', value, ']];' ) else indented( level, index, string.format('%q',value), ';' ) end else indented( level, index, tostring(value), ';' ) end end end --}}} --{{{ local function dumpvar( value, limit, name ) local function dumpvar( value, limit, name ) dumpvisited = {} dumpval( 0, name or tostring(value), value, limit ) end --}}} --{{{ local function show(file,line,before,after) --show +/-N lines of a file around line M local function show(file,line,before,after) line = tonumber(line or 1) before = tonumber(before or 10) after = tonumber(after or before) -- Try to find the file in the Orxonox resources local text = luaState:getSourceCode(file) if text == "" then if not string.find(file,'%.') then file = file..'.lua' end local f = io.open(file,'r') if not f then --{{{ try to find the file in the path -- -- looks for a file in the package path -- local path = package.path or LUA_PATH or '' for c in string.gmatch (path, "[^;]+") do local c = string.gsub (c, "%?%.lua", file) f = io.open (c,'r') if f then break end end --}}} if f then -- Read file into 'text' text = f:read("*a") f:close() else io.write('Cannot find '..file..'\n') return end end end -- Transform line endings to \n text:gsub("\r\n", "\n") -- Windows to Unix text:gsub("\r", "\n") -- Mac to Unix if text[-1] ~= "\n" then text = text.."\n" end -- Print requested lines local i = 0 for l in text:gmatch("[^\n]*[\n]") do i = i + 1 if i >= (line-before) then if i > (line+after) then break end if i == line then io.write(i..'***\t'..l) else io.write(i..'\t'..l) end end end end --}}} --{{{ local function tracestack(l) local function gi( i ) return function() i=i+1 return debug.getinfo(i),i end end local function gl( level, j ) return function() j=j+1 return debug.getlocal( level, j ) end end local function gu( func, k ) return function() k=k+1 return debug.getupvalue( func, k ) end end local traceinfo local function tracestack(l) local l = l + 1 --NB: +1 to get level relative to caller traceinfo = {} traceinfo.pausemsg = pausemsg for ar,i in gi(l) do table.insert( traceinfo, ar ) local names = {} local values = {} for n,v in gl(i,0) do if string.sub(n,1,1) ~= '(' then --ignore internal control variables table.insert( names, n ) table.insert( values, v ) end end if #names > 0 then ar.lnames = names ar.lvalues = values end if ar.func then local names = {} local values = {} for n,v in gu(ar.func,0) do if string.sub(n,1,1) ~= '(' then --ignore internal control variables table.insert( names, n ) table.insert( values, v ) end end if #names > 0 then ar.unames = names ar.uvalues = values end end end end --}}} --{{{ local function trace() local function trace(set) local mark for level,ar in ipairs(traceinfo) do if level == set then mark = '***' else mark = '' end io.write('['..level..']'..mark..'\t'..(ar.name or ar.what)..' in '..ar.short_src..':'..ar.currentline..'\n') end end --}}} --{{{ local function info() local function info() dumpvar( traceinfo, 0, 'traceinfo' ) end --}}} --{{{ local function set_breakpoint(file, line) local function set_breakpoint(file, line) if not breakpoints[line] then breakpoints[line] = {} end breakpoints[line][file] = true end --}}} --{{{ local function remove_breakpoint(file, line) local function remove_breakpoint(file, line) if breakpoints[line] then breakpoints[line][file] = nil end end --}}} --{{{ local function has_breakpoint(file, line) --allow for 'sloppy' file names --search for file and all variations walking up its directory hierachy --ditto for the file with no extension local function has_breakpoint(file, line) if not breakpoints[line] then return false end local noext = string.gsub(file,"(%..-)$",'',1) if noext == file then noext = nil end while file do if breakpoints[line][file] then return true end file = string.match(file,"[:/\](.+)$") end while noext do if breakpoints[line][noext] then return true end noext = string.match(noext,"[:/\](.+)$") end return false end --}}} --{{{ local function capture_vars(ref,level,line) local function capture_vars(ref,level,line) --get vars, file and line for the given level relative to debug_hook offset by ref local lvl = ref + level --NB: This includes an offset of +1 for the call to here --{{{ capture variables local ar = debug.getinfo(lvl, "f") if not ar then return {},'?',0 end local vars = {__UPVALUES__={}, __LOCALS__={}} local i local func = ar.func if func then i = 1 while true do local name, value = debug.getupvalue(func, i) if not name then break end if string.sub(name,1,1) ~= '(' then --NB: ignoring internal control variables vars[name] = value vars.__UPVALUES__[i] = name end i = i + 1 end vars.__ENVIRONMENT__ = getfenv(func) end vars.__GLOBALS__ = getfenv(0) i = 1 while true do local name, value = debug.getlocal(lvl, i) if not name then break end if string.sub(name,1,1) ~= '(' then --NB: ignoring internal control variables vars[name] = value vars.__LOCALS__[i] = name end i = i + 1 end vars.__VARSLEVEL__ = level if func then --NB: Do not do this until finished filling the vars table setmetatable(vars, { __index = getfenv(func), __newindex = getfenv(func) }) end --NB: Do not read or write the vars table anymore else the metatable functions will get invoked! --}}} local file = getinfo(lvl, "source") if string.find(file, "@") == 1 then file = string.sub(file, 2) end -- Orxonox changes: Our resource system is case sensisive, even on Windows --if IsWindows then file = string.lower(file) end if not line then line = getinfo(lvl, "currentline") end return vars,file,line end --}}} --{{{ local function restore_vars(ref,vars) local function restore_vars(ref,vars) if type(vars) ~= 'table' then return end local level = vars.__VARSLEVEL__ --NB: This level is relative to debug_hook offset by ref if not level then return end level = level + ref --NB: This includes an offset of +1 for the call to here local i local written_vars = {} i = 1 while true do local name, value = debug.getlocal(level, i) if not name then break end if vars[name] and string.sub(name,1,1) ~= '(' then --NB: ignoring internal control variables debug.setlocal(level, i, vars[name]) written_vars[name] = true end i = i + 1 end local ar = debug.getinfo(level, "f") if not ar then return end local func = ar.func if func then i = 1 while true do local name, value = debug.getupvalue(func, i) if not name then break end if vars[name] and string.sub(name,1,1) ~= '(' then --NB: ignoring internal control variables if not written_vars[name] then debug.setupvalue(func, i, vars[name]) end written_vars[name] = true end i = i + 1 end end end --}}} --{{{ local function trace_event(event, line, level) local function print_trace(level,depth,event,file,line,name) --NB: level here is relative to the caller of trace_event, so offset by 2 to get to there level = level + 2 local file = file or getinfo(level,'short_src') local line = line or getinfo(level,'currentline') local name = name or getinfo(level,'name') local prefix = '' if current_thread ~= 'main' then prefix = '['..tostring(current_thread)..'] ' end io.write(prefix.. string.format('%08.2f:%02i.',os.clock(),depth).. string.rep('.',depth%32).. (file or '')..' ('..(line or '')..') '.. (name or '').. ' ('..event..')\n') end local function trace_event(event, line, level) if event == 'return' and trace_returns then --note the line info for later ret_file = getinfo(level+1,'short_src') ret_line = getinfo(level+1,'currentline') ret_name = getinfo(level+1,'name') end if event ~= 'line' then return end local slevel = stack_level[current_thread] local tlevel = trace_level[current_thread] if trace_calls and slevel > tlevel then --we are now in the function called, so look back 1 level further to find the calling file and line print_trace(level+1,slevel-1,'c',nil,nil,getinfo(level+1,'name')) end if trace_returns and slevel < tlevel then print_trace(level,slevel,'r',ret_file,ret_line,ret_name) end if trace_lines then print_trace(level,slevel,'l') end trace_level[current_thread] = stack_level[current_thread] end --}}} --{{{ local function debug_hook(event, line, level, thread) local function debug_hook(event, line, level, thread) if not started then debug.sethook() return end current_thread = thread or 'main' local level = level or 2 trace_event(event,line,level) if event == "call" then stack_level[current_thread] = stack_level[current_thread] + 1 elseif event == "return" then stack_level[current_thread] = stack_level[current_thread] - 1 if stack_level[current_thread] < 0 then stack_level[current_thread] = 0 end else local vars,file,line = capture_vars(level,1,line) local stop, ev, idx = false, events.STEP, 0 while true do for index, value in pairs(watches) do setfenv(value.func, vars) local status, res = pcall(value.func) if status and res then ev, idx = events.WATCH, index stop = true break end end if stop then break end if (step_into) or (step_over and (stack_level[current_thread] <= step_level[current_thread] or stack_level[current_thread] == 0)) then step_lines = step_lines - 1 if step_lines < 1 then ev, idx = events.STEP, 0 break end end if has_breakpoint(file, line) then ev, idx = events.BREAK, 0 break end return end tracestack(level) local last_next = 1 local err, next = assert(coroutine.resume(coro_debugger, ev, vars, file, line, idx)) while true do if next == 'cont' then return elseif next == 'stop' then started = false debug.sethook() return elseif tonumber(next) then --get vars for given level or last level next = tonumber(next) if next == 0 then next = last_next end last_next = next restore_vars(level,vars) vars, file, line = capture_vars(level,next) err, next = assert(coroutine.resume(coro_debugger, events.SET, vars, file, line, idx)) else io.write('Unknown command from debugger_loop: '..tostring(next)..'\n') io.write('Stopping debugger\n') next = 'stop' end end end end --}}} --{{{ local function report(ev, vars, file, line, idx_watch) local function report(ev, vars, file, line, idx_watch) local vars = vars or {} local file = file or '?' local line = line or 0 local prefix = '' if current_thread ~= 'main' then prefix = '['..tostring(current_thread)..'] ' end if ev == events.STEP then io.write(prefix.."Paused at file "..file.." line "..line..' ('..stack_level[current_thread]..')\n') elseif ev == events.BREAK then io.write(prefix.."Paused at file "..file.." line "..line..' ('..stack_level[current_thread]..') (breakpoint)\n') elseif ev == events.WATCH then io.write(prefix.."Paused at file "..file.." line "..line..' ('..stack_level[current_thread]..')'.." (watch expression "..idx_watch.. ": ["..watches[idx_watch].exp.."])\n") elseif ev == events.SET then --do nothing else io.write(prefix.."Error in application: "..file.." line "..line.."\n") end if ev ~= events.SET then if pausemsg and pausemsg ~= '' then io.write('Message: '..pausemsg..'\n') end pausemsg = '' end return vars, file, line end --}}} --{{{ local function debugger_loop(server) local function debugger_loop(ev, vars, file, line, idx_watch) io.write("Lua Debugger\n") local eval_env, breakfile, breakline = report(ev, vars, file, line, idx_watch) io.write("Type 'help' for commands\n") local command, args --{{{ local function getargs(spec) --get command arguments according to the given spec from the args string --the spec has a single character for each argument, arguments are separated --by white space, the spec characters can be one of: -- F for a filename (defaults to breakfile if - given in args) -- L for a line number (defaults to breakline if - given in args) -- N for a number -- V for a variable name -- S for a string local function getargs(spec) local res={} local char,arg local ptr=1 for i=1,string.len(spec) do char = string.sub(spec,i,i) if char == 'F' then _,ptr,arg = string.find(args..' ',"%s*([%w%p]*)%s*",ptr) if not arg or arg == '' then arg = '-' end if arg == '-' then arg = breakfile end elseif char == 'L' then _,ptr,arg = string.find(args..' ',"%s*([%w%p]*)%s*",ptr) if not arg or arg == '' then arg = '-' end if arg == '-' then arg = breakline end arg = tonumber(arg) or 0 elseif char == 'N' then _,ptr,arg = string.find(args..' ',"%s*([%w%p]*)%s*",ptr) if not arg or arg == '' then arg = '0' end arg = tonumber(arg) or 0 elseif char == 'V' then _,ptr,arg = string.find(args..' ',"%s*([%w%p]*)%s*",ptr) if not arg or arg == '' then arg = '' end elseif char == 'S' then _,ptr,arg = string.find(args..' ',"%s*([%w%p]*)%s*",ptr) if not arg or arg == '' then arg = '' end else arg = '' end table.insert(res,arg or '') end return unpack(res) end --}}} while true do io.write("[DEBUG]> ") local line = io.read("*line") if line == nil then io.write('\n'); line = 'exit' end if string.find(line, "^[a-z]+") then command = string.sub(line, string.find(line, "^[a-z]+")) args = string.gsub(line,"^[a-z]+%s*",'',1) --strip command off line else command = '' end if command == "setb" then --{{{ set breakpoint local line, filename = getargs('LF') if filename ~= '' and line ~= '' then set_breakpoint(filename,line) io.write("Breakpoint set in file "..filename..' line '..line..'\n') else io.write("Bad request\n") end --}}} elseif command == "delb" then --{{{ delete breakpoint local line, filename = getargs('LF') if filename ~= '' and line ~= '' then remove_breakpoint(filename, line) io.write("Breakpoint deleted from file "..filename..' line '..line.."\n") else io.write("Bad request\n") end --}}} elseif command == "delallb" then --{{{ delete all breakpoints breakpoints = {} io.write('All breakpoints deleted\n') --}}} elseif command == "listb" then --{{{ list breakpoints for i, v in pairs(breakpoints) do for ii, vv in pairs(v) do io.write("Break at: "..i..' in '..ii..'\n') end end --}}} elseif command == "setw" then --{{{ set watch expression if args and args ~= '' then local func = loadstring("return(" .. args .. ")") local newidx = #watches + 1 watches[newidx] = {func = func, exp = args} io.write("Set watch exp no. " .. newidx..'\n') else io.write("Bad request\n") end --}}} elseif command == "delw" then --{{{ delete watch expression local index = tonumber(args) if index then watches[index] = nil io.write("Watch expression deleted\n") else io.write("Bad request\n") end --}}} elseif command == "delallw" then --{{{ delete all watch expressions watches = {} io.write('All watch expressions deleted\n') --}}} elseif command == "listw" then --{{{ list watch expressions for i, v in pairs(watches) do io.write("Watch exp. " .. i .. ": " .. v.exp..'\n') end --}}} elseif command == "run" then --{{{ run until breakpoint step_into = false step_over = false eval_env, breakfile, breakline = report(coroutine.yield('cont')) --}}} elseif command == "step" then --{{{ step N lines (into functions) local N = tonumber(args) or 1 step_over = false step_into = true step_lines = tonumber(N or 1) eval_env, breakfile, breakline = report(coroutine.yield('cont')) --}}} elseif command == "over" then --{{{ step N lines (over functions) local N = tonumber(args) or 1 step_into = false step_over = true step_lines = tonumber(N or 1) step_level[current_thread] = stack_level[current_thread] eval_env, breakfile, breakline = report(coroutine.yield('cont')) --}}} elseif command == "out" then --{{{ step N lines (out of functions) local N = tonumber(args) or 1 step_into = false step_over = true step_lines = 1 step_level[current_thread] = stack_level[current_thread] - tonumber(N or 1) eval_env, breakfile, breakline = report(coroutine.yield('cont')) --}}} elseif command == "goto" then --{{{ step until reach line local N = tonumber(args) if N then step_over = false step_into = false if has_breakpoint(breakfile,N) then eval_env, breakfile, breakline = report(coroutine.yield('cont')) else local bf = breakfile set_breakpoint(breakfile,N) eval_env, breakfile, breakline = report(coroutine.yield('cont')) if breakfile == bf and breakline == N then remove_breakpoint(breakfile,N) end end else io.write("Bad request\n") end --}}} elseif command == "set" then --{{{ set/show context level local level = args if level and level == '' then level = nil end if level then eval_env, breakfile, breakline = report(coroutine.yield(level)) end if eval_env.__VARSLEVEL__ then io.write('Level: '..eval_env.__VARSLEVEL__..'\n') else io.write('No level set\n') end --}}} elseif command == "vars" then --{{{ list context variables local depth = args if depth and depth == '' then depth = nil end depth = tonumber(depth) or 1 dumpvar(eval_env, depth+1, 'variables') --}}} elseif command == "glob" then --{{{ list global variables local depth = args if depth and depth == '' then depth = nil end depth = tonumber(depth) or 1 dumpvar(eval_env.__GLOBALS__,depth+1,'globals') --}}} elseif command == "fenv" then --{{{ list function environment variables local depth = args if depth and depth == '' then depth = nil end depth = tonumber(depth) or 1 dumpvar(eval_env.__ENVIRONMENT__,depth+1,'environment') --}}} elseif command == "ups" then --{{{ list upvalue names dumpvar(eval_env.__UPVALUES__,2,'upvalues') --}}} elseif command == "locs" then --{{{ list locals names dumpvar(eval_env.__LOCALS__,2,'upvalues') --}}} elseif command == "what" then --{{{ show where a function is defined if args and args ~= '' then local v = eval_env local n = nil for w in string.gmatch(args,"[%w_]+") do v = v[w] if n then n = n..'.'..w else n = w end if not v then break end end if type(v) == 'function' then local def = debug.getinfo(v,'S') if def then io.write(def.what..' in '..def.short_src..' '..def.linedefined..'..'..def.lastlinedefined..'\n') else io.write('Cannot get info for '..v..'\n') end else io.write(v..' is not a function\n') end else io.write("Bad request\n") end --}}} elseif command == "dump" then --{{{ dump a variable local name, depth = getargs('VN') if name ~= '' then if depth == '' or depth == 0 then depth = nil end depth = tonumber(depth or 1) local v = eval_env local n = nil for w in string.gmatch(name,"[^%.]+") do --get everything between dots if tonumber(w) then v = v[tonumber(w)] else v = v[w] end if n then n = n..'.'..w else n = w end if not v then break end end dumpvar(v,depth+1,n) else io.write("Bad request\n") end --}}} elseif command == "show" then --{{{ show file around a line or the current breakpoint local line, file, before, after = getargs('LFNN') if before == 0 then before = 10 end if after == 0 then after = before end if file ~= '' and file ~= "=stdin" then show(file,line,before,after) else io.write('Nothing to show\n') end --}}} elseif command == "poff" then --{{{ turn pause command off pause_off = true --}}} elseif command == "pon" then --{{{ turn pause command on pause_off = false --}}} elseif command == "tron" then --{{{ turn tracing on/off local option = getargs('S') trace_calls = false trace_returns = false trace_lines = false if string.find(option,'c') then trace_calls = true end if string.find(option,'r') then trace_returns = true end if string.find(option,'l') then trace_lines = true end --}}} elseif command == "trace" then --{{{ dump a stack trace trace(eval_env.__VARSLEVEL__) --}}} elseif command == "info" then --{{{ dump all debug info captured info() --}}} elseif command == "pause" then --{{{ not allowed in here io.write('pause() should only be used in the script you are debugging\n') --}}} elseif command == "help" then --{{{ help local command = getargs('S') if command ~= '' and hints[command] then io.write(hints[command]..'\n') else for _,v in pairs(hints) do local _,_,h = string.find(v,"(.+)|") io.write(h..'\n') end end --}}} elseif command == "exit" then --{{{ exit debugger return 'stop' --}}} elseif line ~= '' then --{{{ just execute whatever it is in the current context --map line starting with "=..." to "return ..." if string.sub(line,1,1) == '=' then line = string.gsub(line,'=','return ',1) end local ok, func = pcall(loadstring,line) if func == nil then --Michael.Bringmann@lsi.com io.write("Compile error: "..line..'\n') elseif not ok then io.write("Compile error: "..func..'\n') else setfenv(func, eval_env) local res = {pcall(func)} if res[1] then if res[2] then table.remove(res,1) for _,v in ipairs(res) do io.write(tostring(v)) io.write('\t') end io.write('\n') end --update in the context eval_env, breakfile, breakline = report(coroutine.yield(0)) else io.write("Run error: "..res[2]..'\n') end end --}}} end end end --}}} --{{{ coroutine.create --This function overrides the built-in for the purposes of propagating --the debug hook settings from the creator into the created coroutine. _G.coroutine.create = function(f) local thread local hook, mask, count = debug.gethook() if hook then local function thread_hook(event,line) hook(event,line,3,thread) end thread = cocreate(function(...) stack_level[thread] = 0 trace_level[thread] = 0 step_level [thread] = 0 debug.sethook(thread_hook,mask,count) return f(...) end) return thread else return cocreate(f) end end --}}} --{{{ coroutine.wrap --This function overrides the built-in for the purposes of propagating --the debug hook settings from the creator into the created coroutine. _G.coroutine.wrap = function(f) local thread local hook, mask, count = debug.gethook() if hook then local function thread_hook(event,line) hook(event,line,3,thread) end thread = cowrap(function(...) stack_level[thread] = 0 trace_level[thread] = 0 step_level [thread] = 0 debug.sethook(thread_hook,mask,count) return f(...) end) return thread else return cowrap(f) end end --}}} --{{{ function pause() -- -- Starts/resumes a debug session -- function pause(x) if pause_off then return end --being told to ignore pauses pausemsg = x or 'pause' local lines local src = getinfo(2,'short_src') if src == "stdin" then lines = 1 --if in a console session, stop now else lines = 2 --if in a script, stop when get out of pause() end if started then --we'll stop now 'cos the existing debug hook will grab us step_lines = lines step_into = true else coro_debugger = cocreate(debugger_loop) --NB: Use original coroutune.create --set to stop when get out of pause() trace_level[current_thread] = 0 step_level [current_thread] = 0 stack_level[current_thread] = 1 step_lines = lines step_into = true started = true debug.sethook(debug_hook, "crl") --NB: this will cause an immediate entry to the debugger_loop end end --}}} --{{{ function dump() --shows the value of the given variable, only really useful --when the variable is a table --see dump debug command hints for full semantics function dump(v,depth) dumpvar(v,(depth or 1)+1,tostring(v)) end --}}} --{{{ function debug.traceback(x) local _traceback = debug.traceback --note original function --override standard function debug.traceback = function(x) local assertmsg = _traceback(x) --do original function pause(x) --let user have a look at stuff return assertmsg --carry on end _TRACEBACK = debug.traceback --Lua 5.0 function --}}}