diff --git a/src/commands.def b/src/commands.def index 9a93e0002d..3a101e9f3c 100644 --- a/src/commands.def +++ b/src/commands.def @@ -5969,6 +5969,7 @@ struct COMMAND_ARG SCRIPT_DEBUG_mode_Subargs[] = { /* SCRIPT DEBUG argument table */ struct COMMAND_ARG SCRIPT_DEBUG_Args[] = { {MAKE_ARG("mode",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_NONE,3,NULL),.subargs=SCRIPT_DEBUG_mode_Subargs}, +{MAKE_ARG("engine",ARG_TYPE_STRING,-1,NULL,NULL,"9.1.0",CMD_ARG_OPTIONAL,0,NULL)}, }; /********** SCRIPT EXISTS ********************/ @@ -6115,7 +6116,7 @@ struct COMMAND_ARG SCRIPT_SHOW_Args[] = { /* SCRIPT command table */ struct COMMAND_STRUCT SCRIPT_Subcommands[] = { -{MAKE_CMD("debug","Sets the debug mode of server-side Lua scripts.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"scripting",COMMAND_GROUP_SCRIPTING,SCRIPT_DEBUG_History,0,SCRIPT_DEBUG_Tips,0,scriptCommand,3,CMD_NOSCRIPT,ACL_CATEGORY_SCRIPTING,SCRIPT_DEBUG_Keyspecs,0,NULL,1),.args=SCRIPT_DEBUG_Args}, +{MAKE_CMD("debug","Sets the debug mode of server-side Lua scripts.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"scripting",COMMAND_GROUP_SCRIPTING,SCRIPT_DEBUG_History,0,SCRIPT_DEBUG_Tips,0,scriptCommand,-3,CMD_NOSCRIPT,ACL_CATEGORY_SCRIPTING,SCRIPT_DEBUG_Keyspecs,0,NULL,2),.args=SCRIPT_DEBUG_Args}, {MAKE_CMD("exists","Determines whether server-side Lua scripts exist in the script cache.","O(N) with N being the number of scripts to check (so checking a single script is an O(1) operation).","2.6.0",CMD_DOC_NONE,NULL,NULL,"scripting",COMMAND_GROUP_SCRIPTING,SCRIPT_EXISTS_History,0,SCRIPT_EXISTS_Tips,2,scriptCommand,-3,CMD_NOSCRIPT|CMD_STALE,ACL_CATEGORY_SCRIPTING,SCRIPT_EXISTS_Keyspecs,0,NULL,1),.args=SCRIPT_EXISTS_Args}, {MAKE_CMD("flush","Removes all server-side Lua scripts from the script cache.","O(N) with N being the number of scripts in cache","2.6.0",CMD_DOC_NONE,NULL,NULL,"scripting",COMMAND_GROUP_SCRIPTING,SCRIPT_FLUSH_History,1,SCRIPT_FLUSH_Tips,2,scriptCommand,-2,CMD_NOSCRIPT|CMD_STALE,ACL_CATEGORY_SCRIPTING,SCRIPT_FLUSH_Keyspecs,0,NULL,1),.args=SCRIPT_FLUSH_Args}, {MAKE_CMD("help","Returns helpful text about the different subcommands.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"scripting",COMMAND_GROUP_SCRIPTING,SCRIPT_HELP_History,0,SCRIPT_HELP_Tips,0,scriptCommand,2,CMD_LOADING|CMD_STALE,ACL_CATEGORY_SCRIPTING,SCRIPT_HELP_Keyspecs,0,NULL,0)}, diff --git a/src/commands/script-debug.json b/src/commands/script-debug.json index ebba38a6ca..9000f9fc08 100644 --- a/src/commands/script-debug.json +++ b/src/commands/script-debug.json @@ -4,7 +4,7 @@ "complexity": "O(1)", "group": "scripting", "since": "3.2.0", - "arity": 3, + "arity": -3, "container": "SCRIPT", "function": "scriptCommand", "command_flags": [ @@ -34,6 +34,12 @@ "token": "NO" } ] + }, + { + "name": "engine", + "type": "string", + "optional": true, + "since": "9.1.0" } ], "reply_schema": { diff --git a/src/eval.c b/src/eval.c index f2f9a273f0..f6fc94ba3c 100644 --- a/src/eval.c +++ b/src/eval.c @@ -49,7 +49,6 @@ #include "monotonic.h" #include "resp_parser.h" #include "script.h" -#include "lua/debug_lua.h" #include "scripting_engine.h" #include "sds.h" @@ -561,8 +560,9 @@ void evalShaRoCommand(client *c) { void scriptCommand(client *c) { if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr, "help")) { const char *help[] = { - "DEBUG (YES|SYNC|NO)", - " Set the debug mode for subsequent scripts executed.", + "DEBUG (YES|SYNC|NO) []", + " Set the debug mode for subsequent scripts executed of the specified engine.", + " Default engine name: 'lua'", "EXISTS [ ...]", " Return information about the existence of the scripts in the script cache.", "FLUSH [ASYNC|SYNC]", @@ -614,19 +614,35 @@ void scriptCommand(client *c) { zfree(sha); } else if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr, "kill")) { scriptKill(c, 1); - } else if (c->argc == 3 && !strcasecmp(c->argv[1]->ptr, "debug")) { + } else if ((c->argc == 3 || c->argc == 4) && !strcasecmp(c->argv[1]->ptr, "debug")) { if (clientHasPendingReplies(c)) { addReplyError(c, "SCRIPT DEBUG must be called outside a pipeline"); return; } + + const char *engine_name = c->argc == 4 ? c->argv[3]->ptr : "lua"; + scriptingEngine *en = scriptingEngineManagerFind(engine_name); + if (en == NULL) { + addReplyErrorFormat(c, "No scripting engine found with name '%s' to enable debug", engine_name); + return; + } + serverAssert(en != NULL); + + sds err; if (!strcasecmp(c->argv[2]->ptr, "no")) { - ldbDisable(c); + scriptingEngineDebuggerDisable(c); addReply(c, shared.ok); } else if (!strcasecmp(c->argv[2]->ptr, "yes")) { - ldbEnable(c); + if (scriptingEngineDebuggerEnable(c, en, &err) != C_OK) { + addReplyErrorSds(c, err); + return; + } addReply(c, shared.ok); } else if (!strcasecmp(c->argv[2]->ptr, "sync")) { - ldbEnable(c); + if (scriptingEngineDebuggerEnable(c, en, &err) != C_OK) { + addReplyErrorSds(c, err); + return; + } addReply(c, shared.ok); c->flag.lua_debug_sync = 1; } else { @@ -674,11 +690,11 @@ unsigned long evalScriptsMemory(void) { /* 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)) { + if (scriptingEngineDebuggerStartSession(c)) { evalGenericCommand(c, evalsha); - ldbEndSession(c); + scriptingEngineDebuggerEndSession(c); } else { - ldbDisable(c); + scriptingEngineDebuggerDisable(c); } } diff --git a/src/lua/debug_lua.c b/src/lua/debug_lua.c index 52157932a7..9b295fb71b 100644 --- a/src/lua/debug_lua.c +++ b/src/lua/debug_lua.c @@ -7,29 +7,20 @@ #include "debug_lua.h" #include "script_lua.h" -#include "../connection.h" -#include "../adlist.h" #include "../server.h" #include #include #include -#include /* --------------------------------------------------------------------------- * LDB: Lua debugging facilities * ------------------------------------------------------------------------- */ /* Debugger shared state is stored inside this global structure. */ -#define LDB_BREAKPOINTS_MAX 64 /* Max number of breakpoints. */ -#define LDB_MAX_LEN_DEFAULT 256 /* Default len limit for replies / var dumps. */ +#define LDB_BREAKPOINTS_MAX 64 /* Max number of breakpoints. */ struct ldbState { - connection *conn; /* Connection 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 commands executed since last stop.*/ - list *children; /* All forked debugging sessions pids. */ 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 regardless of breakpoints. */ @@ -37,28 +28,17 @@ struct ldbState { sds *src; /* Lua script source code split by line. */ int lines; /* Number of lines in 'src'. */ int currentline; /* Current line number. */ - sds cbuf; /* Debugger client command buffer. */ - size_t maxlen; /* Max var dump / reply length. */ - int maxlen_hint_sent; /* Did we already hint about "set maxlen"? */ } ldb; /* Initialize Lua debugger data structures. */ void ldbInit(void) { - ldb.conn = NULL; ldb.active = 0; - ldb.logs = listCreate(); - listSetFreeMethod(ldb.logs, sdsfreeVoid); - ldb.children = listCreate(); + ldb.bpcount = 0; + ldb.step = 0; + ldb.luabp = 0; ldb.src = NULL; ldb.lines = 0; - ldb.cbuf = sdsempty(); -} - -/* Remove all the pending messages in the specified list. */ -void ldbFlushLog(list *log) { - listNode *ln; - - while ((ln = listFirst(log)) != NULL) listDelNode(log, ln); + ldb.currentline = -1; } int ldbIsEnabled(void) { @@ -66,122 +46,27 @@ int ldbIsEnabled(void) { } /* Enable debug mode of Lua scripts for this client. */ -void ldbEnable(client *c) { - c->flag.lua_debug = 1; - ldbFlushLog(ldb.logs); - ldb.conn = c->conn; +void ldbEnable(void) { + ldb.active = 1; ldb.step = 1; ldb.bpcount = 0; ldb.luabp = 0; - sdsfree(ldb.cbuf); - ldb.cbuf = sdsempty(); - ldb.maxlen = LDB_MAX_LEN_DEFAULT; - ldb.maxlen_hint_sent = 0; } /* Exit debugging mode from the POV of client. This function is not enough * to properly shut down a client debugging session, see ldbEndSession() * for more information. */ -void ldbDisable(client *c) { - c->flag.lua_debug = 0; - c->flag.lua_debug_sync = 0; -} - -/* Append a log entry to the specified LDB log. */ -void ldbLog(sds entry) { - listAddNodeTail(ldb.logs, entry); -} - -/* A version of ldbLog() which prevents producing logs greater than - * ldb.maxlen. The first time the limit is reached a hint is generated - * to inform the user that reply trimming can be disabled using the - * debugger "maxlen" command. */ -void ldbLogWithMaxLen(sds entry) { - int trimmed = 0; - if (ldb.maxlen && sdslen(entry) > ldb.maxlen) { - sdsrange(entry, 0, ldb.maxlen - 1); - entry = sdscatlen(entry, " ...", 4); - trimmed = 1; - } - ldbLog(entry); - if (trimmed && ldb.maxlen_hint_sent == 0) { - ldb.maxlen_hint_sent = 1; - ldbLog(sdsnew(" The above reply was trimmed. Use 'maxlen 0' to disable trimming.")); - } -} - -/* Send ldb.logs 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 ldbSendLogs(void) { - sds proto = sdsempty(); - proto = sdscatfmt(proto, "*%i\r\n", (int)listLength(ldb.logs)); - while (listLength(ldb.logs)) { - listNode *ln = listFirst(ldb.logs); - proto = sdscatlen(proto, "+", 1); - sdsmapchars(ln->value, "\r\n", " ", 2); - proto = sdscatsds(proto, ln->value); - proto = sdscatlen(proto, "\r\n", 2); - listDelNode(ldb.logs, ln); - } - if (connWrite(ldb.conn, proto, sdslen(proto)) == -1) { - /* Avoid warning. We don't check the return value of write() - * since the next read() will catch the I/O error and will - * close the debugging session. */ - } - sdsfree(proto); +void ldbDisable(void) { + ldb.step = 0; + ldb.active = 0; } -/* Start a debugging session before calling EVAL implementation. - * The technique 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 the server 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->flag.lua_debug_sync; - if (ldb.forked) { - pid_t cp = serverFork(CHILD_TYPE_LDB); - if (cp == -1) { - addReplyErrorFormat(c, "Fork() failed: can't run EVAL in debugging mode: %s", strerror(errno)); - return 0; - } else if (cp == 0) { - /* Child. Let's ignore important signals handled by the parent. */ - struct sigaction act; - sigemptyset(&act.sa_mask); - act.sa_flags = 0; - act.sa_handler = SIG_IGN; - sigaction(SIGTERM, &act, NULL); - sigaction(SIGINT, &act, NULL); - - /* Log the creation of the child and close the listening - * socket to make sure if the parent crashes a reset is sent - * to the clients. */ - serverLog(LL_NOTICE, "%s forked for debugging eval", SERVER_TITLE); - } else { - /* Parent */ - listAddNodeTail(ldb.children, (void *)(unsigned long)cp); - freeClientAsync(c); /* Close the client in the parent side. */ - return 0; - } - } else { - serverLog(LL_NOTICE, "%s synchronous debugging eval session started", SERVER_TITLE); - } - - /* Setup our debugging session. */ - connBlock(ldb.conn); - connSendTimeout(ldb.conn, 5000); +void ldbStart(robj *source) { ldb.active = 1; /* First argument of EVAL is the script itself. We split it into different * lines since this is the way the debugger accesses the source code. */ - sds srcstring = sdsdup(c->argv[1]->ptr); + sds srcstring = sdsdup(source->ptr); size_t srclen = sdslen(srcstring); while (srclen && (srcstring[srclen - 1] == '\n' || srcstring[srclen - 1] == '\r')) { srcstring[--srclen] = '\0'; @@ -189,81 +74,32 @@ int ldbStartSession(client *c) { sdssetlen(srcstring, srclen); ldb.src = sdssplitlen(srcstring, sdslen(srcstring), "\n", 1, &ldb.lines); sdsfree(srcstring); - return 1; } -/* End a debugging session after the EVAL call with debugging enabled - * returned. */ -void ldbEndSession(client *c) { - /* Emit the remaining logs and an mark. */ - ldbLog(sdsnew("")); - ldbSendLogs(); - - /* If it's a fork()ed session, we just exit. */ - if (ldb.forked) { - writeToClient(c); - serverLog(LL_NOTICE, "Lua debugging session child exiting"); - exitFromChild(0); - } else { - serverLog(LL_NOTICE, "%s synchronous debugging eval session ended", SERVER_TITLE); - } - - /* Otherwise let's restore client's state. */ - connNonBlock(ldb.conn); - connSendTimeout(ldb.conn, 0); - - /* Close the client connection after sending the final EVAL reply - * in order to signal the end of the debugging session. */ - c->flag.close_after_reply = 1; - - /* Cleanup. */ +void ldbEnd(void) { sdsfreesplitres(ldb.src, ldb.lines); ldb.lines = 0; ldb.active = 0; } -/* If the specified pid is among the list of children spawned for - * forked debugging sessions, it is removed from the children list. - * If the pid was found non-zero is returned. */ -int ldbRemoveChild(int pid) { - listNode *ln = listSearchKey(ldb.children, (void *)(unsigned long)pid); - if (ln) { - listDelNode(ldb.children, ln); - return 1; - } - return 0; +void ldbLog(sds entry) { + scriptingEngineDebuggerLog(createObject(OBJ_STRING, entry)); } -/* Return the number of children we still did not receive termination - * acknowledge via wait() in the parent process. */ -int ldbPendingChildren(void) { - return listLength(ldb.children); +void ldbSendLogs(void) { + scriptingEngineDebuggerFlushLogs(); } -/* Kill all the forked sessions. */ -void ldbKillForkedSessions(void) { - listIter li; - listNode *ln; - - listRewind(ldb.children, &li); - while ((ln = listNext(&li))) { - pid_t pid = (unsigned long)ln->value; - serverLog(LL_NOTICE, "Killing debugging session %ld", (long)pid); - kill(pid, SIGKILL); - } - listRelease(ldb.children); - ldb.children = listCreate(); -} /* Return a pointer to ldb.src source code line, considering line to be * one-based, and returning a special string for out of range lines. */ -char *ldbGetSourceLine(int line) { +static char *ldbGetSourceLine(int line) { int idx = line - 1; if (idx < 0 || idx >= ldb.lines) return ""; return ldb.src[idx]; } /* Return true if there is a breakpoint in the specified line. */ -int ldbIsBreakpoint(int line) { +static int ldbIsBreakpoint(int line) { int j; for (j = 0; j < ldb.bpcount; j++) @@ -274,7 +110,7 @@ int ldbIsBreakpoint(int line) { /* Add the specified breakpoint. Ignore it if we already reached the max. * Returns 1 if the breakpoint was added (or was already set). 0 if there is * no space for the breakpoint or if the line is invalid. */ -int ldbAddBreakpoint(int line) { +static int ldbAddBreakpoint(int line) { if (line <= 0 || line > ldb.lines) return 0; if (!ldbIsBreakpoint(line) && ldb.bpcount != LDB_BREAKPOINTS_MAX) { ldb.bp[ldb.bpcount++] = line; @@ -285,7 +121,7 @@ int ldbAddBreakpoint(int line) { /* Remove the specified breakpoint, returning 1 if the operation was * performed or 0 if there was no such breakpoint. */ -int ldbDelBreakpoint(int line) { +static int ldbDelBreakpoint(int line) { int j; for (j = 0; j < ldb.bpcount; j++) { @@ -298,67 +134,6 @@ int ldbDelBreakpoint(int line) { return 0; } -/* Expect a valid multi-bulk command in the debugging client query buffer. - * On success the command is parsed and returned as an array of SDS strings, - * otherwise NULL is returned and there is to read more buffer. */ -sds *ldbReplParseCommand(int *argcp, char **err) { - static char *protocol_error = "protocol error"; - sds *argv = NULL; - int argc = 0; - if (sdslen(ldb.cbuf) == 0) return NULL; - - /* Working on a copy is simpler in this case. We can modify it freely - * for the sake of simpler parsing. */ - sds copy = sdsdup(ldb.cbuf); - char *p = copy; - - /* This RESP parser is a joke... just the simplest thing that - * works in this context. It is also very forgiving regarding broken - * protocol. */ - - /* Seek and parse *\r\n. */ - p = strchr(p, '*'); - if (!p) goto protoerr; - char *plen = p + 1; /* Multi bulk len pointer. */ - p = strstr(p, "\r\n"); - if (!p) goto keep_reading; - *p = '\0'; - p += 2; - *argcp = atoi(plen); - if (*argcp <= 0 || *argcp > 1024) goto protoerr; - - /* Parse each argument. */ - argv = zmalloc(sizeof(sds) * (*argcp)); - argc = 0; - while (argc < *argcp) { - /* reached the end but there should be more data to read */ - if (*p == '\0') goto keep_reading; - - if (*p != '$') goto protoerr; - plen = p + 1; /* Bulk string len pointer. */ - p = strstr(p, "\r\n"); - if (!p) goto keep_reading; - *p = '\0'; - p += 2; - int slen = atoi(plen); /* Length of this arg. */ - if (slen <= 0 || slen > 1024) goto protoerr; - if ((size_t)(p + slen + 2 - copy) > sdslen(copy)) goto keep_reading; - argv[argc++] = sdsnewlen(p, slen); - p += slen; /* Skip the already parsed argument. */ - if (p[0] != '\r' || p[1] != '\n') goto protoerr; - p += 2; /* Skip \r\n. */ - } - sdsfree(copy); - return argv; - -protoerr: - *err = protocol_error; -keep_reading: - sdsfreesplitres(argv, argc); - sdsfree(copy); - return NULL; -} - /* Log the specified line in the Lua debugger output. */ void ldbLogSourceLine(int lnum) { char *line = ldbGetSourceLine(lnum); @@ -383,7 +158,7 @@ void ldbLogSourceLine(int lnum) { * around the specified line is shown. When a line number is specified * the amount of context (lines before/after) is specified via the * 'context' argument. */ -void ldbList(int around, int context) { +static void ldbList(int around, int context) { int j; for (j = 1; j <= ldb.lines; j++) { @@ -479,163 +254,23 @@ sds ldbCatStackValue(sds s, lua_State *lua, int idx) { /* Produce a debugger log entry representing the value of the Lua object * currently on the top of the stack. The element is neither popped nor modified. * Check ldbCatStackValue() for the actual implementation. */ -void ldbLogStackValue(lua_State *lua, char *prefix) { +static void ldbLogStackValue(lua_State *lua, char *prefix) { sds s = sdsnew(prefix); s = ldbCatStackValue(s, lua, -1); - ldbLogWithMaxLen(s); -} - -char *ldbRespToHuman_Int(sds *o, char *reply); -char *ldbRespToHuman_Bulk(sds *o, char *reply); -char *ldbRespToHuman_Status(sds *o, char *reply); -char *ldbRespToHuman_MultiBulk(sds *o, char *reply); -char *ldbRespToHuman_Set(sds *o, char *reply); -char *ldbRespToHuman_Map(sds *o, char *reply); -char *ldbRespToHuman_Null(sds *o, char *reply); -char *ldbRespToHuman_Bool(sds *o, char *reply); -char *ldbRespToHuman_Double(sds *o, char *reply); - -/* Get RESP 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 *ldbRespToHuman(sds *o, char *reply) { - char *p = reply; - switch (*p) { - case ':': p = ldbRespToHuman_Int(o, reply); break; - case '$': p = ldbRespToHuman_Bulk(o, reply); break; - case '+': p = ldbRespToHuman_Status(o, reply); break; - case '-': p = ldbRespToHuman_Status(o, reply); break; - case '*': p = ldbRespToHuman_MultiBulk(o, reply); break; - case '~': p = ldbRespToHuman_Set(o, reply); break; - case '%': p = ldbRespToHuman_Map(o, reply); break; - case '_': p = ldbRespToHuman_Null(o, reply); break; - case '#': p = ldbRespToHuman_Bool(o, reply); break; - case ',': p = ldbRespToHuman_Double(o, reply); break; - } - return p; -} - -/* The following functions are helpers for ldbRespToHuman(), each - * take care of a given RESP return type. */ - -char *ldbRespToHuman_Int(sds *o, char *reply) { - char *p = strchr(reply + 1, '\r'); - *o = sdscatlen(*o, reply + 1, p - reply - 1); - return p + 2; -} - -char *ldbRespToHuman_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 *ldbRespToHuman_Status(sds *o, char *reply) { - char *p = strchr(reply + 1, '\r'); - - *o = sdscatrepr(*o, reply, p - reply); - return p + 2; -} - -char *ldbRespToHuman_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 = ldbRespToHuman(o, p); - if (j != mbulklen - 1) *o = sdscatlen(*o, ",", 1); - } - *o = sdscatlen(*o, "]", 1); - return p; -} - -char *ldbRespToHuman_Set(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; - *o = sdscatlen(*o, "~(", 2); - for (j = 0; j < mbulklen; j++) { - p = ldbRespToHuman(o, p); - if (j != mbulklen - 1) *o = sdscatlen(*o, ",", 1); - } - *o = sdscatlen(*o, ")", 1); - return p; -} - -char *ldbRespToHuman_Map(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; - *o = sdscatlen(*o, "{", 1); - for (j = 0; j < mbulklen; j++) { - p = ldbRespToHuman(o, p); - *o = sdscatlen(*o, " => ", 4); - p = ldbRespToHuman(o, p); - if (j != mbulklen - 1) *o = sdscatlen(*o, ",", 1); - } - *o = sdscatlen(*o, "}", 1); - return p; -} - -char *ldbRespToHuman_Null(sds *o, char *reply) { - char *p = strchr(reply + 1, '\r'); - *o = sdscatlen(*o, "(null)", 6); - return p + 2; -} - -char *ldbRespToHuman_Bool(sds *o, char *reply) { - char *p = strchr(reply + 1, '\r'); - if (reply[1] == 't') - *o = sdscatlen(*o, "#true", 5); - else - *o = sdscatlen(*o, "#false", 6); - return p + 2; -} - -char *ldbRespToHuman_Double(sds *o, char *reply) { - char *p = strchr(reply + 1, '\r'); - *o = sdscatlen(*o, "(double) ", 9); - *o = sdscatlen(*o, reply + 1, p - reply - 1); - return p + 2; + scriptingEngineDebuggerLogWithMaxLen(createObject(OBJ_STRING, s)); } /* Log a RESP reply as debugger output, in a human readable format. * If the resulting string is longer than 'len' plus a few more chars * used as prefix, it gets truncated. */ void ldbLogRespReply(char *reply) { - sds log = sdsnew(" "); - ldbRespToHuman(&log, reply); - ldbLogWithMaxLen(log); + scriptingEngineDebuggerLogRespReplyStr(reply); } /* 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. */ -void ldbPrint(lua_State *lua, char *varname) { +static void ldbPrint(lua_State *lua, char *varname) { lua_Debug ar; int l = 0; /* Stack level. */ @@ -667,7 +302,7 @@ void ldbPrint(lua_State *lua, char *varname) { /* Implements the "print" command (without arguments) of the Lua debugger. * Prints all the variables in the current stack frame. */ -void ldbPrintAll(lua_State *lua) { +static void ldbPrintAll(lua_State *lua) { lua_Debug ar; int vars = 0; @@ -692,7 +327,7 @@ void ldbPrintAll(lua_State *lua) { } /* Implements the break command to list, add and remove breakpoints. */ -void ldbBreak(sds *argv, int argc) { +static void ldbBreak(robj **argv, int argc) { if (argc == 1) { if (ldb.bpcount == 0) { ldbLog(sdsnew("No breakpoints set. Use 'b ' to add one.")); @@ -705,7 +340,7 @@ void ldbBreak(sds *argv, int argc) { } else { int j; for (j = 1; j < argc; j++) { - char *arg = argv[j]; + char *arg = argv[j]->ptr; long line; if (!string2l(arg, sdslen(arg), &line)) { ldbLog(sdscatfmt(sdsempty(), "Invalid argument:'%s'", arg)); @@ -735,9 +370,13 @@ 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) { +static void ldbEval(lua_State *lua, robj **argv, int argc) { /* Glue the script together if it is composed of multiple arguments. */ - sds code = sdsjoinsds(argv + 1, argc - 1, " ", 1); + sds code = sdsempty(); + for (int j = 1; j < argc; j++) { + code = sdscatsds(code, argv[j]->ptr); + if (j != argc - 1) code = sdscatlen(code, " ", 1); + } sds expr = sdscatsds(sdsnew("return "), code); /* Try to compile it as an expression, prepending "return ". */ @@ -769,7 +408,7 @@ void ldbEval(lua_State *lua, sds *argv, int argc) { * the implementation very simple: we just call the Lua server.call() command * implementation, with ldb.step enabled, so as a side effect the command * and its reply are logged. */ -void ldbServer(lua_State *lua, sds *argv, int argc) { +static void ldbServer(lua_State *lua, robj **argv, int argc) { int j; if (!lua_checkstack(lua, argc + 1)) { @@ -787,7 +426,7 @@ void ldbServer(lua_State *lua, sds *argv, int argc) { lua_pushstring(lua, "call"); lua_gettable(lua, -2); /* Stack: server, server.call */ for (j = 1; j < argc; j++) - lua_pushlstring(lua, argv[j], sdslen(argv[j])); + lua_pushlstring(lua, argv[j]->ptr, sdslen(argv[j]->ptr)); ldb.step = 1; /* Force server.call() to log. */ lua_pcall(lua, argc - 1, 1, 0); /* Stack: server, result */ ldb.step = 0; /* Disable logging. */ @@ -796,7 +435,7 @@ void ldbServer(lua_State *lua, sds *argv, int argc) { /* Implements "trace" command of the Lua debugger. It just prints a backtrace * querying Lua starting from the current callframe back to the outer one. */ -void ldbTrace(lua_State *lua) { +static void ldbTrace(lua_State *lua) { lua_Debug ar; int level = 0; @@ -813,155 +452,177 @@ void ldbTrace(lua_State *lua) { } } -/* Implements the debugger "maxlen" command. It just queries or sets the - * ldb.maxlen variable. */ -void ldbMaxlen(sds *argv, int argc) { - if (argc == 2) { - int newval = atoi(argv[1]); - ldb.maxlen_hint_sent = 1; /* User knows about this command. */ - if (newval != 0 && newval <= 60) newval = 60; - ldb.maxlen = newval; +#define CONTINUE_SCRIPT_EXECUTION 0 +#define CONTINUE_READ_NEXT_COMMAND 1 + +static int stepCommandHandler(robj **argv, size_t argc, void *context) { + UNUSED(argv); + UNUSED(argc); + UNUSED(context); + ldb.step = 1; + return CONTINUE_SCRIPT_EXECUTION; +} + +static int continueCommandHandler(robj **argv, size_t argc, void *context) { + UNUSED(argv); + UNUSED(argc); + UNUSED(context); + return CONTINUE_SCRIPT_EXECUTION; +} + +static int listCommandHandler(robj **argv, size_t argc, void *context) { + UNUSED(context); + int around = ldb.currentline, ctx = 5; + if (argc > 1) { + int num = atoi(argv[1]->ptr); + if (num > 0) around = num; } - if (ldb.maxlen) { - ldbLog(sdscatprintf(sdsempty(), " replies are truncated at %d bytes.", (int)ldb.maxlen)); + if (argc > 2) ctx = atoi(argv[2]->ptr); + ldbList(around, ctx); + scriptingEngineDebuggerFlushLogs(); + return CONTINUE_READ_NEXT_COMMAND; +} + +static int wholeCommandHandler(robj **argv, size_t argc, void *context) { + UNUSED(argv); + UNUSED(argc); + UNUSED(context); + ldbList(1, 1000000); + scriptingEngineDebuggerFlushLogs(); + return CONTINUE_READ_NEXT_COMMAND; +} + +static int printCommandHandler(robj **argv, size_t argc, void *context) { + serverAssert(context != NULL); + lua_State *lua = context; + if (argc == 2) { + ldbPrint(lua, argv[1]->ptr); } else { - ldbLog(sdscatprintf(sdsempty(), " replies are unlimited.")); - } + ldbPrintAll(lua); + } + scriptingEngineDebuggerFlushLogs(); + return CONTINUE_READ_NEXT_COMMAND; +} + +static int breakCommandHandler(robj **argv, size_t argc, void *context) { + UNUSED(context); + ldbBreak(argv, argc); + scriptingEngineDebuggerFlushLogs(); + return CONTINUE_READ_NEXT_COMMAND; +} + +static int traceCommandHandler(robj **argv, size_t argc, void *context) { + UNUSED(argv); + UNUSED(argc); + UNUSED(context); + lua_State *lua = context; + ldbTrace(lua); + scriptingEngineDebuggerFlushLogs(); + return CONTINUE_READ_NEXT_COMMAND; +} + +static int evalCommandHandler(robj **argv, size_t argc, void *context) { + serverAssert(context != NULL); + lua_State *lua = context; + ldbEval(lua, argv, argc); + scriptingEngineDebuggerFlushLogs(); + return CONTINUE_READ_NEXT_COMMAND; +} + +static int valkeyCommandHandler(robj **argv, size_t argc, void *context) { + serverAssert(context != NULL); + lua_State *lua = context; + ldbServer(lua, argv, argc); + scriptingEngineDebuggerFlushLogs(); + return CONTINUE_READ_NEXT_COMMAND; +} + +static int abortCommandHandler(robj **argv, size_t argc, void *context) { + UNUSED(argv); + UNUSED(argc); + UNUSED(context); + serverAssert(context != NULL); + lua_State *lua = context; + luaPushError(lua, "script aborted for user request"); + luaError(lua); + return CONTINUE_READ_NEXT_COMMAND; +} + +static debuggerCommand *commands_array_cache = NULL; +static size_t commands_array_len = 0; + +void ldbGenerateDebuggerCommandsArray(lua_State *lua, + const debuggerCommand **commands, + size_t *commands_len) { + static debuggerCommandParam list_params[] = { + {.name = "line", .optional = 1}, + {.name = "ctx", .optional = 1}, + }; + + static debuggerCommandParam print_params[] = { + {.name = "var", .optional = 1}, + }; + + static debuggerCommandParam break_params[] = { + {.name = "line|-line", .optional = 1}, + }; + + static debuggerCommandParam eval_params[] = { + {.name = "code", .optional = 0, .variadic = 1}, + }; + + static debuggerCommandParam valkey_params[] = { + {.name = "cmd", .optional = 0, .variadic = 1}, + }; + + if (commands_array_cache == NULL) { + debuggerCommand commands_array[] = { + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("step", 1, NULL, 0, "Run current line and stop again.", 0, stepCommandHandler), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("next", 1, NULL, 0, "Alias for step.", 0, stepCommandHandler), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("continue", 1, NULL, 0, "Run till next breakpoint.", 0, continueCommandHandler), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("list", 1, list_params, 2, "List source code around a specific line. If no line is specified the list is printed around the current line. [ctx] specifies how many lines to show before/after [line].", 0, listCommandHandler), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("whole", 1, NULL, 0, "List all source code. Alias for 'list 1 1000000'.", 0, wholeCommandHandler), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND_WITH_CTX("print", 1, print_params, 1, "Show the value of the specified variable [var]. Can also show global vars KEYS and ARGV. If no [var] is specidied, shows the value of all local variables.", 0, printCommandHandler, lua), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("break", 1, break_params, 1, "Add/Remove a breakpoint to the specified line. If no [line] is specified, it shows all breakpoints. When line = 0, it removes all breakpoints.", 0, breakCommandHandler), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND_WITH_CTX("trace", 1, NULL, 0, "Show a backtrace.", 0, traceCommandHandler, lua), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND_WITH_CTX("eval", 1, eval_params, 1, "Execute some Lua code (in a different callframe).", 0, evalCommandHandler, lua), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND_WITH_CTX("valkey", 1, valkey_params, 1, "Execute a command.", 0, valkeyCommandHandler, lua), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND_WITH_CTX("redis", 1, valkey_params, 1, NULL, 1, valkeyCommandHandler, lua), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND_WITH_CTX(SERVER_API_NAME, 0, valkey_params, 1, NULL, 1, valkeyCommandHandler, lua), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND_WITH_CTX("abort", 1, NULL, 0, "Stop the execution of the script. In sync mode dataset changes will be retained.", 0, abortCommandHandler, lua), + }; + + commands_array_len = sizeof(commands_array) / sizeof(debuggerCommand); + + commands_array_cache = zmalloc(sizeof(debuggerCommand) * commands_array_len); + memcpy(commands_array_cache, &commands_array, sizeof(commands_array)); + } + + *commands = commands_array_cache; + *commands_len = commands_array_len; } /* Read debugging commands from client. * Return C_OK if the debugging session is continuing, otherwise * C_ERR if the client closed the connection or is timing out. */ int ldbRepl(lua_State *lua) { - sds *argv; - int argc; - char *err = NULL; - - /* We continue processing commands until a command that should return - * to the Lua interpreter is found. */ - while (1) { - while ((argv = ldbReplParseCommand(&argc, &err)) == NULL) { - char buf[1024]; - if (err) { - luaPushError(lua, err); - luaError(lua); - } - int nread = connRead(ldb.conn, buf, sizeof(buf)); - if (nread <= 0) { - /* Make sure the script runs without user input since the - * client is no longer connected. */ - ldb.step = 0; - ldb.bpcount = 0; - return C_ERR; - } - ldb.cbuf = sdscatlen(ldb.cbuf, buf, nread); - /* after 1M we will exit with an error - * so that the client will not blow the memory - */ - if (sdslen(ldb.cbuf) > 1 << 20) { - sdsfree(ldb.cbuf); - ldb.cbuf = sdsempty(); - luaPushError(lua, "max client buffer reached"); - luaError(lua); - } - } + int client_disconnected = 0; + robj *err = NULL; - /* Flush the old buffer. */ - sdsfree(ldb.cbuf); - ldb.cbuf = sdsempty(); - - /* Execute the command. */ - if (!strcasecmp(argv[0], "h") || !strcasecmp(argv[0], "help")) { - ldbLog(sdsnew("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]ontinue Run till next breakpoint.")); - ldbLog(sdsnew("[l]ist List source code around current line.")); - ldbLog(sdsnew("[l]ist [line] List source code around [line].")); - ldbLog(sdsnew(" line = 0 means: current position.")); - ldbLog(sdsnew("[l]ist [line] [ctx] In this form [ctx] specifies how many lines")); - ldbLog(sdsnew(" to show before/after [line].")); - ldbLog(sdsnew("[w]hole List all source code. Alias for 'list 1 1000000'.")); - ldbLog(sdsnew("[p]rint Show all the local variables.")); - ldbLog(sdsnew("[p]rint Show the value of the specified variable.")); - ldbLog(sdsnew(" Can also show global vars KEYS and ARGV.")); - ldbLog(sdsnew("[b]reak Show all breakpoints.")); - ldbLog(sdsnew("[b]reak Add a breakpoint to the specified line.")); - ldbLog(sdsnew("[b]reak - Remove breakpoint from the specified line.")); - ldbLog(sdsnew("[b]reak 0 Remove all breakpoints.")); - ldbLog(sdsnew("[t]race Show a backtrace.")); - ldbLog(sdsnew("[e]val Execute some Lua code (in a different callframe).")); - ldbLog(sdsnew("[v]alkey Execute a command.")); - ldbLog(sdsnew("[m]axlen [len] Trim logged replies and Lua var dumps to len.")); - ldbLog(sdsnew(" Specifying zero as means unlimited.")); - ldbLog(sdsnew("[a]bort Stop the execution of the script. In sync")); - ldbLog(sdsnew(" mode dataset changes will be retained.")); - ldbLog(sdsnew("")); - ldbLog(sdsnew("Debugger functions you can call from Lua scripts:")); - ldbLog(sdsnew("server.debug() Produce logs in the debugger console.")); - ldbLog(sdsnew("server.breakpoint() Stop execution like if there was a breakpoint in the")); - ldbLog(sdsnew(" next line of code.")); - ldbSendLogs(); - } else if (!strcasecmp(argv[0], "s") || !strcasecmp(argv[0], "step") || !strcasecmp(argv[0], "n") || - !strcasecmp(argv[0], "next")) { - ldb.step = 1; - break; - } else if (!strcasecmp(argv[0], "c") || !strcasecmp(argv[0], "continue")) { - break; - } else if (!strcasecmp(argv[0], "t") || !strcasecmp(argv[0], "trace")) { - ldbTrace(lua); - ldbSendLogs(); - } else if (!strcasecmp(argv[0], "m") || !strcasecmp(argv[0], "maxlen")) { - ldbMaxlen(argv, argc); - ldbSendLogs(); - } 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 (!strcasecmp(argv[0], "a") || !strcasecmp(argv[0], "abort")) { - luaPushError(lua, "script aborted for user request"); - luaError(lua); - } else if (argc > 1 && ((!strcasecmp(argv[0], "r") || !strcasecmp(argv[0], "redis")) || - (!strcasecmp(argv[0], "v") || !strcasecmp(argv[0], "valkey")) || - !strcasecmp(argv[0], SERVER_API_NAME))) { - /* [r]redis or [v]alkey calls a command. We accept "server" too, but - * not "s" because that's "step". Neither can we use [c]all because - * "c" is continue. */ - ldbServer(lua, argv, argc); - ldbSendLogs(); - } else if ((!strcasecmp(argv[0], "p") || !strcasecmp(argv[0], "print"))) { - if (argc == 2) - ldbPrint(lua, argv[1]); - else - ldbPrintAll(lua); - ldbSendLogs(); - } else if (!strcasecmp(argv[0], "l") || !strcasecmp(argv[0], "list")) { - int around = ldb.currentline, ctx = 5; - if (argc > 1) { - int num = atoi(argv[1]); - if (num > 0) around = num; - } - if (argc > 2) ctx = atoi(argv[2]); - ldbList(around, ctx); - ldbSendLogs(); - } else if (!strcasecmp(argv[0], "w") || !strcasecmp(argv[0], "whole")) { - ldbList(1, 1000000); - ldbSendLogs(); - } else { - ldbLog(sdsnew(" Unknown Lua debugger command or " - "wrong number of arguments.")); - ldbSendLogs(); - } + scriptingEngineDebuggerProcessCommands(&client_disconnected, &err); - /* Free the command vector. */ - sdsfreesplitres(argv, argc); + if (err) { + luaPushError(lua, err->ptr); + decrRefCount(err); + luaError(lua); + } else if (client_disconnected) { + /* Make sure the script runs without user input since the + * client is no longer connected. */ + ldb.step = 0; + ldb.bpcount = 0; + return C_ERR; } - /* Free the current command argv if we break inside the while loop. */ - sdsfreesplitres(argv, argc); return C_OK; } diff --git a/src/lua/debug_lua.h b/src/lua/debug_lua.h index c66ee040a2..c197bae9ea 100644 --- a/src/lua/debug_lua.h +++ b/src/lua/debug_lua.h @@ -1,32 +1,37 @@ #ifndef _LUA_DEBUG_H_ #define _LUA_DEBUG_H_ +#include "../scripting_engine.h" + typedef char *sds; +typedef struct serverObject robj; typedef struct lua_State lua_State; typedef struct client client; void ldbInit(void); int ldbIsEnabled(void); -void ldbDisable(client *c); -void ldbEnable(client *c); -int ldbStartSession(client *c); -void ldbEndSession(client *c); +void ldbDisable(void); +void ldbEnable(void); int ldbIsActive(void); +void ldbStart(robj *source); +void ldbEnd(void); +void ldbLog(sds entry); +void ldbSendLogs(void); +void ldbLogRespReply(char *reply); + int ldbGetCurrentLine(void); void ldbSetCurrentLine(int line); +void ldbLogSourceLine(int lnum); +sds ldbCatStackValue(sds s, lua_State *lua, int idx); void ldbSetBreakpointOnNextLine(int enable); int ldbIsBreakpointOnNextLineEnabled(void); int ldbShouldBreak(void); int ldbIsStepEnabled(void); void ldbSetStepMode(int enable); -void ldbLogSourceLine(int lnum); -void ldbLog(sds entry); -void ldbLogRespReply(char *reply); + int ldbRepl(lua_State *lua); -void ldbSendLogs(void); -sds ldbCatStackValue(sds s, lua_State *lua, int idx); -int ldbRemoveChild(int pid); -int ldbPendingChildren(void); -void ldbKillForkedSessions(void); +void ldbGenerateDebuggerCommandsArray(lua_State *lua, + const debuggerCommand **commands, + size_t *commands_len); #endif /* _LUA_DEBUG_H_ */ diff --git a/src/lua/engine_lua.c b/src/lua/engine_lua.c index f680d0eff3..5951ceaf38 100644 --- a/src/lua/engine_lua.c +++ b/src/lua/engine_lua.c @@ -8,9 +8,6 @@ #include "script_lua.h" #include "debug_lua.h" -#include "../dict.h" -#include "../adlist.h" - #define LUA_ENGINE_NAME "LUA" #define REGISTRY_ERROR_HANDLER_NAME "__ERROR_HANDLER__" @@ -365,6 +362,55 @@ static void luaEngineFreeFunction(ValkeyModuleCtx *module_ctx, zfree(compiled_function); } +static debuggerEnableRet luaEngineDebuggerEnable(ValkeyModuleCtx *module_ctx, + engineCtx *engine_ctx, + subsystemType type, + const debuggerCommand **commands, + size_t *commands_len) { + UNUSED(module_ctx); + + if (type != VMSE_EVAL) { + return VMSE_DEBUG_NOT_SUPPORTED; + } + + ldbEnable(); + + luaEngineCtx *lua_engine_ctx = engine_ctx; + ldbGenerateDebuggerCommandsArray(lua_engine_ctx->eval_lua, + commands, + commands_len); + + return VMSE_DEBUG_ENABLED; +} + +static void luaEngineDebuggerDisable(ValkeyModuleCtx *module_ctx, + engineCtx *engine_ctx, + subsystemType type) { + UNUSED(module_ctx); + UNUSED(engine_ctx); + UNUSED(type); + ldbDisable(); +} + +static void luaEngineDebuggerStart(ValkeyModuleCtx *module_ctx, + engineCtx *engine_ctx, + subsystemType type, + robj *source) { + UNUSED(module_ctx); + UNUSED(engine_ctx); + UNUSED(type); + ldbStart(source); +} + +static void luaEngineDebuggerEnd(ValkeyModuleCtx *module_ctx, + engineCtx *engine_ctx, + subsystemType type) { + UNUSED(module_ctx); + UNUSED(engine_ctx); + UNUSED(type); + ldbEnd(); +} + int luaEngineInitEngine(void) { ldbInit(); @@ -376,6 +422,10 @@ int luaEngineInitEngine(void) { .get_function_memory_overhead = luaEngineFunctionMemoryOverhead, .reset_env = luaEngineResetEvalEnv, .get_memory_info = luaEngineGetMemoryInfo, + .debugger_enable = luaEngineDebuggerEnable, + .debugger_disable = luaEngineDebuggerDisable, + .debugger_start = luaEngineDebuggerStart, + .debugger_end = luaEngineDebuggerEnd, }; return scriptingEngineManagerRegister(LUA_ENGINE_NAME, diff --git a/src/module.c b/src/module.c index 299dcbac13..17f05eefa5 100644 --- a/src/module.c +++ b/src/module.c @@ -13426,6 +13426,10 @@ int VM_RdbSave(ValkeyModuleCtx *ctx, ValkeyModuleRdbStream *stream, int flags) { return VALKEYMODULE_OK; } +/* -------------------------------------------------------------------------- + * ## Scripting Engine API + * -------------------------------------------------------------------------- */ + /* Registers a new scripting engine in the server. * * - `module_ctx`: the module context object. @@ -13497,6 +13501,62 @@ ValkeyModuleScriptingEngineExecutionState VM_GetFunctionExecutionState( return ret == SCRIPT_CONTINUE ? VMSE_STATE_EXECUTING : VMSE_STATE_KILLED; } +/* Function to send string messages to the client during a debug session. + * These messages are buffered in memory, and are only sent to the client when + * `ValkeyModule_VM_ScriptingEngineDebuggerFlushLogs` is called. + * + * - `msg`: the message to send. + * + * - `truncate`: if set to 1, the message will be truncated to the maximum length + * configured in the debugger settings. + */ +void VM_ScriptingEngineDebuggerLog(ValkeyModuleString *msg, int truncate) { + if (truncate) { + scriptingEngineDebuggerLogWithMaxLen(msg); + } else { + scriptingEngineDebuggerLog(msg); + } +} + +/* Function to log a RESP reply C string as debugger output, in a human readable + * format. + * + * If the resulting string is longer than the maximum text length, configured in + * the debugger settings, plus a few more chars used as prefix, it gets truncated. + */ +void VM_ScriptingEngineDebuggerLogRespReplyStr(const char *reply) { + scriptingEngineDebuggerLogRespReplyStr(reply); +} + +/* Function to log a RESP reply as debugger output, in a human readable format. + * + * If the resulting string is longer than the maximum text length, configured in + * the debugger settings, plus a few more chars used as prefix, it gets truncated. + */ +void VM_ScriptingEngineDebuggerLogRespReply(ValkeyModuleCallReply *reply) { + size_t proto_len; + const char *proto = callReplyGetProto(reply, &proto_len); + scriptingEngineDebuggerLogRespReplyStr(proto); +} + +/* Function to send all debugger messages in the memory buffer written with the + * `ValkeyModule_ScriptingEngineDebuggerLog` function. + */ +void VM_ScriptingEngineDebuggerFlushLogs(void) { + scriptingEngineDebuggerFlushLogs(); +} + +/* Function used to process debugger commands sent by the client. + * + * This function in conjunction with `ValkeyModule_ScriptingEngineDebuggerLog` and + * `ValkeyModule_ScriptingEngineDebuggerFlushLogs` allows to implement an + * interactive debugging session for scripts executed by the scripting engine. + */ +void VM_ScriptingEngineDebuggerProcessCommands(int *client_disconnected, + ValkeyModuleString **err) { + scriptingEngineDebuggerProcessCommands(client_disconnected, err); +} + /* MODULE command. * * MODULE LIST @@ -14371,4 +14431,9 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(RegisterScriptingEngine); REGISTER_API(UnregisterScriptingEngine); REGISTER_API(GetFunctionExecutionState); + REGISTER_API(ScriptingEngineDebuggerLog); + REGISTER_API(ScriptingEngineDebuggerLogRespReplyStr); + REGISTER_API(ScriptingEngineDebuggerLogRespReply); + REGISTER_API(ScriptingEngineDebuggerFlushLogs); + REGISTER_API(ScriptingEngineDebuggerProcessCommands); } diff --git a/src/scripting_engine.c b/src/scripting_engine.c index 59d37a53f8..2ddf57ec51 100644 --- a/src/scripting_engine.c +++ b/src/scripting_engine.c @@ -11,6 +11,15 @@ #include "server.h" #include "valkeymodule.h" +/* First ABI version */ +#define SCRIPTING_ENGINE_ABI_VERSION_1 1 +/* Version when new compile function signature was introduced */ +#define SCRIPTING_ENGINE_ABI_VERSION_2 2 +/* Version when environment reset function was introduced */ +#define SCRIPTING_ENGINE_ABI_VERSION_3 3 +/* Version when debugger support was introduced */ +#define SCRIPTING_ENGINE_ABI_VERSION_4 4 + /* Module context object cache size is set to 3 because at each moment there can * be at most 3 module contexts in use by the scripting engine. * @@ -77,6 +86,7 @@ dictType engineDictType = { */ int scriptingEngineManagerInit(void) { engineMgr.engines = dictCreate(&engineDictType); + scriptingEngineDebuggerInit(); return C_OK; } @@ -94,6 +104,17 @@ size_t scriptingEngineManagerGetMemoryUsage(void) { return dictMemUsage(engineMgr.engines) + sizeof(engineMgr); } +static inline void scriptingEngineInitializeEngineMethods(scriptingEngine *engine, engineMethods *methods) { + if (methods->version < SCRIPTING_ENGINE_ABI_VERSION_4) { + serverLog(LL_WARNING, "Registering scripting engine '%s' with ABI version '%lu'", + engine->name, + (unsigned long)methods->version); + memcpy(&engine->impl.methods, methods, sizeof(engineMethodsV3)); + } else { + engine->impl.methods = *methods; + } +} + /* Registers a new scripting engine in the engine manager. * * - `engine_name`: the name of the scripting engine. This name will match @@ -129,11 +150,11 @@ int scriptingEngineManagerRegister(const char *engine_name, .module = engine_module, .impl = { .ctx = engine_ctx, - .methods = *engine_methods, }, .client = c, .module_ctx_cache = {0}, }; + scriptingEngineInitializeEngineMethods(e, engine_methods); for (size_t i = 0; i < MODULE_CTX_CACHE_SIZE; i++) { e->module_ctx_cache[i] = moduleAllocateContext(); @@ -253,7 +274,7 @@ compiledFunction **scriptingEngineCallCompileCode(scriptingEngine *engine, compiledFunction **functions = NULL; ValkeyModuleCtx *module_ctx = engineSetupModuleCtx(COMMON_MODULE_CTX_INDEX, engine, NULL); - if (engine->impl.methods.version == 1) { + if (engine->impl.methods.version == SCRIPTING_ENGINE_ABI_VERSION_1) { functions = engine->impl.methods.compile_code_v1( module_ctx, engine->impl.ctx, @@ -336,7 +357,7 @@ callableLazyEnvReset *scriptingEngineCallResetEnvFunc(scriptingEngine *engine, ValkeyModuleCtx *module_ctx = engineSetupModuleCtx(COMMON_MODULE_CTX_INDEX, engine, NULL); callableLazyEnvReset *callback = NULL; - if (engine->impl.methods.version <= 2) { + if (engine->impl.methods.version < SCRIPTING_ENGINE_ABI_VERSION_3) { /* For backward compatibility with scripting engine modules that * implement version 1 or 2 of the scripting engine ABI, we call the * reset_eval_env_v1 function, which is only implemented for resetting @@ -376,3 +397,835 @@ engineMemoryInfo scriptingEngineCallGetMemoryInfo(scriptingEngine *engine, engineTeardownModuleCtx(GET_MEMORY_MODULE_CTX_INDEX, engine); return mem_info; } + +debuggerEnableRet scriptingEngineCallDebuggerEnable(scriptingEngine *engine, + subsystemType type, + const debuggerCommand **commands, + size_t *commands_len) { + if (engine->impl.methods.version < SCRIPTING_ENGINE_ABI_VERSION_4) { + serverLog(LL_WARNING, "Scripting engine '%s' uses ABI version '%lu', which does not support debugger API", + scriptingEngineGetName(engine), + (unsigned long)engine->impl.methods.version); + return VMSE_DEBUG_NOT_SUPPORTED; + } + + if (engine->impl.methods.debugger_enable == NULL || + engine->impl.methods.debugger_disable == NULL || + engine->impl.methods.debugger_start == NULL || + engine->impl.methods.debugger_end == NULL) { + return VMSE_DEBUG_NOT_SUPPORTED; + } + + ValkeyModuleCtx *module_ctx = engineSetupModuleCtx(COMMON_MODULE_CTX_INDEX, engine, NULL); + debuggerEnableRet ret = engine->impl.methods.debugger_enable( + module_ctx, + engine->impl.ctx, + type, + commands, + commands_len); + engineTeardownModuleCtx(COMMON_MODULE_CTX_INDEX, engine); + return ret; +} + +void scriptingEngineCallDebuggerDisable(scriptingEngine *engine, + subsystemType type) { + serverAssert(engine->impl.methods.version >= SCRIPTING_ENGINE_ABI_VERSION_4); + serverAssert(engine->impl.methods.debugger_disable != NULL); + + ValkeyModuleCtx *module_ctx = engineSetupModuleCtx(COMMON_MODULE_CTX_INDEX, engine, NULL); + engine->impl.methods.debugger_disable( + module_ctx, + engine->impl.ctx, + type); + engineTeardownModuleCtx(COMMON_MODULE_CTX_INDEX, engine); +} + +void scriptingEngineCallDebuggerStart(scriptingEngine *engine, + subsystemType type, + robj *source) { + serverAssert(engine->impl.methods.version >= SCRIPTING_ENGINE_ABI_VERSION_4); + serverAssert(engine->impl.methods.debugger_start != NULL); + + ValkeyModuleCtx *module_ctx = engineSetupModuleCtx(COMMON_MODULE_CTX_INDEX, engine, NULL); + engine->impl.methods.debugger_start( + module_ctx, + engine->impl.ctx, + type, + source); + engineTeardownModuleCtx(COMMON_MODULE_CTX_INDEX, engine); +} + +void scriptingEngineCallDebuggerEnd(scriptingEngine *engine, + subsystemType type) { + serverAssert(engine->impl.methods.version >= SCRIPTING_ENGINE_ABI_VERSION_4); + serverAssert(engine->impl.methods.debugger_end != NULL); + + ValkeyModuleCtx *module_ctx = engineSetupModuleCtx(COMMON_MODULE_CTX_INDEX, engine, NULL); + engine->impl.methods.debugger_end( + module_ctx, + engine->impl.ctx, + type); + engineTeardownModuleCtx(COMMON_MODULE_CTX_INDEX, engine); +} + +#define DS_MAX_LEN_DEFAULT 256 /* Default len limit for replies / var dumps. */ + +typedef struct debugState { + scriptingEngine *engine; /* The scripting engine. */ + const debuggerCommand *commands; /* The array of debugger commands exported by the scripting engine. */ + size_t commands_len; /* The length of the commands array. */ + connection *conn; /* Connection 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 commands executed since last stop.*/ + list *children; /* All forked debugging sessions pids. */ + sds cbuf; /* Debugger client command buffer. */ + size_t maxlen; /* Max var dump / reply length. */ + int maxlen_hint_sent; /* Did we already hint about "set maxlen"? */ +} debugState; + +static debugState ds; + +static inline void freeLogEntry(void *obj) { + decrRefCount((robj *)obj); +} + +/* Initialize script debugger data structures. */ +void scriptingEngineDebuggerInit(void) { + ds.engine = NULL; + ds.conn = NULL; + ds.active = 0; + ds.logs = listCreate(); + listSetFreeMethod(ds.logs, freeLogEntry); + ds.children = listCreate(); + ds.cbuf = sdsempty(); +} + +/* Remove all the pending messages in the specified list. */ +void debugScriptFlushLog(list *log) { + listNode *ln; + + while ((ln = listFirst(log)) != NULL) listDelNode(log, ln); +} + +/* Enable debug mode of scripts for this client. */ +int scriptingEngineDebuggerEnable(client *c, scriptingEngine *engine, sds *err) { + debuggerEnableRet ret = scriptingEngineCallDebuggerEnable( + engine, + VMSE_EVAL, + &ds.commands, + &ds.commands_len); + + if (ret == VMSE_DEBUG_NOT_SUPPORTED) { + *err = sdscatfmt(sdsempty(), + "The scripting engine '%s' does not support interactive script debugging", + scriptingEngineGetName(engine)); + return C_ERR; + } else if (ret == VMSE_DEBUG_ENABLE_FAIL) { + *err = sdscatfmt(sdsempty(), + "The scripting engine '%s' failed to initialize interactive script debugging", + scriptingEngineGetName(engine)); + return C_ERR; + } + ds.engine = engine; + c->flag.lua_debug = 1; + debugScriptFlushLog(ds.logs); + ds.conn = c->conn; + sdsfree(ds.cbuf); + ds.cbuf = sdsempty(); + ds.maxlen = DS_MAX_LEN_DEFAULT; + ds.maxlen_hint_sent = 0; + return C_OK; +} + +/* Exit debugging mode from the POV of client. This function is not enough + * to properly shut down a client debugging session, see scriptingEngineDebuggerEndSession() + * for more information. */ +void scriptingEngineDebuggerDisable(client *c) { + if (ds.engine == NULL) { + /* No debug session enabled. */ + return; + } + + ds.commands = NULL; + ds.commands_len = 0; + c->flag.lua_debug = 0; + c->flag.lua_debug_sync = 0; + scriptingEngineCallDebuggerDisable(ds.engine, VMSE_EVAL); +} + +/* Append a log entry to the specified debug state log. */ +void scriptingEngineDebuggerLog(robj *entry) { + listAddNodeTail(ds.logs, entry); +} + +/* A version of scriptingEngineDebuggerLog() which prevents producing logs greater than + * ds.maxlen. The first time the limit is reached a hint is generated + * to inform the user that reply trimming can be disabled using the + * debugger "maxlen" command. */ +void scriptingEngineDebuggerLogWithMaxLen(robj *entry) { + int trimmed = 0; + + if (ds.maxlen && sdslen(entry->ptr) > ds.maxlen) { + sdsrange(entry->ptr, 0, ds.maxlen - 1); + entry->ptr = sdscatlen(entry->ptr, " ...", 4); + trimmed = 1; + } + scriptingEngineDebuggerLog(entry); + if (trimmed && ds.maxlen_hint_sent == 0) { + ds.maxlen_hint_sent = 1; + scriptingEngineDebuggerLog( + createObject(OBJ_STRING, sdsnew(" The above reply was trimmed. Use 'maxlen 0' to disable trimming."))); + } +} + +/* Implements the debugger "maxlen" command. It just queries or sets the + * ldb.maxlen variable. */ +void scriptingEngineDebuggerSetMaxlen(size_t max) { + size_t newval = max; + ds.maxlen_hint_sent = 1; /* User knows about this command. */ + if (newval != 0 && newval <= 60) newval = 60; + ds.maxlen = newval; +} + +/* Send ds.logs 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 scriptingEngineDebuggerFlushLogs(void) { + sds proto = sdsempty(); + proto = sdscatfmt(proto, "*%i\r\n", (int)listLength(ds.logs)); + while (listLength(ds.logs)) { + listNode *ln = listFirst(ds.logs); + robj *msg = ln->value; + proto = sdscatlen(proto, "+", 1); + sdsmapchars(msg->ptr, "\r\n", " ", 2); + proto = sdscatsds(proto, msg->ptr); + proto = sdscatlen(proto, "\r\n", 2); + listDelNode(ds.logs, ln); + } + if (connWrite(ds.conn, proto, sdslen(proto)) == -1) { + /* Avoid warning. We don't check the return value of write() + * since the next read() will catch the I/O error and will + * close the debugging session. */ + } + sdsfree(proto); +} + +/* Start a debugging session before calling EVAL implementation. + * The technique we use is to capture the client socket file descriptor, + * in order to perform direct I/O with it from within the scripting engine + * hooks. This way we don't have to re-enter the server 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 scriptingEngineDebuggerEndSession() only if + * scriptDebugStartSession() returned 1. */ +int scriptingEngineDebuggerStartSession(client *c) { + ds.forked = !c->flag.lua_debug_sync; + if (ds.forked) { + pid_t cp = serverFork(CHILD_TYPE_LDB); + if (cp == -1) { + addReplyErrorFormat(c, "Fork() failed: can't run EVAL in debugging mode: %s", strerror(errno)); + return 0; + } else if (cp == 0) { + /* Child. Let's ignore important signals handled by the parent. */ + struct sigaction act; + sigemptyset(&act.sa_mask); + act.sa_flags = 0; + act.sa_handler = SIG_IGN; + sigaction(SIGTERM, &act, NULL); + sigaction(SIGINT, &act, NULL); + + /* Log the creation of the child and close the listening + * socket to make sure if the parent crashes a reset is sent + * to the clients. */ + serverLog(LL_NOTICE, "%s forked for debugging eval", SERVER_TITLE); + } else { + /* Parent */ + listAddNodeTail(ds.children, (void *)(unsigned long)cp); + freeClientAsync(c); /* Close the client in the parent side. */ + return 0; + } + } else { + serverLog(LL_NOTICE, "%s synchronous debugging eval session started", SERVER_TITLE); + } + + /* Setup our debugging session. */ + connBlock(ds.conn); + connSendTimeout(ds.conn, 5000); + ds.active = 1; + + scriptingEngineCallDebuggerStart(ds.engine, VMSE_EVAL, c->argv[1]); + return 1; +} + +/* End a debugging session after the EVAL call with debugging enabled + * returned. */ +void scriptingEngineDebuggerEndSession(client *c) { + serverAssert(ds.active); + + /* Emit the remaining logs and an mark. */ + scriptingEngineDebuggerLog(createObject(OBJ_STRING, sdsnew(""))); + scriptingEngineDebuggerFlushLogs(); + + /* If it's a fork()ed session, we just exit. */ + if (ds.forked) { + writeToClient(c); + serverLog(LL_NOTICE, "Lua debugging session child exiting"); + exitFromChild(0); + } else { + serverLog(LL_NOTICE, "%s synchronous debugging eval session ended", SERVER_TITLE); + } + + /* Otherwise let's restore client's state. */ + connNonBlock(ds.conn); + connSendTimeout(ds.conn, 0); + + /* Close the client connection after sending the final EVAL reply + * in order to signal the end of the debugging session. */ + c->flag.close_after_reply = 1; + + scriptingEngineCallDebuggerEnd(ds.engine, VMSE_EVAL); +} + +/* If the specified pid is among the list of children spawned for + * forked debugging sessions, it is removed from the children list. + * If the pid was found non-zero is returned. */ +int scriptingEngineDebuggerRemoveChild(int pid) { + listNode *ln = listSearchKey(ds.children, (void *)(unsigned long)pid); + if (ln) { + listDelNode(ds.children, ln); + return 1; + } + return 0; +} + +/* Return the number of children we still did not receive termination + * acknowledge via wait() in the parent process. */ +int scriptingEngineDebuggerPendingChildren(void) { + return listLength(ds.children); +} + +/* Kill all the forked sessions. */ +void scriptingEngineDebuggerKillForkedSessions(void) { + listIter li; + listNode *ln; + + listRewind(ds.children, &li); + while ((ln = listNext(&li))) { + pid_t pid = (unsigned long)ln->value; + serverLog(LL_NOTICE, "Killing debugging session %ld", (long)pid); + kill(pid, SIGKILL); + } + listRelease(ds.children); + ds.children = listCreate(); +} + +/* Expect a valid multi-bulk command in the debugging client query buffer. + * On success the command is parsed and returned as an array of object strings, + * otherwise NULL is returned and there is to read more buffer. */ +static robj **readReadCommandInternal(size_t *argc, robj **err) { + static const char *protocol_error = "protocol error"; + serverAssert(err != NULL && *err == NULL); + serverAssert(argc != NULL && *argc == 0); + robj **argv = NULL; + size_t largc = 0; + if (sdslen(ds.cbuf) == 0) return NULL; + + /* Working on a copy is simpler in this case. We can modify it freely + * for the sake of simpler parsing. */ + sds copy = sdsdup(ds.cbuf); + char *p = copy; + + /* This RESP parser is a joke... just the simplest thing that + * works in this context. It is also very forgiving regarding broken + * protocol. */ + + /* Seek and parse *\r\n. */ + p = strchr(p, '*'); + if (!p) goto protoerr; + char *plen = p + 1; /* Multi bulk len pointer. */ + p = strstr(p, "\r\n"); + if (!p) goto keep_reading; + *p = '\0'; + p += 2; + *argc = atoi(plen); + if (*argc <= 0 || *argc > 1024) goto protoerr; + + /* Parse each argument. */ + argv = zmalloc(sizeof(robj *) * (*argc)); + largc = 0; + while (largc < *argc) { + /* reached the end but there should be more data to read */ + if (*p == '\0') goto keep_reading; + + if (*p != '$') goto protoerr; + plen = p + 1; /* Bulk string len pointer. */ + p = strstr(p, "\r\n"); + if (!p) goto keep_reading; + *p = '\0'; + p += 2; + int slen = atoi(plen); /* Length of this arg. */ + if (slen <= 0 || slen > 1024) goto protoerr; + if ((size_t)(p + slen + 2 - copy) > sdslen(copy)) goto keep_reading; + argv[largc++] = createStringObject(p, slen); + p += slen; /* Skip the already parsed argument. */ + if (p[0] != '\r' || p[1] != '\n') goto protoerr; + p += 2; /* Skip \r\n. */ + } + sdsfree(copy); + return argv; + +protoerr: + *err = createStringObject(protocol_error, strlen(protocol_error)); +keep_reading: + for (size_t i = 0; i < largc; i++) { + decrRefCount(argv[i]); + } + zfree(argv); + sdsfree(copy); + return NULL; +} + +static sds *wrapText(const char *text, size_t max_len, size_t *count) { + sds *lines = NULL; + *count = 0; + + const char *p = text; + size_t text_len = strlen(p); + + while ((size_t)(p - text) < text_len) { + size_t len = strlen(p); + char *line = zmalloc(sizeof(char) * (max_len + 1)); + line[max_len] = 0; + + strncpy(line, p, max_len); + + if (len > max_len) { + char *lastspace = strrchr(line, ' '); + if (lastspace != NULL) { + *lastspace = 0; + } + + p += (lastspace - line) + 1; + } else { + p += len; + } + + lines = zrealloc(lines, sizeof(sds) * (*count + 1)); + lines[*count] = sdsnew(line); + zfree(line); + (*count)++; + } + + return lines; +} + +static void printCommandHelp(const debuggerCommand *command, + int name_width, + int line_width) { + sds msg = sdsempty(); + + /* Format the command name according to the prefix length. */ + if (command->prefix_len > 0 && command->prefix_len < strlen(command->name)) { + sds prefix = sdsnewlen(command->name, command->prefix_len); + msg = sdscatfmt(msg, "[%S]%s", prefix, command->name + command->prefix_len); + sdsfree(prefix); + } else { + msg = sdscatfmt(msg, "%s", command->name); + } + + /* Format the command parameters. */ + for (size_t i = 0; i < command->params_len; i++) { + if (command->params[i].optional) { + msg = sdscatfmt(msg, " [%s]", command->params[i].name); + } else { + msg = sdscatfmt(msg, " <%s>", command->params[i].name); + } + } + + msg = sdscatprintf(msg, "%*s ", -(name_width - (int)sdslen(msg) - 1), ""); + + /* If the command name plus the parameters don't fit in the respective + * space slot, then start the description of the command in the next line.*/ + int breakline = (int)sdslen(msg) > name_width; + if (breakline) { + scriptingEngineDebuggerLog(createObject(OBJ_STRING, msg)); + } + + size_t count = 0; + sds *lines = wrapText(command->desc, line_width - name_width, &count); + for (size_t i = 0; i < count; i++) { + if (i == 0 && !breakline) { + msg = sdscatsds(msg, lines[i]); + } else { + msg = sdscatprintf(sdsempty(), "%*s%s", name_width, "", lines[i]); + } + scriptingEngineDebuggerLog(createObject(OBJ_STRING, msg)); + sdsfree(lines[i]); + } + zfree(lines); +} + +#define HELP_LINE_WIDTH 70 +#define HELP_CMD_NAME_WIDTH 21 + +#define CONTINUE_SCRIPT_EXECUTION 0 +#define CONTINUE_READ_NEXT_COMMAND 1 + +static int printHelpMessage(robj **argv, size_t argc, void *context); + +/* Handler for the "maxlen" debugger command. */ +static int maxlenCommandHandler(robj **argv, size_t argc, void *context) { + UNUSED(context); + + if (argc == 1) { + /* Show current value */ + sds msg = sdscatfmt(sdsempty(), "Current maxlen is %U", ds.maxlen); + scriptingEngineDebuggerLog(createObject(OBJ_STRING, msg)); + } else if (argc == 2) { + long long new_maxlen; + if (string2ll(argv[1]->ptr, sdslen(argv[1]->ptr), &new_maxlen) && new_maxlen >= 0) { + scriptingEngineDebuggerSetMaxlen((size_t)new_maxlen); + if (new_maxlen == 0) { + sds msg = sdscatfmt(sdsempty(), " replies are not truncated."); + scriptingEngineDebuggerLog(createObject(OBJ_STRING, msg)); + } else { + sds msg = sdscatfmt(sdsempty(), " replies are truncated at %U bytes.", ds.maxlen); + scriptingEngineDebuggerLog(createObject(OBJ_STRING, msg)); + } + } else { + scriptingEngineDebuggerLog(createObject(OBJ_STRING, sdsnew(" Invalid maxlen value."))); + } + } else { + scriptingEngineDebuggerLog(createObject(OBJ_STRING, sdsnew(" Wrong number of arguments for 'maxlen'."))); + } + scriptingEngineDebuggerFlushLogs(); + return CONTINUE_READ_NEXT_COMMAND; +} + +static debuggerCommand builtins[] = { + { + .name = "help", + .prefix_len = 1, + .desc = "Show this help.", + .handler = printHelpMessage, + }, + { + .name = "maxlen", + .prefix_len = 3, + .desc = "Trim logged replies to len. Specifying zero as means unlimited. " + "If no is specified, the current value is shown. " + "Usage: maxlen [len]", + .handler = maxlenCommandHandler, + .params = (debuggerCommandParam[]){ + {.name = "len", .optional = 1, .variadic = 0}}, + .params_len = 1, + }}; + +static size_t builtins_len = sizeof(builtins) / sizeof(debuggerCommand); + +static int printHelpMessage(robj **argv, size_t argc, void *context) { + UNUSED(argv); + UNUSED(argc); + UNUSED(context); + + sds title = sdscatfmt(sdsempty(), "%s debugger help:", scriptingEngineGetName(ds.engine)); + scriptingEngineDebuggerLog(createObject(OBJ_STRING, title)); + + // Print built-in commands first. + for (size_t i = 0; i < builtins_len; i++) { + if (!builtins[i].invisible) { + printCommandHelp(&builtins[i], HELP_CMD_NAME_WIDTH, HELP_LINE_WIDTH); + } + } + + for (size_t i = 0; i < ds.commands_len; i++) { + if (!ds.commands[i].invisible) { + printCommandHelp(&ds.commands[i], HELP_CMD_NAME_WIDTH, HELP_LINE_WIDTH); + } + } + + scriptingEngineDebuggerFlushLogs(); + + return CONTINUE_READ_NEXT_COMMAND; +} + +static int checkCommandParameters(const debuggerCommand *cmd, size_t argc) { + size_t args_count = argc - 1; + size_t mandatory_params_count = 0; + int has_variadic_param = 0; + + for (size_t i = 0; i < cmd->params_len; i++) { + if (!cmd->params[i].optional) { + mandatory_params_count++; + } + if (cmd->params[i].variadic) { + has_variadic_param = 1; + } + } + + if (has_variadic_param && args_count > 0) { + /* If command has a variadic parameter then we just require at least + * one argument present. */ + return 1; + } + + if (args_count < mandatory_params_count) { + /* Reject command because there is not enough arguments passed. */ + return 0; + } + + if (args_count > cmd->params_len) { + /* Reject command because there are more arguments than parameters. */ + return 0; + } + + return 1; +} + +static const debuggerCommand *findCommand(robj **argv, size_t argc) { + // Check built-in commands first. + for (size_t i = 0; i < builtins_len; i++) { + const debuggerCommand *cmd = &builtins[i]; + if ((sdslen(argv[0]->ptr) == cmd->prefix_len && + strncasecmp(cmd->name, argv[0]->ptr, cmd->prefix_len) == 0) || + strcasecmp(cmd->name, argv[0]->ptr) == 0) { + if (checkCommandParameters(cmd, argc)) { + return cmd; + } + } + } + + // Then check the commands exported by the scripting engine. + for (size_t i = 0; i < ds.commands_len; i++) { + const debuggerCommand *cmd = &ds.commands[i]; + if ((sdslen(argv[0]->ptr) == cmd->prefix_len && + strncasecmp(cmd->name, argv[0]->ptr, cmd->prefix_len) == 0) || + strcasecmp(cmd->name, argv[0]->ptr) == 0) { + if (checkCommandParameters(cmd, argc)) { + return cmd; + } + } + } + return NULL; +} + +static int findAndExecuteCommand(robj **argv, size_t argc) { + const debuggerCommand *cmd = findCommand(argv, argc); + if (cmd == NULL) { + scriptingEngineDebuggerLog(createObject( + OBJ_STRING, + sdsnew(" Unknown debugger command or wrong number of arguments."))); + scriptingEngineDebuggerFlushLogs(); + return CONTINUE_READ_NEXT_COMMAND; + } + + return cmd->handler(argv, argc, cmd->context); +} + +void scriptingEngineDebuggerProcessCommands(int *client_disconnected, robj **err) { + static const char *max_buffer_error = "max client buffer reached"; + + serverAssert(err != NULL); + robj **argv = NULL; + *client_disconnected = 0; + *err = NULL; + + while (1) { + size_t argc = 0; + while ((argv = readReadCommandInternal(&argc, err)) == NULL) { + if (*err) { + break; + } + + char buf[1024]; + int nread = connRead(ds.conn, buf, sizeof(buf)); + if (nread <= 0) { + *client_disconnected = 1; + break; + } + + ds.cbuf = sdscatlen(ds.cbuf, buf, nread); + /* after 1M we will exit with an error + * so that the client will not blow the memory + */ + if (sdslen(ds.cbuf) > 1 << 20) { + *err = createStringObject(max_buffer_error, strlen(max_buffer_error)); + return; + } + } + + serverAssert(argv != NULL || *err || *client_disconnected); + + sdsfree(ds.cbuf); + ds.cbuf = sdsempty(); + + if (*err || *client_disconnected) { + return; + } + + int res = findAndExecuteCommand(argv, argc); + + /* Free the command vector. */ + for (size_t i = 0; i < argc; i++) { + decrRefCount(argv[i]); + } + zfree(argv); + + if (res != CONTINUE_READ_NEXT_COMMAND) { + return; + } + } +} + +static const char *debugScriptRespToHuman_Int(sds *o, const char *reply); +static const char *debugScriptRespToHuman_Bulk(sds *o, const char *reply); +static const char *debugScriptRespToHuman_Status(sds *o, const char *reply); +static const char *debugScriptRespToHuman_MultiBulk(sds *o, const char *reply); +static const char *debugScriptRespToHuman_Set(sds *o, const char *reply); +static const char *debugScriptRespToHuman_Map(sds *o, const char *reply); +static const char *debugScriptRespToHuman_Null(sds *o, const char *reply); +static const char *debugScriptRespToHuman_Bool(sds *o, const char *reply); +static const char *debugScriptRespToHuman_Double(sds *o, const char *reply); + +/* Get RESP 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. */ +static const char *debugScriptRespToHuman(sds *o, const char *reply) { + const char *p = reply; + switch (*p) { + case ':': p = debugScriptRespToHuman_Int(o, reply); break; + case '$': p = debugScriptRespToHuman_Bulk(o, reply); break; + case '+': p = debugScriptRespToHuman_Status(o, reply); break; + case '-': p = debugScriptRespToHuman_Status(o, reply); break; + case '*': p = debugScriptRespToHuman_MultiBulk(o, reply); break; + case '~': p = debugScriptRespToHuman_Set(o, reply); break; + case '%': p = debugScriptRespToHuman_Map(o, reply); break; + case '_': p = debugScriptRespToHuman_Null(o, reply); break; + case '#': p = debugScriptRespToHuman_Bool(o, reply); break; + case ',': p = debugScriptRespToHuman_Double(o, reply); break; + } + return p; +} + +/* The following functions are helpers for debugScriptRespToHuman(), each + * take care of a given RESP return type. */ + +static const char *debugScriptRespToHuman_Int(sds *o, const char *reply) { + const char *p = strchr(reply + 1, '\r'); + *o = sdscatlen(*o, reply + 1, p - reply - 1); + return p + 2; +} + +static const char *debugScriptRespToHuman_Bulk(sds *o, const char *reply) { + const 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; + } +} + +static const char *debugScriptRespToHuman_Status(sds *o, const char *reply) { + const char *p = strchr(reply + 1, '\r'); + + *o = sdscatrepr(*o, reply, p - reply); + return p + 2; +} + +static const char *debugScriptRespToHuman_MultiBulk(sds *o, const char *reply) { + const 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 = debugScriptRespToHuman(o, p); + if (j != mbulklen - 1) *o = sdscatlen(*o, ",", 1); + } + *o = sdscatlen(*o, "]", 1); + return p; +} + +static const char *debugScriptRespToHuman_Set(sds *o, const char *reply) { + const char *p = strchr(reply + 1, '\r'); + long long mbulklen; + int j = 0; + + string2ll(reply + 1, p - reply - 1, &mbulklen); + p += 2; + *o = sdscatlen(*o, "~(", 2); + for (j = 0; j < mbulklen; j++) { + p = debugScriptRespToHuman(o, p); + if (j != mbulklen - 1) *o = sdscatlen(*o, ",", 1); + } + *o = sdscatlen(*o, ")", 1); + return p; +} + +static const char *debugScriptRespToHuman_Map(sds *o, const char *reply) { + const char *p = strchr(reply + 1, '\r'); + long long mbulklen; + int j = 0; + + string2ll(reply + 1, p - reply - 1, &mbulklen); + p += 2; + *o = sdscatlen(*o, "{", 1); + for (j = 0; j < mbulklen; j++) { + p = debugScriptRespToHuman(o, p); + *o = sdscatlen(*o, " => ", 4); + p = debugScriptRespToHuman(o, p); + if (j != mbulklen - 1) *o = sdscatlen(*o, ",", 1); + } + *o = sdscatlen(*o, "}", 1); + return p; +} + +static const char *debugScriptRespToHuman_Null(sds *o, const char *reply) { + const char *p = strchr(reply + 1, '\r'); + *o = sdscatlen(*o, "(null)", 6); + return p + 2; +} + +static const char *debugScriptRespToHuman_Bool(sds *o, const char *reply) { + const char *p = strchr(reply + 1, '\r'); + if (reply[1] == 't') + *o = sdscatlen(*o, "#true", 5); + else + *o = sdscatlen(*o, "#false", 6); + return p + 2; +} + +static const char *debugScriptRespToHuman_Double(sds *o, const char *reply) { + const char *p = strchr(reply + 1, '\r'); + *o = sdscatlen(*o, "(double) ", 9); + *o = sdscatlen(*o, reply + 1, p - reply - 1); + return p + 2; +} + +/* Log a RESP reply C string as debugger output, in a human readable format. + * If the resulting string is longer than 'len' plus a few more chars used as + * prefix, it gets truncated. */ +void scriptingEngineDebuggerLogRespReplyStr(const char *reply) { + sds log = sdsnew(" "); + debugScriptRespToHuman(&log, reply); + scriptingEngineDebuggerLogWithMaxLen(createObject(OBJ_STRING, log)); +} diff --git a/src/scripting_engine.h b/src/scripting_engine.h index ef0249adca..45ace10a2c 100644 --- a/src/scripting_engine.h +++ b/src/scripting_engine.h @@ -2,6 +2,7 @@ #define _SCRIPTING_ENGINE_H_ #include "server.h" +#include "valkeymodule.h" // Forward declaration of the engine structure. typedef struct scriptingEngine scriptingEngine; @@ -14,6 +15,10 @@ typedef ValkeyModuleScriptingEngineCompiledFunction compiledFunction; typedef ValkeyModuleScriptingEngineSubsystemType subsystemType; typedef ValkeyModuleScriptingEngineMemoryInfo engineMemoryInfo; typedef ValkeyModuleScriptingEngineCallableLazyEnvReset callableLazyEnvReset; +typedef ValkeyModuleScriptingEngineDebuggerEnableRet debuggerEnableRet; +typedef ValkeyModuleScriptingEngineDebuggerCommand debuggerCommand; +typedef ValkeyModuleScriptingEngineDebuggerCommandParam debuggerCommandParam; +typedef ValkeyModuleScriptingEngineMethodsV3 engineMethodsV3; typedef ValkeyModuleScriptingEngineMethods engineMethods; /* @@ -84,4 +89,53 @@ callableLazyEnvReset *scriptingEngineCallResetEnvFunc(scriptingEngine *engine, engineMemoryInfo scriptingEngineCallGetMemoryInfo(scriptingEngine *engine, subsystemType type); +debuggerEnableRet scriptingEngineCallDebuggerEnable(scriptingEngine *engine, + subsystemType type, + const debuggerCommand **commands, + size_t *commands_len); + +void scriptingEngineCallDebuggerDisable(scriptingEngine *engine, + subsystemType type); + +void scriptingEngineCallDebuggerStart(scriptingEngine *engine, + subsystemType type, + robj *source); + +void scriptingEngineCallDebuggerEnd(scriptingEngine *engine, + subsystemType type); + +/* + * API of scripting engine remote debugger. + */ +void scriptingEngineDebuggerInit(void); + +int scriptingEngineDebuggerEnable(client *c, scriptingEngine *engine, sds *err); + +void scriptingEngineDebuggerDisable(client *c); + +int scriptingEngineDebuggerStartSession(client *c); + +void scriptingEngineDebuggerEndSession(client *c); + +void scriptingEngineDebuggerLog(robj *entry); + +void scriptingEngineDebuggerLogWithMaxLen(robj *entry); + +void scriptingEngineDebuggerSetMaxlen(size_t max); + +size_t scriptingEngineDebuggerGetMaxlen(void); + +void scriptingEngineDebuggerFlushLogs(void); + +void scriptingEngineDebuggerProcessCommands(int *client_disconnected, robj **err); + +void scriptingEngineDebuggerLogRespReplyStr(const char *reply); + +int scriptingEngineDebuggerRemoveChild(int pid); + +int scriptingEngineDebuggerPendingChildren(void); + +void scriptingEngineDebuggerKillForkedSessions(void); + + #endif /* _SCRIPTING_ENGINE_H_ */ diff --git a/src/server.c b/src/server.c index 46a20d1aee..20f4f269bc 100644 --- a/src/server.c +++ b/src/server.c @@ -51,7 +51,6 @@ #include "module.h" #include "scripting_engine.h" #include "lua/engine_lua.h" -#include "lua/debug_lua.h" #include "eval.h" #include "trace/trace_commands.h" @@ -1406,7 +1405,7 @@ void checkChildrenDone(void) { } resetChildState(); } else { - if (!ldbRemoveChild(pid)) { + if (!scriptingEngineDebuggerRemoveChild(pid)) { serverLog(LL_WARNING, "Warning, detected child with unmatched pid: %ld", (long)pid); } } @@ -1584,7 +1583,7 @@ long long serverCron(struct aeEventLoop *eventLoop, long long id, void *clientDa } /* Check if a background saving or AOF rewrite in progress terminated. */ - if (hasActiveChildProcess() || ldbPendingChildren()) { + if (hasActiveChildProcess() || scriptingEngineDebuggerPendingChildren()) { run_with_period(1000) receiveChildInfo(); checkChildrenDone(); } else { @@ -4715,7 +4714,7 @@ int finishShutdown(void) { } /* Kill all the Lua debugger forked sessions. */ - ldbKillForkedSessions(); + scriptingEngineDebuggerKillForkedSessions(); /* Kill the saving child if there is a background saving in progress. We want to avoid race conditions, for instance our saving child may diff --git a/src/valkey-cli.c b/src/valkey-cli.c index e1bfec489c..c902d1255f 100644 --- a/src/valkey-cli.c +++ b/src/valkey-cli.c @@ -3533,12 +3533,27 @@ static int evalMode(int argc, char **argv) { } fclose(fp); + char *engine_name = NULL; + if (script[0] == '#' && script[1] == '!') { + const char *sp = strpbrk(script, "\r\n "); + engine_name = strndup(script + 2, (sp - script) - 2); + } else { + engine_name = strdup("lua"); + } + /* If we are debugging a script, enable the Lua debugger. */ if (config.eval_ldb) { - valkeyReply *reply = valkeyCommand(context, config.eval_ldb_sync ? "SCRIPT DEBUG sync" : "SCRIPT DEBUG yes"); + valkeyReply *reply = valkeyCommand( + context, + config.eval_ldb_sync ? "SCRIPT DEBUG sync %s" : "SCRIPT DEBUG yes %s", + engine_name ? engine_name : ""); if (reply) freeReplyObject(reply); } + if (engine_name) { + free(engine_name); + } + /* Create our argument vector */ argv2 = zmalloc(sizeof(sds) * (argc + 3)); argv2[0] = sdsnew("EVAL"); diff --git a/src/valkeymodule.h b/src/valkeymodule.h index 6eb199eb67..df5ea78afe 100644 --- a/src/valkeymodule.h +++ b/src/valkeymodule.h @@ -1077,48 +1077,208 @@ typedef ValkeyModuleScriptingEngineMemoryInfo (*ValkeyModuleScriptingEngineGetMe ValkeyModuleScriptingEngineCtx *engine_ctx, ValkeyModuleScriptingEngineSubsystemType type); +typedef enum ValkeyModuleScriptingEngineDebuggerEnableRet { + VMSE_DEBUG_NOT_SUPPORTED, /* The scripting engine does not support debugging. */ + VMSE_DEBUG_ENABLED, /* The scripting engine has enabled the debugging mode. */ + VMSE_DEBUG_ENABLE_FAIL, /* The scripting engine failed to enable the debugging mode. */ +} ValkeyModuleScriptingEngineDebuggerEnableRet; + +typedef int (*ValkeyModuleScriptingEngineDebuggerCommandHandlerFunc)( + ValkeyModuleString **argv, + size_t argc, + void *context); + +/* Current ABI version for scripting engine debugger commands. */ +#define VALKEYMODULE_SCRIPTING_ENGINE_ABI_DEBUGGER_COMMAND_VERSION 1UL + +/* The structure that represents the parameter of a debugger command. */ +typedef struct ValkeyModuleScriptingEngineDebuggerCommandParam { + const char *name; + int optional; + int variadic; + +} ValkeyModuleScriptingEngineDebuggerCommandParam; + +/* The structure that represents a debugger command. */ +typedef struct ValkeyModuleScriptingEngineDebuggerCommand { + uint64_t version; /* Version of this structure for ABI compat. */ + + const char *name; /* The command name. */ + const size_t prefix_len; /* The prefix of the command name that can be used as a short name. */ + const ValkeyModuleScriptingEngineDebuggerCommandParam *params; /* The array of parameters of this command. */ + size_t params_len; /* The length of the array of parameters. */ + const char *desc; /* The description of the command that is shown in the help message. */ + int invisible; /* Whether this command should be hidden in the help message. */ + ValkeyModuleScriptingEngineDebuggerCommandHandlerFunc handler; /* The function pointer that implements this command. */ + void *context; /* The pointer to a context structure that is passed when invoking the command handler. */ +} ValkeyModuleScriptingEngineDebuggerCommandV1; + +#define ValkeyModuleScriptingEngineDebuggerCommand ValkeyModuleScriptingEngineDebuggerCommandV1 + +#define VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND(NAME, PREFIX, PARAMS, PARAMS_LEN, DESC, INVISIBLE, HANDLER) \ + { \ + .version = VALKEYMODULE_SCRIPTING_ENGINE_ABI_DEBUGGER_COMMAND_VERSION, \ + .name = NAME, \ + .prefix_len = PREFIX, \ + .params = PARAMS, \ + .params_len = PARAMS_LEN, \ + .desc = DESC, \ + .invisible = INVISIBLE, \ + .handler = HANDLER, \ + .context = NULL} + +#define VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND_WITH_CTX(NAME, PREFIX, PARAMS, PARAMS_LEN, DESC, INVISIBLE, HANDLER, CTX) \ + { \ + .version = VALKEYMODULE_SCRIPTING_ENGINE_ABI_DEBUGGER_COMMAND_VERSION, \ + .name = NAME, \ + .prefix_len = PREFIX, \ + .params = PARAMS, \ + .params_len = PARAMS_LEN, \ + .desc = DESC, \ + .invisible = INVISIBLE, \ + .handler = HANDLER, \ + .context = CTX} + +/* The callback function called when `SCRIPT DEBUG (YES|SYNC)` command is called + * to enable the remote debugger when executing a compiled function. + * + * - `module_ctx`: the module runtime context. + * + * - `engine_ctx`: the scripting engine runtime context. + * + * - `type`: the subsystem type. Either EVAL or FUNCTION. + * + * - `commands`: the array of commands exposed by the remote debugger + * implemented by this scripting engine. + * + * - `commands_len`: the length of the commands array. + * + * Returns an enum value of type `ValkeyModuleScriptingEngineDebuggerEnableRet`. + * Check the enum comments for more details. + */ +typedef ValkeyModuleScriptingEngineDebuggerEnableRet (*ValkeyModuleScriptingEngineDebuggerEnableFunc)( + ValkeyModuleCtx *module_ctx, + ValkeyModuleScriptingEngineCtx *engine_ctx, + ValkeyModuleScriptingEngineSubsystemType type, + const ValkeyModuleScriptingEngineDebuggerCommand **commands, + size_t *commands_length); + +/* The callback function called when `SCRIPT DEBUG NO` command is called to + * disable the remote debugger. + * + * - `module_ctx`: the module runtime context. + * + * - `engine_ctx`: the scripting engine runtime context. + * + * - `type`: the subsystem type. Either EVAL or FUNCTION. + */ +typedef void (*ValkeyModuleScriptingEngineDebuggerDisableFunc)( + ValkeyModuleCtx *module_ctx, + ValkeyModuleScriptingEngineCtx *engine_ctx, + ValkeyModuleScriptingEngineSubsystemType type); + +/* The callback function called just before the execution of a compiled function + * when the debugging mode is enabled. + * + * - `module_ctx`: the module runtime context. + * + * - `engine_ctx`: the scripting engine runtime context. + * + * - `type`: the subsystem type. Either EVAL or FUNCTION. + * + * - `source`: the original source code from where the code of the compiled + * function was compiled. + */ +typedef void (*ValkeyModuleScriptingEngineDebuggerStartFunc)( + ValkeyModuleCtx *module_ctx, + ValkeyModuleScriptingEngineCtx *engine_ctx, + ValkeyModuleScriptingEngineSubsystemType type, + ValkeyModuleString *source); + +/* The callback function called just after the execution of a compiled function + * when the debugging mode is enabled. + * + * - `module_ctx`: the module runtime context. + * + * - `engine_ctx`: the scripting engine runtime context. + * + * - `type`: the subsystem type. Either EVAL or FUNCTION. + */ +typedef void (*ValkeyModuleScriptingEngineDebuggerEndFunc)( + ValkeyModuleCtx *module_ctx, + ValkeyModuleScriptingEngineCtx *engine_ctx, + ValkeyModuleScriptingEngineSubsystemType type); + /* Current ABI version for scripting engine modules. */ /* Version Changelog: * 1. Initial version. * 2. Changed the `compile_code` callback to support binary data in the source code. * 3. Renamed reset_eval_env callback to reset_env and added a type parameter to be * able to reset both EVAL or FUNCTION scripts env. + * 4. Added support for new debugging commands. */ -#define VALKEYMODULE_SCRIPTING_ENGINE_ABI_VERSION 3L +#define VALKEYMODULE_SCRIPTING_ENGINE_ABI_VERSION 4UL + +#define VALKEYMODULE_SCRIPTING_ENGINE_METHODS_STRUCT_FIELDS_V3 \ + struct { \ + /* Compile code function callback. When a new script is loaded, this \ + * callback will be called with the script code, compiles it, and returns a \ + * list of `ValkeyModuleScriptingEngineCompiledFunc` objects. */ \ + union { \ + ValkeyModuleScriptingEngineCompileCodeFuncV1 compile_code_v1; \ + ValkeyModuleScriptingEngineCompileCodeFunc compile_code; \ + }; \ + \ + /* Function callback to free the memory of a registered engine function. */ \ + ValkeyModuleScriptingEngineFreeFunctionFunc free_function; \ + \ + \ + /* The callback function called when `FCALL` command is called on a function \ + * registered in this engine. */ \ + ValkeyModuleScriptingEngineCallFunctionFunc call_function; \ + \ + /* Function callback to return memory overhead for a given function. */ \ + ValkeyModuleScriptingEngineGetFunctionMemoryOverheadFunc get_function_memory_overhead; \ + \ + /* The callback function used to reset the runtime environment used \ + * by the scripting engine for EVAL scripts or FUNCTION scripts. */ \ + union { \ + ValkeyModuleScriptingEngineResetEvalFuncV2 reset_eval_env_v2; \ + ValkeyModuleScriptingEngineResetEnvFunc reset_env; \ + }; \ + \ + /* Function callback to get the used memory by the engine. */ \ + ValkeyModuleScriptingEngineGetMemoryInfoFunc get_memory_info; \ + } typedef struct ValkeyModuleScriptingEngineMethods { uint64_t version; /* Version of this structure for ABI compat. */ - /* Compile code function callback. When a new script is loaded, this - * callback will be called with the script code, compiles it, and returns a - * list of `ValkeyModuleScriptingEngineCompiledFunc` objects. */ - union { - ValkeyModuleScriptingEngineCompileCodeFuncV1 compile_code_v1; - ValkeyModuleScriptingEngineCompileCodeFunc compile_code; - }; - /* Function callback to free the memory of a registered engine function. */ - ValkeyModuleScriptingEngineFreeFunctionFunc free_function; + VALKEYMODULE_SCRIPTING_ENGINE_METHODS_STRUCT_FIELDS_V3; - /* The callback function called when `FCALL` command is called on a function - * registered in this engine. */ - ValkeyModuleScriptingEngineCallFunctionFunc call_function; +} ValkeyModuleScriptingEngineMethodsV3; - /* Function callback to return memory overhead for a given function. */ - ValkeyModuleScriptingEngineGetFunctionMemoryOverheadFunc get_function_memory_overhead; +typedef struct ValkeyModuleScriptingEngineMethodsV4 { + uint64_t version; /* Version of this structure for ABI compat. */ - /* The callback function used to reset the runtime environment used - * by the scripting engine for EVAL scripts or FUNCTION scripts. */ - union { - ValkeyModuleScriptingEngineResetEvalFuncV2 reset_eval_env_v2; - ValkeyModuleScriptingEngineResetEnvFunc reset_env; - }; + VALKEYMODULE_SCRIPTING_ENGINE_METHODS_STRUCT_FIELDS_V3; + + /* Function callback to enable the debugger for the future execution of scripts. */ + ValkeyModuleScriptingEngineDebuggerEnableFunc debugger_enable; + + /* Function callback to disable the debugger. */ + ValkeyModuleScriptingEngineDebuggerDisableFunc debugger_disable; - /* Function callback to get the used memory by the engine. */ - ValkeyModuleScriptingEngineGetMemoryInfoFunc get_memory_info; + /* Function callback to start the debugger on a particular script. */ + ValkeyModuleScriptingEngineDebuggerStartFunc debugger_start; -} ValkeyModuleScriptingEngineMethodsV1; + /* Function callback to end the debugger on a particular script. */ + ValkeyModuleScriptingEngineDebuggerEndFunc debugger_end; -#define ValkeyModuleScriptingEngineMethods ValkeyModuleScriptingEngineMethodsV1 + +} ValkeyModuleScriptingEngineMethodsV4; + +#define ValkeyModuleScriptingEngineMethods ValkeyModuleScriptingEngineMethodsV4 /* ------------------------- End of common defines ------------------------ */ @@ -1986,6 +2146,19 @@ VALKEYMODULE_API int (*ValkeyModule_UnregisterScriptingEngine)(ValkeyModuleCtx * VALKEYMODULE_API ValkeyModuleScriptingEngineExecutionState (*ValkeyModule_GetFunctionExecutionState)(ValkeyModuleScriptingEngineServerRuntimeCtx *server_ctx) VALKEYMODULE_ATTR; +VALKEYMODULE_API void (*ValkeyModule_ScriptingEngineDebuggerLog)(ValkeyModuleString *msg, + int truncate) VALKEYMODULE_ATTR; + +VALKEYMODULE_API void (*ValkeyModule_ScriptingEngineDebuggerLogRespReplyStr)(const char *reply) VALKEYMODULE_ATTR; + +VALKEYMODULE_API void (*ValkeyModule_ScriptingEngineDebuggerLogRespReply)(ValkeyModuleCallReply *reply) VALKEYMODULE_ATTR; + +VALKEYMODULE_API void (*ValkeyModule_ScriptingEngineDebuggerFlushLogs)(void) VALKEYMODULE_ATTR; + +VALKEYMODULE_API void (*ValkeyModule_ScriptingEngineDebuggerProcessCommands)(int *client_disconnected, + ValkeyModuleString **err) VALKEYMODULE_ATTR; + + #define ValkeyModule_IsAOFClient(id) ((id) == UINT64_MAX) /* This is included inline inside each Valkey module. */ @@ -2357,6 +2530,11 @@ static int ValkeyModule_Init(ValkeyModuleCtx *ctx, const char *name, int ver, in VALKEYMODULE_GET_API(RegisterScriptingEngine); VALKEYMODULE_GET_API(UnregisterScriptingEngine); VALKEYMODULE_GET_API(GetFunctionExecutionState); + VALKEYMODULE_GET_API(ScriptingEngineDebuggerLog); + VALKEYMODULE_GET_API(ScriptingEngineDebuggerLogRespReplyStr); + VALKEYMODULE_GET_API(ScriptingEngineDebuggerLogRespReply); + VALKEYMODULE_GET_API(ScriptingEngineDebuggerFlushLogs); + VALKEYMODULE_GET_API(ScriptingEngineDebuggerProcessCommands); if (ValkeyModule_IsModuleNameBusy && ValkeyModule_IsModuleNameBusy(name)) return VALKEYMODULE_ERR; ValkeyModule_SetModuleAttribs(ctx, name, ver, apiver); diff --git a/tests/modules/helloscripting.c b/tests/modules/helloscripting.c index f0ee317c54..66792f953a 100644 --- a/tests/modules/helloscripting.c +++ b/tests/modules/helloscripting.c @@ -1,7 +1,8 @@ #include "valkeymodule.h" -#include #include +#include +#include #include #include @@ -96,11 +97,21 @@ typedef struct HelloProgram { uint32_t num_functions; } HelloProgram; + +typedef struct HelloDebugCtx { + int enabled; + int stop_on_next_instr; + int abort; + const uint32_t *stack; + uint32_t sp; +} HelloDebugCtx; + /* * Struct that represents the runtime context of an HELLO program. */ typedef struct HelloLangCtx { HelloProgram *program; + HelloDebugCtx debug; } HelloLangCtx; @@ -241,10 +252,60 @@ static ValkeyModuleScriptingEngineExecutionState executeSleepInst(ValkeyModuleSc return state; } +static void helloDebuggerLogCurrentInstr(uint32_t pc, HelloInst *instr) { + ValkeyModuleString *msg = NULL; + switch (instr->kind) { + case CONSTI: + case ARGS: + msg = ValkeyModule_CreateStringPrintf(NULL, ">>> %3u: %s %u", pc, HelloInstKindStr[instr->kind], instr->param.integer); + break; + case SLEEP: + case RETURN: + msg = ValkeyModule_CreateStringPrintf(NULL, ">>> %3u: %s", pc, HelloInstKindStr[instr->kind]); + break; + case FUNCTION: + case _NUM_INSTRUCTIONS: + ValkeyModule_Assert(0); + } + + ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); +} + +static int helloDebuggerInstrHook(uint32_t pc, HelloInst *instr) { + helloDebuggerLogCurrentInstr(pc, instr); + ValkeyModule_ScriptingEngineDebuggerFlushLogs(); + + int client_disconnected = 0; + ValkeyModuleString *err; + ValkeyModule_ScriptingEngineDebuggerProcessCommands(&client_disconnected, &err); + + if (err) { + ValkeyModule_ScriptingEngineDebuggerLog(err, 0); + goto error; + } else if (client_disconnected) { + ValkeyModuleString *msg = ValkeyModule_CreateStringPrintf(NULL, "ERROR: Client socket disconnected"); + ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); + goto error; + } + + return 1; + +error: + ValkeyModule_ScriptingEngineDebuggerFlushLogs(); + return 0; +} + +typedef enum { + FINISHED, + KILLED, + ABORTED, +} HelloExecutionState; + /* * Executes an HELLO function. */ -static ValkeyModuleScriptingEngineExecutionState executeHelloLangFunction(ValkeyModuleScriptingEngineServerRuntimeCtx *server_ctx, +static HelloExecutionState executeHelloLangFunction(ValkeyModuleScriptingEngineServerRuntimeCtx *server_ctx, + HelloDebugCtx *debug_ctx, HelloFunc *func, ValkeyModuleString **args, int nargs, @@ -253,10 +314,19 @@ static ValkeyModuleScriptingEngineExecutionState executeHelloLangFunction(Valkey uint32_t stack[64]; uint32_t val = 0; int sp = 0; - ValkeyModuleScriptingEngineExecutionState state = VMSE_STATE_EXECUTING; for (uint32_t pc = 0; pc < func->num_instructions; pc++) { HelloInst instr = func->instructions[pc]; + if (debug_ctx->enabled && debug_ctx->stop_on_next_instr) { + debug_ctx->stack = stack; + debug_ctx->sp = sp; + if (!helloDebuggerInstrHook(pc, &instr)) { + return ABORTED; + } + if (debug_ctx->abort) { + return ABORTED; + } + } switch (instr.kind) { case CONSTI: stack[sp++] = instr.param.integer; @@ -271,8 +341,11 @@ static ValkeyModuleScriptingEngineExecutionState executeHelloLangFunction(Valkey break; } case SLEEP: { + ValkeyModule_Assert(sp > 0); val = stack[--sp]; - state = executeSleepInst(server_ctx, val); + if (executeSleepInst(server_ctx, val) == VMSE_STATE_KILLED) { + return KILLED; + } break; } case RETURN: { @@ -280,7 +353,7 @@ static ValkeyModuleScriptingEngineExecutionState executeHelloLangFunction(Valkey val = stack[--sp]; ValkeyModule_Assert(sp == 0); *result = val; - return state; + return FINISHED; } case FUNCTION: case _NUM_INSTRUCTIONS: @@ -289,7 +362,7 @@ static ValkeyModuleScriptingEngineExecutionState executeHelloLangFunction(Valkey } ValkeyModule_Assert(0); - return state; + return ABORTED; } static ValkeyModuleScriptingEngineMemoryInfo engineGetMemoryInfo(ValkeyModuleCtx *module_ctx, @@ -412,18 +485,24 @@ callHelloLangFunction(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineSubsystemType type, ValkeyModuleString **keys, size_t nkeys, ValkeyModuleString **args, size_t nargs) { - VALKEYMODULE_NOT_USED(engine_ctx); VALKEYMODULE_NOT_USED(keys); VALKEYMODULE_NOT_USED(nkeys); ValkeyModule_Assert(type == VMSE_EVAL || type == VMSE_FUNCTION); + HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; HelloFunc *func = (HelloFunc *)compiled_function->function; uint32_t result; - ValkeyModuleScriptingEngineExecutionState state = executeHelloLangFunction(server_ctx, func, args, nargs, &result); - ValkeyModule_Assert(state == VMSE_STATE_KILLED || state == VMSE_STATE_EXECUTING); - - if (state == VMSE_STATE_KILLED) { + HelloExecutionState state = executeHelloLangFunction( + server_ctx, + &ctx->debug, + func, + args, + nargs, + &result); + ValkeyModule_Assert(state == KILLED || state == FINISHED || state == ABORTED); + + if (state == KILLED) { if (type == VMSE_EVAL) { ValkeyModule_ReplyWithError(module_ctx, "ERR Script killed by user with SCRIPT KILL."); return; @@ -433,8 +512,12 @@ callHelloLangFunction(ValkeyModuleCtx *module_ctx, return; } } - - ValkeyModule_ReplyWithLongLong(module_ctx, result); + else if (state == ABORTED) { + ValkeyModule_ReplyWithError(module_ctx, "ERR execution aborted during debugging session"); + } + else { + ValkeyModule_ReplyWithLongLong(module_ctx, result); + } } static ValkeyModuleScriptingEngineCallableLazyEnvReset *helloResetEvalEnv(ValkeyModuleCtx *module_ctx, @@ -457,6 +540,144 @@ static ValkeyModuleScriptingEngineCallableLazyEnvReset *helloResetEnv(ValkeyModu return NULL; } +static int helloDebuggerStepCommand(ValkeyModuleString **argv, size_t argc, void *context) { + VALKEYMODULE_NOT_USED(argv); + VALKEYMODULE_NOT_USED(argc); + HelloDebugCtx *ctx = context; + ctx->stop_on_next_instr = 1; + return 0; +} + +static int helloDebuggerContinueCommand(ValkeyModuleString **argv, size_t argc, void *context) { + VALKEYMODULE_NOT_USED(argv); + VALKEYMODULE_NOT_USED(argc); + HelloDebugCtx *ctx = context; + ctx->stop_on_next_instr = 0; + return 0; +} + +static int helloDebuggerStackCommand(ValkeyModuleString **argv, size_t argc, void *context) { + HelloDebugCtx *ctx = context; + ValkeyModuleString *msg = NULL; + + if (argc > 1) { + long long n; + ValkeyModule_StringToLongLong(argv[1], &n); + uint32_t index = (uint32_t)n; + + if (index >= ctx->sp) { + ValkeyModuleString *msg = ValkeyModule_CreateStringPrintf(NULL, "Index out of range. Current stack size: %u", ctx->sp); + ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); + } + else { + uint32_t value = ctx->stack[ctx->sp - index - 1]; + msg = ValkeyModule_CreateStringPrintf(NULL, "[%u] %u", index, value); + ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); + } + } + else { + msg = ValkeyModule_CreateStringPrintf(NULL, "Stack contents:"); + if (ctx->sp == 0) { + msg = ValkeyModule_CreateStringPrintf(NULL, "[empty]"); + ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); + } + else { + ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); + for (uint32_t i=0; i < ctx->sp; i++) { + uint32_t value = ctx->stack[ctx->sp - i - 1]; + if (i == 0) { + msg = ValkeyModule_CreateStringPrintf(NULL, "top -> [%u] %u", i, value); + } else { + msg = ValkeyModule_CreateStringPrintf(NULL, " [%u] %u", i, value); + } + ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); + } + } + } + + ValkeyModule_ScriptingEngineDebuggerFlushLogs(); + return 1; +} + +static int helloDebuggerAbortCommand(ValkeyModuleString **argv, size_t argc, void *context) { + VALKEYMODULE_NOT_USED(argv); + VALKEYMODULE_NOT_USED(argc); + HelloDebugCtx *ctx = context; + ctx->abort = 1; + return 0; +} + +#define COMMAND_COUNT (4) + +static ValkeyModuleScriptingEngineDebuggerCommandParam stack_params[1] = { + { + .name = "index", + .optional = 1 + } +}; + +static ValkeyModuleScriptingEngineDebuggerCommand helloDebuggerCommands[COMMAND_COUNT] = { + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("step", 1, NULL, 0, "Execute current instruction.", 0 ,helloDebuggerStepCommand), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("continue", 1, NULL, 0, "Continue normal execution.", 0, helloDebuggerContinueCommand), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("stack", 2, stack_params, 1, "Print stack contents. If index is specified, print only the value at index. Indexes start at 0 (top = 0).", 0, helloDebuggerStackCommand), + VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("abort", 1, NULL, 0, "Abort execution.", 0, helloDebuggerAbortCommand), +}; + +static ValkeyModuleScriptingEngineDebuggerEnableRet helloDebuggerEnable(ValkeyModuleCtx *module_ctx, + ValkeyModuleScriptingEngineCtx *engine_ctx, + ValkeyModuleScriptingEngineSubsystemType type, + const ValkeyModuleScriptingEngineDebuggerCommand **commands, + size_t *commands_len) { + VALKEYMODULE_NOT_USED(module_ctx); + VALKEYMODULE_NOT_USED(type); + + HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; + ctx->debug = (HelloDebugCtx) {.enabled = 1}; + *commands = helloDebuggerCommands; + *commands_len = COMMAND_COUNT; + + for (int i=0; i < COMMAND_COUNT; i++) { + helloDebuggerCommands[i].context = &ctx->debug; + } + return VMSE_DEBUG_ENABLED; +} + +static void helloDebuggerDisable(ValkeyModuleCtx *module_ctx, + ValkeyModuleScriptingEngineCtx *engine_ctx, + ValkeyModuleScriptingEngineSubsystemType type) { + VALKEYMODULE_NOT_USED(module_ctx); + VALKEYMODULE_NOT_USED(type); + + HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; + ctx->debug = (HelloDebugCtx){0}; + +} + +static void helloDebuggerStart(ValkeyModuleCtx *module_ctx, + ValkeyModuleScriptingEngineCtx *engine_ctx, + ValkeyModuleScriptingEngineSubsystemType type, + ValkeyModuleString *code) { + VALKEYMODULE_NOT_USED(module_ctx); + VALKEYMODULE_NOT_USED(type); + VALKEYMODULE_NOT_USED(code); + + HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; + ctx->debug.stop_on_next_instr = 1; +} + +static void helloDebuggerEnd(ValkeyModuleCtx *module_ctx, + ValkeyModuleScriptingEngineCtx *engine_ctx, + ValkeyModuleScriptingEngineSubsystemType type) { + VALKEYMODULE_NOT_USED(module_ctx); + VALKEYMODULE_NOT_USED(type); + + HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; + ctx->debug.stop_on_next_instr = 0; + ctx->debug.abort = 0; + ctx->debug.stack = NULL; + ctx->debug.sp = 0; +} + int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { @@ -482,11 +703,14 @@ int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, hello_ctx = ValkeyModule_Alloc(sizeof(HelloLangCtx)); hello_ctx->program = NULL; + hello_ctx->debug.enabled = 0; + - ValkeyModuleScriptingEngineMethods methods; + ValkeyModuleScriptingEngineMethodsV3 methodsV3; + ValkeyModuleScriptingEngineMethodsV4 methodsV4; if (abi_version <= 2) { - methods = (ValkeyModuleScriptingEngineMethods) { + methodsV3 = (ValkeyModuleScriptingEngineMethodsV3) { .version = abi_version, .compile_code = createHelloLangEngine, .free_function = engineFreeFunction, @@ -495,8 +719,18 @@ int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, .reset_eval_env_v2 = helloResetEvalEnv, .get_memory_info = engineGetMemoryInfo, }; + } else if (abi_version <= 3) { + methodsV3 = (ValkeyModuleScriptingEngineMethodsV3) { + .version = abi_version, + .compile_code = createHelloLangEngine, + .free_function = engineFreeFunction, + .call_function = callHelloLangFunction, + .get_function_memory_overhead = engineFunctionMemoryOverhead, + .reset_env = helloResetEnv, + .get_memory_info = engineGetMemoryInfo, + }; } else { - methods = (ValkeyModuleScriptingEngineMethods) { + methodsV4 = (ValkeyModuleScriptingEngineMethodsV4) { .version = abi_version, .compile_code = createHelloLangEngine, .free_function = engineFreeFunction, @@ -504,13 +738,22 @@ int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, .get_function_memory_overhead = engineFunctionMemoryOverhead, .reset_env = helloResetEnv, .get_memory_info = engineGetMemoryInfo, + .debugger_enable = helloDebuggerEnable, + .debugger_disable = helloDebuggerDisable, + .debugger_start = helloDebuggerStart, + .debugger_end = helloDebuggerEnd, }; } + ValkeyModuleScriptingEngineMethods *methods = abi_version <= 3 ? + (ValkeyModuleScriptingEngineMethods *)&methodsV3 : + (ValkeyModuleScriptingEngineMethods *)&methodsV4; + ValkeyModule_RegisterScriptingEngine(ctx, "HELLO", hello_ctx, - &methods); + methods); + return VALKEYMODULE_OK; } diff --git a/tests/unit/moduleapi/scriptingengine.tcl b/tests/unit/moduleapi/scriptingengine.tcl index c57b54e51a..65eb2236bf 100644 --- a/tests/unit/moduleapi/scriptingengine.tcl +++ b/tests/unit/moduleapi/scriptingengine.tcl @@ -176,12 +176,36 @@ start_server {tags {"modules"}} { assert_equal $result 0 } + test {Test HELLO debugger} { + r script debug sync hello + set ret [r eval "#!hello\nFUNCTION foo\nARGS 0\nRETURN" 0 167] + assert_equal {{>>> 0: ARGS 0}} $ret + set cmd "*1\r\n\$4\r\nstep\r\n" + r write $cmd + r flush + set ret [r read] + assert_equal {{>>> 1: RETURN}} $ret + set cmd "*1\r\n\$5\r\nstack\r\n" + r write $cmd + r flush + set ret [r read] + assert_equal {{Stack contents:} {top -> [0] 167}} $ret + set cmd "*1\r\n\$1\r\nc\r\n" + r write $cmd + r flush + set ret [r read] + assert_equal {} $ret + r script debug off + reconnect + assert_equal [r ping] {PONG} + } + test {Unload scripting engine module} { set result [r module unload helloengine] assert_equal $result "OK" } - test {Load scripting engine in older version} { + test {Load scripting engine in version before function env reset} { r module load $testmodule 2 r function load $HELLO_PROGRAM set result [r fcall foo 0 123] @@ -189,5 +213,17 @@ start_server {tags {"modules"}} { set result [r function flush async] assert_equal $result {OK} assert_error {ERR Function not found} {r fcall foo 0 123} + set result [r module unload helloengine] + assert_equal $result "OK" + } + + test {Load scripting engine in version before debugger support} { + r module load $testmodule 3 + r function load $HELLO_PROGRAM + set result [r fcall foo 0 123] + assert_equal $result 123 + assert_error {ERR The scripting engine 'HELLO' does not support interactive script debugging} {r script debug sync hello} + set result [r module unload helloengine] + assert_equal $result "OK" } } diff --git a/tests/unit/scripting.tcl b/tests/unit/scripting.tcl index b200443356..fde55e8b7e 100644 --- a/tests/unit/scripting.tcl +++ b/tests/unit/scripting.tcl @@ -1664,13 +1664,13 @@ start_server {tags {"scripting needs:debug external:skip"}} { r script debug sync r eval {return 'hello'} 0 catch {r 'hello\0world'} e - assert_match {*Unknown Lua debugger command*} $e + assert_match {*Unknown debugger command*} $e catch {r 'hello\0'} e - assert_match {*Unknown Lua debugger command*} $e + assert_match {*Unknown debugger command*} $e catch {r '\0hello'} e - assert_match {*Unknown Lua debugger command*} $e + assert_match {*Unknown debugger command*} $e catch {r '\0hello\0'} e - assert_match {*Unknown Lua debugger command*} $e + assert_match {*Unknown debugger command*} $e } test {Test scripting debug lua stack overflow} {