| ------------------------------------------------------------------------------- |
| -- 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 |