print("debugger---") --GDB = true --{{{ history --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 tinsert = table.insert local strfind = string.find local strsub = string.sub local strlower = string.lower local gsub = string.gsub local write = io.write local esc = string.char(26) local gprefix = esc..esc local bpattern print(GDB, WIN) if GDB then require 'dbgl' if WIN then bpattern = '([a-z]:[^:]+):(%d+)' else bpattern = '([^:]+):(%d+)' end end local IsWindows = strfind(strlower(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 step local tracing = false 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 start_t, msg_t local function start_timer(msg) start_t = os.clock() msg_t = msg end local ferr = io.stderr local function errf(fmt, ...) ferr:write(fmt:format(...)) end local function end_timer() -- errf("%s: took %7.2f sec\n",msg_t,os.clock()-start_t) end local hints = { } -- Some 'pretty printing' code. In particular, it will try to expand tables, up to -- a specified number of elements. -- (Based on ilua) local pretty_print_limit = 20 local max_depth = 7 local jstack = {} local push = tinsert local pop = table.remove --local getn = table.getn local function is_map_like(tbl) for k, v in pairs(tbl)do if type(k)~= 'number' then return true end end return false end local function join(tbl, delim, limit, depth) if not limit then limit = pretty_print_limit end if not depth then depth = max_depth end local n = #(tbl) local res = '' local k = 0 -- very important to avoid disgracing ourselves with circular references or -- excessively nested tables... if #(jstack)> depth then return "..." end for i, t in ipairs(jstack)do if tbl == t then return "" end end push(jstack, tbl) -- a table may have a 'list-like' part if it has a non-zero size -- and may have have a 'map-like' part if it has non-numerical keys local is_list, is_map is_list = #(tbl)> 0 is_map = is_map_like(tbl) if is_list then for i, v in ipairs(tbl)do res = res..delim..val2str(v) k = k + 1 if k > limit then res = res.." ... " break end end end if is_map then for key, v in pairs(tbl)do local num = type(key) == 'number' key = tostring(key) if not num or(num and not is_list)then if num then key = '['..key..']' end res = res..delim..key..'='..val2str(v) k = k + 1 if k > limit then res = res.." ... " break end end end end pop(jstack) return strsub(res, 2) end function val2str(val) local tp = type(val) if tp == 'function' then return tostring(val) elseif tp == 'table' then if val.__tostring then return tostring(val) else return '{'..join(val, ',')..'}' end elseif tp == 'string' then return "'"..val.."'" elseif tp == 'number' then return tostring(val) else return tostring(val) end end --{{{ 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, ...) 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 strfind(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) -- SJD: if a qualified module name is given, then we can use that.... if not strfind(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 = gsub(c, "%?%.lua", file) f = io.open(c, 'r') if f then break end end --}}} if not f then write('Cannot find '..file..'\n') return end end local i = 0 for l in f:lines()do i = i + 1 if i >= (line-before)then if i > (line+after)then break end if i == line then write(i..'***\t'..l..'\n') else write(i..'\t'..l..'\n') end end end f:close() 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, tracing) local l = l + 1 --NB: +1 to get level relative to caller traceinfo = {} traceinfo.pausemsg = pausemsg for ar, i in gi(l)do tinsert(traceinfo, ar) if tracing then local names = {} local values = {} for n, v in gl(i, 0)do if strsub(n, 1, 1)~= '(' then --ignore internal control variables tinsert(names, n) tinsert(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 strsub(n, 1, 1)~= '(' then --ignore internal control variables tinsert(names, n) tinsert(values, v) end end if #names > 0 then ar.unames = names ar.uvalues = values end end end end end --}}} --{{{ local function trace() local function trace(set) local mark if not traceinfo then return end for level, ar in ipairs(traceinfo)do local description = (ar.name or ar.what)..' in '..ar.short_src..':'..ar.currentline if GDB then write('#', level, ' ', description) else if level == set then mark = '***' else mark = '' end write('['..level..']\t'..description..' '..mark..'\n') end 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) ferr:write(tostring(file)..' '..tostring(line)..'\n') 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) --when driving with a debugger front-end, there is the opposite problem; --paths are generally given as absolute, and one must convert as necessary --However, breakpoint lookup becomes simpler. local function has_breakpoint(file, line) local bpl = breakpoints[line] if bpl then return breakpoints[line][file] end 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 strsub(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 strsub(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 strfind(file, "@") == 1 then file = strsub(file, 2) end 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 strsub(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 strsub(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 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 curfile, thisfile, project_root_path, dirsep, stepping_out local addresses = {} if IsWindows then dirsep = '\\' else dirsep = '/' end local function set_debug_break() local addr = dbgl.c_addr() if addr and not addresses[addr] then write('//@// '..addr..'\n') stepping_out = true --addresses[addr] = true dbgl.debug_break() end end -- *SJD* optimizations: -- capture_vars() is expensive; now it's only called if: -- - we have active watches -- - if we actually break execution local in_debugger local function debug_hook(event, line, level, thread) local vars, file if not started then debug.sethook()return end current_thread = thread or 'main' --print(event,line,step_into,step_lines) local level = level or 2 if tracing then trace_event(event, line, level)end if event == "call" then if step_into and GDB then -- this might be a C function, prepare to step into it... local ar = debug.getinfo(level, "S") if ar.what == 'C' and not in_debugger then set_debug_break() end end stack_level[current_thread] = stack_level[current_thread] + 1 elseif event == "return" or event == "tail return" then local sl = stack_level[current_thread] - 1 stack_level[current_thread] = sl if sl < 0 then stack_level[current_thread] = 0 end if stepping_out then stepping_out = false -- write(gprefix,thisfile,':',line,'\n') end else local ar = debug.getinfo(level, "S") local rawfile = ar.source if not line then line = debug.getinfo(level, "l").currentline end -- local vars,file,line = capture_vars(level,1,line) --SJD the idea here is to keep track of the _absolute_ filename --we are told up front what the project path is; anything which is relative will be relative to this path. if rawfile ~= curfile then file = rawfile curfile = rawfile if strfind(file, "@") == 1 then file = strsub(file, 2)end if IsWindows then file = strlower(file)end if strsub(file, 1, 1) == '/' or strsub(file, 2, 2) == ':' then -- an absolute path if IsWindows then file = file:gsub('/', '\\')-- canonical for Win32! end elseif project_root_path then -- relative path file = project_root_path..dirsep..file end thisfile = file end local stop, ev, idx = false, events.STEP, 0 while true do if #watches > 0 then -- only capture vars if we're tracing vars = capture_vars(level, 1, line) 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 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 vars = capture_vars(level, 1, line) ev, idx = events.STEP, 0 break end end --print('!!',thisfile,line) if has_breakpoint(thisfile, line)then vars = capture_vars(level, 1, line) ev, idx = events.BREAK, 0 break end return end tracestack(level, tracing) local last_next = 1 local err, next = assert(coroutine.resume(coro_debugger, ev, vars, thisfile, line, idx)) while true do if next == 'cont' then return elseif next == 'stop' then started = false write("Program finished\n") 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 write('Unknown command from debugger_loop: '..tostring(next)..'\n') write('Stopping debugger\n') next = 'stop' end end end end --}}} local display_exprs = {} local function eval(env, line) --~ print('eval "'..line..'"') local ok, func = pcall(loadstring, line) if func == nil or not ok then return nil else setfenv(func, env) return pcall(func) end end local function dump_display(env) for i, disp in ipairs(display_exprs)do local res, value = eval(env, 'return '..disp) if res then write('<'..disp..'> = '..val2str(value)..'\n') 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 = '' --SJD put the message out first, so we know if it's a crash! write '\n' if ev ~= events.SET then if pausemsg and pausemsg ~= '' then write('Message: '..pausemsg..'\n')end pausemsg = '' end if GDB then if ev == events.STEP or ev == events.BREAK or ev == events.WATCH then write(gprefix..file..':'..line..'\n') end else if current_thread ~= 'main' then prefix = '['..tostring(current_thread)..'] ' end if ev == events.STEP then write(prefix.."Paused at file "..file.." line "..line..' ('..stack_level[current_thread]..')\n') dump_display(vars) elseif ev == events.BREAK then write(prefix.."Paused at file "..file.." line "..line..' ('..stack_level[current_thread]..') (breakpoint)\n') end_timer() dump_display(vars) elseif ev == events.WATCH then write(prefix.."Paused at file "..file.." line "..line..' ('..stack_level[current_thread]..')'.." (watch expression "..idx_watch.. ": ["..watches[idx_watch].exp.."])\n") dump_display(vars) elseif ev == events.SET then --do nothing else write(prefix.."Error in application: "..file.." line "..line.."\n") end end return vars, file, line end --}}} local initial_commands = {} local kount = 1 --{{{ local function debugger_loop(server) local prompt if GDB then prompt = '(GDB)\n' else prompt = '(DBG)\n' end local function debugger_loop(ev, vars, file, line, idx_watch) write("Lua Debugger\n") local eval_env, breakfile, breakline = report(ev, vars, file, line, idx_watch) 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 = strsub(spec, i, i) if char == 'F' then _, ptr, arg = strfind(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 = strfind(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 = strfind(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 = strfind(args..' ', "%s*([%w%p]*)%s*", ptr) if not arg or arg == '' then arg = '' end elseif char == 'S' then _, ptr, arg = strfind(args..' ', "%s*([%w%p]*)%s*", ptr) if not arg or arg == '' then arg = '' end else arg = '' end tinsert(res, arg or '') end return unpack(res) end --}}} while true do write(prompt)--SJD temporary local line if initial_commands and #initial_commands > 0 then line = table.remove(initial_commands, 1) else initial_commands = nil line = io.read("*line") end if line == nil then write('\n'); line = 'exit' end if strfind(line, "^[a-z]+")then command = strsub(line, strfind(line, "^[a-z]+")) args = gsub(line, "^[a-z]+%s*", '', 1)--strip command off line else command = '' end --in_debugger = true if command == "setb" or command == 'break' then --{{{ set breakpoint write(args..'\n') local line, filename if command == 'setb' then line, filename = getargs('LF') else filename, line = args:match(bpattern) line = tonumber(line) end if filename ~= '' and line ~= '' and line ~= nil then set_breakpoint(filename, line) write("Breakpoint set in file "..filename..' line '..line..'\n') else write("Bad request\n") end --}}} elseif command == "delb" or command == 'clear' then --{{{ delete breakpoint local line, filename if command == 'delb' then line, filename = getargs('LF') else filename, line = args:match('([^:]+):(%d+)') line = tonumber(line) end if filename ~= '' and line ~= '' then remove_breakpoint(filename, line) write("Breakpoint deleted from file "..filename..' line '..line.."\n") else write("Bad request\n") end --}}} elseif command == "debugbreak" then if GDB then dbgl.debug_break() end elseif command == "delallb" then --{{{ delete all breakpoints breakpoints = {} 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 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} write("Set watch exp no. " .. newidx..'\n') else write("Bad request\n") end --}}} elseif command == "delw" then --{{{ delete watch expression local index = tonumber(args) if index then watches[index] = nil write("Watch expression deleted\n") else write("Bad request\n") end --}}} elseif command == "delallw" then --{{{ delete all watch expressions watches = {} write('All watch expressions deleted\n') --}}} elseif command == "listw" then --{{{ list watch expressions for i, v in pairs(watches)do write("Watch exp. " .. i .. ": " .. v.exp..'\n') end --}}} elseif command == "run" or command == 'cont' 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" or command == 'next' 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" or command == 'finish' 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 write("Bad request\n") end --}}} elseif command == "set" or command == 'frame' 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 write('Level: '..eval_env.__VARSLEVEL__..'\n') else 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 write(def.what..' in '..def.short_src..' '..def.linedefined..'..'..def.lastlinedefined..'\n') else write('Cannot get info for '..v..'\n') end else write(v..' is not a function\n') end else 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 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 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 tracing = false if strfind(option, 'c')then trace_calls = true; tracing = true end if strfind(option, 'r')then trace_returns = true; tracing = true end if strfind(option, 'l')then trace_lines = true; tracing = true end --}}} elseif command == "trace" then --{{{ dump a stack trace trace(eval_env.__VARSLEVEL__) --}}} elseif command == "info" then --{{{ dump all debug info captured write('info?\n') info() --}}} elseif command == "pause" then --{{{ not allowed in here 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 write(hints[command]..'\n') else for _, v in pairs(hints)do local _, _, h = strfind(v, "(.+)|") write(h..'\n') end end --}}} elseif command == "display" then --{{{ Add a variable to the display list local expr = getargs('S') if expr == "" then for i, d in ipairs(display_exprs)do write(d, '\n') end else tinsert(display_exprs, expr) end --}}} elseif command == "undisplay" then --{{{ clear the display list display_exprs = {} --}}} elseif command == "up" or command == "down" then local level = eval_env.__VARSLEVEL__ if command == "up" and level >= 1 then level = level + 1 else level = level - 1 end eval_env, breakfile, breakline = report(coroutine.yield(level)) if eval_env.__VARSLEVEL__ then if GDB then breakfile = project_root_path..dirsep..breakfile end report(events.BREAK, eval_env, breakfile, breakline) end elseif command == "rootpath" then --SJD scite-debug integration project_root_path = getargs('S') curfile = nil -- force renormalization of current filename write('rootpath '..project_root_path..'\n') elseif command == "exit" or command == 'quit' then --{{{ exit debugger return 'stop' --}}} elseif line ~= '' then --{{{ just execute whatever it is in the current context --map line starting with "=..." to "return ..." -- SJD: also 'eval ' and 'print ', which has a special meaning to scite-debug local scite_debug_print local scite_debug_eval = line:find('^eval ') if GDB then scite_debug_print = line:find('^print ')end if scite_debug_eval then line = line:gsub('eval ', 'return ', 1) elseif scite_debug_print then line = line:gsub('print ', 'return ', 1) elseif line:sub(1, 1) == '=' then line = line:gsub('=', 'return ', 1) end write('expr ', line, '\n') local ok, func = pcall(loadstring, line) if func == nil then --Michael.Bringmann@lsi.com write("Compile error: "..line..'\n') elseif not ok then 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) if scite_debug_eval then --SJD give scite-debug a clear marker write('= '..val2str(res[1]), '\n') elseif scite_debug_print then -- GDB-style results! write('$'..kount..' = '..val2str(res[1])..'\n') kount = kount + 1 else for _, v in ipairs(res)do write(tostring(v)) write('\t') end write('\n') end end --update in the context eval_env, breakfile, breakline = report(coroutine.yield(0)) else write("Run error: "..res[2]..'\n') end end --}}} in_debugger = false end end end_timer() 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 --SJD: see if we can open clidebug.cmd local f = io.open('clidebug.cmd') if f then for line in f:lines()do tinsert(initial_commands, line) end f:close() else write('no command file found\n') end start_timer 'BREAK' 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 --}}}