summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--redis.conf14
-rwxr-xr-xruntest-moduleapi1
-rw-r--r--src/acl.c288
-rw-r--r--src/aof.c2
-rw-r--r--src/config.c71
-rw-r--r--src/db.c15
-rw-r--r--src/module.c106
-rw-r--r--src/networking.c18
-rw-r--r--src/pubsub.c5
-rw-r--r--src/redismodule.h2
-rw-r--r--src/replication.c5
-rw-r--r--src/scripting.c2
-rw-r--r--src/sentinel.c44
-rw-r--r--src/server.c1029
-rw-r--r--src/server.h61
-rw-r--r--src/t_string.c15
-rw-r--r--tests/modules/Makefile3
-rw-r--r--tests/modules/subcommands.c55
-rw-r--r--tests/unit/acl.tcl107
-rw-r--r--tests/unit/info.tcl4
-rw-r--r--tests/unit/introspection-2.tcl22
-rw-r--r--tests/unit/latency-monitor.tcl2
-rw-r--r--tests/unit/moduleapi/keyspecs.tcl12
-rw-r--r--tests/unit/moduleapi/subcommands.tcl19
-rw-r--r--tests/unit/type/stream.tcl4
25 files changed, 1497 insertions, 409 deletions
diff --git a/redis.conf b/redis.conf
index 2ea8190f5..108b9ff63 100644
--- a/redis.conf
+++ b/redis.conf
@@ -818,8 +818,10 @@ replica-priority 100
# will still work.
# skip-sanitize-payload RESTORE dump-payload sanitation is skipped.
# sanitize-payload RESTORE dump-payload is sanitized (default).
-# +<command> Allow the execution of that command
-# -<command> Disallow the execution of that command
+# +<command> Allow the execution of that command.
+# May be used with `|` for allowing subcommands (e.g "+config|get")
+# -<command> Disallow the execution of that command.
+# May be used with `|` for blocking subcommands (e.g "-config|set")
# +@<category> Allow the execution of all the commands in such category
# with valid categories are like @admin, @set, @sortedset, ...
# and so forth, see the full list in the server.c file where
@@ -827,10 +829,10 @@ replica-priority 100
# The special category @all means all the commands, but currently
# present in the server, and that will be loaded in the future
# via modules.
-# +<command>|subcommand Allow a specific subcommand of an otherwise
-# disabled command. Note that this form is not
-# allowed as negative like -DEBUG|SEGFAULT, but
-# only additive starting with "+".
+# +<command>|first-arg Allow a specific first argument of an otherwise
+# disabled command. Note that this form is not
+# allowed as negative like -SELECT|1, but
+# only additive starting with "+".
# allcommands Alias for +@all. Note that it implies the ability to execute
# all the future commands loaded via the modules system.
# nocommands Alias for -@all.
diff --git a/runtest-moduleapi b/runtest-moduleapi
index f6b2a1b72..12bd7f274 100755
--- a/runtest-moduleapi
+++ b/runtest-moduleapi
@@ -42,4 +42,5 @@ $TCLSH tests/test_helper.tcl \
--single unit/moduleapi/datatype2 \
--single unit/moduleapi/cluster \
--single unit/moduleapi/aclcheck \
+--single unit/moduleapi/subcommands \
"${@}"
diff --git a/src/acl.c b/src/acl.c
index 6947fc204..199e050dd 100644
--- a/src/acl.c
+++ b/src/acl.c
@@ -101,9 +101,9 @@ struct ACLUserFlag {
{NULL,0} /* Terminator. */
};
-void ACLResetSubcommandsForCommand(user *u, unsigned long id);
-void ACLResetSubcommands(user *u);
-void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub);
+void ACLResetFirstArgsForCommand(user *u, unsigned long id);
+void ACLResetFirstArgs(user *u);
+void ACLAddAllowedFirstArg(user *u, unsigned long id, const char *sub);
void ACLFreeLogEntry(void *le);
/* The length of the string representation of a hashed password. */
@@ -254,7 +254,7 @@ user *ACLCreateUser(const char *name, size_t namelen) {
user *u = zmalloc(sizeof(*u));
u->name = sdsnewlen(name,namelen);
u->flags = USER_FLAG_DISABLED | server.acl_pubsub_default;
- u->allowed_subcommands = NULL;
+ u->allowed_firstargs = NULL;
u->passwords = listCreate();
u->patterns = listCreate();
u->channels = listCreate();
@@ -296,7 +296,7 @@ void ACLFreeUser(user *u) {
listRelease(u->passwords);
listRelease(u->patterns);
listRelease(u->channels);
- ACLResetSubcommands(u);
+ ACLResetFirstArgs(u);
zfree(u);
}
@@ -343,15 +343,15 @@ void ACLCopyUser(user *dst, user *src) {
memcpy(dst->allowed_commands,src->allowed_commands,
sizeof(dst->allowed_commands));
dst->flags = src->flags;
- ACLResetSubcommands(dst);
- /* Copy the allowed subcommands array of array of SDS strings. */
- if (src->allowed_subcommands) {
+ ACLResetFirstArgs(dst);
+ /* Copy the allowed first-args array of array of SDS strings. */
+ if (src->allowed_firstargs) {
for (int j = 0; j < USER_COMMAND_BITS_COUNT; j++) {
- if (src->allowed_subcommands[j]) {
- for (int i = 0; src->allowed_subcommands[j][i]; i++)
+ if (src->allowed_firstargs[j]) {
+ for (int i = 0; src->allowed_firstargs[j][i]; i++)
{
- ACLAddAllowedSubcommand(dst, j,
- src->allowed_subcommands[j][i]);
+ ACLAddAllowedFirstArg(dst, j,
+ src->allowed_firstargs[j][i]);
}
}
}
@@ -413,6 +413,38 @@ void ACLSetUserCommandBit(user *u, unsigned long id, int value) {
}
}
+/* This function is used to allow/block a specific command.
+ * Allowing/blocking a container command also applies for its subcommands */
+void ACLChangeCommandPerm(user *u, struct redisCommand *cmd, int allow) {
+ unsigned long id = cmd->id;
+ ACLSetUserCommandBit(u,id,allow);
+ ACLResetFirstArgsForCommand(u,id);
+ if (cmd->subcommands_dict) {
+ dictEntry *de;
+ dictIterator *di = dictGetSafeIterator(cmd->subcommands_dict);
+ while((de = dictNext(di)) != NULL) {
+ struct redisCommand *sub = (struct redisCommand *)dictGetVal(de);
+ ACLSetUserCommandBit(u,sub->id,allow);
+ }
+ }
+}
+
+void ACLSetUserCommandBitsForCategoryLogic(dict *commands, user *u, uint64_t cflag, int value) {
+ dictIterator *di = dictGetIterator(commands);
+ dictEntry *de;
+ while ((de = dictNext(di)) != NULL) {
+ struct redisCommand *cmd = dictGetVal(de);
+ if (cmd->flags & CMD_MODULE) continue; /* Ignore modules commands. */
+ if (cmd->flags & cflag) {
+ ACLChangeCommandPerm(u,cmd,value);
+ }
+ if (cmd->subcommands_dict) {
+ ACLSetUserCommandBitsForCategoryLogic(cmd->subcommands_dict, u, cflag, value);
+ }
+ }
+ dictReleaseIterator(di);
+}
+
/* This is like ACLSetUserCommandBit(), but instead of setting the specified
* ID, it will check all the commands in the category specified as argument,
* and will set all the bits corresponding to such commands to the specified
@@ -422,18 +454,26 @@ void ACLSetUserCommandBit(user *u, unsigned long id, int value) {
int ACLSetUserCommandBitsForCategory(user *u, const char *category, int value) {
uint64_t cflag = ACLGetCommandCategoryFlagByName(category);
if (!cflag) return C_ERR;
- dictIterator *di = dictGetIterator(server.orig_commands);
+ ACLSetUserCommandBitsForCategoryLogic(server.orig_commands, u, cflag, value);
+ return C_OK;
+}
+
+void ACLCountCategoryBitsForCommands(dict *commands, user *u, unsigned long *on, unsigned long *off, uint64_t cflag) {
+ dictIterator *di = dictGetIterator(commands);
dictEntry *de;
while ((de = dictNext(di)) != NULL) {
struct redisCommand *cmd = dictGetVal(de);
- if (cmd->flags & CMD_MODULE) continue; /* Ignore modules commands. */
if (cmd->flags & cflag) {
- ACLSetUserCommandBit(u,cmd->id,value);
- ACLResetSubcommandsForCommand(u,cmd->id);
+ if (ACLGetUserCommandBit(u,cmd->id))
+ (*on)++;
+ else
+ (*off)++;
+ }
+ if (cmd->subcommands_dict) {
+ ACLCountCategoryBitsForCommands(cmd->subcommands_dict, u, on, off, cflag);
}
}
dictReleaseIterator(di);
- return C_OK;
}
/* Return the number of commands allowed (on) and denied (off) for the user 'u'
@@ -447,19 +487,46 @@ int ACLCountCategoryBitsForUser(user *u, unsigned long *on, unsigned long *off,
if (!cflag) return C_ERR;
*on = *off = 0;
- dictIterator *di = dictGetIterator(server.orig_commands);
+ ACLCountCategoryBitsForCommands(server.orig_commands, u, on, off, cflag);
+ return C_OK;
+}
+
+sds ACLDescribeUserCommandRulesSingleCommands(user *u, user *fakeuser, sds rules, dict *commands) {
+ dictIterator *di = dictGetIterator(commands);
dictEntry *de;
while ((de = dictNext(di)) != NULL) {
struct redisCommand *cmd = dictGetVal(de);
- if (cmd->flags & cflag) {
- if (ACLGetUserCommandBit(u,cmd->id))
- (*on)++;
- else
- (*off)++;
+ int userbit = ACLGetUserCommandBit(u,cmd->id);
+ int fakebit = ACLGetUserCommandBit(fakeuser,cmd->id);
+ if (userbit != fakebit) {
+ rules = sdscatlen(rules, userbit ? "+" : "-", 1);
+ sds fullname = getFullCommandName(cmd);
+ rules = sdscat(rules,fullname);
+ sdsfree(fullname);
+ rules = sdscatlen(rules," ",1);
+ ACLChangeCommandPerm(fakeuser,cmd,userbit);
+ }
+
+ if (cmd->subcommands_dict)
+ rules = ACLDescribeUserCommandRulesSingleCommands(u,fakeuser,rules,cmd->subcommands_dict);
+
+ /* Emit the first-args if there are any. */
+ if (userbit == 0 && u->allowed_firstargs &&
+ u->allowed_firstargs[cmd->id])
+ {
+ for (int j = 0; u->allowed_firstargs[cmd->id][j]; j++) {
+ rules = sdscatlen(rules,"+",1);
+ sds fullname = getFullCommandName(cmd);
+ rules = sdscat(rules,fullname);
+ sdsfree(fullname);
+ rules = sdscatlen(rules,"|",1);
+ rules = sdscatsds(rules,u->allowed_firstargs[cmd->id][j]);
+ rules = sdscatlen(rules," ",1);
+ }
}
}
dictReleaseIterator(di);
- return C_OK;
+ return rules;
}
/* This function returns an SDS string representing the specified user ACL
@@ -563,33 +630,7 @@ sds ACLDescribeUserCommandRules(user *u) {
}
/* Fix the final ACLs with single commands differences. */
- dictIterator *di = dictGetIterator(server.orig_commands);
- dictEntry *de;
- while ((de = dictNext(di)) != NULL) {
- struct redisCommand *cmd = dictGetVal(de);
- int userbit = ACLGetUserCommandBit(u,cmd->id);
- int fakebit = ACLGetUserCommandBit(fakeuser,cmd->id);
- if (userbit != fakebit) {
- rules = sdscatlen(rules, userbit ? "+" : "-", 1);
- rules = sdscat(rules,cmd->name);
- rules = sdscatlen(rules," ",1);
- ACLSetUserCommandBit(fakeuser,cmd->id,userbit);
- }
-
- /* Emit the subcommands if there are any. */
- if (userbit == 0 && u->allowed_subcommands &&
- u->allowed_subcommands[cmd->id])
- {
- for (int j = 0; u->allowed_subcommands[cmd->id][j]; j++) {
- rules = sdscatlen(rules,"+",1);
- rules = sdscat(rules,cmd->name);
- rules = sdscatlen(rules,"|",1);
- rules = sdscatsds(rules,u->allowed_subcommands[cmd->id][j]);
- rules = sdscatlen(rules," ",1);
- }
- }
- }
- dictReleaseIterator(di);
+ rules = ACLDescribeUserCommandRulesSingleCommands(u,fakeuser,rules,server.orig_commands);
/* Trim the final useless space. */
sdsrange(rules,0,-2);
@@ -683,67 +724,66 @@ sds ACLDescribeUser(user *u) {
struct redisCommand *ACLLookupCommand(const char *name) {
struct redisCommand *cmd;
sds sdsname = sdsnew(name);
- cmd = dictFetchValue(server.orig_commands, sdsname);
+ cmd = lookupCommandBySdsLogic(server.orig_commands,sdsname);
sdsfree(sdsname);
return cmd;
}
-/* Flush the array of allowed subcommands for the specified user
+/* Flush the array of allowed first-args for the specified user
* and command ID. */
-void ACLResetSubcommandsForCommand(user *u, unsigned long id) {
- if (u->allowed_subcommands && u->allowed_subcommands[id]) {
- for (int i = 0; u->allowed_subcommands[id][i]; i++)
- sdsfree(u->allowed_subcommands[id][i]);
- zfree(u->allowed_subcommands[id]);
- u->allowed_subcommands[id] = NULL;
+void ACLResetFirstArgsForCommand(user *u, unsigned long id) {
+ if (u->allowed_firstargs && u->allowed_firstargs[id]) {
+ for (int i = 0; u->allowed_firstargs[id][i]; i++)
+ sdsfree(u->allowed_firstargs[id][i]);
+ zfree(u->allowed_firstargs[id]);
+ u->allowed_firstargs[id] = NULL;
}
}
-/* Flush the entire table of subcommands. This is useful on +@all, -@all
+/* Flush the entire table of first-args. This is useful on +@all, -@all
* or similar to return back to the minimal memory usage (and checks to do)
* for the user. */
-void ACLResetSubcommands(user *u) {
- if (u->allowed_subcommands == NULL) return;
+void ACLResetFirstArgs(user *u) {
+ if (u->allowed_firstargs == NULL) return;
for (int j = 0; j < USER_COMMAND_BITS_COUNT; j++) {
- if (u->allowed_subcommands[j]) {
- for (int i = 0; u->allowed_subcommands[j][i]; i++)
- sdsfree(u->allowed_subcommands[j][i]);
- zfree(u->allowed_subcommands[j]);
+ if (u->allowed_firstargs[j]) {
+ for (int i = 0; u->allowed_firstargs[j][i]; i++)
+ sdsfree(u->allowed_firstargs[j][i]);
+ zfree(u->allowed_firstargs[j]);
}
}
- zfree(u->allowed_subcommands);
- u->allowed_subcommands = NULL;
+ zfree(u->allowed_firstargs);
+ u->allowed_firstargs = NULL;
}
-/* Add a subcommand to the list of subcommands for the user 'u' and
+/* Add a first-arh to the list of subcommands for the user 'u' and
* the command id specified. */
-void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) {
- /* If this is the first subcommand to be configured for
- * this user, we have to allocate the subcommands array. */
- if (u->allowed_subcommands == NULL) {
- u->allowed_subcommands = zcalloc(USER_COMMAND_BITS_COUNT *
- sizeof(sds*));
+void ACLAddAllowedFirstArg(user *u, unsigned long id, const char *sub) {
+ /* If this is the first first-arg to be configured for
+ * this user, we have to allocate the first-args array. */
+ if (u->allowed_firstargs == NULL) {
+ u->allowed_firstargs = zcalloc(USER_COMMAND_BITS_COUNT * sizeof(sds*));
}
/* We also need to enlarge the allocation pointing to the
* null terminated SDS array, to make space for this one.
* To start check the current size, and while we are here
- * make sure the subcommand is not already specified inside. */
+ * make sure the first-arg is not already specified inside. */
long items = 0;
- if (u->allowed_subcommands[id]) {
- while(u->allowed_subcommands[id][items]) {
+ if (u->allowed_firstargs[id]) {
+ while(u->allowed_firstargs[id][items]) {
/* If it's already here do not add it again. */
- if (!strcasecmp(u->allowed_subcommands[id][items],sub)) return;
+ if (!strcasecmp(u->allowed_firstargs[id][items],sub))
+ return;
items++;
}
}
/* Now we can make space for the new item (and the null term). */
items += 2;
- u->allowed_subcommands[id] = zrealloc(u->allowed_subcommands[id],
- sizeof(sds)*items);
- u->allowed_subcommands[id][items-2] = sdsnew(sub);
- u->allowed_subcommands[id][items-1] = NULL;
+ u->allowed_firstargs[id] = zrealloc(u->allowed_firstargs[id], sizeof(sds)*items);
+ u->allowed_firstargs[id][items-2] = sdsnew(sub);
+ u->allowed_firstargs[id][items-1] = NULL;
}
/* Set user properties according to the string "op". The following
@@ -753,8 +793,10 @@ void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) {
* off Disable the user: it's no longer possible to authenticate
* with this user, however the already authenticated connections
* will still work.
- * +<command> Allow the execution of that command
- * -<command> Disallow the execution of that command
+ * +<command> Allow the execution of that command.
+ * May be used with `|` for allowing subcommands (e.g "+config|get")
+ * -<command> Disallow the execution of that command.
+ * May be used with `|` for blocking subcommands (e.g "-config|set")
* +@<category> Allow the execution of all the commands in such category
* with valid categories are like @admin, @set, @sortedset, ...
* and so forth, see the full list in the server.c file where
@@ -762,10 +804,10 @@ void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) {
* The special category @all means all the commands, but currently
* present in the server, and that will be loaded in the future
* via modules.
- * +<command>|subcommand Allow a specific subcommand of an otherwise
- * disabled command. Note that this form is not
- * allowed as negative like -DEBUG|SEGFAULT, but
- * only additive starting with "+".
+ * +<command>|first-arg Allow a specific first argument of an otherwise
+ * disabled command. Note that this form is not
+ * allowed as negative like -SELECT|1, but
+ * only additive starting with "+".
* allcommands Alias for +@all. Note that it implies the ability to execute
* all the future commands loaded via the modules system.
* nocommands Alias for -@all.
@@ -866,13 +908,13 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) {
{
memset(u->allowed_commands,255,sizeof(u->allowed_commands));
u->flags |= USER_FLAG_ALLCOMMANDS;
- ACLResetSubcommands(u);
+ ACLResetFirstArgs(u);
} else if (!strcasecmp(op,"nocommands") ||
!strcasecmp(op,"-@all"))
{
memset(u->allowed_commands,0,sizeof(u->allowed_commands));
u->flags &= ~USER_FLAG_ALLCOMMANDS;
- ACLResetSubcommands(u);
+ ACLResetFirstArgs(u);
} else if (!strcasecmp(op,"nopass")) {
u->flags |= USER_FLAG_NOPASS;
listEmpty(u->passwords);
@@ -952,24 +994,25 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) {
sdsfree(newpat);
u->flags &= ~USER_FLAG_ALLCHANNELS;
} else if (op[0] == '+' && op[1] != '@') {
- if (strchr(op,'|') == NULL) {
- if (ACLLookupCommand(op+1) == NULL) {
+ if (strrchr(op,'|') == NULL) {
+ struct redisCommand *cmd = ACLLookupCommand(op+1);
+ if (cmd == NULL) {
errno = ENOENT;
return C_ERR;
}
- unsigned long id = ACLGetCommandID(op+1);
- ACLSetUserCommandBit(u,id,1);
- ACLResetSubcommandsForCommand(u,id);
+ ACLChangeCommandPerm(u,cmd,1);
} else {
/* Split the command and subcommand parts. */
char *copy = zstrdup(op+1);
- char *sub = strchr(copy,'|');
+ char *sub = strrchr(copy,'|');
sub[0] = '\0';
sub++;
+ struct redisCommand *cmd = ACLLookupCommand(copy);
+
/* Check if the command exists. We can't check the
* subcommand to see if it is valid. */
- if (ACLLookupCommand(copy) == NULL) {
+ if (cmd == NULL) {
zfree(copy);
errno = ENOENT;
return C_ERR;
@@ -983,22 +1026,38 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) {
return C_ERR;
}
- unsigned long id = ACLGetCommandID(copy);
- /* Add the subcommand to the list of valid ones, if the command is not set. */
- if (ACLGetUserCommandBit(u,id) == 0) {
- ACLAddAllowedSubcommand(u,id,sub);
+ if (cmd->subcommands_dict) {
+ /* If user is trying to allow a valid subcommand we can just add its unique ID */
+ struct redisCommand *cmd = ACLLookupCommand(op+1);
+ if (cmd == NULL) {
+ zfree(copy);
+ errno = ENOENT;
+ return C_ERR;
+ }
+ ACLChangeCommandPerm(u,cmd,1);
+ } else {
+ /* If user is trying to use the ACL mech to block SELECT except SELECT 0 or
+ * block DEBUG except DEBUG OBJECT (DEBUG subcommands are not considered
+ * subcommands for now) we use the allowed_firstargs mechanism. */
+ struct redisCommand *cmd = ACLLookupCommand(copy);
+ if (cmd == NULL) {
+ zfree(copy);
+ errno = ENOENT;
+ return C_ERR;
+ }
+ /* Add the first-arg to the list of valid ones. */
+ ACLAddAllowedFirstArg(u,cmd->id,sub);
}
zfree(copy);
}
} else if (op[0] == '-' && op[1] != '@') {
- if (ACLLookupCommand(op+1) == NULL) {
+ struct redisCommand *cmd = ACLLookupCommand(op+1);
+ if (cmd == NULL) {
errno = ENOENT;
return C_ERR;
}
- unsigned long id = ACLGetCommandID(op+1);
- ACLSetUserCommandBit(u,id,0);
- ACLResetSubcommandsForCommand(u,id);
+ ACLChangeCommandPerm(u,cmd,0);
} else if ((op[0] == '+' || op[0] == '-') && op[1] == '@') {
int bitval = op[0] == '+' ? 1 : 0;
if (ACLSetUserCommandBitsForCategory(u,op+2,bitval) == C_ERR) {
@@ -1138,7 +1197,6 @@ int ACLAuthenticateUser(client *c, robj *username, robj *password) {
* command name, so that a command retains the same ID in case of modules that
* are unloaded and later reloaded. */
unsigned long ACLGetCommandID(const char *cmdname) {
-
sds lowername = sdsnew(cmdname);
sdstolower(lowername);
if (commandId == NULL) commandId = raxNew();
@@ -1225,23 +1283,23 @@ int ACLCheckCommandPerm(const user *u, struct redisCommand *cmd, robj **argv, in
if (!(u->flags & USER_FLAG_ALLCOMMANDS) && !(cmd->flags & CMD_NO_AUTH))
{
/* If the bit is not set we have to check further, in case the
- * command is allowed just with that specific subcommand. */
+ * command is allowed just with that specific first argument. */
if (ACLGetUserCommandBit(u,id) == 0) {
- /* Check if the subcommand matches. */
+ /* Check if the first argument matches. */
if (argc < 2 ||
- u->allowed_subcommands == NULL ||
- u->allowed_subcommands[id] == NULL)
+ u->allowed_firstargs == NULL ||
+ u->allowed_firstargs[id] == NULL)
{
return ACL_DENIED_CMD;
}
long subid = 0;
while (1) {
- if (u->allowed_subcommands[id][subid] == NULL)
+ if (u->allowed_firstargs[id][subid] == NULL)
return ACL_DENIED_CMD;
- if (!strcasecmp(argv[1]->ptr,
- u->allowed_subcommands[id][subid]))
- break; /* Subcommand match found. Stop here. */
+ int idx = cmd->parent ? 2 : 1;
+ if (!strcasecmp(argv[idx]->ptr,u->allowed_firstargs[id][subid]))
+ break; /* First argument match found. Stop here. */
subid++;
}
}
diff --git a/src/aof.c b/src/aof.c
index f9df2b1c8..0090c9472 100644
--- a/src/aof.c
+++ b/src/aof.c
@@ -793,7 +793,7 @@ int loadAppendOnlyFile(char *filename) {
}
/* Command lookup */
- cmd = lookupCommand(argv[0]->ptr);
+ cmd = lookupCommand(argv,argc);
if (!cmd) {
serverLog(LL_WARNING,
"Unknown command '%s' reading the append only file",
diff --git a/src/config.c b/src/config.c
index 29c2a2195..c044ca516 100644
--- a/src/config.c
+++ b/src/config.c
@@ -573,7 +573,7 @@ void loadServerConfigFromString(char *config) {
} else if (!strcasecmp(argv[0],"list-max-ziplist-value") && argc == 2) {
/* DEAD OPTION */
} else if (!strcasecmp(argv[0],"rename-command") && argc == 3) {
- struct redisCommand *cmd = lookupCommand(argv[1]);
+ struct redisCommand *cmd = lookupCommandBySds(argv[1]);
int retval;
if (!cmd) {
@@ -2735,18 +2735,11 @@ standardConfig configs[] = {
};
/*-----------------------------------------------------------------------------
- * CONFIG command entry point
+ * CONFIG HELP
*----------------------------------------------------------------------------*/
-void configCommand(client *c) {
- /* Only allow CONFIG GET while loading. */
- if (server.loading && strcasecmp(c->argv[1]->ptr,"get")) {
- addReplyError(c,"Only CONFIG GET is allowed during loading");
- return;
- }
-
- if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
- const char *help[] = {
+void configHelpCommand(client *c) {
+ const char *help[] = {
"GET <pattern>",
" Return parameters matching the glob-like <pattern> and their values.",
"SET <directive> <value>",
@@ -2756,32 +2749,36 @@ void configCommand(client *c) {
"REWRITE",
" Rewrite the configuration file.",
NULL
- };
-
- addReplyHelp(c, help);
- } else if (!strcasecmp(c->argv[1]->ptr,"set") && c->argc == 4) {
- configSetCommand(c);
- } else if (!strcasecmp(c->argv[1]->ptr,"get") && c->argc == 3) {
- configGetCommand(c);
- } else if (!strcasecmp(c->argv[1]->ptr,"resetstat") && c->argc == 2) {
- resetServerStats();
- resetCommandTableStats();
- resetErrorTableStats();
- addReply(c,shared.ok);
- } else if (!strcasecmp(c->argv[1]->ptr,"rewrite") && c->argc == 2) {
- if (server.configfile == NULL) {
- addReplyError(c,"The server is running without a config file");
- return;
- }
- if (rewriteConfig(server.configfile, 0) == -1) {
- serverLog(LL_WARNING,"CONFIG REWRITE failed: %s", strerror(errno));
- addReplyErrorFormat(c,"Rewriting config file: %s", strerror(errno));
- } else {
- serverLog(LL_WARNING,"CONFIG REWRITE executed with success.");
- addReply(c,shared.ok);
- }
- } else {
- addReplySubcommandSyntaxError(c);
+ };
+
+ addReplyHelp(c, help);
+}
+
+/*-----------------------------------------------------------------------------
+ * CONFIG RESETSTAT
+ *----------------------------------------------------------------------------*/
+
+void configResetStatCommand(client *c) {
+ resetServerStats();
+ resetCommandTableStats(server.commands);
+ resetErrorTableStats();
+ addReply(c,shared.ok);
+}
+
+/*-----------------------------------------------------------------------------
+ * CONFIG REWRITE
+ *----------------------------------------------------------------------------*/
+
+void configRewriteCommand(client *c) {
+ if (server.configfile == NULL) {
+ addReplyError(c,"The server is running without a config file");
return;
}
+ if (rewriteConfig(server.configfile, 0) == -1) {
+ serverLog(LL_WARNING,"CONFIG REWRITE failed: %s", strerror(errno));
+ addReplyErrorFormat(c,"Rewriting config file: %s", strerror(errno));
+ } else {
+ serverLog(LL_WARNING,"CONFIG REWRITE executed with success.");
+ addReply(c,shared.ok);
+ }
}
diff --git a/src/db.c b/src/db.c
index 23cf220db..b0d379a16 100644
--- a/src/db.c
+++ b/src/db.c
@@ -1873,21 +1873,6 @@ int lcsGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *r
return result->numkeys;
}
-/* Helper function to extract keys from memory command.
- * MEMORY USAGE <key> */
-int memoryGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
- UNUSED(cmd);
-
- getKeysPrepareResult(result, 1);
- if (argc >= 3 && !strcasecmp(argv[1]->ptr,"usage")) {
- result->keys[0] = 2;
- result->numkeys = 1;
- return result->numkeys;
- }
- result->numkeys = 0;
- return 0;
-}
-
/* XREAD [BLOCK <milliseconds>] [COUNT <count>] [GROUP <groupname> <ttl>]
* STREAMS key_1 key_2 ... key_N ID_1 ID_2 ... ID_N */
int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
diff --git a/src/module.c b/src/module.c
index 9cf30938d..029b5ddca 100644
--- a/src/module.c
+++ b/src/module.c
@@ -830,6 +830,8 @@ int64_t commandKeySpecsFlagsFromString(const char *s) {
return flags;
}
+RedisModuleCommandProxy *moduleCreateCommandProxy(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, int64_t flags, int firstkey, int lastkey, int keystep);
+
/* Register a new command in the Redis server, that will be handled by
* calling the function pointer 'cmdfunc' using the RedisModule calling
* convention. The function returns REDISMODULE_ERR if the specified command
@@ -888,7 +890,7 @@ int64_t commandKeySpecsFlagsFromString(const char *s) {
* Normally this is used by a command that is used
* to authenticate a client.
* * **"may-replicate"**: This command may generate replication traffic, even
- * though it's not a write command.
+ * though it's not a write command.
*
* The last three parameters specify which arguments of the new command are
* Redis keys. See https://redis.io/commands/command for more information.
@@ -917,16 +919,24 @@ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc c
if ((flags & CMD_MODULE_NO_CLUSTER) && server.cluster_enabled)
return REDISMODULE_ERR;
+ /* Check if the command name is busy. */
+ if (lookupCommandByCString(name) != NULL)
+ return REDISMODULE_ERR;
+
+ RedisModuleCommandProxy *cp = moduleCreateCommandProxy(ctx, name, cmdfunc, flags, firstkey, lastkey, keystep);
+ cp->rediscmd->arity = cmdfunc ? -1 : -2;
+
+ dictAdd(server.commands,sdsnew(name),cp->rediscmd);
+ dictAdd(server.orig_commands,sdsnew(name),cp->rediscmd);
+ cp->rediscmd->id = ACLGetCommandID(name); /* ID used for ACL. */
+ return REDISMODULE_OK;
+}
+
+RedisModuleCommandProxy *moduleCreateCommandProxy(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, int64_t flags, int firstkey, int lastkey, int keystep) {
struct redisCommand *rediscmd;
RedisModuleCommandProxy *cp;
sds cmdname = sdsnew(name);
- /* Check if the command name is busy. */
- if (lookupCommand(cmdname) != NULL) {
- sdsfree(cmdname);
- return REDISMODULE_ERR;
- }
-
/* Create a command "proxy", which is a structure that is referenced
* in the command table, so that the generic command that works as
* binding between modules and Redis, can know what function to call
@@ -940,7 +950,6 @@ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc c
cp->rediscmd = zmalloc(sizeof(*rediscmd));
cp->rediscmd->name = cmdname;
cp->rediscmd->proc = RedisModuleCommandDispatcher;
- cp->rediscmd->arity = -1;
cp->rediscmd->flags = flags | CMD_MODULE;
cp->rediscmd->getkeys_proc = (redisGetKeysProc*)(unsigned long)cp;
cp->rediscmd->key_specs_max = STATIC_KEY_SPECS_NUM;
@@ -967,12 +976,70 @@ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc c
cp->rediscmd->calls = 0;
cp->rediscmd->rejected_calls = 0;
cp->rediscmd->failed_calls = 0;
- dictAdd(server.commands,sdsdup(cmdname),cp->rediscmd);
- dictAdd(server.orig_commands,sdsdup(cmdname),cp->rediscmd);
- cp->rediscmd->id = ACLGetCommandID(cmdname); /* ID used for ACL. */
+ return cp;
+}
+
+/* Very similar to RedisModule_CreateCommand except that it is used to create
+ * a subcommand, associated with another, container, command.
+ *
+ * Example: If a module has a configuration command, MODULE.CONFIG, then
+ * GET and SET should be individual subcommands, while MODULE.CONFIG is
+ * a command, but should not be registered with a valid `funcptr`:
+ *
+ * if (RedisModule_CreateCommand(ctx,"module.config",NULL,"",0,0,0) == REDISMODULE_ERR)
+ * return REDISMODULE_ERR;
+ *
+ * if (RedisModule_CreateSubcommand(ctx,"container.config","set",cmd_config_set,"",0,0,0) == REDISMODULE_ERR)
+ * return REDISMODULE_ERR;
+ *
+ * if (RedisModule_CreateSubcommand(ctx,"container.config","get",cmd_config_get,"",0,0,0) == REDISMODULE_ERR)
+ * return REDISMODULE_ERR;
+ *
+ */
+int RM_CreateSubcommand(RedisModuleCtx *ctx, const char *parent_name, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) {
+ int64_t flags = strflags ? commandFlagsFromString((char*)strflags) : 0;
+ if (flags == -1) return REDISMODULE_ERR;
+ if ((flags & CMD_MODULE_NO_CLUSTER) && server.cluster_enabled)
+ return REDISMODULE_ERR;
+
+ struct redisCommand *parent_cmd = lookupCommandByCString(parent_name);
+
+ if (!parent_cmd || !(parent_cmd->flags & CMD_MODULE))
+ return REDISMODULE_ERR;
+
+ if (parent_cmd->parent)
+ return REDISMODULE_ERR; /* We don't allow more than one level of subcommands */
+
+ RedisModuleCommandProxy *parent_cp = (void*)(unsigned long)parent_cmd->getkeys_proc;
+ if (parent_cp->module != ctx->module)
+ return REDISMODULE_ERR;
+
+ /* Check if the command name is busy within the parent command. */
+ if (parent_cmd->subcommands_dict && lookupCommandByCStringLogic(parent_cmd->subcommands_dict, name) != NULL)
+ return REDISMODULE_ERR;
+
+ RedisModuleCommandProxy *cp = moduleCreateCommandProxy(ctx, name, cmdfunc, flags, firstkey, lastkey, keystep);
+ cp->rediscmd->arity = -2;
+
+ commandAddSubcommand(parent_cmd, cp->rediscmd);
return REDISMODULE_OK;
}
+/* Return `struct RedisModule *` as `void *` to avoid exposing it outside of module.c. */
+void *moduleGetHandleByName(char *modulename) {
+ return dictFetchValue(modules,modulename);
+}
+
+/* Returns 1 if `cmd` is a command of the module `modulename`. 0 otherwise. */
+int moduleIsModuleCommand(void *module_handle, struct redisCommand *cmd) {
+ if (cmd->proc != RedisModuleCommandDispatcher)
+ return 0;
+ if (module_handle == NULL)
+ return 0;
+ RedisModuleCommandProxy *cp = (void*)(unsigned long)cmd->getkeys_proc;
+ return (cp->module == module_handle);
+}
+
void extendKeySpecsIfNeeded(struct redisCommand *cmd) {
/* We extend even if key_specs_num == key_specs_max because
* this function is called prior to adding a new spec */
@@ -1095,7 +1162,7 @@ int moduleSetCommandKeySpecFindKeys(RedisModuleCtx *ctx, const char *name, int i
*
* Example:
*
- * if (RedisModule_CreateCommand(ctx,"kspec.smove",kspec_legacy,"",0,0,0) == REDISMODULE_ERR)
+ * if (RedisModule_CreateCommand(ctx,"kspec.smove",kspec_legacy,"",0,0,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
*
* if (RedisModule_AddCommandKeySpec(ctx,"kspec.smove","read write",&spec_id) == REDISMODULE_ERR)
@@ -1112,6 +1179,11 @@ int moduleSetCommandKeySpecFindKeys(RedisModuleCtx *ctx, const char *name, int i
* if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.smove",spec_id,0,1,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
*
+ * It is also possible to use this API on subcommands (See RedisModule_CreateSubcommand).
+ * The name of the subcommand should be the name of the parent command + "|" + name of subcommand.
+ * Example:
+ * RedisModule_AddCommandKeySpec(ctx,"module.config|get","read",&spec_id)
+ *
* Returns REDISMODULE_OK on success
*/
int RM_AddCommandKeySpec(RedisModuleCtx *ctx, const char *name, const char *specflags, int *spec_id) {
@@ -4858,7 +4930,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
/* Lookup command now, after filters had a chance to make modifications
* if necessary.
*/
- cmd = lookupCommand(c->argv[0]->ptr);
+ cmd = lookupCommand(c->argv,c->argc);
if (!cmd) {
errno = ENOENT;
goto cleanup;
@@ -7373,7 +7445,7 @@ int RM_ACLCheckCommandPermissions(RedisModuleUser *user, RedisModuleString **arg
struct redisCommand *cmd;
/* Find command */
- if ((cmd = lookupCommand(argv[0]->ptr)) == NULL) {
+ if ((cmd = lookupCommand(argv, argc)) == NULL) {
errno = ENOENT;
return REDISMODULE_ERR;
}
@@ -9745,8 +9817,7 @@ void moduleCommand(client *c) {
NULL
};
addReplyHelp(c, help);
- } else
- if (!strcasecmp(subcmd,"load") && c->argc >= 3) {
+ } else if (!strcasecmp(subcmd,"load") && c->argc >= 3) {
robj **argv = NULL;
int argc = 0;
@@ -9964,7 +10035,7 @@ int *RM_GetCommandKeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc,
int *res = NULL;
/* Find command */
- if ((cmd = lookupCommand(argv[0]->ptr)) == NULL) {
+ if ((cmd = lookupCommand(argv,argc)) == NULL) {
errno = ENOENT;
return NULL;
}
@@ -10243,6 +10314,7 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API(Free);
REGISTER_API(Strdup);
REGISTER_API(CreateCommand);
+ REGISTER_API(CreateSubcommand);
REGISTER_API(SetModuleAttribs);
REGISTER_API(IsModuleNameBusy);
REGISTER_API(WrongArity);
diff --git a/src/networking.c b/src/networking.c
index 9c1b46052..d514c11a5 100644
--- a/src/networking.c
+++ b/src/networking.c
@@ -2399,7 +2399,8 @@ sds catClientInfoString(sds s, client *client) {
/* Compute the total memory consumed by this client. */
size_t obufmem, total_mem = getClientMemoryUsage(client, &obufmem);
- return sdscatfmt(s,
+ sds cmdname = client->lastcmd ? getFullCommandName(client->lastcmd) : NULL;
+ sds ret = sdscatfmt(s,
"id=%U addr=%s laddr=%s %s name=%s age=%I idle=%I flags=%s db=%i sub=%i psub=%i multi=%i qbuf=%U qbuf-free=%U argv-mem=%U multi-mem=%U obl=%U oll=%U omem=%U tot-mem=%U events=%s cmd=%s user=%s redir=%I resp=%i",
(unsigned long long) client->id,
getClientPeerId(client),
@@ -2422,10 +2423,13 @@ sds catClientInfoString(sds s, client *client) {
(unsigned long long) obufmem, /* should not include client->buf since we want to see 0 for static clients. */
(unsigned long long) total_mem,
events,
- client->lastcmd ? client->lastcmd->name : "NULL",
+ cmdname ? cmdname : "NULL",
client->user ? client->user->name : "(superuser)",
(client->flags & CLIENT_TRACKING) ? (long long) client->client_tracking_redirection : -1,
client->resp);
+ if (cmdname)
+ sdsfree(cmdname);
+ return ret;
}
sds getAllClientsInfoString(int type) {
@@ -2568,6 +2572,8 @@ void clientCommand(client *c) {
" Control the replies sent to the current connection.",
"SETNAME <name>",
" Assign the name <name> to the current connection.",
+"GETNAME",
+" Get the name of the current connection.",
"UNBLOCK <clientid> [TIMEOUT|ERROR]",
" Unblock the specified blocked client.",
"TRACKING (ON|OFF) [REDIRECT <id>] [BCAST] [PREFIX <prefix> [...]]",
@@ -2575,6 +2581,8 @@ void clientCommand(client *c) {
" Control server assisted client side caching.",
"TRACKINGINFO",
" Report tracking status for the current connection.",
+"NO-EVICT (ON|OFF)",
+" Protect current client connection from eviction.",
NULL
};
addReplyHelp(c, help);
@@ -2638,7 +2646,7 @@ NULL
return;
}
} else if (!strcasecmp(c->argv[1]->ptr,"no-evict") && c->argc == 3) {
- /* CLIENT PROTECT ON|OFF */
+ /* CLIENT NO-EVICT ON|OFF */
if (!strcasecmp(c->argv[2]->ptr,"on")) {
c->flags |= CLIENT_NO_EVICT;
addReply(c,shared.ok);
@@ -3195,7 +3203,7 @@ void replaceClientCommandVector(client *c, int argc, robj **argv) {
for (j = 0; j < c->argc; j++)
if (c->argv[j])
c->argv_len_sum += getStringObjectLen(c->argv[j]);
- c->cmd = lookupCommandOrOriginal(c->argv[0]->ptr);
+ c->cmd = lookupCommandOrOriginal(c->argv,c->argc);
serverAssertWithInfo(c,NULL,c->cmd != NULL);
}
@@ -3227,7 +3235,7 @@ void rewriteClientCommandArgument(client *c, int i, robj *newval) {
/* If this is the command name make sure to fix c->cmd. */
if (i == 0) {
- c->cmd = lookupCommandOrOriginal(c->argv[0]->ptr);
+ c->cmd = lookupCommandOrOriginal(c->argv,c->argc);
serverAssertWithInfo(c,NULL,c->cmd != NULL);
}
}
diff --git a/src/pubsub.c b/src/pubsub.c
index e0bbc6d94..6da5b18cf 100644
--- a/src/pubsub.c
+++ b/src/pubsub.c
@@ -406,6 +406,11 @@ void punsubscribeCommand(client *c) {
/* PUBLISH <channel> <message> */
void publishCommand(client *c) {
+ if (server.sentinel_mode) {
+ sentinelPublishCommand(c);
+ return;
+ }
+
int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
if (server.cluster_enabled)
clusterPropagatePublish(c->argv[1],c->argv[2]);
diff --git a/src/redismodule.h b/src/redismodule.h
index 85c963d58..68272e3f1 100644
--- a/src/redismodule.h
+++ b/src/redismodule.h
@@ -604,6 +604,7 @@ REDISMODULE_API void * (*RedisModule_Calloc)(size_t nmemb, size_t size) REDISMOD
REDISMODULE_API char * (*RedisModule_Strdup)(const char *str) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_GetApi)(const char *, void *) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) REDISMODULE_ATTR;
+REDISMODULE_API int (*RedisModule_CreateSubcommand)(RedisModuleCtx *ctx, const char *parent_name, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_IsModuleNameBusy)(const char *name) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_WrongArity)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
@@ -924,6 +925,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(Realloc);
REDISMODULE_GET_API(Strdup);
REDISMODULE_GET_API(CreateCommand);
+ REDISMODULE_GET_API(CreateSubcommand);
REDISMODULE_GET_API(SetModuleAttribs);
REDISMODULE_GET_API(IsModuleNameBusy);
REDISMODULE_GET_API(WrongArity);
diff --git a/src/replication.c b/src/replication.c
index dcb407491..eb574d0ea 100644
--- a/src/replication.c
+++ b/src/replication.c
@@ -2812,6 +2812,11 @@ void replicaofCommand(client *c) {
* (master or slave) and additional information related to replication
* in an easy to process format. */
void roleCommand(client *c) {
+ if (server.sentinel_mode) {
+ sentinelRoleCommand(c);
+ return;
+ }
+
if (server.masterhost == NULL) {
listIter li;
listNode *ln;
diff --git a/src/scripting.c b/src/scripting.c
index 5b7b529ff..374ebaf91 100644
--- a/src/scripting.c
+++ b/src/scripting.c
@@ -817,7 +817,7 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) {
}
/* Command lookup */
- cmd = lookupCommand(argv[0]->ptr);
+ cmd = lookupCommand(argv,argc);
if (!cmd || ((cmd->arity > 0 && cmd->arity != argc) ||
(argc < -cmd->arity)))
{
diff --git a/src/sentinel.c b/src/sentinel.c
index 2a2068eef..dfbb8018f 100644
--- a/src/sentinel.c
+++ b/src/sentinel.c
@@ -456,32 +456,10 @@ dictType renamedCommandsDictType = {
/* =========================== Initialization =============================== */
-void sentinelCommand(client *c);
-void sentinelInfoCommand(client *c);
void sentinelSetCommand(client *c);
-void sentinelPublishCommand(client *c);
-void sentinelRoleCommand(client *c);
void sentinelConfigGetCommand(client *c);
void sentinelConfigSetCommand(client *c);
-struct redisCommand sentinelcmds[] = {
- {"ping",pingCommand,1,"fast @connection"},
- {"sentinel",sentinelCommand,-2,"admin"},
- {"subscribe",subscribeCommand,-2,"pub-sub"},
- {"unsubscribe",unsubscribeCommand,-1,"pub-sub"},
- {"psubscribe",psubscribeCommand,-2,"pub-sub"},
- {"punsubscribe",punsubscribeCommand,-1,"pub-sub"},
- {"publish",sentinelPublishCommand,3,"pub-sub fast"},
- {"info",sentinelInfoCommand,-1,"random @dangerous"},
- {"role",sentinelRoleCommand,1,"fast read-only @dangerous"},
- {"client",clientCommand,-2,"admin random @connection"},
- {"shutdown",shutdownCommand,-1,"admin"},
- {"auth",authCommand,-2,"no-auth fast @connection"},
- {"hello",helloCommand,-1,"no-auth fast @connection"},
- {"acl",aclCommand,-2,"admin"},
- {"command",commandCommand,-1, "random @connection"}
-};
-
/* this array is used for sentinel config lookup, which need to be loaded
* before monitoring masters config to avoid dependency issues */
const char *preMonitorCfgName[] = {
@@ -507,28 +485,6 @@ void freeSentinelLoadQueueEntry(void *item);
/* Perform the Sentinel mode initialization. */
void initSentinel(void) {
- unsigned int j;
-
- /* Remove usual Redis commands from the command table, then just add
- * the SENTINEL command. */
- dictEmpty(server.commands,NULL);
- dictEmpty(server.orig_commands,NULL);
- ACLClearCommandID();
- for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) {
- int retval;
- struct redisCommand *cmd = sentinelcmds+j;
- cmd->id = ACLGetCommandID(cmd->name); /* Assign the ID used for ACL. */
- retval = dictAdd(server.commands, sdsnew(cmd->name), cmd);
- serverAssert(retval == DICT_OK);
- retval = dictAdd(server.orig_commands, sdsnew(cmd->name), cmd);
- serverAssert(retval == DICT_OK);
-
- /* Translate the command string flags description into an actual
- * set of flags. */
- if (populateSingleCommand(cmd,cmd->sflags) == C_ERR)
- serverPanic("Unsupported command flag");
- }
-
/* Initialize various data structures. */
sentinel.current_epoch = 0;
sentinel.masters = dictCreate(&instancesDictType);
diff --git a/src/server.c b/src/server.c
index 78ef2925f..17672fbd6 100644
--- a/src/server.c
+++ b/src/server.c
@@ -171,6 +171,10 @@ struct redisServer server; /* Server global state */
* or may just execute read commands. A command can not be marked
* both "write" and "may-replicate"
*
+ * sentinel: This command is present in sentinel mode too.
+ *
+ * sentinel-only: This command is present only when in sentinel mode.
+ *
* The following additional flags are only used in order to put commands
* in a specific ACL category. Commands can have multiple ACL categories.
* See redis.conf for the exact meaning of each.
@@ -193,11 +197,499 @@ struct redisServer server; /* Server global state */
* TYPE, EXPIRE*, PEXPIRE*, TTL, PTTL, ...
*/
+struct redisCommand configSubcommands[] = {
+ {"set",configSetCommand,4,
+ "admin ok-stale no-script"},
-struct redisCommand redisCommandTable[] = {
- {"module",moduleCommand,-2,
+ {"get",configGetCommand,3,
+ "admin ok-loading ok-stale no-script"},
+
+ {"resetstat",configResetStatCommand,2,
+ "admin ok-stale no-script"},
+
+ {"rewrite",configRewriteCommand,2,
+ "admin ok-stale no-script"},
+
+ {"help",configHelpCommand,2,
+ "ok-stale ok-loading"},
+
+ {NULL},
+};
+
+struct redisCommand xinfoSubcommands[] = {
+ {"consumers",xinfoCommand,4,
+ "read-only random @stream",
+ {{"read",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"groups",xinfoCommand,3,
+ "read-only @stream",
+ {{"read",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"stream",xinfoCommand,-3,
+ "read-only @stream",
+ {{"read",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"help",xinfoCommand,2,
+ "ok-stale ok-loading @stream"},
+
+ {NULL},
+};
+
+struct redisCommand xgroupSubcommands[] = {
+ {"create",xgroupCommand,-5,
+ "write use-memory @stream",
+ {{"write",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"setid",xgroupCommand,5,
+ "write @stream",
+ {{"write",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"destroy",xgroupCommand,4,
+ "write @stream",
+ {{"write",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"createconsumer",xgroupCommand,5,
+ "write use-memory @stream",
+ {{"write",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"delconsumer",xgroupCommand,5,
+ "write @stream",
+ {{"write",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"help",xgroupCommand,2,
+ "ok-stale ok-loading @stream"},
+
+ {NULL},
+};
+
+struct redisCommand commandSubcommands[] = {
+ {"count",commandCountCommand,2,
+ "ok-loading ok-stale @connection"},
+
+ {"list",commandListCommand,-2,
+ "ok-loading ok-stale @connection"},
+
+ {"info",commandInfoCommand,-3,
+ "ok-loading ok-stale @connection"},
+
+ {"getkeys",commandGetKeysCommand,-4,
+ "ok-loading ok-stale @connection"},
+
+ {"help",commandHelpCommand,2,
+ "ok-loading ok-stale @connection"},
+
+ {NULL},
+};
+
+struct redisCommand memorySubcommands[] = {
+ {"doctor",memoryCommand,2,
+ "random"},
+
+ {"stats",memoryCommand,2,
+ "random"},
+
+ {"malloc-stats",memoryCommand,2,
+ "random"},
+
+ {"purge",memoryCommand,2,
+ ""},
+
+ {"usage",memoryCommand,-3,
+ "read-only",
+ {{"read",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"help",memoryCommand,2,
+ "ok-stale ok-loading"},
+
+ {NULL},
+};
+
+struct redisCommand aclSubcommands[] = {
+ {"cat",aclCommand,-2,
+ "no-script ok-loading ok-stale sentinel"},
+
+ {"deluser",aclCommand,-3,
+ "admin no-script ok-loading ok-stale sentinel"},
+
+ {"genpass",aclCommand,-2,
+ "no-script ok-loading ok-stale sentinel"},
+
+ {"getuser",aclCommand,3,
+ "admin no-script ok-loading ok-stale sentinel"},
+
+ {"list",aclCommand,2,
+ "admin no-script ok-loading ok-stale sentinel"},
+
+ {"load",aclCommand,2,
+ "admin no-script ok-loading ok-stale sentinel"},
+
+ {"log",aclCommand,-2,
+ "admin no-script ok-loading ok-stale sentinel"},
+
+ {"save",aclCommand,2,
+ "admin no-script ok-loading ok-stale sentinel"},
+
+ {"setuser",aclCommand,-3,
+ "admin no-script ok-loading ok-stale sentinel"},
+
+ {"users",aclCommand,2,
+ "admin no-script ok-loading ok-stale sentinel"},
+
+ {"whoami",aclCommand,2,
+ "no-script ok-loading ok-stale sentinel"},
+
+ {"help",aclCommand,2,
+ "ok-stale ok-loading sentinel"},
+
+ {NULL},
+};
+
+struct redisCommand latencySubcommands[] = {
+ {"doctor",latencyCommand,2,
+ "admin no-script ok-loading ok-stale"},
+
+ {"graph",latencyCommand,3,
+ "admin no-script ok-loading ok-stale"},
+
+ {"history",latencyCommand,3,
+ "admin no-script ok-loading ok-stale"},
+
+ {"latest",latencyCommand,2,
+ "admin no-script ok-loading ok-stale"},
+
+ {"reset",latencyCommand,-2,
+ "admin no-script ok-loading ok-stale"},
+
+ {"help",latencyCommand,2,
+ "ok-stale ok-loading"},
+
+ {NULL},
+};
+
+struct redisCommand moduleSubcommands[] = {
+ {"list",moduleCommand,2,
"admin no-script"},
+ {"load",moduleCommand,-3,
+ "admin no-script"},
+
+ {"unload",moduleCommand,3,
+ "admin no-script"},
+
+ {"help",moduleCommand,2,
+ "ok-stale ok-loading"},
+
+ {NULL},
+};
+
+struct redisCommand slowlogSubcommands[] = {
+ {"get",slowlogCommand,-2,
+ "admin random ok-loading ok-stale"},
+
+ {"len",slowlogCommand,2,
+ "admin random ok-loading ok-stale"},
+
+ {"reset",slowlogCommand,2,
+ "admin ok-loading ok-stale"},
+
+ {"help",slowlogCommand,2,
+ "ok-stale ok-loading"},
+
+ {NULL},
+};
+
+struct redisCommand objectSubcommands[] = {
+ {"encoding",objectCommand,3,
+ "read-only @keyspace",
+ {{"read",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"freq",objectCommand,3,
+ "read-only random @keyspace",
+ {{"read",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"idletime",objectCommand,3,
+ "read-only random @keyspace",
+ {{"read",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"refcount",objectCommand,3,
+ "read-only @keyspace",
+ {{"read",
+ KSPEC_BS_INDEX,.bs.index={2},
+ KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+
+ {"help",objectCommand,2,
+ "ok-stale ok-loading @keyspace"},
+
+ {NULL},
+};
+
+struct redisCommand scriptSubcommands[] = {
+ {"debug",scriptCommand,3,
+ "no-script @scripting"},
+
+ {"exists",scriptCommand,-3,
+ "no-script @scripting"},
+
+ {"flush",scriptCommand,-2,
+ "may-replicate no-script @scripting"},
+
+ {"kill",scriptCommand,2,
+ "no-script @scripting"},
+
+ {"load",scriptCommand,3,
+ "may-replicate no-script @scripting"},
+
+ {"help",scriptCommand,2,
+ "ok-loading ok-stale @scripting"},
+
+ {NULL},
+};
+
+struct redisCommand clientSubcommands[] = {
+ {"caching",clientCommand,3,
+ "no-script ok-loading ok-stale @connection"},
+
+ {"getredir",clientCommand,2,
+ "no-script ok-loading ok-stale @connection"},
+
+ {"id",clientCommand,2,
+ "no-script ok-loading ok-stale @connection"},
+
+ {"info",clientCommand,2,
+ "no-script random ok-loading ok-stale @connection"},
+
+ {"kill",clientCommand,-3,
+ "admin no-script ok-loading ok-stale @connection"},
+
+ {"list",clientCommand,-2,
+ "admin no-script random ok-loading ok-stale @connection"},
+
+ {"unpause",clientCommand,2,
+ "admin no-script ok-loading ok-stale @connection"},
+
+ {"pause",clientCommand,-3,
+ "admin no-script ok-loading ok-stale @connection"},
+
+ {"reply",clientCommand,3,
+ "no-script ok-loading ok-stale @connection"},
+
+ {"setname",clientCommand,3,
+ "no-script ok-loading ok-stale @connection"},
+
+ {"getname",clientCommand,2,
+ "no-script ok-loading ok-stale @connection"},
+
+ {"unblock",clientCommand,-3,
+ "admin no-script ok-loading ok-stale @connection"},
+
+ {"tracking",clientCommand,-3,
+ "no-script ok-loading ok-stale @connection"},
+
+ {"trackinginfo",clientCommand,2,
+ "admin no-script ok-loading ok-stale @connection"},
+
+ {"no-evict",clientCommand,3,
+ "admin no-script ok-loading ok-stale @connection"},
+
+ {"help",clientCommand,2,
+ "ok-loading ok-stale @connection"},
+
+ {NULL},
+};
+
+struct redisCommand stralgoSubcommands[] = {
+ {"lcs",stralgoCommand,-5,
+ "read-only @string",
+ {{"read incomplete", /* We can't use "keyword" here because we may give false information. */
+ KSPEC_BS_UNKNOWN,{{0}},
+ KSPEC_FK_UNKNOWN,{{0}}}},
+ lcsGetKeys},
+
+ {"help",stralgoCommand,2,
+ "ok-loading ok-stale @string"},
+
+ {NULL},
+};
+
+struct redisCommand pubsubSubcommands[] = {
+ {"channels",pubsubCommand,-2,
+ "pub-sub ok-loading ok-stale"},
+
+ {"numpat",pubsubCommand,2,
+ "pub-sub ok-loading ok-stale"},
+
+ {"numsub",pubsubCommand,-2,
+ "pub-sub ok-loading ok-stale"},
+
+ {"help",pubsubCommand,2,
+ "ok-loading ok-stale"},
+
+ {NULL},
+};
+
+struct redisCommand clusterSubcommands[] = {
+ {"addslots",clusterCommand,-3,
+ "admin ok-stale random"},
+
+ {"bumpepoch",clusterCommand,2,
+ "admin ok-stale random"},
+
+ {"count-failure-reports",clusterCommand,3,
+ "admin ok-stale random"},
+
+ {"countkeysinslots",clusterCommand,3,
+ "ok-stale random"},
+
+ {"delslots",clusterCommand,-3,
+ "admin ok-stale random"},
+
+ {"failover",clusterCommand,-2,
+ "admin ok-stale random"},
+
+ {"forget",clusterCommand,3,
+ "admin ok-stale random"},
+
+ {"getkeysinslot",clusterCommand,4,
+ "ok-stale random"},
+
+ {"flushslots",clusterCommand,2,
+ "admin ok-stale random"},
+
+ {"info",clusterCommand,2,
+ "ok-stale random"},
+
+ {"keyslot",clusterCommand,3,
+ "ok-stale random"},
+
+ {"meet",clusterCommand,-4,
+ "admin ok-stale random"},
+
+ {"myid",clusterCommand,2,
+ "ok-stale random"},
+
+ {"nodes",clusterCommand,2,
+ "ok-stale random"},
+
+ {"replicate",clusterCommand,3,
+ "admin ok-stale random"},
+
+ {"reset",clusterCommand,3,
+ "admin ok-stale random"},
+
+ {"set-config-epoch",clusterCommand,3,
+ "admin ok-stale random"},
+
+ {"setslot",clusterCommand,-4,
+ "admin ok-stale random"},
+
+ {"replicas",clusterCommand,3,
+ "admin ok-stale random"},
+
+ {"saveconfig",clusterCommand,2,
+ "admin ok-stale random"},
+
+ {"slots",clusterCommand,2,
+ "ok-stale random"},
+
+ {"help",clusterCommand,2,
+ "ok-loading ok-stale"},
+
+ {NULL},
+};
+
+struct redisCommand sentinelSubcommands[] = {
+ {"ckquorum",sentinelCommand,3,
+ "admin only-sentinel"},
+
+ {"config",sentinelCommand,-3,
+ "admin only-sentinel"},
+
+ {"debug",sentinelCommand,2,
+ "admin only-sentinel"},
+
+ {"get-master-addr-by-name",sentinelCommand,3,
+ "admin only-sentinel"},
+
+ {"failover",sentinelCommand,3,
+ "admin only-sentinel"},
+
+ {"flushconfig",sentinelCommand,2,
+ "admin only-sentinel"},
+
+ {"info-cache",sentinelCommand,3,
+ "admin only-sentinel"},
+
+ {"is-master-down-by-addr",sentinelCommand,6,
+ "admin only-sentinel"},
+
+ {"master",sentinelCommand,3,
+ "admin only-sentinel"},
+
+ {"masters",sentinelCommand,2,
+ "admin only-sentinel"},
+
+ {"monitor",sentinelCommand,6,
+ "admin only-sentinel"},
+
+ {"myid",sentinelCommand,2,
+ "admin only-sentinel"},
+
+ {"pending-scripts",sentinelCommand,2,
+ "admin only-sentinel"},
+
+ {"remove",sentinelCommand,3,
+ "admin only-sentinel"},
+
+ {"replicas",sentinelCommand,3,
+ "admin only-sentinel"},
+
+ {"reset",sentinelCommand,3,
+ "admin only-sentinel"},
+
+ {"sentinels",sentinelCommand,3,
+ "admin only-sentinel"},
+
+ {"set",sentinelCommand,5,
+ "admin only-sentinel"},
+
+ {"simulate-failure",sentinelCommand,3,
+ "admin only-sentinel"},
+
+ {"help",sentinelCommand,2,
+ "ok-loading ok-stale only-sentinel"},
+
+ {NULL},
+};
+
+struct redisCommand redisCommandTable[] = {
+ {"module",NULL,-2,
+ "",
+ .subcommands=moduleSubcommands},
+
{"get",getCommand,2,
"read-only fast @string",
{{"read",
@@ -1028,13 +1520,17 @@ struct redisCommand redisCommandTable[] = {
"read-only fast @keyspace"},
{"auth",authCommand,-2,
- "no-auth no-script ok-loading ok-stale fast @connection"},
+ "no-auth no-script ok-loading ok-stale fast sentinel @connection"},
/* We don't allow PING during loading since in Redis PING is used as
* failure detection, and a loading server is considered to be
* not available. */
{"ping",pingCommand,-1,
- "ok-stale fast @connection"},
+ "ok-stale fast sentinel @connection"},
+
+ {"sentinel",NULL,-2,
+ "admin only-sentinel",
+ .subcommands=sentinelSubcommands},
{"echo",echoCommand,2,
"fast @connection"},
@@ -1049,7 +1545,7 @@ struct redisCommand redisCommandTable[] = {
"admin no-script"},
{"shutdown",shutdownCommand,-1,
- "admin no-script ok-loading ok-stale"},
+ "admin no-script ok-loading ok-stale sentinel"},
{"lastsave",lastsaveCommand,1,
"random fast ok-loading ok-stale @admin @dangerous"},
@@ -1101,7 +1597,7 @@ struct redisCommand redisCommandTable[] = {
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"info",infoCommand,-1,
- "ok-loading ok-stale random @dangerous"},
+ "ok-loading ok-stale random sentinel @dangerous"},
{"monitor",monitorCommand,1,
"admin no-script ok-loading ok-stale"},
@@ -1149,31 +1645,33 @@ struct redisCommand redisCommandTable[] = {
"admin no-script ok-stale"},
{"role",roleCommand,1,
- "ok-loading ok-stale no-script fast @admin @dangerous"},
+ "ok-loading ok-stale no-script fast sentinel @admin @dangerous"},
{"debug",debugCommand,-2,
"admin no-script ok-loading ok-stale"},
- {"config",configCommand,-2,
- "admin ok-loading ok-stale no-script"},
+ {"config",NULL,-2,
+ "",
+ .subcommands=configSubcommands},
{"subscribe",subscribeCommand,-2,
- "pub-sub no-script ok-loading ok-stale"},
+ "pub-sub no-script ok-loading ok-stale sentinel"},
{"unsubscribe",unsubscribeCommand,-1,
- "pub-sub no-script ok-loading ok-stale"},
+ "pub-sub no-script ok-loading ok-stale sentinel"},
{"psubscribe",psubscribeCommand,-2,
- "pub-sub no-script ok-loading ok-stale"},
+ "pub-sub no-script ok-loading ok-stale sentinel"},
{"punsubscribe",punsubscribeCommand,-1,
- "pub-sub no-script ok-loading ok-stale"},
+ "pub-sub no-script ok-loading ok-stale sentinel"},
{"publish",publishCommand,3,
- "pub-sub ok-loading ok-stale fast may-replicate"},
+ "pub-sub ok-loading ok-stale fast may-replicate sentinel"},
- {"pubsub",pubsubCommand,-2,
- "pub-sub ok-loading ok-stale random"},
+ {"pubsub",NULL,-2,
+ "",
+ .subcommands=pubsubSubcommands},
{"watch",watchCommand,-2,
"no-script fast ok-loading ok-stale @transaction",
@@ -1184,8 +1682,9 @@ struct redisCommand redisCommandTable[] = {
{"unwatch",unwatchCommand,1,
"no-script fast ok-loading ok-stale @transaction"},
- {"cluster",clusterCommand,-2,
- "admin ok-stale random"},
+ {"cluster",NULL,-2,
+ "",
+ .subcommands=clusterSubcommands},
{"restore",restoreCommand,-4,
"write use-memory @keyspace @dangerous",
@@ -1224,24 +1723,20 @@ struct redisCommand redisCommandTable[] = {
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
- {"object",objectCommand,-2,
- "read-only random @keyspace",
- {{"read",
- KSPEC_BS_INDEX,.bs.index={2},
- KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+ {"object",NULL,-2,
+ "",
+ .subcommands=objectSubcommands},
- {"memory",memoryCommand,-2,
- "random read-only",
- {{"read",
- KSPEC_BS_KEYWORD,.bs.keyword={"USAGE",1},
- KSPEC_FK_RANGE,.fk.range={0,1,0}}},
- memoryGetKeys},
+ {"memory",NULL,-2,
+ "",
+ .subcommands=memorySubcommands},
- {"client",clientCommand,-2,
- "admin no-script random ok-loading ok-stale @connection"},
+ {"client",NULL,-2,
+ "sentinel",
+ .subcommands=clientSubcommands},
{"hello",helloCommand,-1,
- "no-auth no-script fast ok-loading ok-stale @connection"},
+ "no-auth no-script fast ok-loading ok-stale sentinel @connection"},
/* EVAL can modify the dataset, however it is not flagged as a write
* command since we do the check while running commands from Lua.
@@ -1277,11 +1772,13 @@ struct redisCommand redisCommandTable[] = {
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
evalGetKeys},
- {"slowlog",slowlogCommand,-2,
- "admin random ok-loading ok-stale"},
+ {"slowlog",NULL,-2,
+ "",
+ .subcommands=slowlogSubcommands},
- {"script",scriptCommand,-2,
- "no-script may-replicate @scripting"},
+ {"script",NULL,-2,
+ "",
+ .subcommands=scriptSubcommands},
{"time",timeCommand,1,
"random fast ok-loading ok-stale"},
@@ -1311,7 +1808,8 @@ struct redisCommand redisCommandTable[] = {
"no-script @connection"},
{"command",commandCommand,-1,
- "ok-loading ok-stale random @connection"},
+ "ok-loading ok-stale random sentinel @connection",
+ .subcommands=commandSubcommands},
{"geoadd",geoaddCommand,-5,
"write use-memory @geo",
@@ -1465,11 +1963,9 @@ struct redisCommand redisCommandTable[] = {
KSPEC_FK_RANGE,.fk.range={-1,1,2}}},
xreadGetKeys},
- {"xgroup",xgroupCommand,-2,
- "write use-memory @stream",
- {{"write",
- KSPEC_BS_INDEX,.bs.index={2},
- KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+ {"xgroup",NULL,-2,
+ "",
+ .subcommands=xgroupSubcommands},
{"xsetid",xsetidCommand,3,
"write use-memory fast @stream",
@@ -1501,11 +1997,9 @@ struct redisCommand redisCommandTable[] = {
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
- {"xinfo",xinfoCommand,-2,
- "read-only random @stream",
- {{"write",
- KSPEC_BS_INDEX,.bs.index={2},
- KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
+ {"xinfo",NULL,-2,
+ "",
+ .subcommands=xinfoSubcommands},
{"xdel",xdelCommand,-3,
"write fast @stream",
@@ -1525,21 +2019,20 @@ struct redisCommand redisCommandTable[] = {
{"host:",securityWarningCommand,-1,
"ok-loading ok-stale read-only"},
- {"latency",latencyCommand,-2,
- "admin no-script ok-loading ok-stale"},
+ {"latency",NULL,-2,
+ "",
+ .subcommands=latencySubcommands},
{"lolwut",lolwutCommand,-1,
"read-only fast"},
- {"acl",aclCommand,-2,
- "admin no-script ok-loading ok-stale"},
+ {"acl",NULL,-2,
+ "",
+ .subcommands=aclSubcommands},
- {"stralgo",stralgoCommand,-2,
- "read-only @string",
- {{"read incomplete", /* We can't use "keyword" here because we may give false information. */
- KSPEC_BS_UNKNOWN,{{0}},
- KSPEC_FK_UNKNOWN,{{0}}}},
- lcsGetKeys},
+ {"stralgo",NULL,-2,
+ "",
+ .subcommands=stralgoSubcommands},
{"reset",resetCommand,1,
"no-script ok-stale ok-loading fast @connection"},
@@ -3942,6 +4435,18 @@ void populateCommandLegacyRangeSpec(struct redisCommand *c) {
c->legacy_range_key_spec.fk.range.limit = 0;
}
+void commandAddSubcommand(struct redisCommand *parent, struct redisCommand *subcommand) {
+ if (!parent->subcommands_dict)
+ parent->subcommands_dict = dictCreate(&commandTableDictType);
+
+ subcommand->parent = parent; /* Assign the parent command */
+ sds fullname = getFullCommandName(subcommand);
+ subcommand->id = ACLGetCommandID(fullname); /* Assign the ID used for ACL. */
+ sdsfree(fullname);
+
+ serverAssert(dictAdd(parent->subcommands_dict, sdsnew(subcommand->name), subcommand) == DICT_OK);
+}
+
/* Parse the flags string description 'strflags' and set them to the
* command 'c'. If the flags are all valid C_OK is returned, otherwise
* C_ERR is returned (yet the recognized flags are set in the command). */
@@ -3987,6 +4492,10 @@ int populateSingleCommand(struct redisCommand *c, char *strflags) {
c->flags |= CMD_NO_AUTH;
} else if (!strcasecmp(flag,"may-replicate")) {
c->flags |= CMD_MAY_REPLICATE;
+ } else if (!strcasecmp(flag,"sentinel")) {
+ c->flags |= CMD_SENTINEL;
+ } else if (!strcasecmp(flag,"only-sentinel")) {
+ c->flags |= CMD_ONLY_SENTINEL;
} else {
/* Parse ACL categories here if the flag name starts with @. */
uint64_t catflag;
@@ -4041,6 +4550,20 @@ int populateSingleCommand(struct redisCommand *c, char *strflags) {
/* Handle the "movablekeys" flag (must be done after populating all key specs). */
populateCommandMovableKeys(c);
+ /* Handle subcommands */
+ if (c->subcommands) {
+ for (int j = 0; c->subcommands[j].name; j++) {
+ struct redisCommand *sub = c->subcommands+j;
+
+ /* Translate the command string flags description into an actual
+ * set of flags. */
+ if (populateSingleCommand(sub,sub->sflags) == C_ERR)
+ serverPanic("Unsupported command flag or key spec flag");
+
+ commandAddSubcommand(c,sub);
+ }
+ }
+
return C_OK;
}
@@ -4052,14 +4575,23 @@ void populateCommandTable(void) {
for (j = 0; j < numcommands; j++) {
struct redisCommand *c = redisCommandTable+j;
+
+ if (!(c->flags & CMD_SENTINEL) && server.sentinel_mode)
+ continue;
+
+ if (c->flags & CMD_ONLY_SENTINEL && !server.sentinel_mode)
+ continue;
+
int retval1, retval2;
+ /* Assign the ID used for ACL. */
+ c->id = ACLGetCommandID(c->name);
+
/* Translate the command string flags description into an actual
* set of flags. */
if (populateSingleCommand(c,c->sflags) == C_ERR)
serverPanic("Unsupported command flag or key spec flag");
- c->id = ACLGetCommandID(c->name); /* Assign the ID used for ACL. */
retval1 = dictAdd(server.commands, sdsnew(c->name), c);
/* Populate an additional dictionary that will be unaffected
* by rename-command statements in redis.conf. */
@@ -4068,18 +4600,20 @@ void populateCommandTable(void) {
}
}
-void resetCommandTableStats(void) {
+void resetCommandTableStats(dict* commands) {
struct redisCommand *c;
dictEntry *de;
dictIterator *di;
- di = dictGetSafeIterator(server.commands);
+ di = dictGetSafeIterator(commands);
while((de = dictNext(di)) != NULL) {
c = (struct redisCommand *) dictGetVal(de);
c->microseconds = 0;
c->calls = 0;
c->rejected_calls = 0;
c->failed_calls = 0;
+ if (c->subcommands_dict)
+ resetCommandTableStats(c->subcommands_dict);
}
dictReleaseIterator(di);
@@ -4127,19 +4661,62 @@ void redisOpArrayFree(redisOpArray *oa) {
/* ====================== Commands lookup and execution ===================== */
-struct redisCommand *lookupCommand(sds name) {
- return dictFetchValue(server.commands, name);
+struct redisCommand *lookupCommandLogic(dict *commands, robj **argv, int argc) {
+ struct redisCommand *base_cmd = dictFetchValue(commands, argv[0]->ptr);
+ int has_subcommands = base_cmd && base_cmd->subcommands_dict;
+ if (argc == 1 || !has_subcommands) {
+ /* Note: It is possible that base_cmd->proc==NULL (e.g. CONFIG) */
+ return base_cmd;
+ } else {
+ /* Note: Currently we support just one level of subcommands */
+ return dictFetchValue(base_cmd->subcommands_dict, argv[1]->ptr);
+ }
}
-struct redisCommand *lookupCommandByCString(const char *s) {
+struct redisCommand *lookupCommand(robj **argv, int argc) {
+ return lookupCommandLogic(server.commands,argv,argc);
+}
+
+struct redisCommand *lookupCommandBySdsLogic(dict *commands, sds s) {
+ int argc, j;
+ sds *strings = sdssplitlen(s,sdslen(s),"|",1,&argc);
+ if (strings == NULL)
+ return NULL;
+ if (argc > 2) {
+ /* Currently we support just one level of subcommands */
+ sdsfreesplitres(strings,argc);
+ return NULL;
+ }
+
+ robj objects[argc];
+ robj *argv[argc];
+ for (j = 0; j < argc; j++) {
+ initStaticStringObject(objects[j],strings[j]);
+ argv[j] = &objects[j];
+ }
+
+ struct redisCommand *cmd = lookupCommandLogic(commands,argv,argc);
+ sdsfreesplitres(strings,argc);
+ return cmd;
+}
+
+struct redisCommand *lookupCommandBySds(sds s) {
+ return lookupCommandBySdsLogic(server.commands,s);
+}
+
+struct redisCommand *lookupCommandByCStringLogic(dict *commands, const char *s) {
struct redisCommand *cmd;
sds name = sdsnew(s);
- cmd = dictFetchValue(server.commands, name);
+ cmd = lookupCommandBySdsLogic(commands,name);
sdsfree(name);
return cmd;
}
+struct redisCommand *lookupCommandByCString(const char *s) {
+ return lookupCommandByCStringLogic(server.commands,s);
+}
+
/* Lookup the command in the current table, if not found also check in
* the original table containing the original command names unaffected by
* redis.conf rename-command statement.
@@ -4147,10 +4724,10 @@ struct redisCommand *lookupCommandByCString(const char *s) {
* This is used by functions rewriting the argument vector such as
* rewriteClientCommandVector() in order to set client->cmd pointer
* correctly even if the command was renamed. */
-struct redisCommand *lookupCommandOrOriginal(sds name) {
- struct redisCommand *cmd = dictFetchValue(server.commands, name);
+struct redisCommand *lookupCommandOrOriginal(robj **argv ,int argc) {
+ struct redisCommand *cmd = lookupCommandLogic(server.commands, argv, argc);
- if (!cmd) cmd = dictFetchValue(server.orig_commands,name);
+ if (!cmd) cmd = lookupCommandLogic(server.orig_commands, argv, argc);
return cmd;
}
@@ -4608,19 +5185,26 @@ int processCommand(client *c) {
/* Now lookup the command and check ASAP about trivial error conditions
* such as wrong arity, bad command name and so forth. */
- c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
+ c->cmd = c->lastcmd = lookupCommand(c->argv,c->argc);
if (!c->cmd) {
+ if (lookupCommandBySds(c->argv[0]->ptr)) {
+ /* If we can't find the command but argv[0] by itself is a command
+ * it means we're dealing with an invalid subcommand. Print Help. */
+ addReplySubcommandSyntaxError(c);
+ return C_OK;
+ }
sds args = sdsempty();
int i;
for (i=1; i < c->argc && sdslen(args) < 128; i++)
- args = sdscatprintf(args, "`%.*s`, ", 128-(int)sdslen(args), (char*)c->argv[i]->ptr);
- rejectCommandFormat(c,"unknown command `%s`, with args beginning with: %s",
+ args = sdscatprintf(args, "'%.*s' ", 128-(int)sdslen(args), (char*)c->argv[i]->ptr);
+ rejectCommandFormat(c,"unknown command '%s', with args beginning with: %s",
(char*)c->argv[0]->ptr, args);
sdsfree(args);
return C_OK;
} else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
- (c->argc < -c->cmd->arity)) {
- rejectCommandFormat(c,"wrong number of arguments for '%s' command",
+ (c->argc < -c->cmd->arity))
+ {
+ rejectCommandFormat(c,"wrong number of arguments for '%s' command or subcommand",
c->cmd->name);
return C_OK;
}
@@ -5134,6 +5718,7 @@ void addReplyFlagsForCommand(client *c, struct redisCommand *cmd) {
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_FAST, "fast");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_NO_AUTH, "no_auth");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_MAY_REPLICATE, "may_replicate");
+ /* "sentinel" and "only-sentinel" are hidden on purpose. */
if (cmd->movablekeys) {
addReplyStatus(c, "movablekeys");
flagcount += 1;
@@ -5238,6 +5823,24 @@ void addReplyCommandKeyArgs(client *c, struct redisCommand *cmd) {
}
}
+void addReplyCommand(client *c, struct redisCommand *cmd);
+
+void addReplyCommandSubCommands(client *c, struct redisCommand *cmd) {
+ if (!cmd->subcommands_dict) {
+ addReplySetLen(c, 0);
+ return;
+ }
+
+ addReplyArrayLen(c, dictSize(cmd->subcommands_dict));
+ dictEntry *de;
+ dictIterator *di = dictGetSafeIterator(cmd->subcommands_dict);
+ while((de = dictNext(di)) != NULL) {
+ struct redisCommand *sub = (struct redisCommand *)dictGetVal(de);
+ addReplyCommand(c,sub);
+ }
+ dictReleaseIterator(di);
+}
+
/* Output the representation of a Redis command. Used by the COMMAND command. */
void addReplyCommand(client *c, struct redisCommand *cmd) {
if (!cmd) {
@@ -5251,8 +5854,8 @@ void addReplyCommand(client *c, struct redisCommand *cmd) {
lastkey += firstkey;
keystep = cmd->legacy_range_key_spec.fk.range.keystep;
}
- /* We are adding: command name, arg count, flags, first, last, offset, categories, key args */
- addReplyArrayLen(c, 8);
+ /* We are adding: command name, arg count, flags, first, last, offset, categories, key args, subcommands */
+ addReplyArrayLen(c, 9);
addReplyBulkCString(c, cmd->name);
addReplyLongLong(c, cmd->arity);
addReplyFlagsForCommand(c, cmd);
@@ -5261,70 +5864,188 @@ void addReplyCommand(client *c, struct redisCommand *cmd) {
addReplyLongLong(c, keystep);
addReplyCommandCategories(c,cmd);
addReplyCommandKeyArgs(c,cmd);
+ addReplyCommandSubCommands(c,cmd);
}
}
-/* COMMAND <subcommand> <args> */
+/* Helper for COMMAND(S) command
+ *
+ * COMMAND(S) GETKEYS arg0 arg1 arg2 ... */
+void getKeysSubcommand(client *c) {
+ struct redisCommand *cmd = lookupCommand(c->argv+2,c->argc-2);
+ getKeysResult result = GETKEYS_RESULT_INIT;
+ int j;
+
+ if (!cmd) {
+ addReplyError(c,"Invalid command specified");
+ return;
+ } else if (cmd->getkeys_proc == NULL && cmd->key_specs_num == 0) {
+ addReplyError(c,"The command has no key arguments");
+ return;
+ } else if ((cmd->arity > 0 && cmd->arity != c->argc-2) ||
+ ((c->argc-2) < -cmd->arity))
+ {
+ addReplyError(c,"Invalid number of arguments specified for command");
+ return;
+ }
+
+ if (!getKeysFromCommand(cmd,c->argv+2,c->argc-2,&result)) {
+ addReplyError(c,"Invalid arguments specified for command");
+ } else {
+ addReplyArrayLen(c,result.numkeys);
+ for (j = 0; j < result.numkeys; j++) addReplyBulk(c,c->argv[result.keys[j]+2]);
+ }
+ getKeysFreeResult(&result);
+}
+
+/* COMMAND (no args) */
void commandCommand(client *c) {
dictIterator *di;
dictEntry *de;
- if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
- const char *help[] = {
+ addReplyArrayLen(c, dictSize(server.commands));
+ di = dictGetIterator(server.commands);
+ while ((de = dictNext(di)) != NULL) {
+ addReplyCommand(c, dictGetVal(de));
+ }
+ dictReleaseIterator(di);
+}
+
+/* COMMAND COUNT */
+void commandCountCommand(client *c) {
+ addReplyLongLong(c, dictSize(server.commands));
+}
+
+typedef enum {
+ COMMAND_LIST_FILTER_MODULE,
+ COMMAND_LIST_FILTER_ACLCAT,
+ COMMAND_LIST_FILTER_PATTERN,
+} commandListFilterType;
+
+typedef struct {
+ commandListFilterType type;
+ sds arg;
+ struct {
+ int valid;
+ union {
+ uint64_t aclcat;
+ void *module_handle;
+ } u;
+ } cache;
+} commandListFilter;
+
+int shouldFilterFromCommandList(struct redisCommand *cmd, commandListFilter *filter) {
+ switch (filter->type) {
+ case (COMMAND_LIST_FILTER_MODULE):
+ if (!filter->cache.valid) {
+ filter->cache.u.module_handle = moduleGetHandleByName(filter->arg);
+ filter->cache.valid = 1;
+ }
+ return !moduleIsModuleCommand(filter->cache.u.module_handle, cmd);
+ case (COMMAND_LIST_FILTER_ACLCAT): {
+ if (!filter->cache.valid) {
+ filter->cache.u.aclcat = ACLGetCommandCategoryFlagByName(filter->arg);
+ filter->cache.valid = 1;
+ }
+ uint64_t cat = filter->cache.u.aclcat;
+ if (cat == 0)
+ return 1; /* Invalid ACL category */
+ return (!(cmd->flags & cat));
+ break;
+ }
+ case (COMMAND_LIST_FILTER_PATTERN):
+ return !stringmatchlen(filter->arg, sdslen(filter->arg), cmd->name, strlen(cmd->name), 1);
+ default:
+ serverPanic("Invalid filter type %d", filter->type);
+ }
+}
+
+/* COMMAND LIST [FILTERBY (MODULE <module-name>|ACLCAT <cat>|PATTERN <pattern>)] */
+void commandListCommand(client *c) {
+
+ /* Parse options. */
+ int i = 2, got_filter = 0;
+ commandListFilter filter = {0};
+ for (; i < c->argc; i++) {
+ int moreargs = (c->argc-1) - i; /* Number of additional arguments. */
+ char *opt = c->argv[i]->ptr;
+ if (!strcasecmp(opt,"filterby") && moreargs == 2) {
+ char *filtertype = c->argv[i+1]->ptr;
+ if (!strcasecmp(filtertype,"module")) {
+ filter.type = COMMAND_LIST_FILTER_MODULE;
+ } else if (!strcasecmp(filtertype,"aclcat")) {
+ filter.type = COMMAND_LIST_FILTER_ACLCAT;
+ } else if (!strcasecmp(filtertype,"pattern")) {
+ filter.type = COMMAND_LIST_FILTER_PATTERN;
+ } else {
+ addReplyErrorObject(c,shared.syntaxerr);
+ return;
+ }
+ got_filter = 1;
+ filter.arg = c->argv[i+2]->ptr;
+ i += 2;
+ } else {
+ addReplyErrorObject(c,shared.syntaxerr);
+ return;
+ }
+ }
+
+ dictIterator *di;
+ dictEntry *de;
+
+ di = dictGetIterator(server.commands);
+ if (!got_filter) {
+ addReplySetLen(c, dictSize(server.commands));
+ while ((de = dictNext(di)) != NULL) {
+ struct redisCommand *cmd = dictGetVal(de);
+ addReplyBulkCString(c,cmd->name);
+ }
+ } else {
+ int numcmds = 0;
+ void *replylen = addReplyDeferredLen(c);
+ while ((de = dictNext(di)) != NULL) {
+ struct redisCommand *cmd = dictGetVal(de);
+ if (!shouldFilterFromCommandList(cmd,&filter)) {
+ addReplyBulkCString(c,cmd->name);
+ numcmds++;
+ }
+ }
+ setDeferredArrayLen(c,replylen,numcmds);
+ }
+ dictReleaseIterator(di);
+}
+
+/* COMMAND INFO <command-name> [<command-name> ...] */
+void commandInfoCommand(client *c) {
+ int i;
+ addReplyArrayLen(c, c->argc-2);
+ for (i = 2; i < c->argc; i++) {
+ addReplyCommand(c, lookupCommandBySds(c->argv[i]->ptr));
+ }
+}
+
+/* COMMAND GETKEYS arg0 arg1 arg2 ... */
+void commandGetKeysCommand(client *c) {
+ getKeysSubcommand(c);
+}
+
+/* COMMAND HELP */
+void commandHelpCommand(client *c) {
+ const char *help[] = {
"(no subcommand)",
" Return details about all Redis commands.",
"COUNT",
" Return the total number of commands in this Redis server.",
+"LIST",
+" Return a list of all commands in this Redis server.",
+"INFO <command-name> [<command-name> ...]",
+" Return details about multiple Redis commands.",
"GETKEYS <full-command>",
" Return the keys from a full Redis command.",
-"INFO [<command-name> ...]",
-" Return details about multiple Redis commands.",
NULL
- };
- addReplyHelp(c, help);
- } else if (c->argc == 1) {
- addReplyArrayLen(c, dictSize(server.commands));
- di = dictGetIterator(server.commands);
- while ((de = dictNext(di)) != NULL) {
- addReplyCommand(c, dictGetVal(de));
- }
- dictReleaseIterator(di);
- } else if (!strcasecmp(c->argv[1]->ptr, "info")) {
- int i;
- addReplyArrayLen(c, c->argc-2);
- for (i = 2; i < c->argc; i++) {
- addReplyCommand(c, dictFetchValue(server.commands, c->argv[i]->ptr));
- }
- } else if (!strcasecmp(c->argv[1]->ptr, "count") && c->argc == 2) {
- addReplyLongLong(c, dictSize(server.commands));
- } else if (!strcasecmp(c->argv[1]->ptr,"getkeys") && c->argc >= 3) {
- struct redisCommand *cmd = lookupCommand(c->argv[2]->ptr);
- getKeysResult result = GETKEYS_RESULT_INIT;
- int j;
-
- if (!cmd) {
- addReplyError(c,"Invalid command specified");
- return;
- } else if (cmd->getkeys_proc == NULL && cmd->key_specs_num == 0) {
- addReplyError(c,"The command has no key arguments");
- return;
- } else if ((cmd->arity > 0 && cmd->arity != c->argc-2) ||
- ((c->argc-2) < -cmd->arity))
- {
- addReplyError(c,"Invalid number of arguments specified for command");
- return;
- }
+ };
- if (!getKeysFromCommand(cmd,c->argv+2,c->argc-2,&result)) {
- addReplyError(c,"Invalid arguments specified for command");
- } else {
- addReplyArrayLen(c,result.numkeys);
- for (j = 0; j < result.numkeys; j++) addReplyBulk(c,c->argv[result.keys[j]+2]);
- }
- getKeysFreeResult(&result);
- } else {
- addReplySubcommandSyntaxError(c);
- }
+ addReplyHelp(c, help);
}
/* Convert an amount of bytes into a human readable string in the form
@@ -5356,6 +6077,14 @@ void bytesToHuman(char *s, unsigned long long n) {
}
}
+sds getFullCommandName(struct redisCommand *cmd) {
+ if (!cmd->parent) {
+ return sdsnew(cmd->name);
+ } else {
+ return sdscatfmt(sdsempty(),"%s|%s",cmd->parent->name,cmd->name);
+ }
+}
+
/* Characters we sanitize on INFO output to maintain expected format. */
static char unsafe_info_chars[] = "#:\n\r";
static char unsafe_info_chars_substs[] = "____"; /* Must be same length as above */
@@ -5375,6 +6104,35 @@ const char *getSafeInfoString(const char *s, size_t len, char **tmp) {
sizeof(unsafe_info_chars)-1);
}
+sds genRedisInfoStringCommandStats(sds info, dict *commands) {
+ struct redisCommand *c;
+ dictEntry *de;
+ dictIterator *di;
+ di = dictGetSafeIterator(commands);
+ while((de = dictNext(di)) != NULL) {
+ char *tmpsafe;
+ c = (struct redisCommand *) dictGetVal(de);
+ if (c->calls || c->failed_calls || c->rejected_calls) {
+ sds cmdnamesds = getFullCommandName(c);
+
+ info = sdscatprintf(info,
+ "cmdstat_%s:calls=%lld,usec=%lld,usec_per_call=%.2f"
+ ",rejected_calls=%lld,failed_calls=%lld\r\n",
+ getSafeInfoString(cmdnamesds, sdslen(cmdnamesds), &tmpsafe), c->calls, c->microseconds,
+ (c->calls == 0) ? 0 : ((float)c->microseconds/c->calls),
+ c->rejected_calls, c->failed_calls);
+ if (tmpsafe != NULL) zfree(tmpsafe);
+ sdsfree(cmdnamesds);
+ }
+ if (c->subcommands_dict) {
+ info = genRedisInfoStringCommandStats(info, c->subcommands_dict);
+ }
+ }
+ dictReleaseIterator(di);
+
+ return info;
+}
+
/* Create the string returned by the INFO command. This is decoupled
* by the INFO command itself as we need to report the same information
* on memory corruption problems. */
@@ -6041,25 +6799,7 @@ sds genRedisInfoString(const char *section) {
if (allsections || !strcasecmp(section,"commandstats")) {
if (sections++) info = sdscat(info,"\r\n");
info = sdscatprintf(info, "# Commandstats\r\n");
-
- struct redisCommand *c;
- dictEntry *de;
- dictIterator *di;
- di = dictGetSafeIterator(server.commands);
- while((de = dictNext(di)) != NULL) {
- char *tmpsafe;
- c = (struct redisCommand *) dictGetVal(de);
- if (!c->calls && !c->failed_calls && !c->rejected_calls)
- continue;
- info = sdscatprintf(info,
- "cmdstat_%s:calls=%lld,usec=%lld,usec_per_call=%.2f"
- ",rejected_calls=%lld,failed_calls=%lld\r\n",
- getSafeInfoString(c->name, strlen(c->name), &tmpsafe), c->calls, c->microseconds,
- (c->calls == 0) ? 0 : ((float)c->microseconds/c->calls),
- c->rejected_calls, c->failed_calls);
- if (tmpsafe != NULL) zfree(tmpsafe);
- }
- dictReleaseIterator(di);
+ info = genRedisInfoStringCommandStats(info, server.commands);
}
/* Error statistics */
if (allsections || defsections || !strcasecmp(section,"errorstats")) {
@@ -6120,6 +6860,11 @@ sds genRedisInfoString(const char *section) {
}
void infoCommand(client *c) {
+ if (server.sentinel_mode) {
+ sentinelInfoCommand(c);
+ return;
+ }
+
char *section = c->argc == 2 ? c->argv[1]->ptr : "default";
if (c->argc > 2) {
diff --git a/src/server.h b/src/server.h
index 4b6e776c5..b911697a1 100644
--- a/src/server.h
+++ b/src/server.h
@@ -233,6 +233,9 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
#define CMD_CATEGORY_TRANSACTION (1ULL<<38)
#define CMD_CATEGORY_SCRIPTING (1ULL<<39)
+#define CMD_SENTINEL (1ULL<<40) /* "sentinel" flag */
+#define CMD_ONLY_SENTINEL (1ULL<<41) /* "only-sentinel" flag */
+
/* AOF states */
#define AOF_OFF 0 /* AOF is off */
#define AOF_ON 1 /* AOF is on */
@@ -886,20 +889,29 @@ typedef struct {
uint64_t flags; /* See USER_FLAG_* */
/* The bit in allowed_commands is set if this user has the right to
- * execute this command. In commands having subcommands, if this bit is
- * set, then all the subcommands are also available.
+ * execute this command.
*
* If the bit for a given command is NOT set and the command has
- * subcommands, Redis will also check allowed_subcommands in order to
+ * allowed first-args, Redis will also check allowed_firstargs in order to
* understand if the command can be executed. */
uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];
- /* This array points, for each command ID (corresponding to the command
+ /* NOTE: allowed_firstargs is a transformation of the old mechanism for allowing
+ * subcommands (now, subcommands are actually commands, with their own
+ * ACL ID)
+ * We had to keep allowed_firstargs (previously called allowed_subcommands)
+ * in order to support the widespread abuse of ACL rules to block a command
+ * with a specific argv[1] (which is not a subcommand at all).
+ * For example, a user can use the rule "-select +select|0" to block all
+ * SELECT commands, except "SELECT 0".
+ * It can also be applied for subcommands: "+config -config|set +config|set|loglevel"
+ *
+ * This array points, for each command ID (corresponding to the command
* bit set in allowed_commands), to an array of SDS strings, terminated by
- * a NULL pointer, with all the sub commands that can be executed for
- * this command. When no subcommands matching is used, the field is just
+ * a NULL pointer, with all the first-args that are allowed for
+ * this command. When no first-arg matching is used, the field is just
* set to NULL to avoid allocating USER_COMMAND_BITS_COUNT pointers. */
- sds **allowed_subcommands;
+ sds **allowed_firstargs;
list *passwords; /* A list of SDS valid passwords for this user. */
list *patterns; /* A list of allowed key patterns. If this field is NULL
the user cannot mention any key in a command, unless
@@ -1824,8 +1836,10 @@ struct redisCommand {
char *sflags; /* Flags as string representation, one char per flag. */
keySpec key_specs_static[STATIC_KEY_SPECS_NUM];
/* Use a function to determine keys arguments in a command line.
- * Used for Redis Cluster redirect. */
+ * Used for Redis Cluster redirect (may be NULL) */
redisGetKeysProc *getkeys_proc;
+ /* Array of subcommands (may be NULL) */
+ struct redisCommand *subcommands;
/* Runtime data */
uint64_t flags; /* The actual flags, obtained from the 'sflags' field. */
@@ -1844,6 +1858,8 @@ struct redisCommand {
int key_specs_num;
int key_specs_max;
int movablekeys; /* See populateCommandMovableKeys */
+ dict *subcommands_dict;
+ struct redisCommand *parent;
};
struct redisError {
@@ -1980,6 +1996,8 @@ robj *moduleTypeDupOrReply(client *c, robj *fromkey, robj *tokey, int todb, robj
int moduleDefragValue(robj *key, robj *obj, long *defragged, int dbid);
int moduleLateDefrag(robj *key, robj *value, unsigned long *cursor, long long endtime, long long *defragged, int dbid);
long moduleDefragGlobals(void);
+void *moduleGetHandleByName(char *modulename);
+int moduleIsModuleCommand(void *module_handle, struct redisCommand *cmd);
/* Utils */
long long ustime(void);
@@ -2420,9 +2438,12 @@ void removeSignalHandlers(void);
int createSocketAcceptHandler(socketFds *sfd, aeFileProc *accept_handler);
int changeListenPort(int port, socketFds *sfd, aeFileProc *accept_handler);
int changeBindAddr(sds *addrlist, int addrlist_len);
-struct redisCommand *lookupCommand(sds name);
+struct redisCommand *lookupCommand(robj **argv ,int argc);
+struct redisCommand *lookupCommandBySdsLogic(dict *commands, sds s);
+struct redisCommand *lookupCommandBySds(sds s);
+struct redisCommand *lookupCommandByCStringLogic(dict *commands, const char *s);
struct redisCommand *lookupCommandByCString(const char *s);
-struct redisCommand *lookupCommandOrOriginal(sds name);
+struct redisCommand *lookupCommandOrOriginal(robj **argv ,int argc);
void call(client *c, int flags);
void propagate(int dbid, robj **argv, int argc, int flags);
void alsoPropagate(int dbid, robj **argv, int argc, int target);
@@ -2448,7 +2469,7 @@ void usage(void);
void updateDictResizePolicy(void);
int htNeedsResize(dict *dict);
void populateCommandTable(void);
-void resetCommandTableStats(void);
+void resetCommandTableStats(dict* commands);
void resetErrorTableStats(void);
void adjustOpenFilesLimit(void);
void incrementErrorCount(const char *fullerr, size_t namelen);
@@ -2606,7 +2627,6 @@ int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *
int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
-int memoryGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int lcsGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int lmpopGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int blmpopGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
@@ -2624,6 +2644,10 @@ void queueSentinelConfig(sds *argv, int argc, int linenum, sds line);
void loadSentinelConfigFromQueue(void);
void sentinelIsRunning(void);
void sentinelCheckConfigFile(void);
+void sentinelCommand(client *c);
+void sentinelInfoCommand(client *c);
+void sentinelPublishCommand(client *c);
+void sentinelRoleCommand(client *c);
/* redis-check-rdb & aof */
int redis_check_rdb(char *rdbfilename, FILE *fp);
@@ -2694,6 +2718,11 @@ void authCommand(client *c);
void pingCommand(client *c);
void echoCommand(client *c);
void commandCommand(client *c);
+void commandCountCommand(client *c);
+void commandListCommand(client *c);
+void commandInfoCommand(client *c);
+void commandGetKeysCommand(client *c);
+void commandHelpCommand(client *c);
void setCommand(client *c);
void setnxCommand(client *c);
void setexCommand(client *c);
@@ -2846,7 +2875,11 @@ void hgetallCommand(client *c);
void hexistsCommand(client *c);
void hscanCommand(client *c);
void hrandfieldCommand(client *c);
-void configCommand(client *c);
+void configSetCommand(client *c);
+void configGetCommand(client *c);
+void configResetStatCommand(client *c);
+void configRewriteCommand(client *c);
+void configHelpCommand(client *c);
void hincrbyCommand(client *c);
void hincrbyfloatCommand(client *c);
void subscribeCommand(client *c);
@@ -2937,6 +2970,7 @@ void _serverPanic(const char *file, int line, const char *msg, ...);
#endif
void serverLogObjectDebugInfo(const robj *o);
void sigsegvHandler(int sig, siginfo_t *info, void *secret);
+sds getFullCommandName(struct redisCommand *cmd);
const char *getSafeInfoString(const char *s, size_t len, char **tmp);
sds genRedisInfoString(const char *section);
sds genModulesInfoString(sds info);
@@ -2948,6 +2982,7 @@ int memtest_preserving_test(unsigned long *m, size_t bytes, int passes);
void mixDigest(unsigned char *digest, void *ptr, size_t len);
void xorDigest(unsigned char *digest, void *ptr, size_t len);
int populateSingleCommand(struct redisCommand *c, char *strflags);
+void commandAddSubcommand(struct redisCommand *parent, struct redisCommand *subcommand);
void populateCommandMovableKeys(struct redisCommand *cmd);
void debugDelay(int usec);
void killIOThreads(void);
diff --git a/src/t_string.c b/src/t_string.c
index fd938a9ec..f1c1fdca2 100644
--- a/src/t_string.c
+++ b/src/t_string.c
@@ -724,11 +724,20 @@ void strlenCommand(client *c) {
* STRALGO <algorithm> ... arguments ... */
void stralgoLCS(client *c); /* This implements the LCS algorithm. */
void stralgoCommand(client *c) {
- /* Select the algorithm. */
- if (!strcasecmp(c->argv[1]->ptr,"lcs")) {
+ char *subcmd = c->argv[1]->ptr;
+
+ if (c->argc == 2 && !strcasecmp(subcmd,"help")) {
+ const char *help[] = {
+"LCS",
+" Run the longest common subsequence algorithm.",
+NULL
+ };
+ addReplyHelp(c, help);
+ } else if (!strcasecmp(subcmd,"lcs")) {
stralgoLCS(c);
} else {
- addReplyErrorObject(c,shared.syntaxerr);
+ addReplySubcommandSyntaxError(c);
+ return;
}
}
diff --git a/tests/modules/Makefile b/tests/modules/Makefile
index 56c25dd18..bd51b3b97 100644
--- a/tests/modules/Makefile
+++ b/tests/modules/Makefile
@@ -43,7 +43,8 @@ TEST_MODULES = \
zset.so \
stream.so \
aclcheck.so \
- list.so
+ list.so \
+ subcommands.so
.PHONY: all
diff --git a/tests/modules/subcommands.c b/tests/modules/subcommands.c
new file mode 100644
index 000000000..caee16ca2
--- /dev/null
+++ b/tests/modules/subcommands.c
@@ -0,0 +1,55 @@
+#include "redismodule.h"
+
+#define UNUSED(V) ((void) V)
+
+int cmd_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int cmd_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ int spec_id;
+
+ if (RedisModule_Init(ctx, "subcommands", 1, REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"subcommands.bitarray",NULL,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateSubcommand(ctx,"subcommands.bitarray","set",cmd_set,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_AddCommandKeySpec(ctx,"subcommands.bitarray|set","write",&spec_id) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"subcommands.bitarray|set",spec_id,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"subcommands.bitarray|set",spec_id,0,1,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateSubcommand(ctx,"subcommands.bitarray","get",cmd_get,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_AddCommandKeySpec(ctx,"subcommands.bitarray|get","read",&spec_id) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"subcommands.bitarray|get",spec_id,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"subcommands.bitarray|get",spec_id,0,1,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* Sanity */
+ RedisModule_Assert(RedisModule_CreateSubcommand(ctx,"bitarray","get",NULL,"",0,0,0) == REDISMODULE_ERR);
+ RedisModule_Assert(RedisModule_CreateSubcommand(ctx,"subcommands.bitarray","get",NULL,"",0,0,0) == REDISMODULE_ERR);
+ RedisModule_Assert(RedisModule_CreateSubcommand(ctx,"subcommands.bitarray|get","get",NULL,"",0,0,0) == REDISMODULE_ERR);
+
+ return REDISMODULE_OK;
+}
diff --git a/tests/unit/acl.tcl b/tests/unit/acl.tcl
index f51f59987..03a3ba926 100644
--- a/tests/unit/acl.tcl
+++ b/tests/unit/acl.tcl
@@ -251,12 +251,71 @@ start_server {tags {"acl external:skip"}} {
test {ACLs can include single subcommands} {
r ACL setuser newuser +@all -client
r ACL setuser newuser +client|id +client|setname
+ set cmdstr [dict get [r ACL getuser newuser] commands]
+ assert_match {+@all*-client*+client|id*} $cmdstr
+ assert_match {+@all*-client*+client|setname*} $cmdstr
+ r CLIENT ID; # Should not fail
+ r CLIENT SETNAME foo ; # Should not fail
+ catch {r CLIENT KILL type master} e
+ set e
+ } {*NOPERM*}
+
+ test {ACLs can exclude single subcommands, case 1} {
+ r ACL setuser newuser +@all -client|kill
+ set cmdstr [dict get [r ACL getuser newuser] commands]
+ assert_equal {+@all -client|kill} $cmdstr
r CLIENT ID; # Should not fail
r CLIENT SETNAME foo ; # Should not fail
catch {r CLIENT KILL type master} e
set e
} {*NOPERM*}
+ test {ACLs can exclude single subcommands, case 2} {
+ r ACL setuser newuser -@all +acl +config -config|set
+ set cmdstr [dict get [r ACL getuser newuser] commands]
+ assert_match {*+config*} $cmdstr
+ assert_match {*-config|set*} $cmdstr
+ r CONFIG GET loglevel; # Should not fail
+ catch {r CONFIG SET loglevel debug} e
+ set e
+ } {*NOPERM*}
+
+ test {ACLs can include a subcommand with a specific arg} {
+ r ACL setuser newuser +@all -config|get
+ r ACL setuser newuser +config|get|appendonly
+ set cmdstr [dict get [r ACL getuser newuser] commands]
+ assert_match {*-config|get*} $cmdstr
+ assert_match {*+config|get|appendonly*} $cmdstr
+ r CONFIG GET appendonly; # Should not fail
+ catch {r CONFIG GET loglevel} e
+ set e
+ } {*NOPERM*}
+
+ test {ACLs including of a type includes also subcommands} {
+ r ACL setuser newuser -@all +acl +@stream
+ r XADD key * field value
+ r XINFO STREAM key
+ }
+
+ test {ACLs can block SELECT of all but a specific DB} {
+ r ACL setuser newuser -@all +acl +select|0
+ set cmdstr [dict get [r ACL getuser newuser] commands]
+ assert_match {*+select|0*} $cmdstr
+ r SELECT 0
+ catch {r SELECT 1} e
+ set e
+ } {*NOPERM*}
+
+ test {ACLs can block all DEBUG subcommands except one} {
+ r ACL setuser newuser -@all +acl +incr +debug|object
+ set cmdstr [dict get [r ACL getuser newuser] commands]
+ assert_match {*+debug|object*} $cmdstr
+ r INCR key
+ r DEBUG OBJECT key
+ catch {r DEBUG SEGFAULT} e
+ set e
+ } {*NOPERM*}
+
test {ACLs set can include subcommands, if already full command exists} {
r ACL setuser bob +memory|doctor
set cmdstr [dict get [r ACL getuser bob] commands]
@@ -272,10 +331,56 @@ start_server {tags {"acl external:skip"}} {
# Validate the new commands has got engulfed to +@all.
set cmdstr [dict get [r ACL getuser bob] commands]
assert_equal {+@all} $cmdstr
+
+ r ACL setuser bob >passwd1 on
+ r AUTH bob passwd1
r CLIENT ID; # Should not fail
r MEMORY DOCTOR; # Should not fail
}
+ test {ACLs set can exclude subcommands, if already full command exists} {
+ r ACL setuser alice +@all -memory|doctor
+ set cmdstr [dict get [r ACL getuser alice] commands]
+ assert_equal {+@all -memory|doctor} $cmdstr
+
+ r ACL setuser alice >passwd1 on
+ r AUTH alice passwd1
+
+ catch {r MEMORY DOCTOR} e
+ assert_match {*NOPERM*} $e
+ r MEMORY STATS ;# should work
+
+ # Validate the commands have got engulfed to -memory.
+ r ACL setuser alice +@all -memory
+ set cmdstr [dict get [r ACL getuser alice] commands]
+ assert_equal {+@all -memory} $cmdstr
+
+ catch {r MEMORY DOCTOR} e
+ assert_match {*NOPERM*} $e
+ catch {r MEMORY STATS} e
+ assert_match {*NOPERM*} $e
+
+ # Appending to the existing access string of alice.
+ r ACL setuser alice -@all
+
+ # Now, alice can't do anything, we need to auth newuser to execute ACL GETUSER
+ r AUTH newuser passwd1
+
+ # Validate the new commands has got engulfed to -@all.
+ set cmdstr [dict get [r ACL getuser alice] commands]
+ assert_equal {-@all} $cmdstr
+
+ r AUTH alice passwd1
+
+ catch {r GET key} e
+ assert_match {*NOPERM*} $e
+ catch {r MEMORY STATS} e
+ assert_match {*NOPERM*} $e
+
+ # Auth newuser before the next test
+ r AUTH newuser passwd1
+ }
+
# Note that the order of the generated ACL rules is not stable in Redis
# so we need to match the different parts and not as a whole string.
test {ACL GETUSER is able to translate back command permissions} {
@@ -459,7 +564,7 @@ start_server {tags {"acl external:skip"}} {
test {ACL HELP should not have unexpected options} {
catch {r ACL help xxx} e
- assert_match "*Unknown subcommand or wrong number of arguments*" $e
+ assert_match "*wrong number of arguments*" $e
}
test {Delete a user that the client doesn't use} {
diff --git a/tests/unit/info.tcl b/tests/unit/info.tcl
index 6fafad398..9c76f8f03 100644
--- a/tests/unit/info.tcl
+++ b/tests/unit/info.tcl
@@ -51,7 +51,7 @@ start_server {tags {"info" "external:skip"}} {
assert_equal [s total_error_replies] 0
catch {r eval {redis.pcall('XGROUP', 'CREATECONSUMER', 's1', 'mygroup', 'consumer') return } 0} e
assert_match {*count=1*} [errorstat ERR]
- assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat xgroup]
+ assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat xgroup\\|createconsumer]
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} [cmdstat eval]
# EVAL command errors should still be pinpointed to him
@@ -83,7 +83,7 @@ start_server {tags {"info" "external:skip"}} {
catch {r XGROUP CREATECONSUMER mystream mygroup consumer} e
assert_match {NOGROUP*} $e
assert_match {*count=1*} [errorstat NOGROUP]
- assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat xgroup]
+ assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat xgroup\\|createconsumer]
r config resetstat
assert_match {} [errorstat NOGROUP]
}
diff --git a/tests/unit/introspection-2.tcl b/tests/unit/introspection-2.tcl
index 60f932d7d..478631c3f 100644
--- a/tests/unit/introspection-2.tcl
+++ b/tests/unit/introspection-2.tcl
@@ -76,4 +76,26 @@ start_server {tags {"introspection"}} {
assert_match {*calls=1,*} [cmdstat expire]
assert_match {*calls=1,*} [cmdstat geoadd]
} {} {needs:config-resetstat}
+
+ test {COMMAND GETKEYS GET} {
+ assert_equal {key} [r command getkeys get key]
+ }
+
+ test {COMMAND GETKEYS MEMORY USAGE} {
+ assert_equal {key} [r command getkeys memory usage key]
+ }
+
+ test {COMMAND GETKEYS XGROUP} {
+ assert_equal {key} [r command getkeys xgroup create key groupname $]
+ }
+
+ test "COMMAND LIST FILTERBY ACLCAT" {
+ set reply [r command list filterby aclcat hyperloglog]
+ assert_equal [lsort $reply] {pfadd pfcount pfdebug pfmerge pfselftest}
+ }
+
+ test "COMMAND LIST FILTERBY PATTERN" {
+ set reply [r command list filterby pattern pf*]
+ assert_equal [lsort $reply] {pfadd pfcount pfdebug pfmerge pfselftest}
+ }
}
diff --git a/tests/unit/latency-monitor.tcl b/tests/unit/latency-monitor.tcl
index 50f7e9618..5002d9ffe 100644
--- a/tests/unit/latency-monitor.tcl
+++ b/tests/unit/latency-monitor.tcl
@@ -74,6 +74,6 @@ start_server {tags {"latency-monitor needs:latency"}} {
test {LATENCY HELP should not have unexpected options} {
catch {r LATENCY help xxx} e
- assert_match "*Unknown subcommand or wrong number of arguments*" $e
+ assert_match "*wrong number of arguments*" $e
}
}
diff --git a/tests/unit/moduleapi/keyspecs.tcl b/tests/unit/moduleapi/keyspecs.tcl
index 5178579eb..5f43e78ab 100644
--- a/tests/unit/moduleapi/keyspecs.tcl
+++ b/tests/unit/moduleapi/keyspecs.tcl
@@ -5,16 +5,22 @@ start_server {tags {"modules"}} {
test "Module key specs: Legacy" {
set reply [r command info kspec.legacy]
- assert_equal $reply {{kspec.legacy -1 {} 1 2 1 {} {{flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}}}}
+ assert_equal $reply {{kspec.legacy -1 {} 1 2 1 {} {{flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}} {}}}
}
test "Module key specs: Complex specs, case 1" {
set reply [r command info kspec.complex1]
- assert_equal $reply {{kspec.complex1 -1 movablekeys 1 1 1 {} {{flags {} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type keyword spec {keyword STORE startfrom 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type keyword spec {keyword KEYS startfrom 2}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}}}}}
+ assert_equal $reply {{kspec.complex1 -1 movablekeys 1 1 1 {} {{flags {} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type keyword spec {keyword STORE startfrom 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type keyword spec {keyword KEYS startfrom 2}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}}} {}}}
}
test "Module key specs: Complex specs, case 2" {
set reply [r command info kspec.complex2]
- assert_equal $reply {{kspec.complex2 -1 movablekeys 1 2 1 {} {{flags write begin_search {type keyword spec {keyword STORE startfrom 5}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type index spec {index 3}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}} {flags write begin_search {type keyword spec {keyword MOREKEYS startfrom 5}} find_keys {type range spec {lastkey -1 keystep 1 limit 0}}}}}}
+ assert_equal $reply {{kspec.complex2 -1 movablekeys 1 2 1 {} {{flags write begin_search {type keyword spec {keyword STORE startfrom 5}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type index spec {index 3}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}} {flags write begin_search {type keyword spec {keyword MOREKEYS startfrom 5}} find_keys {type range spec {lastkey -1 keystep 1 limit 0}}}} {}}}
+ }
+
+ test "Module command list filtering" {
+ ;# Note: we piggyback this tcl file to test the general functionality of command list filtering
+ set reply [r command list filterby module keyspecs]
+ assert_equal [lsort $reply] {kspec.complex1 kspec.complex2 kspec.legacy}
}
}
diff --git a/tests/unit/moduleapi/subcommands.tcl b/tests/unit/moduleapi/subcommands.tcl
new file mode 100644
index 000000000..ab8d67416
--- /dev/null
+++ b/tests/unit/moduleapi/subcommands.tcl
@@ -0,0 +1,19 @@
+set testmodule [file normalize tests/modules/subcommands.so]
+
+start_server {tags {"modules"}} {
+ r module load $testmodule
+
+ test "Module subcommands via COMMAND" {
+ set reply [r command info subcommands.bitarray]
+ set subcmds [lindex [lindex $reply 0] 8]
+ assert_equal [lsort $subcmds] {{get -2 {} 1 1 1 {} {{flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}} {}} {set -2 {} 1 1 1 {} {{flags write begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}} {}}}
+ }
+
+ test "Module pure-container command fails on arity error" {
+ catch {r subcommands.bitarray} e
+ assert_match {*wrong number of arguments*} $e
+
+ # Subcommands can be called
+ assert_equal [r subcommands.bitarray get k1] {OK}
+ }
+}
diff --git a/tests/unit/type/stream.tcl b/tests/unit/type/stream.tcl
index 98cdbacef..0a30393f3 100644
--- a/tests/unit/type/stream.tcl
+++ b/tests/unit/type/stream.tcl
@@ -712,11 +712,11 @@ start_server {tags {"stream needs:debug"} overrides {appendonly yes aof-use-rdb-
start_server {tags {"stream"}} {
test {XGROUP HELP should not have unexpected options} {
catch {r XGROUP help xxx} e
- assert_match "*Unknown subcommand or wrong number of arguments*" $e
+ assert_match "*wrong number of arguments*" $e
}
test {XINFO HELP should not have unexpected options} {
catch {r XINFO help xxx} e
- assert_match "*Unknown subcommand or wrong number of arguments*" $e
+ assert_match "*wrong number of arguments*" $e
}
}