--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 = getn(tbl) local res = '' local k = 0 -- very important to avoid disgracing ourselves with circular references or -- excessively nested tables... if getn(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 = getn(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 --}}} -- this value distinguishes temporary from persistent breakpoints local TEMPORARY = 1 --{{{ local function set_breakpoint(file, line) local function set_breakpoint(file, line, value) if not breakpoints[line] then breakpoints[line] = {} end breakpoints[line][file] = value 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) 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 local file_cache = {} local function exists (path) if file_cache[path] ~= nil then return file_cache[path] end local f = io.open(path,'r') local ret --~ print('checked',path) if f then f:close() ret = true else ret = false end file_cache[path] = ret return ret 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 get_canonical_filename (file) if strfind(file,"@") == 1 then file = strsub(file,2) if strfind(file,".",1,true) == 1 then file = strsub(file,3) end if step_into and project_root_path and not exists(file) then step_into = false step_over = true end end local abspath = strsub(file,1,1) == '/' or strsub(file,2,2) == ':' if not abspath and project_root_path then -- relative path file = project_root_path..dirsep..file end if IsWindows then file = strlower(file) file = file:gsub('/','\\') -- canonical for Win32! file = file:gsub(' ','%%') end return file end 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 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 file = get_canonical_filename(file) --~ print('rawfile',rawfile,line) 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) local bkval = has_breakpoint(thisfile, line) if bkval then vars = capture_vars(level,1,line) ev, idx = events.BREAK, 0 if bkval == TEMPORARY then remove_breakpoint(thisfile,line) end 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 postfix = '' --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 ~= '' and pausemsg ~= 'debug' then if not pausemsg:find('(%S+):(%d+)') then -- the message did not have an explicit file:line, -- so let's try to find a valid Lua frame which called this frame local level = 3 while level <= #traceinfo and traceinfo[level].currentline == -1 do level = level + 1 end local ar = traceinfo[level] pausemsg = get_canonical_filename(ar.source)..':'..ar.currentline..' '..pausemsg end 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 postfix = '['..tostring(current_thread)..'] ' end if ev == events.STEP then write("Paused at file "..file.." line "..line..' ('..stack_level[current_thread]..') '..postfix..'\n') dump_display(vars) elseif ev == events.BREAK then write("Paused at file "..file.." line "..line..' ('..stack_level[current_thread]..') (breakpoint) '..postfix..'\n') end_timer() dump_display(vars) elseif ev == events.WATCH then write("Paused at file "..file.." line "..line..' ('..stack_level[current_thread]..')'.." (watch expression "..idx_watch.. ": ["..watches[idx_watch].exp.."])"..postfix.."\n") dump_display(vars) elseif ev == events.SET then --do nothing else write("Error in application: "..file.." line "..line.." "..postfix.."\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) 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 if command == "setb" or command == 'break' or command == 'tb' then --{{{ set breakpoint local line, filename if command ~= 'break' then line,filename = getargs('LF') else filename,line = args:match(bpattern) line = tonumber(line) end if filename ~= '' and line ~= '' and line ~= nil then local val = true if command == 'tb' then val = TEMPORARY end set_breakpoint(filename,line,val) 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,true) 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 = tonumber(args) if level and level == '' then level = nil end -- find the first valid Lua frame that called this frame! while level <= #traceinfo and traceinfo[level].currentline == -1 do level = level + 1 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(os.getenv('TMP')..'\\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(...) local args = {...} local message = "" if #args == 0 then -- "",2 args = {"",2} elseif #args == 1 then -- message,2 args[1] = args[1] or "" message = args[1] table.insert(args,2) elseif #args == 2 then -- message,level+1 args[1] = args[1] or "" message = args[1] args[2] = args[2]+1 elseif #args == 3 then -- thread,message,level+1 message = args[2] args[3] = args[3]+1 end local assertmsg = _traceback(unpack(args)) --do original function if not DEBUG_TRACEBACK_NO_PAUSE then pause(message) --let user have a look at stuff end return assertmsg --carry on end _TRACEBACK = debug.traceback --Lua 5.0 function --}}}