From 498f634ac5644721eb303a404b21e84dd43f8e1c Mon Sep 17 00:00:00 2001 From: Cloud Wu Date: Mon, 9 Feb 2015 23:40:54 +0800 Subject: [PATCH 1/4] remote debugger --- Makefile | 7 +- lualib-src/lua-debugchannel.c | 195 ++++++++++ lualib-src/{lua_mysqlaux.c => lua-mysqlaux.c} | 0 lualib-src/sproto/README.md | 350 ++++++++++++++++++ lualib/skynet/debug.lua | 5 + lualib/skynet/remotedebug.lua | 130 +++++++ service/debug_agent.lua | 28 ++ service/debug_console.lua | 50 ++- 8 files changed, 759 insertions(+), 6 deletions(-) create mode 100644 lualib-src/lua-debugchannel.c rename lualib-src/{lua_mysqlaux.c => lua-mysqlaux.c} (100%) mode change 100755 => 100644 create mode 100644 lualib-src/sproto/README.md create mode 100644 lualib/skynet/remotedebug.lua create mode 100644 service/debug_agent.lua diff --git a/Makefile b/Makefile index 89f3a730..09734485 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ CSERVICE = snlua logger gate harbor LUA_CLIB = skynet socketdriver bson mongo md5 netpack \ clientsocket memory profile multicast \ cluster crypt sharedata stm sproto lpeg \ - mysqlaux + mysqlaux debugchannel SKYNET_SRC = skynet_main.c skynet_handle.c skynet_module.c skynet_mq.c \ skynet_server.c skynet_start.c skynet_timer.c skynet_error.c \ @@ -120,7 +120,10 @@ $(LUA_CLIB_PATH)/sproto.so : lualib-src/sproto/sproto.c lualib-src/sproto/lsprot $(LUA_CLIB_PATH)/lpeg.so : 3rd/lpeg/lpcap.c 3rd/lpeg/lpcode.c 3rd/lpeg/lpprint.c 3rd/lpeg/lptree.c 3rd/lpeg/lpvm.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) -I3rd/lpeg $^ -o $@ -$(LUA_CLIB_PATH)/mysqlaux.so : lualib-src/lua_mysqlaux.c | $(LUA_CLIB_PATH) +$(LUA_CLIB_PATH)/mysqlaux.so : lualib-src/lua-mysqlaux.c | $(LUA_CLIB_PATH) + $(CC) $(CFLAGS) $(SHARED) $^ -o $@ + +$(LUA_CLIB_PATH)/debugchannel.so : lualib-src/lua-debugchannel.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) $^ -o $@ clean : diff --git a/lualib-src/lua-debugchannel.c b/lualib-src/lua-debugchannel.c new file mode 100644 index 00000000..07c493a8 --- /dev/null +++ b/lualib-src/lua-debugchannel.c @@ -0,0 +1,195 @@ +// only for debug use +#include +#include +#include +#include +#include + +#define METANAME "debugchannel" +#define LOCK(q) while (__sync_lock_test_and_set(&(q)->lock,1)) {} +#define UNLOCK(q) __sync_lock_release(&(q)->lock); + +struct command { + struct command * next; + size_t sz; +}; + +struct channel { + int lock; + int ref; + struct command * head; + struct command * tail; +}; + +static struct channel * +channel_new() { + struct channel * c = malloc(sizeof(*c)); + memset(c, 0 , sizeof(*c)); + c->ref = 1; + + return c; +} + +static struct channel * +channel_connect(struct channel *c) { + struct channel * ret = NULL; + LOCK(c) + if (c->ref == 1) { + ++c->ref; + ret = c; + } + UNLOCK(c) + return ret; +} + +static struct channel * +channel_release(struct channel *c) { + LOCK(c) + --c->ref; + if (c->ref > 0) { + UNLOCK(c) + return c; + } + // never unlock while reference is 0 + struct command * p = c->head; + c->head = NULL; + c->tail = NULL; + while(p) { + struct command *next = p->next; + free(p); + p = next; + } + return NULL; +} + +// call free after channel_read +static struct command * +channel_read(struct channel *c, double timeout) { + struct command * ret = NULL; + LOCK(c) + if (c->head == NULL) { + UNLOCK(c) + int ti = (int)(timeout * 100000); + usleep(ti); + return NULL; + } + ret = c->head; + c->head = ret->next; + if (c->head == NULL) { + c->tail = NULL; + } + UNLOCK(c) + + return ret; +} + +static void +channel_write(struct channel *c, const char * s, size_t sz) { + struct command * cmd = malloc(sizeof(*cmd)+ sz); + cmd->sz = sz; + cmd->next = NULL; + memcpy(cmd+1, s, sz); + LOCK(c) + if (c->tail == NULL) { + c->head = c->tail = cmd; + } else { + c->tail->next = cmd; + c->tail = cmd; + } + UNLOCK(c) +} + +struct channel_box { + struct channel *c; +}; + +static int +lread(lua_State *L) { + struct channel_box *cb = luaL_checkudata(L,1, METANAME); + double ti = luaL_optnumber(L, 2, 0); + struct command * c = channel_read(cb->c, ti); + if (c == NULL) + return 0; + lua_pushlstring(L, (const char *)(c+1), c->sz); + free(c); + return 1; +} + +static int +lwrite(lua_State *L) { + struct channel_box *cb = luaL_checkudata(L,1, METANAME); + size_t sz; + const char * str = luaL_checklstring(L, 2, &sz); + channel_write(cb->c, str, sz); + return 0; +} + +static int +lrelease(lua_State *L) { + struct channel_box *cb = lua_touserdata(L, 1); + if (cb) { + if (channel_release(cb->c) == NULL) { + cb->c = NULL; + } + } + + return 0; +} + +static struct channel * +new_channel(lua_State *L, struct channel *c) { + if (c == NULL) { + c = channel_new(); + } else { + c = channel_connect(c); + } + if (c == NULL) { + luaL_error(L, "new channel failed"); + // never go here + } + struct channel_box * cb = lua_newuserdata(L, sizeof(*cb)); + cb->c = c; + if (luaL_newmetatable(L, METANAME)) { + luaL_Reg l[]={ + { "read", lread }, + { "write", lwrite }, + { NULL, NULL }, + }; + luaL_newlib(L,l); + lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, lrelease); + lua_setfield(L, -2, "__gc"); + } + lua_setmetatable(L, -2); + return c; +} + +static int +lcreate(lua_State *L) { + struct channel *c = new_channel(L, NULL); + lua_pushlightuserdata(L, c); + return 2; +} + +static int +lconnect(lua_State *L) { + struct channel *c = lua_touserdata(L, 1); + if (c == NULL) + return luaL_error(L, "Invalid channel pointer"); + new_channel(L, c); + + return 1; +} + +int +luaopen_debugchannel(lua_State *L) { + luaL_Reg l[] = { + { "create", lcreate }, // for write + { "connect", lconnect }, // for read + { "release", lrelease }, + { NULL, NULL }, + }; + luaL_checkversion(L); + luaL_newlib(L,l); + return 1; +} diff --git a/lualib-src/lua_mysqlaux.c b/lualib-src/lua-mysqlaux.c old mode 100755 new mode 100644 similarity index 100% rename from lualib-src/lua_mysqlaux.c rename to lualib-src/lua-mysqlaux.c diff --git a/lualib-src/sproto/README.md b/lualib-src/sproto/README.md new file mode 100644 index 00000000..8d9efd94 --- /dev/null +++ b/lualib-src/sproto/README.md @@ -0,0 +1,350 @@ +Introduction +====== + +Sproto is an efficient serialization library for C, and focuses on lua binding. It's like Google protocol buffers, but much faster. + +The design is simple. It only supports a few types that lua supports. It can be easily bound to other dynamic languages, or be used directly in C. + +In my i5-2500 @3.3GHz CPU, the benchmark is below: + +The schema in sproto: + +``` +.Person { + name 0 : string + id 1 : integer + email 2 : string + + .PhoneNumber { + number 0 : string + type 1 : integer + } + + phone 3 : *PhoneNumber +} + +.AddressBook { + person 0 : *Person +} +``` + +It's equal to: + +``` +message Person { + required string name = 1; + required int32 id = 2; + optional string email = 3; + + message PhoneNumber { + required string number = 1; + optional int32 type = 2 ; + } + + repeated PhoneNumber phone = 4; +} + +message AddressBook { + repeated Person person = 1; +} +``` + +Use the data: +```lua +local ab = { + person = { + { + name = "Alice", + id = 10000, + phone = { + { number = "123456789" , type = 1 }, + { number = "87654321" , type = 2 }, + } + }, + { + name = "Bob", + id = 20000, + phone = { + { number = "01234567890" , type = 3 }, + } + } + } +} +``` + +library| encode 1M times | decode 1M times | size +-------| --------------- | --------------- | ---- +sproto | 2.15s | 7.84s | 83 bytes +sproto (nopack) |1.58s | 6.93s | 130 bytes +pbc-lua | 6.94s | 16.9s | 69 bytes +lua-cjson | 4.92s | 8.30s | 183 bytes + +* pbc-lua is a google protocol buffers library https://github.com/cloudwu/pbc +* lua-cjson is a json library https://github.com/efelix/lua-cjson + +Lua API +======= + +```lua +local parser = require "sprotoparser" +``` + +* `parser.parse` parses a sproto schema to a binary string. + +The parser is needed for parsing the sproto schema. You can use it to generate binary string offline. The schema text and the parser is not needed when your program is running. + +```lua +local sproto = require "sproto.core" +``` + +* `sproto.newproto(sp)` creates a sproto object by a schema string (generates by parser). +* `sproto.querytype(sp, typename)` queries a type object from a sproto object by typename. +* `sproto.encode(st, luatable)` encodes a lua table by a type object, and generates a string message. +* `sproto.decode(st, message)` decodes a message string generated by sproto.encode with type. +* `sproto.pack(sprotomessage)` packs a string encoded by sproto.encode to reduce the size. +* `sproto.unpack(packedmessage)` unpacks the string packed by sproto.pack. + +The sproto supports protocol tag for RPC. Use `sproto.protocol(tagorname)` to convert the protocol name to the tag id, or convert back from tag id to the name, and returns the request/response message type objects of this protocol. + +RPC API +======= + +There is a lua wrapper for the core API for RPC . + +Read testrpc.lua for detail. + +Schema Language +========== + +Like Protocol Buffers (but unlike json), sproto messages are strongly-typed and are not self-describing. You must define your message structure in a special language. + +You can use sprotoparser library to parse the schema text to a binary string, so that the sproto library can use it. +You can parse them offline and save the string, or you can parse them during your program running. + +The schema text is like this: + +``` +# This is a comment. + +.Person { # . means a user defined type + name 0 : string # string is a build-in type. + id 1 : integer + email 2 : string + + .PhoneNumber { # user defined type can be nest. + number 0 : string + type 1 : integer + } + + phone 3 : *PhoneNumber # *PhoneNumber means an array of PhoneNumber. +} + +.AddressBook { + person 0 : *Person +} + +foobar 1 { # define a new protocol (for RPC used) with tag 1 + request person # Associate the type person with foobar.request + response { # define the foobar.response type + ok 0 : boolean + } +} + +``` + +A schema text can be self-described by the sproto schema language. + +``` +.type { + .field { + name 0 : string + type 1 : string + id 2 : integer + array 3 : boolean + } + name 0 : string + fields 1 : *field +} + +.protocol { + name 0 : string + id 1 : integer + request 2 : string + response 3 : string +} + +.group { + type 0 : *type + protocol 1 : *protocol +} +``` + +Types +======= + +* **string** : binary string +* **integer** : integer, the max length of a integer is signed 64bit. +* **boolean** : true or false + +You can add * before the typename to declare an array. + +User defined type can be any name in alphanumeric characters except the build-in typenames, and nested types are supported. + +* Where are double or real types? + +I have been using Google protocol buffers for many years in many projects, and I found the real types were seldom used. If you really need it, you can use string to serialize the double numbers. + +* Where is enum? + +In lua, enum types are not very useful. You can use integer to define an enum table in lua. + +Wire protocol +======== + +Each integer number must be serialized in little-endian format. + +The sproto message must be a user defined type struct, and a struct is encoded in three parts. The header, the field part, and the data part. +The tag and small integer or boolean will be encoded in field part, and others are in data part. + +All the fields must be encoded in ascending order (by tag). The tags of fields can be discontinuous, if a field is nil. (default value in lua), don't encode it in message. + +The header is a 16bit integer. It is the number of fields. + +Each field in field part is a 16bit integer (n). If n is zero, that means the field data is encoded in data part ; + +If n is even (and not zero), the value of this field is n/2-1 ; + +If n is odd, that means the tags is not continuous, and we should add current tag by (n+1)/2 . + +Read the examples below to see more details. + +Notice: If the tag is not declared in schema, the decoder will simply ignore the field for protocol version compatibility. + +Example 1: + +``` +person { name = "Alice" , age = 13, marital = false } + +03 00 (fn = 3) +00 00 (id = 0, value in data part) +1C 00 (id = 1, value = 13) +02 00 (id = 2, value = false) +05 00 00 00 (sizeof "Alice") +41 6C 69 63 65 ("Alice") +``` + +Example 2: + +``` +person { + name = "Bob", + age = 40, + children = { + { name = "Alice" , age = 13, marital = false }, + } +} + +04 00 (fn = 4) +00 00 (id = 0, value in data part) +52 00 (id = 1, value = 40) +01 00 (skip id = 2) +00 00 (id = 3, value in data part) + +03 00 00 00 (sizeof "Bob") +42 6F 62 ("Bob") + +11 00 00 00 (sizeof struct) +03 00 (fn = 3) +00 00 (id = 0, value in data part) +1C 00 (id = 1, value = 13) +02 00 (id = 2, value = false) +05 00 00 00 (sizeof "Alice") +41 6C 69 63 65 ("Alice") +``` + +0 Packing +======= + +The algorithm is very similar to [Cap'n proto](http://kentonv.github.io/capnproto/), but 0x00 is not treated specially. + +In packed format, the message if padding to 8. Each 8 byte is reduced to a tag byte followed by zero to eight content bytes. +The bits of the tag byte correspond to the bytes of the unpacked word, with the least-significant bit corresponding to the first byte. +Each zero bit indicates that the corresponding byte is zero. The non-zero bytes are packed following the tag. + +For example: + +``` +unpacked (hex): 08 00 00 00 03 00 02 00 19 00 00 00 aa 01 00 00 +packed (hex): 51 08 03 02 31 19 aa 01 +``` + +Tag 0xff is treated specially. A number N is following the 0xff tag. N means (N+1)*8 bytes should be copied directly. +The bytes may or may not contain zeros. Because of this rule, the worst-case space overhead of packing is 2 bytes per 2 KiB of input. + +For example: + +``` +unpacked (hex): 8a (x 30 bytes) +packed (hex): ff 03 8a (x 30 bytes) 00 00 +``` + +C API +===== + +```C +struct sproto * sproto_create(const void * proto, size_t sz); +``` + +Create a sproto object with a schema string encoded by sprotoparser: + +```C +void sproto_release(struct sproto *); +``` + +Release the sproto object: + +```C +int sproto_prototag(struct sproto *, const char * name); +const char * sproto_protoname(struct sproto *, int proto); +// SPROTO_REQUEST(0) : request, SPROTO_RESPONSE(1): response +struct sproto_type * sproto_protoquery(struct sproto *, int proto, int what); +``` + +Convert between tag and name of a protocol, and query the type object of it: + +```C +struct sproto_type * sproto_type(struct sproto *, const char * typename); +``` + +Query the type object from a sproto object: + +```C +typedef int (*sproto_callback)(void *ud, const char *tagname, int type, int index, struct sproto_type *, void *value, int length); + +int sproto_decode(struct sproto_type *, const void * data, int size, sproto_callback cb, void *ud); +int sproto_encode(struct sproto_type *, void * buffer, int size, sproto_callback cb, void *ud); +``` + +encode and decode the sproto message with a user defined callback function. Read the implementation of lsproto.c for more details. + +```C +int sproto_pack(const void * src, int srcsz, void * buffer, int bufsz); +int sproto_unpack(const void * src, int srcsz, void * buffer, int bufsz); +``` + +pack and unpack the message with the 0 packing algorithm. + +JIT +===== +You may also interest in https://github.com/lvzixun/sproto-JIT + +C# version +===== +https://github.com/lvzixun/sproto-Csharp + +Question? +========== + +* Send me an email: http://www.codingnow.com/2000/gmail.gif +* My Blog: http://blog.codingnow.com +* Design: http://blog.codingnow.com/2014/07/ejoyproto.html (in Chinese) diff --git a/lualib/skynet/debug.lua b/lualib/skynet/debug.lua index d834bf23..9fd83ce2 100644 --- a/lualib/skynet/debug.lua +++ b/lualib/skynet/debug.lua @@ -58,6 +58,11 @@ function dbgcmd.TERM(service) skynet.term(service) end +function dbgcmd.REMOTEDEBUG(...) + local remotedebug = require "skynet.remotedebug" + remotedebug.start(export.dispatch, ...) +end + local function _debug_dispatch(session, address, cmd, ...) local f = dbgcmd[cmd] assert(f, cmd) diff --git a/lualib/skynet/remotedebug.lua b/lualib/skynet/remotedebug.lua new file mode 100644 index 00000000..8e0c86c1 --- /dev/null +++ b/lualib/skynet/remotedebug.lua @@ -0,0 +1,130 @@ +local skynet = require "skynet" +local debugchannel = require "debugchannel" +local socket = require "socket" + +local M = {} + +local HOOK_FUNC = "raw_dispatch_message" +local raw_dispatcher + +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 + raw_dispatcher = nil + + 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 tmp = table.pack(...) + for i=1,tmp.n do + tmp[i] = tostring(tmp[i]) + end + table.insert(tmp, "\n") + socket.write(fd, table.concat(tmp, "\t")) + end + + local message = {} + local cond + + local function breakpoint(f) + cond = f + 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 + while true do + if newline then + socket.write(fd, prompt) + newline = false + end + local cmd = channel:read(sleep) + if cmd then + if cmd == "cont" then + break + end + 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))) + 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 + end + end + -- exit debug mode + 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 + end +end + +function M.start(dispatcher, fd, handle) + assert(raw_dispatcher == nil, "Already in debug mode") + skynet.error "Enter debug mode" + local channel = debugchannel.connect(handle) + raw_dispatcher = hook_dispatch(dispatcher, skynet.response(), fd, channel) +end + +return M diff --git a/service/debug_agent.lua b/service/debug_agent.lua new file mode 100644 index 00000000..b4c4667d --- /dev/null +++ b/service/debug_agent.lua @@ -0,0 +1,28 @@ +local skynet = require "skynet" +local debugchannel = require "debugchannel" + +local CMD = {} + +local channel + +function CMD.start(address, fd) + assert(channel == nil, "start more than once") + skynet.error(string.format("Attach to :%08x", address)) + local handle + channel, handle = debugchannel.create() + skynet.call(address, "debug", "REMOTEDEBUG", fd, handle) + -- todo hook + skynet.ret(skynet.pack(nil)) + skynet.exit() +end + +function CMD.cmd(cmdline) + channel:write(cmdline) +end + +skynet.start(function() + skynet.dispatch("lua", function(_,_,cmd,...) + local f = CMD[cmd] + f(...) + end) +end) diff --git a/service/debug_console.lua b/service/debug_console.lua index 050fa2de..75db9343 100644 --- a/service/debug_console.lua +++ b/service/debug_console.lua @@ -48,14 +48,15 @@ local function split_cmdline(cmdline) return split end -local function docmd(cmdline, print) +local function docmd(cmdline, print, fd) local split = split_cmdline(cmdline) + table.insert(split, fd) local cmd = COMMAND[split[1]] local ok, list if cmd then ok, list = pcall(cmd, select(2,table.unpack(split))) else - ok, list = pcall(skynet.call,".launcher","lua", table.unpack(split)) + print("Invalid command, type help for command list") end if ok then @@ -82,7 +83,7 @@ local function console_main_loop(stdin, print) break end if cmdline ~= "" then - docmd(cmdline, print) + docmd(cmdline, print, stdin) end end socket.unlock(stdin) @@ -124,6 +125,7 @@ function COMMAND.help() logon = "logon address", logoff = "logoff address", log = "launch a new lua service with log", + debug = "debug address : debug a lua service", } end @@ -165,11 +167,31 @@ end local function adjust_address(address) if address:sub(1,1) ~= ":" then - address = bit32.replace( tonumber("0x" .. address), skynet.harbor(skynet.self()), 24, 8) + address = tonumber("0x" .. address) | (skynet.harbor(skynet.self()) << 24) end return address end +function COMMAND.list() + return skynet.call(".launcher", "lua", "LIST") +end + +function COMMAND.stat() + return skynet.call(".launcher", "lua", "STAT") +end + +function COMMAND.mem() + return skynet.call(".launcher", "lua", "MEM") +end + +function COMMAND.kill(address) + return skynet.call(".launcher", "lua", "KILL", address) +end + +function COMMAND.gc() + return skynet.call(".launcher", "lua", "GC") +end + function COMMAND.exit(address) skynet.send(adjust_address(address), "debug", "EXIT") end @@ -195,6 +217,26 @@ function COMMAND.info(address) return skynet.call(address,"debug","INFO") end +function COMMAND.debug(address, fd) + address = adjust_address(address) + local agent = skynet.newservice "debug_agent" + local stop + skynet.fork(function() + repeat + local cmdline = socket.readline(fd, "\n") + if not cmdline then + skynet.send(agent, "lua", "cmd", "cont") + break + end + if cmdline ~= "" then + skynet.send(agent, "lua", "cmd", cmdline) + end + until stop or cmdline == "cont" + end) + skynet.call(agent, "lua", "start", address, fd) + stop = true +end + function COMMAND.logon(address) address = adjust_address(address) core.command("LOGON", skynet.address(address)) From e031a353305dbf4ba43f626ea9215b9578ca13d7 Mon Sep 17 00:00:00 2001 From: Cloud Wu Date: Mon, 9 Feb 2015 23:48:45 +0800 Subject: [PATCH 2/4] support empty line --- lualib/skynet/remotedebug.lua | 32 +++++++++++++++++--------------- service/debug_console.lua | 4 +--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lualib/skynet/remotedebug.lua b/lualib/skynet/remotedebug.lua index 8e0c86c1..06dbeb03 100644 --- a/lualib/skynet/remotedebug.lua +++ b/lualib/skynet/remotedebug.lua @@ -65,25 +65,27 @@ local function hook_dispatch(dispatcher, resp, fd, channel) if cmd == "cont" then break end - if sleep then - if cmd == "c" then - print "continue..." - prompt = string.format(":%08x>", address) - newline = true - return + if cmd ~= "" then + if sleep then + if cmd == "c" then + print "continue..." + prompt = string.format(":%08x>", address) + newline = true + return + end end - end - local f = load("return "..cmd, "=(debug)", "t", env) - if not f then - local err - f,err = load(cmd, "=(debug)", "t", env) + local f = load("return "..cmd, "=(debug)", "t", env) if not f then - socket.write(fd, err .. "\n") + 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))) end - end - if f then - print(select(2,pcall(f))) end newline = true else diff --git a/service/debug_console.lua b/service/debug_console.lua index 75db9343..0133d293 100644 --- a/service/debug_console.lua +++ b/service/debug_console.lua @@ -228,9 +228,7 @@ function COMMAND.debug(address, fd) skynet.send(agent, "lua", "cmd", "cont") break end - if cmdline ~= "" then - skynet.send(agent, "lua", "cmd", cmdline) - end + skynet.send(agent, "lua", "cmd", cmdline) until stop or cmdline == "cont" end) skynet.call(agent, "lua", "start", address, fd) From 8792a40845b7e3fe5a09821d88d30c936d2ca30c Mon Sep 17 00:00:00 2001 From: Cloud Wu Date: Wed, 11 Feb 2015 14:18:47 +0800 Subject: [PATCH 3/4] debug step mode --- 3rd/lua/ldebug.c | 19 ++- 3rd/lua/lstate.h | 1 + lualib-src/lua-debugchannel.c | 85 +++++++++++ lualib/skynet.lua | 4 + lualib/skynet/debug.lua | 2 +- lualib/skynet/injectcode.lua | 133 +++++++++++++++++ lualib/skynet/remotedebug.lua | 272 +++++++++++++++++++++++++--------- 7 files changed, 436 insertions(+), 80 deletions(-) create mode 100644 lualib/skynet/injectcode.lua 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) From 00efa6cc8a54593f5122a255e8cee9ef9c9a658c Mon Sep 17 00:00:00 2001 From: Cloud Wu Date: Wed, 11 Feb 2015 14:46:46 +0800 Subject: [PATCH 4/4] cond break point --- lualib/skynet/remotedebug.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lualib/skynet/remotedebug.lua b/lualib/skynet/remotedebug.lua index 0bef884c..338cbff8 100644 --- a/lualib/skynet/remotedebug.lua +++ b/lualib/skynet/remotedebug.lua @@ -134,7 +134,7 @@ local function add_watch_hook() end, "r") end -local function watch_proto(protoname) +local function watch_proto(protoname, cond) 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 @@ -143,8 +143,10 @@ local function watch_proto(protoname) end p.dispatch_origin = dispatch p.dispatch = function(...) - p.dispatch = dispatch -- restore origin dispatch function - add_watch_hook() + if not cond or cond(...) then + p.dispatch = dispatch -- restore origin dispatch function + add_watch_hook() + end dispatch(...) end end @@ -202,7 +204,6 @@ local function hook_dispatch(dispatcher, resp, fd, channel) local function debug_hook() while true do if newline then - socket.write(fd, "\n") socket.write(fd, prompt) newline = false end