blob: 94eb94c7d9b80dc7a015df6ac1d3ffe9e34afdb1 [file] [log] [blame]
-------------------------------------------------------------------------------
-- Copyright (c) 2011 Sierra Wireless and others.
-- All rights reserved. This program and the accompanying materials
-- are made available under the terms of the Eclipse Public License v1.0
-- which accompanies this distribution, and is available at
-- http://www.eclipse.org/legal/epl-v10.html
--
-- Contributors:
-- Sierra Wireless - initial API and implementation
-------------------------------------------------------------------------------
-- This module defines a single "class" which allows to dump arbitrary values by gathering as munch data as
-- possible on it for debugging purposes.
--
-- Recursions are avoided by "flatten" all tables in the structure and referencing them by their ID.
-- The dump format is documented in Confluence Wiki:
-- https://confluence.anyware-tech.com/display/ECLIPSE/Lua+IDE+Internship#LuaIDEInternship-Datadumpformat
--
-- It has two major parts :
-- dump : Result data structure: keys are numbers which are IDs for all dumped tables.
-- Values are table structures.
-- tables : Table dictionary, used to register all dumped tables, keys are tables,
-- values are their ID. Functions upvalues are also stored here in the same way.
--
-- The dump itself is done by type functions, one for each Lua type. For example a complete dump of the VM is done with:
-- local introspection = require"debugintrospection"
-- local dump = introspection:new()
-- dump.dump.root = dump:table(_G) -- so dump.root will be a number which point to the result of _G introspection
--
-- WARNING: do never keep any direct reference to internal fields (dump or tables): a dump object is not re-dumped to
-- avoid making huge data structures (and potentially overflow l2b)
-- Utility function (from ReadyAgent utils, copied here to avoid this dependency for a single function)
local function isregulararray(T)
local n = 1
for k, v in pairs(T) do
if rawget(T, n) == nil then return false end
n = n + 1
end
return true
end
local dump_pool = {}
dump_pool.__index = dump_pool
local all_dumps = setmetatable({ }, { __mode = "k" }) -- register all dumps to avoid to re-dump them (see above warning)
--- Creates a new dump pool with specified options
-- @param dump_locales (boolean) whether local values are dumped
-- @param dump_upvalues (boolean) whether function upvalues are dumped
-- @param dump_metatables (boolean) whether metatables (for tables and userdata) are dumped
-- @param dump_stacks (boolean) whether thread stacks are dumped
-- @param dump_fenv (boolean) whether function environments are dumped
function dump_pool:new(dump_locales, dump_upvalues, dump_metatables, dump_stacks, dump_fenv, keep_reference)
local dump = setmetatable({
current_id = 1,
tables = { },
dump = { },
-- set switches, force a boolean value because nil would mess with __index metamethod
dump_locales = dump_locales and true or false,
dump_upvalues = dump_upvalues and true or false,
dump_metatables = dump_metatables and true or false,
dump_stacks = dump_stacks and true or false,
dump_fenv = dump_fenv and true or false,
keep_reference = keep_reference and true or false,
}, self)
all_dumps[dump] = true
return dump
end
function dump_pool:_next_id()
local id = self.current_id
self.current_id = id + 1
return id
end
function dump_pool:_register_new(value)
local id = self.current_id
self.current_id = id + 1
self.tables[value] = id
return id
end
--- Utility function to factorize all metatable handling
function dump_pool:_metatable(value, result, depth)
--TODO: add support for __pairs and __ipairs ?
if self.dump_metatables then
local mt = getmetatable(value)
if mt then
result.metatable = self[type(mt)](self, mt, depth-1)
if mt.__len then result.length = #value end
end
end
return result
end
--- Adds a field into destination table, if both key and value has been successfully dumped
function dump_pool:_field(dest, key, value, depth)
local dkey, dvalue = self[type(key)](self, key, depth-1), self[type(value)](self, value, depth-1)
if dkey and dvalue then dest[#dest + 1] = { dkey, dvalue } end
end
--- Functions used to extract debug informations from different data types.
-- each function takes the value to debug as parameter and returns its
-- debugging structure (or an id, for tables), modifying the pool if needed.
function dump_pool:table(value, depth)
depth = depth or math.huge
if depth < 0 then return nil end
if all_dumps[value] then return nil end
local id = self.tables[value]
if not id then
-- this is a new table: register it
id = self:_register_new(value)
local t = { type = "table", repr = tostring(value), ref = self.keep_reference and value or nil }
--Detect "arrays" (tables with 1..n keys). Empty tables are really tables
t.array = (not not next(value)) and isregulararray(value)
-- For arrays, make sure that keys are given in 1..n order
for k,v in (t.array and ipairs or pairs)(value) do
self:_field(t, k, v, depth)
end
-- The registered length refers to # result because if actual element count
-- can be known with dumped values
t.length = #value
self:_metatable(value, t, depth)
self.dump[id] = t
end
return id
end
function dump_pool:userdata(value, depth)
depth = depth or math.huge
if depth < 0 then return nil end
return self:_metatable(value, { type = "userdata", repr = tostring(value), ref = self.keep_reference and value or nil }, depth)
end
function dump_pool:thread(value, depth)
depth = depth or math.huge
if depth < 0 then return nil end
local result = { type = "thread", repr = tostring(value), status = coroutine.status(value), ref = self.keep_reference and value or nil }
local stack = self.tables[value]
if self.dump_stacks and not stack then
stack = self:_register_new(value)
local stack_table = { type="special" }
for i=1, math.huge do
if not debug.getinfo(value, i, "f") then break end
-- _filed is not used here because i is not a function and there is no risk to get a nil from number or function
stack_table[#stack_table+1] = { self:number(i, depth - 1), self["function"](self, i, depth - 1, value) }
end
stack_table.repr = tostring(#stack_table).." levels"
self.dump[stack] = stack_table
end
result.stack = stack
return result
end
dump_pool["function"] = function(self, value, depth, thread) -- function is a keyword...
depth = depth or math.huge
if depth < 0 then return nil end
local info = thread and debug.getinfo(thread, value, "nSfl") or debug.getinfo(value, "nSfl")
local func = info.func -- in case of value is a stack index
local result = { type = "function", ref = self.keep_reference and func or nil }
result.kind = info.what
if info.name and #info.name > 0 then result.repr = "function: "..info.name -- put natural name, if available
elseif func then result.repr = tostring(func) -- raw tostring otherwise
else result.repr = "<tail call>" end -- nothing is available for tail calls
if not func then return result end -- there is no more info to gather for tail calls
if info.what ~= "C" then
--TODO: do something if function is not defined in a file
if info.source:sub(1,1) == "@" then
result.file = info.source:sub(2)
end
result.line_from = info.linedefined
result.line_to = info.lastlinedefined
if info.currentline >= 0 then
result.line_current = info.currentline
end
end
-- Dump function env (if different from _G)
local env = getfenv(func)
if self.dump_fenv and env ~= getfenv(0) then
result.environment = self:table(env, depth - 1)
end
-- Dump function upvalues (if any), trated as a table (recursion is handled in the same way)
local upvalues = self.tables[func]
if self.dump_upvalues and not upvalues and func and debug.getupvalue(func, 1) then
-- Register upvalues table into result
local ups_table = { type="special" }
upvalues = self:_register_new(func)
for i=1, math.huge do
local name, val = debug.getupvalue(func, i)
if not name then break end
self:_field(ups_table, name, val, depth)
end
ups_table.repr = tostring(#ups_table)
self.dump[upvalues] = ups_table
end
result.upvalues = upvalues
-- Dump function locales (only for running function, recursion not handled)
if self.dump_locales and type(value) == "number" then
local getlocal = thread and function(...) return debug.getlocal(thread, ...) end or debug.getlocal
if getlocal(value, 1) then
local locales = { type="special" }
local locales_id = self:_next_id()
for i=1, math.huge do
local name, val = getlocal(value, i)
if not name then break
elseif name:sub(1,1) ~= "(" and val ~= self then -- internal values are ignored
self:_field(locales, name, val, depth)
end
end
locales.repr = tostring(#locales)
self.dump[locales_id] = locales
result.locales = locales_id
end
end
return result
end
function dump_pool:string(value, depth)
depth = depth or math.huge
if depth < 0 then return nil end
-- make the string printable (%q pattern keeps real newlines and adds quotes)
return { type = "string", repr = string.format("%q", value):gsub("\\\n", "\\n"), length = #value,
ref = self.keep_reference and value or nil }
end
-- default debug function for other types
setmetatable(dump_pool, {
__index = function(cls, vtype)
return function(self, value, depth)
return (depth == nil or depth >= 0) and { repr = tostring(value), type=vtype, ref = self.keep_reference and value or nil } or nil
end
end
})
return dump_pool