/
debugintrospection.lua
256 lines (220 loc) · 10.3 KB
/
debugintrospection.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
-------------------------------------------------------------------------------
-- 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