522 lines
13 KiB
Lua
Executable File

--------------------------------------------------------------------------------
---------------------- ## ##### ##### ###### -----------------------
---------------------- ## ## ## ## ## ## ## -----------------------
---------------------- ## ## ## ## ## ###### -----------------------
---------------------- ## ## ## ## ## ## -----------------------
---------------------- ###### ##### ##### ## -----------------------
---------------------- -----------------------
----------------------- Lua Object-Oriented Programming ------------------------
--------------------------------------------------------------------------------
-- Project: LOOP Class Library --
-- Release: 2.3 beta --
-- Title : Interactive Inspector of Application State --
-- Author : Renato Maia <maia@inf.puc-rio.br> --
--------------------------------------------------------------------------------
local _G = _G
local assert = assert
local error = error
local getfenv = getfenv
local ipairs = ipairs
local load = load
local loadstring = loadstring
local next = next
local pairs = pairs
local rawget = rawget
local rawset = rawset
local select = select
local setfenv = setfenv
local setmetatable = setmetatable
local type = type
local xpcall = xpcall
local coroutine = require "coroutine"
local debug = require "debug"
local io = require "io"
local table = require "table"
local oo = require "loop.base"
local Viewer = require "loop.debug.Viewer"
module("loop.debug.Inspector", oo.class)
active = true
input = io.stdin
viewer = Viewer
local function call(self, op, ...)
self = self[".thread"]
if self
then return op(self, ...)
else return op(...)
end
end
local self
local infoflags = "Slnuf"
local Command = {}
function Command.see(...)
self.viewer:write(...)
self.viewer.output:write("\n")
end
function Command.loc(which, ...)
local level = self[".level"]
if level then
local index = 1
local name, value
repeat
name, value = call(self, debug.getlocal, level, index)
if not which and name then
local viewer = self.viewer
local output = viewer.output
output:write(name)
output:write(" = ")
viewer:write(value)
output:write("\n")
elseif name == which then
if select("#", ...) == 0
then return value
else return call(self, debug.setlocal, level, index, (...))
end
end
index = index + 1
until not name
end
end
function Command.upv(which, ...)
local func = self[".current"].func
local index = 1
local name, value
repeat
name, value = debug.getupvalue(func, index)
if not which and name then
local viewer = self.viewer
local output = viewer.output
output:write(name," = ")
viewer:write(value)
output:write("\n")
elseif name == which then
if select("#", ...) == 0
then return value
else return debug.setupvalue(func, index, (...))
end
end
index = index + 1
until not name
end
function Command.env(which, ...)
local env = getfenv(self[".current"].func)
if which then
if select("#", ...) == 0
then return env[which]
else env[which] = (...)
end
else
self.viewer:print(env)
end
end
function Command.lua(which, ...)
if which then
if select("#", ...) == 0
then return _G[which]
else _G[which] = (...)
end
else
self.viewer:print(_G)
end
end
function Command.goto(where)
local kind = type(where)
if kind == "thread" then
local status = coroutine.status(where)
if status ~= "running" and status ~= "suspended" then
error("unable to inspect an inactive thread")
end
elseif kind ~= "function" then
error("invalid inspection value, got `"..kind.."' (`function' or `thread' expected)")
end
if self[".level"] then
rawset(self, #self+1, self[".level"])
rawset(self, #self+1, self[".thread"])
else
rawset(self, #self+1, self[".current"].func)
end
if kind == "thread" then
self[".level"] = 1
self[".thread"] = where
self[".current"] = call(self, debug.getinfo, self[".level"], infoflags)
else
self[".level"] = false
self[".thread"] = false
self[".current"] = call(self, debug.getinfo, where, infoflags)
end
end
function Command.goup()
local level = self[".level"]
if level then
local next = call(self, debug.getinfo, level + 1, infoflags)
if next then
rawset(self, #self+1, -1)
self[".level"] = level + 1
self[".current"] = next
else
error("top level reached")
end
else
error("unable to go up in inactive functions")
end
end
function Command.back()
if #self > 0 then
local kind = type(self[#self])
if kind == "number" then
self[".level"] = self[".level"] + self[#self]
self[".current"] = call(self, debug.getinfo, self[".level"], infoflags)
self[#self] = nil
elseif kind == "function" then
self[".level"] = false
self[".thread"] = false
self[".current"] = call(self, debug.getinfo, self[#self], infoflags)
self[#self] = nil
else
self[".thread"] = self[#self]
self[#self] = nil
self[".level"] = self[#self]
self[#self] = nil
self[".current"] = call(self, debug.getinfo, self[".level"], infoflags)
end
else
error("no more backs avaliable")
end
end
function Command.hist()
local index = #self
while self[index] ~= nil do
local kind = type(self[index])
if kind == "number" then
self.viewer:print(" up one level")
index = index - 1
elseif kind == "function" then
self.viewer:print(" left inactive ",self[index])
index = index - 1
else
self.viewer:print(" left ",self[index] or "main thread"," at level ",self[index-1])
index = index - 2
end
end
end
function Command.curr()
local viewer = self.viewer
local level = self[".level"]
if level then
local thread = self[".thread"]
if thread
then viewer:write(thread)
else viewer.output:write("main thread")
end
viewer:print(", level ", call(self, debug.traceback, level, level))
else
viewer:print("inactive function ",self[".current"].func)
end
end
function Command.done()
while #self > 0 do
self[#self] = nil
end
self[".thread"] = false
self[".level"] = false
self[".current"] = false
end
function Command.step(level)
if level == "in" then level = -1
elseif level == "out" then level = 1
else level = 0 end
rawset(self, ".hook", level)
Command.done()
end
function Command.lsbp()
local breaks = {}
for line, files in pairs(self.breaks) do
for file in pairs(files) do
breaks[#breaks+1] = file..":"..line
end
end
table.sort(breaks)
for _, bp in ipairs(breaks) do
self.viewer:print(bp)
end
end
function Command.mkbp(file, line)
assert(type(file) == "string", "usage: mkbp(<file>, <line>)")
assert(type(line) == "number", "usage: mkbp(<file>, <line>)")
self.breaks[line][file] = true
end
function Command.rmbp(file, line)
assert(type(file) == "string", "usage: rmbp(<file>, <line>)")
assert(type(line) == "number", "usage: rmbp(<file>, <line>)")
local files = rawget(self.breaks, line)
if files then
files[file] = nil
if next(files) == nil then
self.breaks[line] = nil
end
end
end
--------------------------------------------------------------------------------
local BreaksListMeta = {
__index = function(self, line)
local files = {}
rawset(self, line, files)
return files
end,
}
function __init(self, object)
self = oo.rawnew(self, object)
self.breaks = setmetatable(self.breaks or {}, BreaksListMeta)
function self.breakhook(event, line)
local level = rawget(self, "break.level")
if event == "line" then
-- check for break points
local files = rawget(self.breaks, line)
if files then
local source = debug.getinfo(2, "S").source
for file in pairs(files) do
if source:find(file, #source - #file + 1, true) then
level = 0
break
end
end
end
if level == nil or level > 0 then return end
self:console(2)
level = rawget(self, ".hook")
rawset(self, ".hook", nil)
if level == nil then self:restorehook() end
elseif level ~= nil then
if event == "call" then
level = level + 1
else
level = level - 1
end
end
rawset(self, "break.level", level)
local hookbak = rawget(self, "hook.bak")
if hookbak then
return hookbak(event, line)
end
end
return self
end
function __index(inspector, field)
if rawget(_M, field) ~= nil then
return rawget(_M, field)
end
if Command[field] then
self = inspector
return Command[field]
end
local name, value
local func = rawget(inspector, ".level")
if func then
local index = 1
repeat
name, value = call(inspector, debug.getlocal, func, index)
if name == field
then return value
else index = index + 1
end
until not name
end
local func = rawget(inspector, ".current")
if func then
func = func.func
local index = 1
repeat
name, value = debug.getupvalue(func, index)
if name == field
then return value
else index = index + 1
end
until not name
value = getfenv(func)[field]
if value ~= nil then return value end
return _G[field]
end
end
function __newindex(inspector, field, value)
if rawget(_M, field) == nil then
local name
local index
local func = inspector[".level"]
if func then
index = 1
repeat
name = call(inspector, debug.getlocal, func, index)
if name == field
then return call(inspector, debug.setlocal, func, index, value)
else index = index + 1
end
until not name
end
func = inspector[".current"]
if func then
func = func.func
index = 1
repeat
name = debug.getupvalue(func, index)
if name == field
then return debug.setupvalue(func, index, value)
else index = index + 1
end
until not name
getfenv(func)[field] = value
return
end
end
rawset(inspector, field, value)
end
local function results(self, success, ...)
if not success then
io.stderr:write(..., "\n")
elseif select("#", ...) > 0 then
self.viewer:write(...)
self.viewer.output:write("\n")
end
end
function console(self, level)
if self.active then
assert(not rawget(self, ".current"),
"cannot invoke inspector operation from the console")
level = level or 1
rawset(self, ".thread", coroutine.running() or false)
rawset(self, ".current", call(self, debug.getinfo, level + 2, infoflags)) -- call, stop
rawset(self, ".level", level + 5) -- call, command, <inspection>, xpcall, stop
local viewer = self.viewer
local input = self.input
local cmd, errmsg
repeat
local info = self[".current"]
viewer.output:write(
info.short_src or info.what,
":",
(info.currentline ~= -1 and info.currentline) or
(info.linedefined ~= -1 and info.linedefined) or "?",
" ",
info.namewhat,
info.namewhat == "" and "" or " ",
info.name or viewer:tostring(info.func),
"> "
)
cmd = assert(input:read())
local short = cmd:match("^%s*([%a_][%w_]*)%s*$")
if short and Command[short]
then cmd = short.."()"
else cmd = cmd:gsub("^%s*=", "return ")
end
cmd, errmsg = loadstring(cmd, "inspection")
if cmd then
setfenv(cmd, self)
results(self, xpcall(cmd, debug.traceback))
else
viewer.output:write(errmsg, "\n")
end
until not rawget(self, ".current")
end
end
--------------------------------------------------------------------------------
function restorehook(self)
if next(self.breaks) == nil then
debug.sethook(
rawget(self, "hook.bak") or nil,
rawget(self, "mask.bak"),
rawget(self, "count.bak")
)
rawset(self, "hook.bak", nil)
rawset(self, "mask.bak", nil)
rawset(self, "count.bak", nil)
rawset(self, "break.level", nil)
end
end
function setuphook(self)
if rawget(self, "hook.bak") ~= self.breakhook then
local hook, mask, count = debug.gethook()
rawset(self, "hook.bak", hook or false)
rawset(self, "mask.bak", mask)
rawset(self, "count.bak", count)
debug.sethook(self.breakhook, "crl", count)
end
end
function stop(self, level)
rawset(self, "break.level", level and level+2 or 3)
self:setuphook()
end
function setbreak(self, file, line)
self.breaks[line][file] = true
self:setuphook()
end
function removebreak(self, file, line)
local files = rawget(self.breaks, line)
if files then
files[file] = nil
if next(files) == nil then
self.breaks[line] = nil
if rawget(self, "break.level") == nil then
self:restorehook()
end
end
end
end
local function ibreaks(self, file, line)
local files = rawget(self.breaks, line)
while files do
file = next(files, file)
if file then
return file, line
end
line, files = next(self.breaks, line)
end
end
function allbreaks(self)
return ibreaks, self, nil, next(self.breaks)
end