summaryrefslogtreecommitdiff
path: root/src/scripting.c
diff options
context:
space:
mode:
authorantirez <antirez@gmail.com>2015-11-06 16:19:59 +0100
committerantirez <antirez@gmail.com>2015-11-17 15:43:20 +0100
commitc494db89b5c2ef34758f599ee46ac7265782ad77 (patch)
treef97b9162ab6a3800bb8c1f2fe5542994e9d67ffc /src/scripting.c
parent7cfdccd94e873c1f350b111332c0cb66b1e65f43 (diff)
downloadredis-c494db89b5c2ef34758f599ee46ac7265782ad77.tar.gz
Lua debugger: foundations implemented.
Diffstat (limited to 'src/scripting.c')
-rw-r--r--src/scripting.c198
1 files changed, 181 insertions, 17 deletions
diff --git a/src/scripting.c b/src/scripting.c
index d571cdb38..dd00f701a 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);
@@ -1039,15 +1059,6 @@ int redis_math_randomseed (lua_State *L) {
}
/* ---------------------------------------------------------------------------
- * 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);
+}
+
+