remote debugger

This commit is contained in:
Cloud Wu
2015-02-09 23:40:54 +08:00
parent 62e70ef755
commit 498f634ac5
8 changed files with 759 additions and 6 deletions

View File

@@ -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 :

View File

@@ -0,0 +1,195 @@
// only for debug use
#include <lua.h>
#include <lauxlib.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#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;
}

View File

350
lualib-src/sproto/README.md Normal file
View File

@@ -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)

View File

@@ -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)

View File

@@ -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

28
service/debug_agent.lua Normal file
View File

@@ -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)

View File

@@ -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))