diff --git a/3rd/lua/ldebug.c b/3rd/lua/ldebug.c index ab10dd99..679686ed 100644 --- a/3rd/lua/ldebug.c +++ b/3rd/lua/ldebug.c @@ -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 diff --git a/3rd/lua/lstate.h b/3rd/lua/lstate.h index 81e12c40..c89b910c 100644 --- a/3rd/lua/lstate.h +++ b/3rd/lua/lstate.h @@ -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; diff --git a/lualib-src/lua-debugchannel.c b/lualib-src/lua-debugchannel.c index 07c493a8..102f065a 100644 --- a/lualib-src/lua-debugchannel.c +++ b/lualib-src/lua-debugchannel.c @@ -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); diff --git a/lualib/skynet.lua b/lualib/skynet.lua index d34a7f45..e538a37b 100644 --- a/lualib/skynet.lua +++ b/lualib/skynet.lua @@ -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 diff --git a/lualib/skynet/debug.lua b/lualib/skynet/debug.lua index 9fd83ce2..0ef6dcb8 100644 --- a/lualib/skynet/debug.lua +++ b/lualib/skynet/debug.lua @@ -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, ...) diff --git a/lualib/skynet/injectcode.lua b/lualib/skynet/injectcode.lua new file mode 100644 index 00000000..de85cb6c --- /dev/null +++ b/lualib/skynet/injectcode.lua @@ -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 + diff --git a/lualib/skynet/remotedebug.lua b/lualib/skynet/remotedebug.lua index 06dbeb03..0bef884c 100644 --- a/lualib/skynet/remotedebug.lua +++ b/lualib/skynet/remotedebug.lua @@ -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)