From cf4700bda4929d8d952df16bdd3ca73019d15249 Mon Sep 17 00:00:00 2001 From: antirez Date: Wed, 11 Nov 2015 10:15:26 +0100 Subject: [PATCH] Lua debugger: output improvements, eval command. --- src/redis-cli.c | 32 ++++++-- src/scripting.c | 208 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 186 insertions(+), 54 deletions(-) diff --git a/src/redis-cli.c b/src/redis-cli.c index 13da858d..84a9c548 100644 --- a/src/redis-cli.c +++ b/src/redis-cli.c @@ -526,11 +526,9 @@ sds sdsCatColorizedLdbReply(sds o, char *s, size_t len) { if (strstr(s,"")) color = "cyan"; if (strstr(s,"")) color = "red"; if (strstr(s,"")) color = "magenta"; - if (isdigit(s[0])) { - char *p = s+1; - while(isdigit(*p)) p++; - if (*p == '*') color = "yellow"; /* Current line. */ - else if (*p == '#') color = "bold"; /* Break point. */ + if (len > 4 && isdigit(s[3])) { + if (s[1] == '>') color = "yellow"; /* Current line. */ + else if (s[2] == '#') color = "bold"; /* Break point. */ } return sdscatcolor(o,s,len,color); } @@ -1030,6 +1028,28 @@ static int issueCommand(int argc, char **argv) { return issueCommandRepeat(argc, argv, config.repeat); } +/* Split the user provided command into multiple SDS arguments. + * This function normally uses sdssplitargs() from sds.c which is able + * to understand "quoted strings", escapes and so forth. However when + * we are in Lua debugging mode and the "eval" command is used, we want + * the remaining Lua script (after "e " or "eval ") to be passed verbatim + * as a single big argument. */ +static sds *cliSplitArgs(char *line, int *argc) { + if (config.eval_ldb && (strstr(line,"eval ") == line || + strstr(line,"e ") == line)) + { + sds *argv = zmalloc(sizeof(sds)*2); + *argc = 2; + int len = strlen(line); + int elen = line[1] == ' ' ? 2 : 5; /* "e " or "eval "? */ + argv[0] = sdsnewlen(line,elen-1); + argv[1] = sdsnewlen(line+elen,len-elen); + return argv; + } else { + return sdssplitargs(line,argc); + } +} + static void repl(void) { sds historyfile = NULL; int history = 0; @@ -1053,7 +1073,7 @@ static void repl(void) { cliRefreshPrompt(); while((line = linenoise(context ? config.prompt : "not connected> ")) != NULL) { if (line[0] != '\0') { - argv = sdssplitargs(line,&argc); + argv = cliSplitArgs(line,&argc); if (history) linenoiseHistoryAdd(line); if (historyfile) linenoiseHistorySave(historyfile); diff --git a/src/scripting.c b/src/scripting.c index f33e6c33..d202cd82 100644 --- a/src/scripting.c +++ b/src/scripting.c @@ -51,9 +51,11 @@ void ldbEnable(client *c); void evalGenericCommandWithDebugging(client *c, int evalsha); void luaLdbLineHook(lua_State *lua, lua_Debug *ar); void ldbLog(sds entry); +void ldbLogRedisReply(char *reply, size_t maxlen); /* Debugger shared state is stored inside this global structure. */ -#define LDB_BREAKPOINTS_MAX 64 +#define LDB_BREAKPOINTS_MAX 64 /* Max number of breakpoints. */ +#define LDB_REPLY_MAX_LOG_LEN 60 /* Max chars when logging a reply. */ struct ldbState { int fd; /* Socket of the debugging client. */ int active; /* Are we debugging EVAL right now? */ @@ -107,13 +109,11 @@ void sha1hex(char *digest, char *script, size_t len) { * Basically we take the arguments, execute the Redis command in the context * of a non connected client, then take the generated reply and convert it * into a suitable Lua type. With this trick the scripting feature does not - * need the introduction of a full Redis internals API. Basically the script + * need the introduction of a full Redis internals API. The script * is like a normal client that bypasses all the slow I/O paths. * * Note: in this function we do not do any sanity check as the reply is * generated by Redis directly. This allows us to go faster. - * The reply string can be altered during the parsing as it is discarded - * after the conversion is completed. * * Errors are returned as a table with a single 'err' field set to the * error string. @@ -123,21 +123,11 @@ char *redisProtocolToLuaType(lua_State *lua, char* reply) { char *p = reply; switch(*p) { - case ':': - p = redisProtocolToLuaType_Int(lua,reply); - break; - case '$': - p = redisProtocolToLuaType_Bulk(lua,reply); - break; - case '+': - p = redisProtocolToLuaType_Status(lua,reply); - break; - case '-': - p = redisProtocolToLuaType_Error(lua,reply); - break; - case '*': - p = redisProtocolToLuaType_MultiBulk(lua,reply); - break; + case ':': p = redisProtocolToLuaType_Int(lua,reply); break; + case '$': p = redisProtocolToLuaType_Bulk(lua,reply); break; + case '+': p = redisProtocolToLuaType_Status(lua,reply); break; + case '-': p = redisProtocolToLuaType_Error(lua,reply); break; + case '*': p = redisProtocolToLuaType_MultiBulk(lua,reply); break; } return p; } @@ -579,12 +569,8 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) { redisProtocolToLuaType(lua,reply); /* If the debugger is active, log the reply from Redis. */ - if (ldb.active && ldb.step) { - sds replycopy = sdsnew(" "); - replycopy = sdscat(replycopy,reply); /* It's always null terminated. */ - if (sdslen(replycopy) > 70) sdsrange(replycopy,0,69); - ldbLog(replycopy); - } + if (ldb.active && ldb.step) + ldbLogRedisReply(reply,LDB_REPLY_MAX_LOG_LEN); /* Sort the output array if needed, assuming it is a non-null multi bulk * reply as expected. */ @@ -1687,6 +1673,25 @@ protoerr: return NULL; } +/* Log the specified line in the Lua debugger output. */ +void ldbLogSourceLine(int lnum) { + char *line = ldbGetSourceLine(lnum); + char *prefix; + int bp = ldbIsBreakpoint(lnum); + int current = ldb.currentline == lnum; + + if (current && bp) + prefix = "->#"; + else if (current) + prefix = "-> "; + else if (bp) + prefix = " #"; + else + prefix = " "; + sds thisline = sdscatprintf(sdsempty(),"%s%-3d %s", prefix, lnum, line); + ldbLog(thisline); +} + /* Implement the "list" command of the Lua debugger. If around is 0 * the whole file is listed, otherwise only a small portion of the file * around the specified line is shown. When a line number is specified @@ -1697,22 +1702,16 @@ void ldbList(int around, int context) { for (j = 1; j <= ldb.lines; j++) { if (around != 0 && abs(around-j) > context) continue; - char *line = ldbGetSourceLine(j); - int mark; - if (ldb.currentline == j) - mark = '*'; - else - mark = ldbIsBreakpoint(j) ? '#' : ':'; - sds thisline = sdscatprintf(sdsempty(),"%d%c %s", j, mark, line); - ldbLog(thisline); + ldbLogSourceLine(j); } } /* Produce a debugger log entry representing the value of the Lua object - * currently on the top of the stack. */ -void ldbLogStackValue(lua_State *lua) { + * currently on the top of the stack. As a side effect the element is + * popped. */ +void ldbLogStackValue(lua_State *lua, char *prefix) { int t = lua_type(lua,-1); - sds s = sdsnew(" "); + sds s = sdsnew(prefix); switch(t) { case LUA_TSTRING: @@ -1751,6 +1750,91 @@ void ldbLogStackValue(lua_State *lua) { lua_pop(lua,1); } +char *ldbRedisProtocolToHuman_Int(sds *o, char *reply); +char *ldbRedisProtocolToHuman_Bulk(sds *o, char *reply); +char *ldbRedisProtocolToHuman_Status(sds *o, char *reply); +char *ldbRedisProtocolToHuman_MultiBulk(sds *o, char *reply); + +/* Get Redis protocol from 'reply' and appends it in human readable form to + * the passed SDS string 'o'. + * + * Note that the SDS string is passed by reference (pointer of pointer to + * char*) so that we can return a modified pointer, as for SDS semantics. */ +char *ldbRedisProtocolToHuman(sds *o, char *reply) { + char *p = reply; + switch(*p) { + case ':': p = ldbRedisProtocolToHuman_Int(o,reply); break; + case '$': p = ldbRedisProtocolToHuman_Bulk(o,reply); break; + case '+': p = ldbRedisProtocolToHuman_Status(o,reply); break; + case '-': p = ldbRedisProtocolToHuman_Status(o,reply); break; + case '*': p = ldbRedisProtocolToHuman_MultiBulk(o,reply); break; + } + return p; +} + +char *ldbRedisProtocolToHuman_Int(sds *o, char *reply) { + char *p = strchr(reply+1,'\r'); + *o = sdscatlen(*o,reply+1,p-reply-1); + return p+2; +} + +char *ldbRedisProtocolToHuman_Bulk(sds *o, char *reply) { + char *p = strchr(reply+1,'\r'); + long long bulklen; + + string2ll(reply+1,p-reply-1,&bulklen); + if (bulklen == -1) { + *o = sdscatlen(*o,"NULL",4); + return p+2; + } else { + *o = sdscatrepr(*o,p+2,bulklen); + return p+2+bulklen+2; + } +} + +char *ldbRedisProtocolToHuman_Status(sds *o, char *reply) { + char *p = strchr(reply+1,'\r'); + + *o = sdscatrepr(*o,reply,p-reply); + return p+2; +} + +char *ldbRedisProtocolToHuman_MultiBulk(sds *o, char *reply) { + char *p = strchr(reply+1,'\r'); + long long mbulklen; + int j = 0; + + string2ll(reply+1,p-reply-1,&mbulklen); + p += 2; + if (mbulklen == -1) { + *o = sdscatlen(*o,"NULL",4); + return p; + } + *o = sdscatlen(*o,"[",1); + for (j = 0; j < mbulklen; j++) { + p = ldbRedisProtocolToHuman(o,p); + if (j != mbulklen-1) *o = sdscatlen(*o,",",1); + } + *o = sdscatlen(*o,"]",1); + return p; +} + +/* Log a Redis reply as debugger output, in an human readable format. + * If the resulting string is longer than 'len' plus a few more chars + * used as prefix, it gets truncated. */ +void ldbLogRedisReply(char *reply, size_t maxlen) { + sds log = sdsnew(" "); + maxlen += sdslen(log); + ldbRedisProtocolToHuman(&log,reply); + /* Trip and add ... if the length was reached, to hint the user it's not + * the whole reply. */ + if (sdslen(log) > maxlen) { + sdsrange(log,0,maxlen-1); + log = sdscatlen(log," ...",4); + } + ldbLog(log); +} + /* Implements the "print" command of the Lua debugger. It scans for Lua * var "varname" starting from the current stack frame up to the top stack * frame. The first matching variable is printed. */ @@ -1765,7 +1849,7 @@ void ldbPrint(lua_State *lua, char *varname) { while((name = lua_getlocal(lua,&ar,i)) != NULL) { i++; if (strcmp(varname,name) == 0) { - ldbLogStackValue(lua); + ldbLogStackValue(lua," "); return; } else { lua_pop(lua,1); /* Discard the var name on the stack. */ @@ -1784,10 +1868,8 @@ void ldbBreak(sds *argv, int argc) { } else { ldbLog(sdscatfmt(sdsempty(),"%i breakpoints set:",ldb.bpcount)); int j; - for (j = 0; j < ldb.bpcount; j++) { - ldbLog(sdscatfmt(sdsempty(),"%i# %s", ldb.bp[j], - ldbGetSourceLine(ldb.bp[j]))); - } + for (j = 0; j < ldb.bpcount; j++) + ldbLogSourceLine(ldb.bp[j]); } } else { int j; @@ -1819,6 +1901,28 @@ void ldbBreak(sds *argv, int argc) { } } +/* Implements the Lua debugger "eval" command. It just compiles the user + * passed fragment of code and executes it, showing the result left on + * the stack. */ +void ldbEval(lua_State *lua, sds *argv, int argc) { + /* Glue the script together if it is composed of multiple arguments. */ + sds code = sdsjoinsds(argv+1,argc-1," ",1); + + if (luaL_loadbuffer(lua,code,sdslen(code),"@ldb_eval")) { + ldbLog(sdscatfmt(sdsempty()," %s",lua_tostring(lua,-1))); + lua_pop(lua,1); + sdsfree(code); + return; + } + sdsfree(code); + if (lua_pcall(lua,0,1,0)) { + ldbLog(sdscatfmt(sdsempty()," %s",lua_tostring(lua,-1))); + lua_pop(lua,1); + return; + } + ldbLogStackValue(lua," "); +} + /* Read debugging commands from client. */ void ldbRepl(lua_State *lua) { sds *argv; @@ -1845,8 +1949,9 @@ void ldbRepl(lua_State *lua) { ldb.cbuf = sdsempty(); /* Execute the command. */ - if (!strcasecmp(argv[0],"help")) { + if (!strcasecmp(argv[0],"h") || !strcasecmp(argv[0],"help")) { ldbLog(sdsnew("Redis Lua debugger help:")); +ldbLog(sdsnew("[h]elp Show this help.")); ldbLog(sdsnew("[s]tep Run current line and stop again.")); ldbLog(sdsnew("[n]ext Alias for step.")); ldbLog(sdsnew("[c]continue Run till next breakpoint.")); @@ -1857,6 +1962,7 @@ ldbLog(sdsnew("[b]eark Show all breakpoints.")); ldbLog(sdsnew("[b]eark Add a breakpoint to the specified line.")); ldbLog(sdsnew("[b]eark - Remove breakpoint from the specified line.")); ldbLog(sdsnew("[b]eark 0 Remove all breakpoints.")); +ldbLog(sdsnew("[e]eval Execute some Lua code in a new callframe.")); ldbSendLogs(); } else if (!strcasecmp(argv[0],"s") || !strcasecmp(argv[0],"step") || !strcasecmp(argv[0],"n") || !strcasecmp(argv[0],"next")) { @@ -1864,9 +1970,12 @@ ldbLog(sdsnew("[b]eark 0 Remove all breakpoints.")); break; } else if (!strcasecmp(argv[0],"c") || !strcasecmp(argv[0],"continue")){ break; - } else if (!strcasecmp(argv[0],"b") || !strcasecmp(argv[0],"break")){ + } else if (!strcasecmp(argv[0],"b") || !strcasecmp(argv[0],"break")) { ldbBreak(argv,argc); ldbSendLogs(); + } else if (!strcasecmp(argv[0],"e") || !strcasecmp(argv[0],"eval")) { + ldbEval(lua,argv,argc); + ldbSendLogs(); } else if (argc == 2 && (!strcasecmp(argv[0],"p") || !strcasecmp(argv[0],"print"))) { @@ -1879,7 +1988,7 @@ ldbLog(sdsnew("[b]eark 0 Remove all breakpoints.")); ldbList(around,ctx); ldbSendLogs(); } else { - ldbLog(sdsnew("Unknown Redis Lua debugger command or " + ldbLog(sdsnew(" Unknown Redis Lua debugger command or " "wrong number of arguments.")); ldbSendLogs(); } @@ -1899,12 +2008,15 @@ void luaLdbLineHook(lua_State *lua, lua_Debug *ar) { lua_getinfo(lua,"Sl",ar); if(strstr(ar->short_src,"user_script") == NULL) return; - if (ldb.step || ldbIsBreakpoint(ar->currentline)) { + int bp = ldbIsBreakpoint(ar->currentline); + if (ldb.step || bp) { + char *reason = bp ? "break point" : "step over"; ldb.currentline = ar->currentline; ldb.step = 0; - int mark = ldbIsBreakpoint(ldb.currentline) ? '#' : '*'; - ldbLog(sdscatprintf(sdsempty(),"%d%c %s", (int)ar->currentline, - mark, ldbGetSourceLine(ar->currentline))); + ldbLog(sdscatprintf(sdsempty(), + "* Stopped at %d, stop reason = %s", + ldb.currentline, reason)); + ldbLogSourceLine(ldb.currentline); ldbSendLogs(); ldbRepl(lua); }