diff --git a/src/aof.c b/src/aof.c index 9bfb9e90..f1a972c2 100644 --- a/src/aof.c +++ b/src/aof.c @@ -218,8 +218,9 @@ void stopAppendOnly(void) { serverLog(LL_NOTICE,"Killing running AOF rewrite child: %ld", (long) server.aof_child_pid); - if (kill(server.aof_child_pid,SIGUSR1) != -1) - wait3(&statloc,0,NULL); + if (kill(server.aof_child_pid,SIGUSR1) != -1) { + while(wait3(&statloc,0,NULL) != server.aof_child_pid); + } /* reset the buffer accumulating changes while the child saves */ aofRewriteBufferReset(); aofRemoveTempFile(server.aof_child_pid); diff --git a/src/redis-cli.c b/src/redis-cli.c index 7c086cfb..3752f7ae 100644 --- a/src/redis-cli.c +++ b/src/redis-cli.c @@ -111,6 +111,7 @@ static struct config { sds mb_delim; char prompt[128]; char *eval; + int eval_ldb; int last_cmd_type; } config; @@ -141,6 +142,7 @@ static long long mstime(void) { static void cliRefreshPrompt(void) { int len; + if (config.eval_ldb) return; if (config.hostsocket != NULL) len = snprintf(config.prompt,sizeof(config.prompt),"redis %s", config.hostsocket); @@ -822,6 +824,8 @@ static int parseOptions(int argc, char **argv) { config.bigkeys = 1; } else if (!strcmp(argv[i],"--eval") && !lastarg) { config.eval = argv[++i]; + } else if (!strcmp(argv[i],"--ldb")) { + config.eval_ldb = 1; } else if (!strcmp(argv[i],"-c")) { config.cluster_mode = 1; } else if (!strcmp(argv[i],"-d") && !lastarg) { @@ -904,6 +908,7 @@ static void usage(void) { " --intrinsic-latency Run a test to measure intrinsic system latency.\n" " The test will run for the specified amount of seconds.\n" " --eval Send an EVAL command using the Lua script at .\n" +" --ldb Used with --eval enable the Redis Lua debugger.\n" " --help Output this help and exit.\n" " --version Output version and exit.\n" "\n" @@ -1071,6 +1076,12 @@ static int evalMode(int argc, char **argv) { } fclose(fp); + /* If we are debugging a script, enable the Lua debugger. */ + if (config.eval_ldb) { + redisReply *reply = redisCommand(context, "SCRIPT DEBUG yes"); + if (reply) freeReplyObject(reply); + } + /* Create our argument vector */ argv2 = zmalloc(sizeof(sds)*(argc+3)); argv2[0] = sdsnew("EVAL"); @@ -1086,7 +1097,12 @@ static int evalMode(int argc, char **argv) { argv2[2] = sdscatprintf(sdsempty(),"%d",keys); /* Call it */ - return issueCommand(argc+3-got_comma, argv2); + int retval = issueCommand(argc+3-got_comma, argv2); + if (config.eval_ldb) { + strncpy(config.prompt,"lua debugger> ",sizeof(config.prompt)); + repl(); + } + return retval; } /*------------------------------------------------------------------------------ @@ -2210,6 +2226,7 @@ int main(int argc, char **argv) { config.stdinarg = 0; config.auth = NULL; config.eval = NULL; + config.eval_ldb = 0; config.last_cmd_type = -1; spectrum_palette = spectrum_palette_color; diff --git a/src/scripting.c b/src/scripting.c index d571cdb3..dd00f701 100644 --- a/src/scripting.c +++ b/src/scripting.c @@ -45,6 +45,25 @@ char *redisProtocolToLuaType_Error(lua_State *lua, char *reply); char *redisProtocolToLuaType_MultiBulk(lua_State *lua, char *reply); int redis_math_random (lua_State *L); int redis_math_randomseed (lua_State *L); +void ldbInit(void); +void ldbDisable(client *c); +void ldbEnable(client *c); +void evalGenericCommandWithDebugging(client *c, int evalsha); +void luaLdbLineHook(lua_State *lua, lua_Debug *ar); + +/* Debugger shared state is stored inside this global structure. */ +#define LDB_BREAKPOINTS_MAX 64 +struct ldbState { + int fd; /* Socket of the debugging client. */ + int active; /* Are we debugging EVAL right now? */ + int forked; /* Is this a fork()ed debugging session? */ + list *logs; /* List of messages to send to the client. */ + list *traces; /* Messages about Redis commands executed since last stop.*/ + int bp[LDB_BREAKPOINTS_MAX]; /* An array of breakpoints line numbers. */ + int bpcount; /* Number of valid entries inside bp. */ + int step; /* Stop at next line ragardless of breakpoints. */ + robj *src; /* Lua script source code. */ +} ldb; /* --------------------------------------------------------------------------- * Utility functions. @@ -821,6 +840,7 @@ void scriptingInit(int setup) { server.lua_timedout = 0; server.lua_always_replicate_commands = 0; /* Only DEBUG can change it.*/ server.lua_time_limit = LUA_SCRIPT_TIME_LIMIT; + ldbInit(); } luaLoadLibraries(lua); @@ -1038,15 +1058,6 @@ int redis_math_randomseed (lua_State *L) { return 0; } -/* --------------------------------------------------------------------------- - * LDB: Redis Lua debugging facilities - * ------------------------------------------------------------------------- */ - -/* Enable debug mode of Lua scripts for this client. */ -void ldbEnable(client *c) { - c->flags |= CLIENT_LUA_DEBUG; -} - /* --------------------------------------------------------------------------- * EVAL and SCRIPT commands implementation * ------------------------------------------------------------------------- */ @@ -1214,13 +1225,21 @@ void evalGenericCommand(client *c, int evalsha) { /* Set a hook in order to be able to stop the script execution if it * is running for too much time. * We set the hook only if the time limit is enabled as the hook will - * make the Lua script execution slower. */ + * make the Lua script execution slower. + * + * If we are debugging, we set instead a "line" hook so that the + * debugger is call-back at every line executed by the script. */ server.lua_caller = c; server.lua_time_start = mstime(); server.lua_kill = 0; - if (server.lua_time_limit > 0 && server.masterhost == NULL) { + if (server.lua_time_limit > 0 && server.masterhost == NULL && + ldb.active == 0) + { lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000); delhook = 1; + } else if (ldb.active) { + lua_sethook(server.lua,luaLdbLineHook,LUA_MASKLINE,0); + delhook = 1; } /* At this point whether this script was never seen before or if it was @@ -1229,7 +1248,7 @@ void evalGenericCommand(client *c, int evalsha) { err = lua_pcall(lua,0,1,-2); /* Perform some cleanup that we need to do both on error and success. */ - if (delhook) lua_sethook(lua,luaMaskCountHook,0,0); /* Disable hook */ + if (delhook) lua_sethook(lua,NULL,0,0); /* Disable hook */ if (server.lua_timedout) { server.lua_timedout = 0; /* Restore the readable handler that was unregistered when the @@ -1308,7 +1327,10 @@ void evalGenericCommand(client *c, int evalsha) { } void evalCommand(client *c) { - evalGenericCommand(c,0); + if (!(c->flags & CLIENT_LUA_DEBUG)) + evalGenericCommand(c,0); + else + evalGenericCommandWithDebugging(c,0); } void evalShaCommand(client *c) { @@ -1320,7 +1342,12 @@ void evalShaCommand(client *c) { addReply(c, shared.noscripterr); return; } - evalGenericCommand(c,1); + if (!(c->flags & CLIENT_LUA_DEBUG)) + evalGenericCommand(c,1); + else { + addReplyError(c,"Please use EVAL instead of EVALSHA for debugging"); + return; + } } void scriptCommand(client *c) { @@ -1366,12 +1393,149 @@ void scriptCommand(client *c) { server.lua_kill = 1; addReply(c,shared.ok); } - } else if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"debug")) { - ldbEnable(c); - addReply(c,shared.ok); + } else if (c->argc == 3 && !strcasecmp(c->argv[1]->ptr,"debug")) { + if (clientHasPendingReplies(c)) { + addReplyError(c,"SCRIPT DEBUG must be called outside a pipeline"); + return; + } + if (!strcasecmp(c->argv[2]->ptr,"no")) { + ldbDisable(c); + } else if (!strcasecmp(c->argv[2]->ptr,"yes")) { + ldbEnable(c); + addReply(c,shared.ok); + } else if (!strcasecmp(c->argv[2]->ptr,"sync")) { + ldbEnable(c); + addReply(c,shared.ok); + c->flags |= CLIENT_LUA_DEBUG_SYNC; + } else { + addReplyError(c,"Use SCRIPT DEBUG yes/async/no"); + } } else { addReplyError(c, "Unknown SCRIPT subcommand or wrong # of args."); } } +/* --------------------------------------------------------------------------- + * LDB: Redis Lua debugging facilities + * ------------------------------------------------------------------------- */ + +/* Initialize Lua debugger data structures. */ +void ldbInit(void) { + ldb.fd = -1; + ldb.active = 0; + ldb.logs = listCreate(); + ldb.traces = listCreate(); + listSetFreeMethod(ldb.logs,(void (*)(void*))sdsfree); + listSetFreeMethod(ldb.traces, (void (*)(void*))sdsfree); + ldb.src = NULL; +} + +/* Remove all the pending messages in the specified list. */ +void ldbFlushLog(list *log) { + listNode *ln; + + while((ln = listFirst(log)) != NULL) + listDelNode(log,ln); +} + +/* Enable debug mode of Lua scripts for this client. */ +void ldbEnable(client *c) { + c->flags |= CLIENT_LUA_DEBUG; + ldbFlushLog(ldb.logs); + ldbFlushLog(ldb.traces); + ldb.fd = c->fd; + ldb.step = 0; + ldb.bpcount = 0; +} + +void ldbDisable(client *c) { + c->flags &= ~(CLIENT_LUA_DEBUG|CLIENT_LUA_DEBUG_SYNC); +} + +/* Append a log entry to the specified LDB log. */ +void ldbLog(list *log, sds entry) { + listAddNodeTail(log,entry); +} + +/* Send ldb.logs and ldb.traces to the debugging client as a multi-bulk + * reply consisting of simple strings. Log entries which include newlines + * have them replaced with spaces. The entries sent are also consumed. */ +void ldbWriteLogs(void) { +} + +/* Start a debugging session before calling EVAL implementation. + * The techique we use is to capture the client socket file descriptor, + * in order to perform direct I/O with it from within Lua hooks. This + * way we don't have to re-enter Redis in order to handle I/O. + * + * The function returns 1 if the caller should proceed to call EVAL, + * and 0 if instead the caller should abort the operation (this happens + * for the parent in a forked session, since it's up to the children + * to continue, or when fork returned an error). + * + * The caller should call ldbEndSession() only if ldbStartSession() + * returned 1. */ +int ldbStartSession(client *c) { + ldb.forked = (c->flags & CLIENT_LUA_DEBUG_SYNC) == 0; + if (ldb.forked) { + pid_t cp = fork(); + if (cp == -1) { + addReplyError(c,"Fork() failed: can't run EVAL in debugging mode."); + return 0; + } else if (cp == 0) { + /* Child */ + serverLog(LL_WARNING,"Redis forked for debugging eval"); + closeListeningSockets(0); + } else { + /* Parent */ + freeClientAsync(c); /* Close the client in the parent side. */ + return 0; + } + } + + /* Setup our debugging session. */ + anetBlock(NULL,ldb.fd); + ldb.active = 1; + ldb.src = c->argv[1]; /* First argument of EVAL is the script itself. */ + incrRefCount(ldb.src); + return 1; +} + +/* End a debugging session after the EVAL call with debugging enabled + * returned. */ +void ldbEndSession(client *c) { + /* If it's a fork()ed session, we just exit. */ + if (ldb.forked) { + writeToClient(c->fd, c, 0); + serverLog(LL_WARNING,"Lua debugging session child exiting"); + exitFromChild(0); + } + + /* Otherwise let's restore client's state. */ + anetNonBlock(NULL,ldb.fd); + ldb.active = 0; + decrRefCount(ldb.src); +} + +/* Wrapper for EVAL / EVALSHA that enables debugging, and makes sure + * that when EVAL returns, whatever happened, the session is ended. */ +void evalGenericCommandWithDebugging(client *c, int evalsha) { + if (ldbStartSession(c)) { + evalGenericCommand(c,evalsha); + ldbEndSession(c); + } else { + ldbDisable(c); + } +} + +/* This is the core of our Lua debugger, called each time Lua is about + * to start executing a new line. */ +void luaLdbLineHook(lua_State *lua, lua_Debug *ar) { + lua_getstack(lua,0,ar); + lua_getinfo(lua,"Sl",ar); + if(strstr(ar->short_src,"user_script") != NULL) + printf("%s %d\n", ar->short_src, (int) ar->currentline); +} + + diff --git a/src/server.c b/src/server.c index 78d1e0fe..b74726df 100644 --- a/src/server.c +++ b/src/server.c @@ -1202,7 +1202,8 @@ int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { backgroundRewriteDoneHandler(exitcode,bysignal); } else { serverLog(LL_WARNING, - "Warning, detected child with unmatched pid: %ld", + "Warning, detected child with unmatched pid: %ld" + " (EVAL forked debugging session?)", (long)pid); } updateDictResizePolicy(); diff --git a/src/server.h b/src/server.h index d549191a..cb252dda 100644 --- a/src/server.h +++ b/src/server.h @@ -251,6 +251,7 @@ typedef long long mstime_t; /* millisecond time type. */ #define CLIENT_REPLY_SKIP_NEXT (1<<23) /* Set CLIENT_REPLY_SKIP for next cmd */ #define CLIENT_REPLY_SKIP (1<<24) /* Don't send just this reply. */ #define CLIENT_LUA_DEBUG (1<<25) /* Run EVAL in debug mode. */ +#define CLIENT_LUA_DEBUG_SYNC (1<<26) /* EVAL debugging without fork() */ /* Client block type (btype field in client structure) * if CLIENT_BLOCKED flag is set. */ @@ -1142,6 +1143,7 @@ int processEventsWhileBlocked(void); int handleClientsWithPendingWrites(void); int clientHasPendingReplies(client *c); void unlinkClient(client *c); +int writeToClient(int fd, client *c, int handler_installed); #ifdef __GNUC__ void addReplyErrorFormat(client *c, const char *fmt, ...)