debug step mode

This commit is contained in:
Cloud Wu
2015-02-11 14:18:47 +08:00
parent e031a35330
commit 8792a40845
7 changed files with 436 additions and 80 deletions

View File

@@ -85,18 +85,25 @@ LUA_API int lua_getstack (lua_State *L, int level, lua_Debug *ar) {
CallInfo *ci;
if (level < 0) return 0; /* invalid (negative) level */
lua_lock(L);
for (ci = L->ci; level > 0 && ci != &L->base_ci; ci = ci->previous)
level--;
if (level == 0 && ci != &L->base_ci) { /* level found? */
if (level == 0 && L->status == LUA_YIELD && isLua(L->ci)) {
ci = &(L->temp_ci);
*ci = *(L->ci);
ci->func = restorestack(L, ci->extra);
status = 1;
ar->i_ci = ci;
} else {
for (ci = L->ci; level > 0 && ci != &L->base_ci; ci = ci->previous)
level--;
if (level == 0 && ci != &L->base_ci) { /* level found? */
status = 1;
ar->i_ci = ci;
}
else status = 0; /* no such level */
}
else status = 0; /* no such level */
lua_unlock(L);
return status;
}
static const char *upvalname (Proto *p, int uv) {
TString *s = check_exp(uv < p->sizeupvalues, p->sp->upvalues[uv].name);
if (s == NULL) return "?";
@@ -130,7 +137,7 @@ static const char *findlocal (lua_State *L, CallInfo *ci, int n,
else
base = ci->func + 1;
if (name == NULL) { /* no 'standard' name? */
StkId limit = (ci == L->ci) ? L->top : ci->next->func;
StkId limit = (ci == L->ci || ci == &(L->temp_ci)) ? L->top : ci->next->func;
if (limit - base >= n && n > 0) /* is 'n' inside 'ci' stack? */
name = "(*temporary)"; /* generic name for any valid slot */
else

View File

@@ -160,6 +160,7 @@ struct lua_State {
struct lua_State *twups; /* list of threads with open upvalues */
struct lua_longjmp *errorJmp; /* current error recover point */
CallInfo base_ci; /* CallInfo for first level (C calling Lua) */
CallInfo temp_ci; /* CallInfo for yield from hook (debug api use) */
lua_Hook hook;
ptrdiff_t errfunc; /* current error handling function (stack index) */
int stacksize;

View File

@@ -181,12 +181,97 @@ lconnect(lua_State *L) {
return 1;
}
static const int HOOKKEY = 0;
/*
** Auxiliary function used by several library functions: check for
** an optional thread as function's first argument and set 'arg' with
** 1 if this argument is present (so that functions can skip it to
** access their other arguments)
*/
static lua_State *getthread (lua_State *L, int *arg) {
if (lua_isthread(L, 1)) {
*arg = 1;
return lua_tothread(L, 1);
}
else {
*arg = 0;
return L; /* function will operate over current thread */
}
}
/*
** Call hook function registered at hook table for the current
** thread (if there is one)
*/
static void hookf (lua_State *L, lua_Debug *ar) {
static const char *const hooknames[] =
{"call", "return", "line", "count", "tail call"};
lua_rawgetp(L, LUA_REGISTRYINDEX, &HOOKKEY);
lua_pushthread(L);
if (lua_rawget(L, -2) == LUA_TFUNCTION) { /* is there a hook function? */
lua_pushstring(L, hooknames[(int)ar->event]); /* push event name */
if (ar->currentline >= 0)
lua_pushinteger(L, ar->currentline); /* push current line */
else lua_pushnil(L);
lua_call(L, 2, 1); /* call hook function */
int yield = lua_toboolean(L, -1);
lua_pop(L,1);
if (yield) {
lua_yield(L, 0);
}
}
}
/*
** Convert a string mask (for 'sethook') into a bit mask
*/
static int makemask (const char *smask, int count) {
int mask = 0;
if (strchr(smask, 'c')) mask |= LUA_MASKCALL;
if (strchr(smask, 'r')) mask |= LUA_MASKRET;
if (strchr(smask, 'l')) mask |= LUA_MASKLINE;
if (count > 0) mask |= LUA_MASKCOUNT;
return mask;
}
static int db_sethook (lua_State *L) {
int arg, mask, count;
lua_Hook func;
lua_State *L1 = getthread(L, &arg);
if (lua_isnoneornil(L, arg+1)) { /* no hook? */
lua_settop(L, arg+1);
func = NULL; mask = 0; count = 0; /* turn off hooks */
}
else {
const char *smask = luaL_checkstring(L, arg+2);
luaL_checktype(L, arg+1, LUA_TFUNCTION);
count = (int)luaL_optinteger(L, arg + 3, 0);
func = hookf; mask = makemask(smask, count);
}
if (lua_rawgetp(L, LUA_REGISTRYINDEX, &HOOKKEY) == LUA_TNIL) {
lua_createtable(L, 0, 2); /* create a hook table */
lua_pushvalue(L, -1);
lua_rawsetp(L, LUA_REGISTRYINDEX, &HOOKKEY); /* set it in position */
lua_pushstring(L, "k");
lua_setfield(L, -2, "__mode"); /** hooktable.__mode = "k" */
lua_pushvalue(L, -1);
lua_setmetatable(L, -2); /* setmetatable(hooktable) = hooktable */
}
lua_pushthread(L1); lua_xmove(L1, L, 1); /* key (thread) */
lua_pushvalue(L, arg + 1); /* value (hook function) */
lua_rawset(L, -3); /* hooktable[L1] = new Lua hook */
lua_sethook(L1, func, mask, count);
return 0;
}
int
luaopen_debugchannel(lua_State *L) {
luaL_Reg l[] = {
{ "create", lcreate }, // for write
{ "connect", lconnect }, // for read
{ "release", lrelease },
{ "sethook", db_sethook },
{ NULL, NULL },
};
luaL_checkversion(L);

View File

@@ -233,6 +233,9 @@ function suspend(co, result, command, param, size)
elseif command == "QUIT" then
-- service exit
return
elseif command == nil then
-- debug trace
return
else
error("Unknown command : " .. command .. "\n" .. debug.traceback(co))
end
@@ -719,6 +722,7 @@ local debug = require "skynet.debug"
debug(skynet, {
dispatch = dispatch_message,
clear = clear_pool,
suspend = suspend,
})
return skynet

View File

@@ -60,7 +60,7 @@ end
function dbgcmd.REMOTEDEBUG(...)
local remotedebug = require "skynet.remotedebug"
remotedebug.start(export.dispatch, ...)
remotedebug.start(export, ...)
end
local function _debug_dispatch(session, address, cmd, ...)

View File

@@ -0,0 +1,133 @@
local debug = debug
local table = table
local FUNC_TEMP=[[
local $ARGS
return function(...)
$SOURCE
end,
function()
return {$LOCALS}
end
]]
local temp = {}
local function wrap_locals(co, source, level, ext_funcs)
if co == coroutine.running() then
level = level + 3
end
local f = debug.getinfo(co, level,"f").func
if f == nil then
return false, "Invalid level"
end
local uv = {}
local locals = {}
local uv_id = {}
local local_id = {}
if ext_funcs then
for k,v in pairs(ext_funcs) do
table.insert(uv, k)
end
end
local i = 1
while true do
local name, value = debug.getlocal(co, level, i)
if name == nil then
break
end
if name:byte() ~= 40 then -- '('
table.insert(uv, name)
table.insert(locals, ("[%d]=%s,"):format(i,name))
local_id[name] = value
end
i = i + 1
end
local i = 1
while true do
local name = debug.getupvalue(f, i)
if name == nil then
break
end
uv_id[name] = i
table.insert(uv, name)
i = i + 1
end
temp.ARGS = table.concat(uv, ",")
temp.SOURCE = source
temp.LOCALS = table.concat(locals)
local full_source = FUNC_TEMP:gsub("%$(%w+)",temp)
local loader, err = load(full_source, "=(debug)")
if loader == nil then
return false, err
end
local func, update = loader()
-- join func's upvalues
local i = 1
while true do
local name = debug.getupvalue(func, i)
if name == nil then
break
end
if ext_funcs then
local v = ext_funcs[name]
if v then
debug.setupvalue(func, i, v)
end
end
local local_value = local_id[name]
if local_value then
debug.setupvalue(func, i, local_value)
end
local upvalue_id = uv_id[name]
if upvalue_id then
debug.upvaluejoin(func, i, f, upvalue_id)
end
i=i+1
end
local vararg, v = debug.getlocal(co, level, -1)
if vararg then
local vargs = { v }
local i = 2
while true do
local vararg,v = debug.getlocal(co, level, -i)
if vararg then
vargs[i] = v
else
break
end
i=i+1
end
return func, update, table.unpack(vargs)
else
return func, update
end
end
local function exec(co, level, func, update, ...)
if not func then
return false, update
end
if co == coroutine.running() then
level = level + 2
end
local rets = table.pack(pcall(func, ...))
if rets[1] then
local needupdate = update()
for k,v in pairs(needupdate) do
debug.setlocal(co, level,k,v)
end
return table.unpack(rets, 1, rets.n)
else
return false, rets[2]
end
end
return function (source, co, level, ext_funcs)
co = co or coroutine.running()
level = level or 0
return exec(co, level, wrap_locals(co, source, level, ext_funcs))
end

View File

@@ -1,36 +1,56 @@
local skynet = require "skynet"
local debugchannel = require "debugchannel"
local socket = require "socket"
local injectrun = require "skynet.injectcode"
local table = table
local debug = debug
local coroutine = coroutine
local sethook = debugchannel.sethook
local M = {}
local HOOK_FUNC = "raw_dispatch_message"
local raw_dispatcher
local print = _G.print
local skynet_suspend
local prompt
local newline
local function change_prompt(s)
newline = true
prompt = s
end
local function replace_upvalue(func, uvname, value)
local i = 1
while true do
local name, uv = debug.getupvalue(func, i)
if name == nil then
break
end
if name == uvname then
if value then
debug.setupvalue(func, i, value)
end
return uv
end
i = i + 1
end
end
local function remove_hook(dispatcher)
assert(raw_dispatcher, "Not in debug mode")
for i=1,8 do
local name, func = debug.getupvalue(dispatcher, i)
if name == HOOK_FUNC then
debug.setupvalue(dispatcher, i, raw_dispatcher)
break
end
end
replace_upvalue(dispatcher, HOOK_FUNC, raw_dispatcher)
raw_dispatcher = nil
print = _G.print
skynet.error "Leave debug mode"
end
local function idle()
skynet.timeout(10,idle) -- idle every 0.1s
end
local function hook_dispatch(dispatcher, resp, fd, channel)
local address = skynet.self()
local prompt = string.format(":%08x>", address)
local newline = true
local function print(...)
local function gen_print(fd)
-- redirect print to socket fd
return function(...)
local tmp = table.pack(...)
for i=1,tmp.n do
tmp[i] = tostring(tmp[i])
@@ -38,91 +58,197 @@ local function hook_dispatch(dispatcher, resp, fd, channel)
table.insert(tmp, "\n")
socket.write(fd, table.concat(tmp, "\t"))
end
end
local message = {}
local cond
local function run_exp(ok, ...)
if ok then
print(...)
end
return ok
end
local function breakpoint(f)
cond = f
local function run_cmd(cmd, env, co, level)
if not run_exp(injectrun("return "..cmd, co, level, env)) then
print(select(2, injectrun(cmd,co, level,env)))
end
end
local ctx_skynet = debug.getinfo(skynet.start,"S").short_src -- skip when enter this source file
local ctx_term = debug.getinfo(run_cmd, "S").short_src -- term when get here
local ctx_active = {}
local linehook
local function skip_hook(mode)
local co = coroutine.running()
local ctx = ctx_active[co]
if mode == "return" then
ctx.level = ctx.level - 1
if ctx.level == 0 then
ctx.needupdate = true
sethook(linehook, "crl")
end
else
ctx.level = ctx.level + 1
end
end
function linehook(mode, line)
local co = coroutine.running()
local ctx = ctx_active[co]
if mode ~= "line" then
ctx.needupdate = true
if mode ~= "return" then
if ctx.next_mode or debug.getinfo(2,"S").short_src == ctx_skynet then
ctx.level = 1
sethook(skip_hook, "cr")
end
end
else
if ctx.needupdate then
ctx.needupdate = false
ctx.filename = debug.getinfo(2, "S").short_src
if ctx.filename == ctx_term then
ctx_active[co] = nil
sethook()
change_prompt(string.format(":%08x>", skynet.self()))
return
end
end
-- Lua 5.3 report currentline seems wrong
change_prompt(string.format("%s(%d)>",ctx.filename, line-1))
return true -- yield
end
end
local function add_watch_hook()
local co = coroutine.running()
local ctx = {}
ctx_active[co] = ctx
local level = 3
sethook(function()
level = level - 1
if level == 0 then
ctx.needupdate = true
sethook(linehook, "crl")
end
end, "r")
end
local function watch_proto(protoname)
local proto = assert(replace_upvalue(skynet.register_protocol, "proto"), "Can't find proto table")
local p = proto[protoname]
local dispatch = p.dispatch_origin or p.dispatch
if p == nil or dispatch == nil then
return "No " .. protoname
end
p.dispatch_origin = dispatch
p.dispatch = function(...)
p.dispatch = dispatch -- restore origin dispatch function
add_watch_hook()
dispatch(...)
end
end
local function remove_watch()
for co in pairs(ctx_active) do
sethook(co)
end
ctx_active = {}
end
local dbgcmd = {}
function dbgcmd.s(co)
local ctx = ctx_active[co]
ctx.next_mode = false
skynet_suspend(co, coroutine.resume(co))
end
function dbgcmd.n(co)
local ctx = ctx_active[co]
ctx.next_mode = true
skynet_suspend(co, coroutine.resume(co))
end
function dbgcmd.c(co)
sethook(co)
ctx_active[co] = nil
change_prompt(string.format(":%08x>", skynet.self()))
skynet_suspend(co, coroutine.resume(co))
end
local function hook_dispatch(dispatcher, resp, fd, channel)
change_prompt(string.format(":%08x>", skynet.self()))
print = gen_print(fd)
local env = {
print = print,
watch = watch_proto
}
local watch_env = {
print = print
}
local function watch_cmd(cmd)
local co = next(ctx_active)
if dbgcmd[cmd] then
dbgcmd[cmd](co)
else
run_cmd(cmd, watch_env, co, 0)
end
end
local env = setmetatable({ skynet = skynet, print = print, hook = breakpoint, m = message }, { __index = _ENV })
local function debug_hook(proto, msg, sz, session, source)
message.proto = proto
message.session = session
message.address = source
message.message = msg
message.size = sz
local sleep = nil
local function debug_hook()
while true do
if newline then
socket.write(fd, "\n")
socket.write(fd, prompt)
newline = false
end
local cmd = channel:read(sleep)
local cmd = channel:read()
if cmd then
if cmd == "cont" then
-- leave debug mode
break
end
if cmd ~= "" then
if sleep then
if cmd == "c" then
print "continue..."
prompt = string.format(":%08x>", address)
newline = true
return
end
end
local f = load("return "..cmd, "=(debug)", "t", env)
if not f then
local err
f,err = load(cmd, "=(debug)", "t", env)
if not f then
socket.write(fd, err .. "\n")
end
end
if f then
print(select(2,pcall(f)))
if next(ctx_active) then
watch_cmd(cmd)
else
run_cmd(cmd, env, coroutine.running(),2)
end
end
newline = true
else
-- no input
if sleep == nil then
if cond and cond(message) then
-- cond break point
prompt = "break>"
sleep = 0.1
print()
newline = true
else
return
end
end
return
end
end
-- exit debug mode
remove_watch()
remove_hook(dispatcher)
resp(true)
end
for i=1,8 do
local name, func = debug.getupvalue(dispatcher, i)
if name == HOOK_FUNC then
local function hook(...)
debug_hook(...)
return func(...)
end
debug.setupvalue(dispatcher, i, hook)
skynet.timeout(0, idle)
return func
end
local func
local function hook(...)
debug_hook()
return func(...)
end
func = replace_upvalue(dispatcher, HOOK_FUNC, hook)
if func then
local function idle()
skynet.timeout(10,idle) -- idle every 0.1s
end
skynet.timeout(0, idle)
end
return func
end
function M.start(dispatcher, fd, handle)
function M.start(import, fd, handle)
local dispatcher = import.dispatch
skynet_suspend = import.suspend
assert(raw_dispatcher == nil, "Already in debug mode")
skynet.error "Enter debug mode"
local channel = debugchannel.connect(handle)