Initial commit
commit
5d2e82b128
|
@ -0,0 +1,2 @@
|
|||
read_globals = {"minetest"}
|
||||
globals = {"dbg"}
|
|
@ -0,0 +1,7 @@
|
|||
Copyright 2022 Lars Müller
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,128 @@
|
|||
# Debug (`dbg`)
|
||||
|
||||
Debugging on steroids for Minetest mods
|
||||
|
||||
## Motivation
|
||||
|
||||
Lua offers very powerful introspective facilities through its `debug` library, which unfortunately almost always go unused due to their clunky APIs.
|
||||
|
||||
Current state-of-the-art in Minetest mod debugging appears to be print/log/chat "debugging" using serialization or `dump`,
|
||||
all of which should be rendered obsolete by `dbg`.
|
||||
|
||||
## API
|
||||
|
||||
**Optionally depend on `dbg` in `mod.conf` to ensure that it is available for load-time debugging.**
|
||||
|
||||
### `dbg()`
|
||||
|
||||
Shorter alias for `dbg.dd()`.
|
||||
|
||||
`debug.debug()` on steroids: Starts a REPL (read-eval-print-loop); equivalent to a breakpoint. Features include:
|
||||
|
||||
* Full access to locals & upvalues of the calling function
|
||||
* Own local table `_L` where `local` debug vars get stored
|
||||
* Ability to enter expressions (rather than statements)
|
||||
* Continuation, which works the same as in the Lua REPL (+ empty lines working)
|
||||
* Pretty-printing using `dbg.pp`, including syntax highlighting
|
||||
|
||||
Enter `cont` to exit without an error. Use `err` to throw after error debugging sessions (`dbg.error`, `dbg.assert`).
|
||||
|
||||
Use EOF (<kbd>Ctrl + D</kbd> on Unix) to exit & shut down Minetest.
|
||||
|
||||
### `dbg.error(message, [level])`
|
||||
|
||||
Starts a debugging session at the given (optional) level, printing the error message.
|
||||
|
||||
### `dbg.assert(value, [message])`
|
||||
|
||||
Returns `value`. Starts an error debugging session if `value` is falsey. Error `message` is optional.
|
||||
|
||||
### `dbg.pp(...)`
|
||||
|
||||
Pretty-prints the given vararg using the default parameters.
|
||||
|
||||
If the argument list of functions is unreliable (see `dbg.getargs_reliable` below),
|
||||
a question mark (`?`) will be appended to the argument list to indicate this.
|
||||
|
||||
### `dbg.ppp(params, ...)`
|
||||
|
||||
Parameterized pretty-print. Requires a custom pretty-printer parameter table `params`:
|
||||
|
||||
* `write = function(str, token_type)`, where `token_type` is optional and may be one of `nil`, `boolean`, `number`, `string`, `reference`, `function` or `type`
|
||||
* `upvalues = true`, whether upvalues should be written
|
||||
|
||||
### `dbg.vars(level)`
|
||||
|
||||
Returns a virtual variable table of locals & upvalues `vars` for the given stacklevel that supports the following operations:
|
||||
|
||||
* Getting: `vars.varname`
|
||||
* Setting: `vars.varname = value`
|
||||
* Looping: `for varname, value in vars() do ... end`
|
||||
|
||||
### `dbg.locals(level)`
|
||||
|
||||
Returns a virtual variable table of local values at the given stack level.
|
||||
|
||||
Locals include upvalues.
|
||||
|
||||
### `dbg.upvals(func)`
|
||||
|
||||
`func` may be either a function or a stack level (including `nil`, which defaults to the stack level of the calling function).
|
||||
|
||||
Returns a virtual variable table of upvalues at the given stack level.
|
||||
|
||||
### `dbg.traceback(level)`
|
||||
|
||||
Formats a stack trace starting at `level`. Similar to Lua's builtin `debug.stacktrace`, but shortens paths and accepts no `message` to prepend.
|
||||
|
||||
### `dbg.stackinfo(level)`
|
||||
|
||||
Returns a list of `info` by repeatedly calling `debug.getinfo` starting with `level` and working down the stack.
|
||||
|
||||
### `dbg.getvararg(level)`
|
||||
|
||||
**Only available on LuaJIT; on PUC Lua 5.1, `dbg.getvararg` will be `nil`.**
|
||||
|
||||
Returns the vararg at the given stack level.
|
||||
|
||||
### `dbg.getargs(func)`
|
||||
|
||||
**Function parameter list detection doesn't work properly on PUC Lua 5.1; unused params are lost and varargs are turned into `arg`.**
|
||||
Use `dbg.getargs_reliable` (boolean) to check for reliability.
|
||||
|
||||
Returns a table containing the argument names of `func` in string form
|
||||
(example: `{"x", "y", "z", "..."}` for `function(x, y, z, ...) end`).
|
||||
|
||||
### `dbg.shorten_path(path)`
|
||||
|
||||
Shortens `path`: If path is a subpath of a mod, it will be shortened to `"<modname>:<subpath>"`.
|
||||
|
||||
## Security
|
||||
|
||||
Debug deliberately exposes the unrestricted `debug` API globally, as well as the `dbg` wrapper API,
|
||||
both of which can be abused to exit the mod security sandbox.
|
||||
|
||||
**Only use `dbg` in environments where you trust all enabled mods.**
|
||||
**Adding `dbg` to `secure.trusted_mods` (recommended) or disabling mod security (not recommended) is required.**
|
||||
|
||||
The `/lua` chatcommand must explicitly be enabled on servers by setting `secure.dbg.lua` to `true`;
|
||||
if enabled, server owners risk unprivileged users gaining access through MITM attacks.
|
||||
|
||||
## Usage
|
||||
|
||||
**Prerequisites:** LuaJIT and a terminal with decent ANSI support are highly recommended.
|
||||
|
||||
### `/dbg`
|
||||
|
||||
Calls `dbg()` to start debugging in the console.
|
||||
|
||||
### `/lua <code>`
|
||||
|
||||
Executes the code and pretty-prints the result(s) to chat.
|
||||
Only available in singleplayer for security reasons (risk of MITM attacks).
|
||||
|
||||
---
|
||||
|
||||
Links: [GitHub](https://github.com/appgurueu/dbg), [ContentDB](https://content.minetest.net/packages/LMD/dbg), [Minetest Forums](https://forum.minetest.net/viewtopic.php?f=9&t=28372)
|
||||
|
||||
License: Written by Lars Müller and licensed under the MIT license (see `License.txt`).
|
|
@ -0,0 +1,46 @@
|
|||
minetest.register_chatcommand("dbg", {
|
||||
description = "Start debugging",
|
||||
privs = { server = true },
|
||||
func = function() return dbg() end,
|
||||
})
|
||||
|
||||
if not minetest.is_singleplayer() then
|
||||
return
|
||||
end
|
||||
|
||||
local token_colors = {
|
||||
["nil"] = "#ff5f00",
|
||||
boolean = "#ff5f00",
|
||||
number = "#0000ff",
|
||||
string = "#008700",
|
||||
reference = "#af0000",
|
||||
["function"] = "#ff0000",
|
||||
type = "#00ffff",
|
||||
}
|
||||
minetest.register_chatcommand("lua", {
|
||||
params = "<code>",
|
||||
description = "Execute Lua code",
|
||||
privs = {server = true},
|
||||
func = function(_, code)
|
||||
local func, err = loadstring("return " .. code, "=cmd")
|
||||
if not func then
|
||||
func, err = loadstring(code, "=cmd")
|
||||
end
|
||||
if not func then
|
||||
return false, minetest.colorize("red", "syntax error: ") .. err
|
||||
end
|
||||
local function handle(status, ...)
|
||||
local rope = {status and minetest.colorize("lime", "returned: ") or minetest.colorize("red", "error: ")}
|
||||
dbg.ppp({
|
||||
write = function(text, token)
|
||||
table.insert(rope, token and minetest.colorize(token_colors[token], text) or text)
|
||||
end,
|
||||
upvalues = false -- keep matters short
|
||||
}, ...)
|
||||
local str = table.concat(rope)
|
||||
return status, #str > 16e3 and (str:sub(1, 16e3) .. " <truncated>") or str
|
||||
end
|
||||
-- Use pcall: No point in proper stacktraces for oneliners
|
||||
return handle(pcall(func))
|
||||
end,
|
||||
})
|
|
@ -0,0 +1,288 @@
|
|||
local debug = ...
|
||||
|
||||
local function vars(where, getvar, setvar)
|
||||
local idx = {}
|
||||
do
|
||||
local i = 1
|
||||
while true do
|
||||
local name = getvar(where, i)
|
||||
if name == nil then break end
|
||||
idx[name] = i
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
return setmetatable({}, {
|
||||
__index = function(_, name)
|
||||
local _, value = getvar(where, assert(idx[name], "no variable with given name"))
|
||||
return value
|
||||
end,
|
||||
__newindex = function(_, name, value)
|
||||
setvar(where, assert(idx[name], "no variable with given name"), value)
|
||||
end,
|
||||
__call = function()
|
||||
local i = 0
|
||||
local function iterate()
|
||||
i = i + 1
|
||||
-- Making this a tail call requires changing `where`
|
||||
local name, value = getvar(where, i)
|
||||
return name, value
|
||||
end
|
||||
return iterate
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
function dbg.locals(level)
|
||||
level = level or 1
|
||||
return vars(level + 1, debug.getlocal, debug.setlocal)
|
||||
end
|
||||
|
||||
function dbg.upvals(func)
|
||||
if type(func) ~= "function" then
|
||||
func = debug.getinfo((func or 1) + 1, "f").func
|
||||
end
|
||||
return vars(func, debug.getupvalue, debug.setupvalue)
|
||||
end
|
||||
|
||||
function dbg.vars(level)
|
||||
level = (level or 1) + 1
|
||||
local func = debug.getinfo(level, "f").func
|
||||
local idx, is_local = {}, {}
|
||||
-- Upvals
|
||||
do
|
||||
local i = 1
|
||||
while true do
|
||||
local name = debug.getupvalue(func, i)
|
||||
if name == nil then break end
|
||||
idx[name] = i
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
-- Locals
|
||||
do
|
||||
local i = 1
|
||||
while true do
|
||||
local name = debug.getlocal(level, i)
|
||||
if name == nil then break end
|
||||
idx[name] = i
|
||||
is_local[name] = true -- might shadow upval
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
return setmetatable({}, {
|
||||
__index = function(_, name)
|
||||
local var_idx = assert(idx[name], "no variable with given name")
|
||||
local _, value
|
||||
if is_local[name] then
|
||||
_, value = debug.getlocal(level, var_idx)
|
||||
else
|
||||
_, value = debug.getupvalue(func, var_idx)
|
||||
end
|
||||
return value
|
||||
end,
|
||||
__newindex = function(_, name, value)
|
||||
local var_idx = assert(idx[name], "no variable with given name")
|
||||
if is_local[name] then
|
||||
debug.setlocal(level, var_idx, value)
|
||||
else
|
||||
debug.setupvalue(func, var_idx, value)
|
||||
end
|
||||
end,
|
||||
__call = function()
|
||||
local i, upvals = 1, true
|
||||
local function iterate()
|
||||
local name, value
|
||||
if upvals then
|
||||
repeat -- search for not-shadowed upvals
|
||||
name, value = debug.getupvalue(func, i)
|
||||
i = i + 1
|
||||
until not is_local[name]
|
||||
if name == nil then
|
||||
i, upvals = 1, false
|
||||
return iterate()
|
||||
end
|
||||
else
|
||||
name, value = debug.getlocal(level, i)
|
||||
i = i + 1
|
||||
end
|
||||
return name, value
|
||||
end
|
||||
return iterate
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
-- Roughly the same format as used by debug.traceback, but paths are shortened
|
||||
local function fmt_callinfo(level)
|
||||
local info = debug.getinfo(level, "Snlf")
|
||||
if not info then
|
||||
return
|
||||
end
|
||||
|
||||
local is_path = info.source:match"^@"
|
||||
local short_src = is_path and dbg.shorten_path(info.short_src) or info.short_src
|
||||
|
||||
local where
|
||||
if (info.namewhat or "") ~= "" then
|
||||
where = "in function " .. info.name
|
||||
elseif info.what == "Lua" then
|
||||
where = ("in function defined at line %d"):format(info.linedefined)
|
||||
elseif info.what == "main" then
|
||||
where = "in main chunk"
|
||||
else
|
||||
where = "?"
|
||||
end
|
||||
|
||||
return short_src .. ":" .. (info.currentline > 0 and ("%d:"):format(info.currentline) or "") .. " " .. where, info.func
|
||||
end
|
||||
|
||||
local max_top_levels, max_bottom_levels = 5, 5
|
||||
function dbg._traceback(level, until_func --[[and including]])
|
||||
level = (level or 1) + 1
|
||||
local res = {"stack traceback:"}
|
||||
local function concat() return table.concat(res, "\n\t") end
|
||||
-- Write top levels
|
||||
for top_level = 1, max_top_levels do
|
||||
local str, func = fmt_callinfo(level + top_level)
|
||||
if not str then return concat() end
|
||||
table.insert(res, str)
|
||||
if func == until_func then return concat() end
|
||||
end
|
||||
local last_written_top_level = level + max_top_levels
|
||||
-- Determine stack depth
|
||||
level = last_written_top_level
|
||||
repeat
|
||||
level = level + 1
|
||||
until not debug.getinfo(level, "")
|
||||
-- Write bottom levels
|
||||
local first_bottom_level = level - max_bottom_levels
|
||||
if last_written_top_level + 1 > first_bottom_level then
|
||||
first_bottom_level = last_written_top_level + 1
|
||||
else
|
||||
table.insert(res, "...")
|
||||
end
|
||||
for bottom_level = first_bottom_level, level - 1 do
|
||||
local str, func = fmt_callinfo(bottom_level)
|
||||
table.insert(res, str)
|
||||
if func == until_func then return concat() end
|
||||
end
|
||||
return concat()
|
||||
end
|
||||
|
||||
-- Hide until_func parameter
|
||||
function dbg.traceback(level)
|
||||
return dbg._traceback(level)
|
||||
end
|
||||
|
||||
function dbg.stackinfo(level)
|
||||
local res = {}
|
||||
while true do
|
||||
local info = debug.getinfo(level)
|
||||
if not info then return res end
|
||||
table.insert(res, info)
|
||||
end
|
||||
end
|
||||
|
||||
--! Only available on Lua 5.2 / LuaJIT; use the `arg` local on Lua 5.1 instead
|
||||
function dbg.getvararg(level)
|
||||
level = (level or 1) + 1
|
||||
local function _getvararg(i)
|
||||
local name, value = debug.getlocal(level, i)
|
||||
if not name then return end
|
||||
return value, _getvararg(i - 1)
|
||||
end
|
||||
return _getvararg(-1)
|
||||
end
|
||||
|
||||
-- Test dbg.getvararg to set it to `nil` if it isn't supported
|
||||
(function(...) -- luacheck: ignore
|
||||
local args = {dbg.getvararg()}
|
||||
if #args == 3 then
|
||||
for i = 1, 3 do
|
||||
if args[i] ~= i then
|
||||
dbg.getvararg = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
dbg.getvararg = nil
|
||||
end
|
||||
end)(1, 2, 3)
|
||||
|
||||
local function nils(n)
|
||||
if n == 1 then return nil end
|
||||
return nil, nils(n - 1)
|
||||
end
|
||||
|
||||
local function getargs(func)
|
||||
-- This function must be explicitly handled
|
||||
-- as otherwise the first call to it might trigger a false-positive in the hook
|
||||
if func == nils then return {"n"} end
|
||||
|
||||
local what = debug.getinfo(func, "S").what
|
||||
if what == "C" then return {"?"} end
|
||||
if what == "main" then return {"..."} end
|
||||
assert(what == "Lua")
|
||||
|
||||
local hook, mask, count = debug.gethook()
|
||||
|
||||
local args = {}
|
||||
debug.sethook(function()
|
||||
local called_func = debug.getinfo(2, "f")
|
||||
if called_func.func ~= func then return end
|
||||
local i = 1
|
||||
while true do
|
||||
local name = debug.getlocal(2, i)
|
||||
if name == nil or name:match"^%(" then break end
|
||||
table.insert(args, name)
|
||||
i = i + 1
|
||||
end
|
||||
error(args)
|
||||
end, "c")
|
||||
local status, _args = pcall(func)
|
||||
assert(not status and args == _args)
|
||||
|
||||
-- Check for vararg by supplying one extraneous param
|
||||
debug.sethook(function()
|
||||
local called_func = debug.getinfo(2, "f")
|
||||
if called_func.func ~= func then return end
|
||||
if debug.getlocal(2, -1) then -- vararg
|
||||
table.insert(args, "...")
|
||||
end
|
||||
error(args)
|
||||
end, "c")
|
||||
status, _args = pcall(func, nils(#args + 1))
|
||||
assert(not status and args == _args)
|
||||
|
||||
debug.sethook(hook, mask, count) -- restore previous hook
|
||||
|
||||
return args
|
||||
end
|
||||
|
||||
local function shallowequals(t1, t2)
|
||||
for k, v in pairs(t1) do
|
||||
if t2[k] ~= v then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Tests to check for PUC Lua 5.1 unreliability
|
||||
-- Ignore "unused argument" warnings
|
||||
-- luacheck: push ignore 212
|
||||
local function test_getargs()
|
||||
for func, expected_arg_list in pairs{
|
||||
[function(x, y, z)end] = {"x", "y", "z"}, -- unused arguments
|
||||
[function(...)end] = {"..."}, -- unused vararg
|
||||
[function(x, y, z, ...)end] = {"x", "y", "z", "..."}, -- both
|
||||
[nils] = {"n"}
|
||||
} do
|
||||
if not shallowequals(getargs(func), expected_arg_list) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
-- luacheck: pop
|
||||
|
||||
dbg.getargs = getargs
|
||||
dbg.getargs_reliable = test_getargs()
|
|
@ -0,0 +1,175 @@
|
|||
local debug = ...
|
||||
local nil_placeholder = {}
|
||||
local function dd(level, caught_err)
|
||||
level = (level or 1) + 1 -- add one stack level for this func (dd)
|
||||
|
||||
print(dbg.traceback(level))
|
||||
|
||||
local func = debug.getinfo(level, "f").func
|
||||
local func_env = getfenv(level)
|
||||
level = level + 3 -- __[new]index + loaded chunk + xpcall
|
||||
local mt = {
|
||||
__index = function(env, varname)
|
||||
-- Debug locals
|
||||
local val = env._L[varname]
|
||||
if val ~= nil then
|
||||
if val == nil_placeholder then
|
||||
return nil
|
||||
end
|
||||
return val
|
||||
end
|
||||
-- Locals of the caller
|
||||
local i = 1
|
||||
while true do
|
||||
local name, value = debug.getlocal(level, i)
|
||||
if name == nil then break end
|
||||
if varname == name then return value end
|
||||
i = i + 1
|
||||
end
|
||||
-- Upvalues of the caller
|
||||
i = 1
|
||||
while true do
|
||||
local name, value = debug.getupvalue(func, i)
|
||||
if name == nil then break end
|
||||
if varname == name then return value end
|
||||
i = i + 1
|
||||
end
|
||||
return func_env[varname]
|
||||
end,
|
||||
__newindex = function(env, varname, value)
|
||||
-- Debug locals
|
||||
if env._L[varname] ~= nil then
|
||||
if value == nil then value = nil_placeholder end
|
||||
env._L[varname] = value
|
||||
return
|
||||
end
|
||||
-- Locals
|
||||
local i = 1
|
||||
while true do
|
||||
local name = debug.getlocal(level, i)
|
||||
if name == nil then break end
|
||||
if varname == name then
|
||||
debug.setlocal(level, i, value)
|
||||
return
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
-- Upvalues
|
||||
i = 1
|
||||
while true do
|
||||
local name = debug.getupvalue(func, i)
|
||||
if name == nil then break end
|
||||
if varname == name then
|
||||
debug.setupvalue(func, i, value)
|
||||
return
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
func_env[varname] = value -- if local by default - how to access parent env then? _ENV?
|
||||
end
|
||||
}
|
||||
|
||||
-- TODO (?) special debug & dbg wrappers with stack level offsets
|
||||
-- Functions: debug.(getinfo|[gs]et(local|upvalue)|traceback), [gs]etfenv, dbg.(* \ getargs)
|
||||
local env = setmetatable({_L = {}, _G = _G, _ENV = func_env}, mt)
|
||||
|
||||
-- Source buffer
|
||||
local buf, buf_i = {}, 0
|
||||
local function getbuf()
|
||||
buf_i = buf_i + 1
|
||||
if not buf[buf_i] then return end
|
||||
return buf[buf_i] .. "\n"
|
||||
end
|
||||
local function loadbuf()
|
||||
buf_i = 0
|
||||
return load(getbuf, "=stdin")
|
||||
end
|
||||
|
||||
while true do
|
||||
io.write(#buf == 0 and "dbg> " or ("[%d]> "):format(#buf + 1))
|
||||
|
||||
local line = io.read()
|
||||
if not line then -- EOF
|
||||
print()
|
||||
minetest.request_shutdown("debugging", true, 0)
|
||||
break
|
||||
end
|
||||
|
||||
if line == "cont" or (caught_err and line == "err") then
|
||||
return line == "err"
|
||||
end
|
||||
|
||||
local chunk, err, continuation
|
||||
if line:match"^%s+$" then -- skip spacing-only lines
|
||||
continuation = #buf ~= 0
|
||||
else
|
||||
if line:match"^=" then
|
||||
line = "return " .. line:sub(2)
|
||||
end
|
||||
buf[#buf + 1] = line
|
||||
chunk, err = loadbuf()
|
||||
if #buf == 1 and not chunk and not line:match"^return " then
|
||||
-- Try implicit return
|
||||
buf = {"return " .. line}
|
||||
chunk, err = loadbuf()
|
||||
end
|
||||
continuation = err and err:find"<eof>" -- same hack as used in the Lua REPL
|
||||
end
|
||||
if chunk then
|
||||
setfenv(chunk, env)
|
||||
|
||||
local hook, mask, count = debug.gethook()
|
||||
local function restore_hook()
|
||||
return debug.sethook(hook, mask, count)
|
||||
end
|
||||
debug.sethook(function()
|
||||
if debug.getinfo(2, "f").func ~= chunk then return end
|
||||
local i = 1
|
||||
while true do
|
||||
local name, value = debug.getlocal(2, i)
|
||||
if name == nil then break end
|
||||
if value == nil then value = nil_placeholder end
|
||||
env._L[name] = value
|
||||
i = i + 1
|
||||
end
|
||||
end, "r");
|
||||
|
||||
(function(status, ...)
|
||||
if status then
|
||||
restore_hook()
|
||||
dbg.pp(...)
|
||||
end
|
||||
end)(xpcall(chunk, function(error)
|
||||
restore_hook()
|
||||
print(dbg._traceback(3, chunk)) -- this handler + [C]: function error -> 2 levels to skip
|
||||
io.write"error: "; dbg.pp(error)
|
||||
end))
|
||||
buf = {} -- clear buffer
|
||||
elseif continuation then
|
||||
if #buf == 1 then -- overwrite first line
|
||||
io.write("\27[F[1]> ", buf[1], "\n")
|
||||
end
|
||||
else
|
||||
io.write("syntax error: ", err, "\n")
|
||||
buf = {} -- clear buffer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function dbg.dd(level)
|
||||
return dd(level)
|
||||
end
|
||||
|
||||
local error = error -- localize error to allow overriding _G.error = dbg.error
|
||||
|
||||
function dbg.error(msg, level)
|
||||
print("caught error: "); dbg.pp(msg)
|
||||
if dd(level, true) then
|
||||
return error(msg, level)
|
||||
end
|
||||
end
|
||||
|
||||
function dbg.assert(value, msg)
|
||||
if not value then dbg.error(msg or "assertion failed!") end
|
||||
return value
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
dbg = {}
|
||||
|
||||
local debug = assert(minetest.request_insecure_environment(), "add dbg to secure.trusted_mods").debug
|
||||
|
||||
local function load(filename)
|
||||
return assert(loadfile(minetest.get_modpath(minetest.get_current_modname()) .. ("/%s.lua"):format(filename)))(debug)
|
||||
end
|
||||
|
||||
load"shorten_path"
|
||||
load"pp"
|
||||
load"dbg"
|
||||
load"dd"
|
||||
|
||||
setmetatable(dbg, {__call = function(_, ...) return dbg.dd(...) end})
|
||||
|
||||
load"chat_commands"
|
||||
|
||||
load"test"
|
||||
|
||||
_G.debug = debug -- deliberately expose the insecure debug library
|
||||
|
||||
-- TODO (...) hook call events to intercept actual assert/error; set nil debug metatable to intercept attempts
|
||||
-- TODO (?) "inf" loop "detection" through (line or instr?) hook?
|
|
@ -0,0 +1,3 @@
|
|||
name = dbg
|
||||
title = Debug
|
||||
description = Debugging on steroids
|
|
@ -0,0 +1,216 @@
|
|||
local debug = ...
|
||||
|
||||
local default_params = {upvalues = true}
|
||||
|
||||
do
|
||||
local write = io.write
|
||||
-- See https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
|
||||
local color_codes = {
|
||||
["nil"] = 202,
|
||||
boolean = 202,
|
||||
number = 21,
|
||||
string = 28,
|
||||
reference = 124,
|
||||
["function"] = 196,
|
||||
type = 14,
|
||||
}
|
||||
function default_params.write(str, token)
|
||||
if token then
|
||||
write(("\27[38;5;%dm"):format(color_codes[token])) -- set appropriate FG color
|
||||
write(str)
|
||||
write("\27[0m") -- reset FG color
|
||||
else
|
||||
write(str)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local keywords = {}
|
||||
for _, keyword in pairs{
|
||||
"and", "break", "do", "else", "elseif", "end",
|
||||
"false", "for", "function", "goto", "if", "in",
|
||||
"local", "nil", "not", "or", "repeat", "return",
|
||||
"then", "true", "until", "while"
|
||||
} do
|
||||
keywords[keyword] = true
|
||||
end
|
||||
|
||||
-- Single quotes don't need to be escaped
|
||||
local escapes = {}
|
||||
for char in ("abfrtv\n\\\""):gmatch"." do
|
||||
local escaped = "\\" .. char
|
||||
escapes[loadstring(('return "%s"'):format(escaped))()] = escaped
|
||||
end
|
||||
-- ("%q"):format(str) doesn't deal with tabs etc. gracefully so we must roll our own
|
||||
local function escape_str(str)
|
||||
return str:gsub(".", escapes):gsub("([^\32-\126])()", function(char, after_pos)
|
||||
if escapes[char] then return end
|
||||
return (str:sub(after_pos, after_pos):match"%d" and "\\%03d" or "\\%d"):format(char:byte())
|
||||
end)
|
||||
end
|
||||
|
||||
-- TODO (?) cross/back reference distinction
|
||||
-- TODO (???) index _G to produce more sensible names (very ugly performance implications; only do at load-time?)
|
||||
local pp = function(params, ...)
|
||||
local varg_len = select("#", ...)
|
||||
local write = params.write or default_params.write
|
||||
local upvalues = params.upvalues
|
||||
if upvalues == nil then
|
||||
upvalues = default_params.upvalues
|
||||
end
|
||||
|
||||
-- Count references
|
||||
|
||||
local refs = {}
|
||||
local function count_refs(val)
|
||||
local typ = type(val)
|
||||
if val == nil or typ == "boolean" or typ == "number" or typ == "string" then return end
|
||||
if refs[val] then
|
||||
refs[val].count = refs[val].count + 1
|
||||
return
|
||||
end
|
||||
refs[val] = {count = 1}
|
||||
if typ == "function" then
|
||||
if upvalues then
|
||||
local i = 1
|
||||
while true do
|
||||
local name, upval = debug.getupvalue(val, i)
|
||||
if name == nil then break end
|
||||
count_refs(upval)
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
elseif typ == "table" then
|
||||
for k, v in pairs(val) do
|
||||
count_refs(k)
|
||||
count_refs(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, varg_len do
|
||||
count_refs(select(i, ...))
|
||||
end
|
||||
|
||||
local ref_id = 1
|
||||
local function pp(val, indent)
|
||||
local function newline()
|
||||
write"\n"
|
||||
for _ = 1, indent do
|
||||
write"\t"
|
||||
end
|
||||
end
|
||||
local typ = type(val)
|
||||
if val == nil then
|
||||
write("nil", "nil")
|
||||
elseif typ == "boolean" then
|
||||
write(val and "true" or "false", "boolean")
|
||||
elseif typ == "number" then
|
||||
write(("%.17g"):format(val), "number")
|
||||
elseif typ == "string" then
|
||||
write('"' .. escape_str(val) .. '"', "string")
|
||||
else -- reference type
|
||||
local ref_info = refs[val]
|
||||
if ref_info.count > 1 then
|
||||
write(("*%d"):format(ref_info.id or ref_id), "reference")
|
||||
if ref_info.id then
|
||||
return -- ID assigned => already written
|
||||
end
|
||||
-- Assign ID
|
||||
ref_info.id = ref_id
|
||||
ref_id = ref_id + 1
|
||||
write" "
|
||||
end
|
||||
if typ == "function" then
|
||||
local info = debug.getinfo(val, "Su")
|
||||
local write_upvals = upvalues and info.nups > 0
|
||||
if write_upvals then
|
||||
write"("; write("function", "function"); write"()"
|
||||
for i = 1, info.nups do
|
||||
local name, value = debug.getupvalue(val, i)
|
||||
newline()
|
||||
write("local", "function"); write" "; write(name); write" = "; pp(value, indent+1)
|
||||
end
|
||||
newline()
|
||||
end
|
||||
write((write_upvals and "return " or "") .. "function", "function")
|
||||
write"("; write(table.concat(dbg.getargs(val), ", ")); write") "
|
||||
local line = ""
|
||||
if info.linedefined > 0 then
|
||||
if info.linedefined == info.lastlinedefined then
|
||||
line = (":%d"):format(info.linedefined)
|
||||
else
|
||||
line = (":%d-%d"):format(info.linedefined, info.lastlinedefined)
|
||||
end
|
||||
end
|
||||
write(dbg.shorten_path(info.short_src)); write(line)
|
||||
write" "; write("end", "function")
|
||||
if write_upvals then
|
||||
indent = indent - 1; newline()
|
||||
write("end", "function"); write")"
|
||||
end
|
||||
elseif typ == "table" then
|
||||
write"{"
|
||||
local len = 0
|
||||
local first = true
|
||||
for _, v in ipairs(val) do
|
||||
if not first then write"," end
|
||||
newline()
|
||||
pp(v, indent+1)
|
||||
len = len + 1
|
||||
first = false
|
||||
end
|
||||
local hash_keys = {}
|
||||
local traversal_order = {}
|
||||
local i = 1
|
||||
for k in pairs(val) do
|
||||
if not (type(k) == "number" and k % 1 == 0 and k >= 1 and k <= len) then
|
||||
table.insert(hash_keys, k)
|
||||
traversal_order[k] = i
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
table.sort(hash_keys, function(a, b)
|
||||
local t_a, t_b = type(a), type(b)
|
||||
if t_a ~= t_b then
|
||||
return t_a < t_b
|
||||
end
|
||||
if t_a == "string" or t_a == "number" then
|
||||
return a < b
|
||||
end
|
||||
return traversal_order[a] < traversal_order[b]
|
||||
end)
|
||||
for _, k in ipairs(hash_keys) do
|
||||
if not first then write"," end
|
||||
local v = val[k]
|
||||
newline()
|
||||
if type(k) == "string" and not keywords[k] and k:match"^[A-Za-z_][A-Za-z%d_]*$" then
|
||||
write(k); write" = "
|
||||
else
|
||||
write"["; pp(k, indent + 1); write"] = "
|
||||
end
|
||||
pp(v, indent + 1)
|
||||
first = false
|
||||
end
|
||||
if next(val) ~= nil then indent = indent - 1; newline() end
|
||||
write"}"
|
||||
else
|
||||
write(("<%s>"):format(typ), "type")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, varg_len do
|
||||
pp(select(i, ...), 1)
|
||||
if i < varg_len then write",\n" end
|
||||
end
|
||||
if varg_len > 0 then write"\n" end
|
||||
end
|
||||
|
||||
function dbg.pp(...)
|
||||
return pp(default_params, ...)
|
||||
end
|
||||
|
||||
function dbg.ppp(params, ...)
|
||||
return pp(params, ...)
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
-- Build a trie (prefix tree) with all mod paths
|
||||
local modpath_trie = {}
|
||||
for _, modname in pairs(minetest.get_modnames()) do
|
||||
local path = minetest.get_modpath(modname)
|
||||
local subtrie = modpath_trie
|
||||
for char in path:gmatch"." do
|
||||
subtrie[char] = subtrie[char] or {}
|
||||
subtrie = subtrie[char]
|
||||
end
|
||||
subtrie["\\"] = modname
|
||||
subtrie["/"] = modname
|
||||
end
|
||||
|
||||
function dbg.shorten_path(path)
|
||||
-- Search for a prefix (paths have at most one prefix)
|
||||
local subtrie = modpath_trie
|
||||
for i = 1, #path do
|
||||
if type(subtrie) == "string" then
|
||||
return subtrie .. ":" .. path:sub(i)
|
||||
end
|
||||
subtrie = subtrie[path:sub(i, i)]
|
||||
if not subtrie then return path end
|
||||
end
|
||||
return path
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
-- Test variable utils
|
||||
local a, b, c = nil, "b", "c" -- luacheck: ignore
|
||||
local function assert_vars(vartype, expected)
|
||||
local vars = dbg[vartype](2)
|
||||
local i = 0
|
||||
for name, value in vars() do
|
||||
assert(vars[name] == value)
|
||||
assert(expected[i + 1] == name and expected[i + 2] == value)
|
||||
vars[name] = 42
|
||||
assert(vars[name] == 42)
|
||||
vars[name] = value
|
||||
assert(vars[name] == value)
|
||||
i = i + 2
|
||||
end
|
||||
assert(i == #expected)
|
||||
end
|
||||
local f = function(c, e) -- luacheck: ignore
|
||||
assert(a == nil)
|
||||
assert(b == "b")
|
||||
assert_vars("upvals", {
|
||||
"a", nil;
|
||||
"b", "b";
|
||||
"assert_vars", assert_vars;
|
||||
})
|
||||
do
|
||||
local upvals = dbg.upvals()
|
||||
upvals.a = "a"
|
||||
assert(upvals.a == "a" and a == "a")
|
||||
end
|
||||
local f, g = "f", "g"
|
||||
assert_vars("locals", {
|
||||
"c", c;
|
||||
"e", e;
|
||||
"f", f;
|
||||
"g", g;
|
||||
})
|
||||
do
|
||||
local locals = dbg.locals()
|
||||
assert(locals.f == "f" and locals.g == "g")
|
||||
locals.c, locals.e = "c", "e"
|
||||
assert(locals.c == "c" and locals.e == "e" and c == "c" and e == "e")
|
||||
end
|
||||
assert_vars("vars", {
|
||||
"a", a;
|
||||
"b", b;
|
||||
"assert_vars", assert_vars;
|
||||
"c", c;
|
||||
"e", e;
|
||||
"f", f;
|
||||
"g", g;
|
||||
})
|
||||
end
|
||||
f()
|
Loading…
Reference in New Issue