blob: 36fb92879d47ff0be2c64024242c9b632f4cb080 [file] [log] [blame]
-- ----------------------------------------------------------------------------
-- Copyright (c) 2011-2012 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:
-- Julien Desgats - initial API and implementation
-- ----------------------------------------------------------------------------
-- Properties generation. Generate a LOM table with data from introspection.
-- ----------------------------------------------------------------------------
local debug = require "debug"
local platform = require "debugger.platform"
local util = require "debugger.util"
local tostring, type, assert, next, rawget, getmetatable, setmetatable, getfenv, select, coyield, cocreate, costatus, coresume, sformat, tconcat =
tostring, type, assert, next, rawget, getmetatable, setmetatable, getfenv, select, coroutine.yield, coroutine.create, coroutine.status, coroutine.resume, string.format, table.concat
local MULTIVAL_MT = { __tostring = function() return "" end }
local probes = { }
-- ---------- --
-- Public API --
-- ---------- --
---
-- Introspection logic. This module implements Lua objects introspection and
-- generates a [DBGP](http://xdebug.org/docs-dbgp.php) compatible
-- [LOM](http://matthewwild.co.uk/projects/luaexpat/lom.html) data scructure.
-- @module debugger.introspection
local M = { }
---
-- Represent the actual data to send to the debugger.
-- Full XML specification can be found in [DBGP specification](http://xdebug.org/docs-dbgp.php#properties-variables-and-values).
-- Modifying properties after their generation is possible (as actual data serialization/sending is delayed)
-- but should be used with care. The XML structure uses the [LOM](http://matthewwild.co.uk/projects/luaexpat/lom.html)
-- format, refer to these documents to get more informations about fields.
--
-- In addition to table fields, it has an array part, `[1]` being the string representation (base64 encoded),
-- possibly followed by chlid properties (@{#DBGPProperty} themselves)
--
-- @field #string tag Always "property"
-- @field #table attr XML attributes, see DBGP specification
-- @type DBGPProperty
---
-- Inpectors table, contain all inspector functions.
-- Keys are either type names (`string`, `number`, ...) or metatables
-- that have a custom inspector attached.
-- @field [parent=#debugger.introspection] #table inspectors
M.inspectors = { }
---
-- Generate a DBGP property if needed. If data is in data pagination and recursion depth ranges,
-- and send a property to the debugger, otherwise drop current property.
-- @param #string name Property name (displayed in IDE)
-- @param #string typename Type name (displayed in IDE)
-- @param #string repr Value string representation
-- @param #DBGPProperty parent Parent property
-- @param #string fullname Lua expression used to get value back in further calls
-- @return #table description
-- @function [parent=#debugger.introspection] property
M.property = coyield
---
-- Adds a probe that will be called for every unknown table/userdata.
-- @param #function probe Inspector function to call.
-- @function [parent=#debugger.introspection] add_probe
M.add_probe = function(probe) probes[#probes + 1] = probe end
---
-- Inspects a Lua value by dispatching it to correct inspector. Inspector functions have the same API.
-- @param #string name Property name (will be displayed by IDE)
-- @param value Value to inspect
-- @param #table parent Parent property (LOM table of the )
-- @param #string fullname Expression used to retrieve `value` for further debugger calls
-- @return #DBGPProperty The inspected value as returned by @{debugger.introspection#debugger.introspection.property}.
-- @return #nil If the value has not been inspected
-- @function [parent=#debugger.introspection] inspect
M.inspect = function(name, value, parent, fullname)
return (M.inspectors[type(value)] or M.inspectors.default)(name, value, parent, fullname)
end
-- ----------------- --
-- Utility functions --
-- ----------------- --
local function default_inspector(name, value, parent, fullname)
return M.property(name, type(value), tostring(value), parent, fullname)
end
-- Inspects types that can have a metatable (table and userdata). Returns
-- 1) generated property
-- 2) boolean indicating whether a custom inspector has been called (in that case, do not process value any further)
local function metatable_inspector(name, value, parent, fullname)
local mt = getmetatable(value)
do
-- find by metatable
local custom = M.inspectors[mt]
if custom then return custom(name, value, parent, fullname), true end
-- or else call probes
for i=1, #probes do
local prop = probes[i](name, value, parent, fullname)
if prop then return prop, true end
end
end
local prop = default_inspector(name, value, parent, fullname)
if mt and prop then
local mtprop = M.inspect("metatable", mt, prop, "metatable["..prop.attr.fullname.."]")
if mtprop then mtprop.attr.type = "special" end
end
return prop, false
end
local function fancy_func_repr(f, info)
local args = {}
for i=1, info.nparams do
args[i] = debug.getlocal(f, i)
end
if info.isvararg then
args[#args+1] = "..."
end
return "function(" .. tconcat(args, ", ") .. ")"
end
--- Generate a name siutable for table index syntax
-- @param name Key name
-- @return #string A table index style index
-- @usage generate_printable_key('foo') => '["foo"]'
-- @usage generate_printable_key(12) => '[12]'
-- @usage generate_printable_key({}) => '[table: 0x12345678]
-- @function [parent=#debugger.introspection] generate_printable_key
local function generate_printable_key(name)
return "[" .. (type(name) == "string" and sformat("%q", name) or tostring(name)) .. "]"
end
M.generate_printable_key = generate_printable_key
-- Used to store complex keys (other than string and number) as they cannot be passed in text
-- For these keys, the resulting expression will not be the key itself but "key_cache[...]"
-- where key_cache must be mapped to this table to resolve key correctly.
M.key_cache = setmetatable({ n=0 }, { __mode = "v" })
local function generate_key(name)
local tname = type(name)
if tname == "string" then return sformat("%q", name)
elseif tname == "number" or tname == "boolean" then return tostring(name)
else -- complex key, use key_cache for lookup
local i = M.key_cache.n
M.key_cache[i] = name
M.key_cache.n = i+1
return "key_cache["..tostring(i).."]"
end
end
--- Generate a usable fullname for a value.
-- Based on parent fullname and key value, return a valid Lua expression.
-- Key can be any value (as anything can act as table key). If it cannot
-- be serialized (only string, number and boolean can), it will be temporarly
-- stored in an internal cache to be retrieved later.
-- @param #string parent Parent fullname
-- @param key The child key to generate fullname for
-- @return #string A valid fullname expression
-- @function [parent=#debugger.introspection] make_fullname
local function make_fullname(parent, key)
return parent .. "[" .. generate_key(key) .. "]"
end
M.make_fullname = make_fullname
-- ---------- --
-- Inspectors --
-- ---------- --
M.inspectors.number = default_inspector
M.inspectors.boolean = default_inspector
M.inspectors["nil"] = default_inspector
M.inspectors.userdata = default_inspector
M.inspectors.thread = default_inspector
M.inspectors.default = default_inspector -- allows 3rd party inspectors to use the default inspector if needed
M.inspectors.userdata = function(name, value, parent, fullname)
return (metatable_inspector(name, value, parent, fullname)) -- drop second return value
end
M.inspectors.string = function(name, value, parent, fullname)
-- escape linebreaks as \n and not as \<0x0A> like %q does
return M.property(name, "string", sformat("%q", value):gsub("\\\n", "\\n"), parent, fullname)
end
M.inspectors["function"] = function(name, value, parent, fullname)
local info = debug.getinfo(value, "nSflu")
local prop
if info.what ~= "C" then
-- try to create a fancy representation if possible
local repr = info.nparams and fancy_func_repr(value, info) or tostring(value)
if info.source:sub(1,1) == "@" then
repr = repr .. "\n" .. platform.get_uri("@" .. info.source) .. "\n" .. tostring(info.linedefined)
end
prop = M.property(name, "function (Lua)", repr, parent, fullname)
else
prop = M.property(name, "function", tostring(value), parent, fullname)
end
if not prop then return nil end
-- (5.1 only) environment is dumped only if it is different from global environment
-- TODO: this is not a correct behavior: environment should be dumped if is different from current stack level one
local fenv = getfenv and getfenv(value)
if fenv and fenv ~= getfenv(0) then
local fenvprop = M.inspect("environment", fenv, prop, "environment["..prop.attr.fullname.."]")
if fenvprop then fenvprop.attr.type = "special" end
end
return prop
end
M.inspectors.table = function(name, value, parent, fullname)
local prop, iscustom = metatable_inspector(name, value, parent, fullname)
if not prop or iscustom then return prop end
-- iterate over table values and detect arrays at the same time
-- next is used to circumvent __pairs metamethod in 5.2
local isarray, i = true, 1
for k,v in next, value, nil do
M.inspect(generate_printable_key(k), v, prop, make_fullname(fullname, k))
-- array detection: keys should be accessible by 1..n keys
isarray = isarray and rawget(value, i) ~= nil
i = i + 1
end
-- empty tables are considered as tables
if isarray and i > 1 then prop.attr.type = "sequence" end
return prop
end
M.inspectors[MULTIVAL_MT] = function(name, value, parent, fullname)
if value.n == 1 then
-- return directly the value as result
return M.inspect(name, value[1], parent, fullname)
else
-- wrap values inside a multival container
local prop = M.property(name, "multival", "", parent, fullname)
if not prop then return nil end
for i=1, value.n do
M.inspect(generate_printable_key(i), value[i], prop, fullname .. "[" .. i .. "]")
end
return prop
end
end
-- ------------ --
-- Internal API --
-- ------------ --
-- Used to inspect "multival" or "vararg" values. The typical use is to pack function result(s) in a single
-- value to inspect. The Multival instances can be passed to make_property as a single value, they will be
-- correctly reported to debugger
function M.Multival(...)
return setmetatable({ n=select("#", ...), ... }, MULTIVAL_MT)
end
--- Makes a property form a name/value pair (and fullname). This is an **internal** function, and should not be used by 3rd party inspectors.
-- @param #number cxt_id Context ID in which this value resides (workaround bug 352316)
-- @param value The value to debug
-- @param name The name associated with value, passed through tostring, so it can be anything
-- @param #string fullname A Lua expression to eval to get that property again (if nil, computed automatically)
-- @param #number depth The maximum property depth (recursive calls)
-- @param #number pagesize maximum children to include
-- @param #number page The page to generate (0 based)
-- @param #number size_limit Optional, if set, the maximum size of the string representation (in bytes)
-- @param #boolean safe_name If true, does not encode the name as table key
-- @return #DBGPProperty root property
-- @function [parent=#debugger.introspection] make_property
--TODO BUG ECLIPSE TOOLSLINUX-99 352316 : as a workaround, context is encoded into the fullname property
M.make_property = function(cxt_id, value, name, fullname, depth, pagesize, page, size_limit, safe_name)
fullname = fullname or "(...)[" .. generate_key(name) .. "]"
if not safe_name then name = generate_printable_key(name) end
local generator = cocreate(function() return M.inspect(name, value, nil, fullname) end)
local propstack = { }
local rootnode
local catchthis = true
local nodestoskip = page * pagesize -- nodes to skip at root level to respect pagination
local fullname_prefix = tostring(cxt_id).."|"
while true do
local succes, name, datatype, repr, parent, fullname = assert(coresume(generator, catchthis and propstack[#propstack] or nil))
-- finalize and pop all finished properties
while propstack[#propstack] ~= parent do
local topop = propstack[#propstack]
topop.attr.fullname = util.rawb64(fullname_prefix .. topop.attr.fullname)
propstack[#propstack] = nil
end
if costatus(generator) == "dead" then break end
local prop = {
tag = "property",
attr = {
children = 0,
pagesize = pagesize,
page = parent and 0 or page,
type = datatype,
name = name,
fullname = fullname,
encoding = "base64",
size = #repr,
},
util.b64(size_limit and repr:sub(1, size_limit) or repr)
}
if parent then
parent.attr.children = 1
parent.attr.numchildren = (parent.attr.numchildren or 0) + 1
-- take pagination into accont to know if node needs to be catched
catchthis = #parent <= pagesize and #propstack <= depth
if parent == rootnode then
catchthis = catchthis and nodestoskip <= 0
nodestoskip = nodestoskip - 1
end
-- add node to tree
if catchthis then
parent[#parent + 1] = prop
propstack[#propstack + 1] = prop
end
else
rootnode = prop
catchthis = true
propstack[#propstack + 1] = prop
end
end
return rootnode
end
return M