luaforwindows/SciTE/scite-debug/lua_clidebugger/debugger.lua

1512 lines
41 KiB
Lua
Executable File

--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 "<self>"
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
--}}}