This commit is contained in:
Matthias Mailänder
2014-05-26 22:02:21 +02:00
parent c58d737400
commit 473ca13b17

View File

@@ -1,411 +1,411 @@
-- tables -- tables
local _G = _G local _G = _G
local string, io, debug, coroutine = string, io, debug, coroutine local string, io, debug, coroutine = string, io, debug, coroutine
-- functions -- functions
local tostring, print, require = tostring, print, require local tostring, print, require = tostring, print, require
local next, assert = next, assert local next, assert = next, assert
local pcall, type, pairs, ipairs = pcall, type, pairs, ipairs local pcall, type, pairs, ipairs = pcall, type, pairs, ipairs
local error = error local error = error
assert(debug, "debug table must be available at this point") assert(debug, "debug table must be available at this point")
local io_open = io.open local io_open = io.open
local string_gmatch = string.gmatch local string_gmatch = string.gmatch
local string_sub = string.sub local string_sub = string.sub
local table_concat = table.concat local table_concat = table.concat
local _M = { local _M = {
max_tb_output_len = 70 -- controls the maximum length of the 'stringified' table before cutting with ' (more...)' max_tb_output_len = 70 -- controls the maximum length of the 'stringified' table before cutting with ' (more...)'
} }
-- this tables should be weak so the elements in them won't become uncollectable -- this tables should be weak so the elements in them won't become uncollectable
local m_known_tables = { [_G] = "_G (global table)" } local m_known_tables = { [_G] = "_G (global table)" }
local function add_known_module(name, desc) local function add_known_module(name, desc)
local ok, mod = pcall(require, name) local ok, mod = pcall(require, name)
if ok then if ok then
m_known_tables[mod] = desc m_known_tables[mod] = desc
end end
end end
add_known_module("string", "string module") add_known_module("string", "string module")
add_known_module("io", "io module") add_known_module("io", "io module")
add_known_module("os", "os module") add_known_module("os", "os module")
add_known_module("table", "table module") add_known_module("table", "table module")
add_known_module("math", "math module") add_known_module("math", "math module")
add_known_module("package", "package module") add_known_module("package", "package module")
add_known_module("debug", "debug module") add_known_module("debug", "debug module")
add_known_module("coroutine", "coroutine module") add_known_module("coroutine", "coroutine module")
-- lua5.2 -- lua5.2
add_known_module("bit32", "bit32 module") add_known_module("bit32", "bit32 module")
-- luajit -- luajit
add_known_module("bit", "bit module") add_known_module("bit", "bit module")
add_known_module("jit", "jit module") add_known_module("jit", "jit module")
local m_user_known_tables = {} local m_user_known_tables = {}
local m_known_functions = {} local m_known_functions = {}
for _, name in ipairs{ for _, name in ipairs{
-- Lua 5.2, 5.1 -- Lua 5.2, 5.1
"assert", "assert",
"collectgarbage", "collectgarbage",
"dofile", "dofile",
"error", "error",
"getmetatable", "getmetatable",
"ipairs", "ipairs",
"load", "load",
"loadfile", "loadfile",
"next", "next",
"pairs", "pairs",
"pcall", "pcall",
"print", "print",
"rawequal", "rawequal",
"rawget", "rawget",
"rawlen", "rawlen",
"rawset", "rawset",
"require", "require",
"select", "select",
"setmetatable", "setmetatable",
"tonumber", "tonumber",
"tostring", "tostring",
"type", "type",
"xpcall", "xpcall",
-- Lua 5.1 -- Lua 5.1
"gcinfo", "gcinfo",
"getfenv", "getfenv",
"loadstring", "loadstring",
"module", "module",
"newproxy", "newproxy",
"setfenv", "setfenv",
"unpack", "unpack",
-- TODO: add table.* etc functions -- TODO: add table.* etc functions
} do } do
if _G[name] then if _G[name] then
m_known_functions[_G[name]] = name m_known_functions[_G[name]] = name
end end
end end
local m_user_known_functions = {} local m_user_known_functions = {}
local function safe_tostring (value) local function safe_tostring (value)
local ok, err = pcall(tostring, value) local ok, err = pcall(tostring, value)
if ok then return err else return ("<failed to get printable value>: '%s'"):format(err) end if ok then return err else return ("<failed to get printable value>: '%s'"):format(err) end
end end
-- Private: -- Private:
-- Parses a line, looking for possible function definitions (in a very na<6E>ve way) -- Parses a line, looking for possible function definitions (in a very na<6E>ve way)
-- Returns '(anonymous)' if no function name was found in the line -- Returns '(anonymous)' if no function name was found in the line
local function ParseLine(line) local function ParseLine(line)
assert(type(line) == "string") assert(type(line) == "string")
--print(line) --print(line)
local match = line:match("^%s*function%s+(%w+)") local match = line:match("^%s*function%s+(%w+)")
if match then if match then
--print("+++++++++++++function", match) --print("+++++++++++++function", match)
return match return match
end end
match = line:match("^%s*local%s+function%s+(%w+)") match = line:match("^%s*local%s+function%s+(%w+)")
if match then if match then
--print("++++++++++++local", match) --print("++++++++++++local", match)
return match return match
end end
match = line:match("^%s*local%s+(%w+)%s+=%s+function") match = line:match("^%s*local%s+(%w+)%s+=%s+function")
if match then if match then
--print("++++++++++++local func", match) --print("++++++++++++local func", match)
return match return match
end end
match = line:match("%s*function%s*%(") -- this is an anonymous function match = line:match("%s*function%s*%(") -- this is an anonymous function
if match then if match then
--print("+++++++++++++function2", match) --print("+++++++++++++function2", match)
return "(anonymous)" return "(anonymous)"
end end
return "(anonymous)" return "(anonymous)"
end end
-- Private: -- Private:
-- Tries to guess a function's name when the debug info structure does not have it. -- Tries to guess a function's name when the debug info structure does not have it.
-- It parses either the file or the string where the function is defined. -- It parses either the file or the string where the function is defined.
-- Returns '?' if the line where the function is defined is not found -- Returns '?' if the line where the function is defined is not found
local function GuessFunctionName(info) local function GuessFunctionName(info)
--print("guessing function name") --print("guessing function name")
if type(info.source) == "string" and info.source:sub(1,1) == "@" then if type(info.source) == "string" and info.source:sub(1,1) == "@" then
local file, err = io_open(info.source:sub(2), "r") local file, err = io_open(info.source:sub(2), "r")
if not file then if not file then
print("file not found: "..tostring(err)) -- whoops! print("file not found: "..tostring(err)) -- whoops!
return "?" return "?"
end end
local line local line
for i = 1, info.linedefined do for i = 1, info.linedefined do
line = file:read("*l") line = file:read("*l")
end end
if not line then if not line then
print("line not found") -- whoops! print("line not found") -- whoops!
return "?" return "?"
end end
return ParseLine(line) return ParseLine(line)
else else
local line local line
local lineNumber = 0 local lineNumber = 0
for l in string_gmatch(info.source, "([^\n]+)\n-") do for l in string_gmatch(info.source, "([^\n]+)\n-") do
lineNumber = lineNumber + 1 lineNumber = lineNumber + 1
if lineNumber == info.linedefined then if lineNumber == info.linedefined then
line = l line = l
break break
end end
end end
if not line then if not line then
print("line not found") -- whoops! print("line not found") -- whoops!
return "?" return "?"
end end
return ParseLine(line) return ParseLine(line)
end end
end end
--- ---
-- Dumper instances are used to analyze stacks and collect its information. -- Dumper instances are used to analyze stacks and collect its information.
-- --
local Dumper = {} local Dumper = {}
Dumper.new = function(thread) Dumper.new = function(thread)
local t = { lines = {} } local t = { lines = {} }
for k,v in pairs(Dumper) do t[k] = v end for k,v in pairs(Dumper) do t[k] = v end
t.dumping_same_thread = (thread == coroutine.running()) t.dumping_same_thread = (thread == coroutine.running())
-- if a thread was supplied, bind it to debug.info and debug.get -- if a thread was supplied, bind it to debug.info and debug.get
-- we also need to skip this additional level we are introducing in the callstack (only if we are running -- we also need to skip this additional level we are introducing in the callstack (only if we are running
-- in the same thread we're inspecting) -- in the same thread we're inspecting)
if type(thread) == "thread" then if type(thread) == "thread" then
t.getinfo = function(level, what) t.getinfo = function(level, what)
if t.dumping_same_thread and type(level) == "number" then if t.dumping_same_thread and type(level) == "number" then
level = level + 1 level = level + 1
end end
return debug.getinfo(thread, level, what) return debug.getinfo(thread, level, what)
end end
t.getlocal = function(level, loc) t.getlocal = function(level, loc)
if t.dumping_same_thread then if t.dumping_same_thread then
level = level + 1 level = level + 1
end end
return debug.getlocal(thread, level, loc) return debug.getlocal(thread, level, loc)
end end
else else
t.getinfo = debug.getinfo t.getinfo = debug.getinfo
t.getlocal = debug.getlocal t.getlocal = debug.getlocal
end end
return t return t
end end
-- helpers for collecting strings to be used when assembling the final trace -- helpers for collecting strings to be used when assembling the final trace
function Dumper:add (text) function Dumper:add (text)
self.lines[#self.lines + 1] = text self.lines[#self.lines + 1] = text
end end
function Dumper:add_f (fmt, ...) function Dumper:add_f (fmt, ...)
self:add(fmt:format(...)) self:add(fmt:format(...))
end end
function Dumper:concat_lines () function Dumper:concat_lines ()
return table_concat(self.lines) return table_concat(self.lines)
end end
--- ---
-- Private: -- Private:
-- Iterates over the local variables of a given function. -- Iterates over the local variables of a given function.
-- --
-- @param level The stack level where the function is. -- @param level The stack level where the function is.
-- --
function Dumper:DumpLocals (level) function Dumper:DumpLocals (level)
local prefix = "\t " local prefix = "\t "
local i = 1 local i = 1
if self.dumping_same_thread then if self.dumping_same_thread then
level = level + 1 level = level + 1
end end
local name, value = self.getlocal(level, i) local name, value = self.getlocal(level, i)
if not name then if not name then
return return
end end
self:add("\tLocal variables:\r\n") self:add("\tLocal variables:\r\n")
while name do while name do
if type(value) == "number" then if type(value) == "number" then
self:add_f("%s%s = number: %g\r\n", prefix, name, value) self:add_f("%s%s = number: %g\r\n", prefix, name, value)
elseif type(value) == "boolean" then elseif type(value) == "boolean" then
self:add_f("%s%s = boolean: %s\r\n", prefix, name, tostring(value)) self:add_f("%s%s = boolean: %s\r\n", prefix, name, tostring(value))
elseif type(value) == "string" then elseif type(value) == "string" then
self:add_f("%s%s = string: %q\r\n", prefix, name, value) self:add_f("%s%s = string: %q\r\n", prefix, name, value)
elseif type(value) == "userdata" then elseif type(value) == "userdata" then
self:add_f("%s%s = %s\r\n", prefix, name, safe_tostring(value)) self:add_f("%s%s = %s\r\n", prefix, name, safe_tostring(value))
elseif type(value) == "nil" then elseif type(value) == "nil" then
self:add_f("%s%s = nil\r\n", prefix, name) self:add_f("%s%s = nil\r\n", prefix, name)
elseif type(value) == "table" then elseif type(value) == "table" then
if m_known_tables[value] then if m_known_tables[value] then
self:add_f("%s%s = %s\r\n", prefix, name, m_known_tables[value]) self:add_f("%s%s = %s\r\n", prefix, name, m_known_tables[value])
elseif m_user_known_tables[value] then elseif m_user_known_tables[value] then
self:add_f("%s%s = %s\r\n", prefix, name, m_user_known_tables[value]) self:add_f("%s%s = %s\r\n", prefix, name, m_user_known_tables[value])
else else
local txt = "{" local txt = "{"
for k,v in pairs(value) do for k,v in pairs(value) do
txt = txt..safe_tostring(k)..":"..safe_tostring(v) txt = txt..safe_tostring(k)..":"..safe_tostring(v)
if #txt > _M.max_tb_output_len then if #txt > _M.max_tb_output_len then
txt = txt.." (more...)" txt = txt.." (more...)"
break break
end end
if next(value, k) then txt = txt..", " end if next(value, k) then txt = txt..", " end
end end
self:add_f("%s%s = %s %s\r\n", prefix, name, safe_tostring(value), txt.."}") self:add_f("%s%s = %s %s\r\n", prefix, name, safe_tostring(value), txt.."}")
end end
elseif type(value) == "function" then elseif type(value) == "function" then
local info = self.getinfo(value, "nS") local info = self.getinfo(value, "nS")
local fun_name = info.name or m_known_functions[value] or m_user_known_functions[value] local fun_name = info.name or m_known_functions[value] or m_user_known_functions[value]
if info.what == "C" then if info.what == "C" then
self:add_f("%s%s = C %s\r\n", prefix, name, (fun_name and ("function: " .. fun_name) or tostring(value))) self:add_f("%s%s = C %s\r\n", prefix, name, (fun_name and ("function: " .. fun_name) or tostring(value)))
else else
local source = info.short_src local source = info.short_src
if source:sub(2,7) == "string" then if source:sub(2,7) == "string" then
source = source:sub(9) source = source:sub(9)
end end
--for k,v in pairs(info) do print(k,v) end --for k,v in pairs(info) do print(k,v) end
fun_name = fun_name or GuessFunctionName(info) fun_name = fun_name or GuessFunctionName(info)
self:add_f("%s%s = Lua function '%s' (defined at line %d of chunk %s)\r\n", prefix, name, fun_name, info.linedefined, source) self:add_f("%s%s = Lua function '%s' (defined at line %d of chunk %s)\r\n", prefix, name, fun_name, info.linedefined, source)
end end
elseif type(value) == "thread" then elseif type(value) == "thread" then
self:add_f("%sthread %q = %s\r\n", prefix, name, tostring(value)) self:add_f("%sthread %q = %s\r\n", prefix, name, tostring(value))
end end
i = i + 1 i = i + 1
name, value = self.getlocal(level, i) name, value = self.getlocal(level, i)
end end
end end
--- ---
-- Public: -- Public:
-- Collects a detailed stack trace, dumping locals, resolving function names when they're not available, etc. -- Collects a detailed stack trace, dumping locals, resolving function names when they're not available, etc.
-- This function is suitable to be used as an error handler with pcall or xpcall -- This function is suitable to be used as an error handler with pcall or xpcall
-- --
-- @param thread An optional thread whose stack is to be inspected (defaul is the current thread) -- @param thread An optional thread whose stack is to be inspected (defaul is the current thread)
-- @param message An optional error string or object. -- @param message An optional error string or object.
-- @param level An optional number telling at which level to start the traceback (default is 1) -- @param level An optional number telling at which level to start the traceback (default is 1)
-- --
-- Returns a string with the stack trace and a string with the original error. -- Returns a string with the stack trace and a string with the original error.
-- --
function _M.stacktrace(thread, message, level) function _M.stacktrace(thread, message, level)
if type(thread) ~= "thread" then if type(thread) ~= "thread" then
-- shift parameters left -- shift parameters left
thread, message, level = nil, thread, message thread, message, level = nil, thread, message
end end
thread = thread or coroutine.running() thread = thread or coroutine.running()
level = level or 1 level = level or 1
local dumper = Dumper.new(thread) local dumper = Dumper.new(thread)
local original_error local original_error
if type(message) == "table" then if type(message) == "table" then
dumper:add("an error object {\r\n") dumper:add("an error object {\r\n")
local first = true local first = true
for k,v in pairs(message) do for k,v in pairs(message) do
if first then if first then
dumper:add(" ") dumper:add(" ")
first = false first = false
else else
dumper:add(",\r\n ") dumper:add(",\r\n ")
end end
dumper:add(safe_tostring(k)) dumper:add(safe_tostring(k))
dumper:add(": ") dumper:add(": ")
dumper:add(safe_tostring(v)) dumper:add(safe_tostring(v))
end end
dumper:add("\r\n}") dumper:add("\r\n}")
original_error = dumper:concat_lines() original_error = dumper:concat_lines()
elseif type(message) == "string" then elseif type(message) == "string" then
dumper:add(message) dumper:add(message)
original_error = message original_error = message
end end
dumper:add("\r\n") dumper:add("\r\n")
dumper:add[[ dumper:add[[
Stack Traceback Stack Traceback
=============== ===============
]] ]]
--print(error_message) --print(error_message)
local level_to_show = level local level_to_show = level
if dumper.dumping_same_thread then level = level + 1 end if dumper.dumping_same_thread then level = level + 1 end
local info = dumper.getinfo(level, "nSlf") local info = dumper.getinfo(level, "nSlf")
while info do while info do
if info.what == "main" then if info.what == "main" then
if string_sub(info.source, 1, 1) == "@" then if string_sub(info.source, 1, 1) == "@" then
dumper:add_f("(%d) main chunk of file '%s' at line %d\r\n", level_to_show, string_sub(info.source, 2), info.currentline) dumper:add_f("(%d) main chunk of file '%s' at line %d\r\n", level_to_show, string_sub(info.source, 2), info.currentline)
else else
dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.short_src, info.currentline) dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.short_src, info.currentline)
end end
elseif info.what == "C" then elseif info.what == "C" then
--print(info.namewhat, info.name) --print(info.namewhat, info.name)
--for k,v in pairs(info) do print(k,v, type(v)) end --for k,v in pairs(info) do print(k,v, type(v)) end
local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name or tostring(info.func) local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name or tostring(info.func)
dumper:add_f("(%d) %s C function '%s'\r\n", level_to_show, info.namewhat, function_name) dumper:add_f("(%d) %s C function '%s'\r\n", level_to_show, info.namewhat, function_name)
--dumper:add_f("%s%s = C %s\r\n", prefix, name, (m_known_functions[value] and ("function: " .. m_known_functions[value]) or tostring(value))) --dumper:add_f("%s%s = C %s\r\n", prefix, name, (m_known_functions[value] and ("function: " .. m_known_functions[value]) or tostring(value)))
elseif info.what == "tail" then elseif info.what == "tail" then
--print("tail") --print("tail")
--for k,v in pairs(info) do print(k,v, type(v)) end--print(info.namewhat, info.name) --for k,v in pairs(info) do print(k,v, type(v)) end--print(info.namewhat, info.name)
dumper:add_f("(%d) tail call\r\n", level_to_show) dumper:add_f("(%d) tail call\r\n", level_to_show)
dumper:DumpLocals(level) dumper:DumpLocals(level)
elseif info.what == "Lua" then elseif info.what == "Lua" then
local source = info.short_src local source = info.short_src
local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name
if source:sub(2, 7) == "string" then if source:sub(2, 7) == "string" then
source = source:sub(9) source = source:sub(9)
end end
local was_guessed = false local was_guessed = false
if not function_name or function_name == "?" then if not function_name or function_name == "?" then
--for k,v in pairs(info) do print(k,v, type(v)) end --for k,v in pairs(info) do print(k,v, type(v)) end
function_name = GuessFunctionName(info) function_name = GuessFunctionName(info)
was_guessed = true was_guessed = true
end end
-- test if we have a file name -- test if we have a file name
local function_type = (info.namewhat == "") and "function" or info.namewhat local function_type = (info.namewhat == "") and "function" or info.namewhat
if info.source and info.source:sub(1, 1) == "@" then if info.source and info.source:sub(1, 1) == "@" then
dumper:add_f("(%d) Lua %s '%s' at file '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "") dumper:add_f("(%d) Lua %s '%s' at file '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
elseif info.source and info.source:sub(1,1) == '#' then elseif info.source and info.source:sub(1,1) == '#' then
dumper:add_f("(%d) Lua %s '%s' at template '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "") dumper:add_f("(%d) Lua %s '%s' at template '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
else else
dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type, function_name, info.currentline, source) dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type, function_name, info.currentline, source)
end end
dumper:DumpLocals(level) dumper:DumpLocals(level)
else else
dumper:add_f("(%d) unknown frame %s\r\n", level_to_show, info.what) dumper:add_f("(%d) unknown frame %s\r\n", level_to_show, info.what)
end end
level = level + 1 level = level + 1
level_to_show = level_to_show + 1 level_to_show = level_to_show + 1
info = dumper.getinfo(level, "nSlf") info = dumper.getinfo(level, "nSlf")
end end
return dumper:concat_lines(), original_error return dumper:concat_lines(), original_error
end end
-- --
-- Adds a table to the list of known tables -- Adds a table to the list of known tables
function _M.add_known_table(tab, description) function _M.add_known_table(tab, description)
if m_known_tables[tab] then if m_known_tables[tab] then
error("Cannot override an already known table") error("Cannot override an already known table")
end end
m_user_known_tables[tab] = description m_user_known_tables[tab] = description
end end
-- --
-- Adds a function to the list of known functions -- Adds a function to the list of known functions
function _M.add_known_function(fun, description) function _M.add_known_function(fun, description)
if m_known_functions[fun] then if m_known_functions[fun] then
error("Cannot override an already known function") error("Cannot override an already known function")
end end
m_user_known_functions[fun] = description m_user_known_functions[fun] = description
end end
return _M return _M