diff --git a/src/commands.c b/src/commands.c index ea1ee52f6..8eebd2d57 100644 --- a/src/commands.c +++ b/src/commands.c @@ -958,6 +958,27 @@ struct redisCommandArg CLIENT_REPLY_Args[] = { {0} }; +/********** CLIENT SETINFO ********************/ + +/* CLIENT SETINFO history */ +#define CLIENT_SETINFO_History NULL + +/* CLIENT SETINFO tips */ +#define CLIENT_SETINFO_tips NULL + +/* CLIENT SETINFO attr argument table */ +struct redisCommandArg CLIENT_SETINFO_attr_Subargs[] = { +{"libname",ARG_TYPE_STRING,-1,"LIB-NAME",NULL,NULL,CMD_ARG_NONE}, +{"libver",ARG_TYPE_STRING,-1,"LIB-VER",NULL,NULL,CMD_ARG_NONE}, +{0} +}; + +/* CLIENT SETINFO argument table */ +struct redisCommandArg CLIENT_SETINFO_Args[] = { +{"attr",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_NONE,.subargs=CLIENT_SETINFO_attr_Subargs}, +{0} +}; + /********** CLIENT SETNAME ********************/ /* CLIENT SETNAME history */ @@ -1051,6 +1072,7 @@ struct redisCommand CLIENT_Subcommands[] = { {"no-touch","Controls whether commands sent by the client will alter the LRU/LFU of the keys they access.","O(1)","7.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_NO_TOUCH_History,CLIENT_NO_TOUCH_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,ACL_CATEGORY_CONNECTION,.args=CLIENT_NO_TOUCH_Args}, {"pause","Stop processing commands from clients for some time","O(1)","3.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_PAUSE_History,CLIENT_PAUSE_tips,clientCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_PAUSE_Args}, {"reply","Instruct the server whether to reply to commands","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_REPLY_History,CLIENT_REPLY_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_REPLY_Args}, +{"setinfo","Set client or connection specific info","O(1)","7.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_SETINFO_History,CLIENT_SETINFO_tips,clientSetinfoCommand,4,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_SETINFO_Args}, {"setname","Set the current connection name","O(1)","2.6.9",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_SETNAME_History,CLIENT_SETNAME_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_SETNAME_Args}, {"tracking","Enable or disable server assisted client side caching support","O(1). Some options may introduce additional complexity.","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_TRACKING_History,CLIENT_TRACKING_tips,clientCommand,-3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_TRACKING_Args}, {"trackinginfo","Return information about server assisted client side caching for the current connection","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_TRACKINGINFO_History,CLIENT_TRACKINGINFO_tips,clientCommand,2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION}, diff --git a/src/commands/client-setinfo.json b/src/commands/client-setinfo.json new file mode 100644 index 000000000..426ff4d28 --- /dev/null +++ b/src/commands/client-setinfo.json @@ -0,0 +1,41 @@ +{ + "SETINFO": { + "summary": "Set client or connection specific info", + "complexity": "O(1)", + "group": "connection", + "since": "7.2.0", + "arity": 4, + "container": "CLIENT", + "function": "clientSetinfoCommand", + "command_flags": [ + "NOSCRIPT", + "LOADING", + "STALE", + "SENTINEL" + ], + "acl_categories": [ + "CONNECTION" + ], + "reply_schema": { + "const": "OK" + }, + "arguments": [ + { + "name": "attr", + "type": "oneof", + "arguments": [ + { + "token": "lib-name", + "name": "libname", + "type": "string" + }, + { + "token": "lib-ver", + "name": "libver", + "type": "string" + } + ] + } + ] + } +} diff --git a/src/networking.c b/src/networking.c index ed14fff72..3b66dd59e 100644 --- a/src/networking.c +++ b/src/networking.c @@ -147,6 +147,8 @@ client *createClient(connection *conn) { #endif c->conn = conn; c->name = NULL; + c->lib_name = NULL; + c->lib_ver = NULL; c->bufpos = 0; c->buf_usable_size = zmalloc_usable_size(c->buf); c->buf_peak = c->buf_usable_size; @@ -1511,6 +1513,9 @@ void clearClientConnectionState(client *c) { c->name = NULL; } + /* Note: lib_name and lib_ver are not reset since they still + * represent the client library behind the connection. */ + /* Selectively clear state flags not covered above */ c->flags &= ~(CLIENT_ASKING|CLIENT_READONLY|CLIENT_PUBSUB|CLIENT_REPLY_OFF| CLIENT_REPLY_SKIP_NEXT|CLIENT_NO_TOUCH|CLIENT_NO_EVICT); @@ -1662,6 +1667,8 @@ void freeClient(client *c) { /* Release other dynamically allocated client structure fields, * and finally release the client structure itself. */ if (c->name) decrRefCount(c->name); + if (c->lib_name) decrRefCount(c->lib_name); + if (c->lib_ver) decrRefCount(c->lib_ver); freeClientMultiState(c); sdsfree(c->peerid); sdsfree(c->sockname); @@ -2775,7 +2782,7 @@ sds catClientInfoString(sds s, client *client) { } 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 ssub=%i multi=%i qbuf=%U qbuf-free=%U argv-mem=%U multi-mem=%U rbs=%U rbp=%U obl=%U oll=%U omem=%U tot-mem=%U events=%s cmd=%s user=%s redir=%I resp=%i", + "id=%U addr=%s laddr=%s %s name=%s age=%I idle=%I flags=%s db=%i sub=%i psub=%i ssub=%i multi=%i qbuf=%U qbuf-free=%U argv-mem=%U multi-mem=%U rbs=%U rbp=%U obl=%U oll=%U omem=%U tot-mem=%U events=%s cmd=%s user=%s redir=%I resp=%i lib-name=%s lib-ver=%s", (unsigned long long) client->id, getClientPeerId(client), getClientSockname(client), @@ -2803,7 +2810,10 @@ sds catClientInfoString(sds s, client *client) { client->lastcmd ? client->lastcmd->fullname : "NULL", client->user ? client->user->name : "(superuser)", (client->flags & CLIENT_TRACKING) ? (long long) client->client_tracking_redirection : -1, - client->resp); + client->resp, + client->lib_name ? (char*)client->lib_name->ptr : "", + client->lib_ver ? (char*)client->lib_ver->ptr : "" + ); return ret; } @@ -2823,6 +2833,20 @@ sds getAllClientsInfoString(int type) { return o; } +/* Check validity of an attribute that's gonna be shown in CLIENT LIST. */ +int validateClientAttr(const char *val) { + /* Check if the charset is ok. We need to do this otherwise + * CLIENT LIST format will break. You should always be able to + * split by space to get the different fields. */ + while (*val) { + if (*val < '!' || *val > '~') { /* ASCII is assumed. */ + return C_ERR; + } + val++; + } + return C_OK; +} + /* Returns C_OK if the name is valid. Returns C_ERR & sets `err` (when provided) otherwise. */ int validateClientName(robj *name, const char **err) { const char *err_msg = "Client names cannot contain spaces, newlines or special characters."; @@ -2830,15 +2854,9 @@ int validateClientName(robj *name, const char **err) { /* We allow setting the client name to an empty string. */ if (len == 0) return C_OK; - /* Otherwise check if the charset is ok. We need to do this otherwise - * CLIENT LIST format will break. You should always be able to - * split by space to get the different fields. */ - char *p = name->ptr; - for (int j = 0; j < len; j++) { - if (p[j] < '!' || p[j] > '~') { /* ASCII is assumed. */ - if (err) *err = err_msg; - return C_ERR; - } + if (validateClientAttr(name->ptr) == C_ERR) { + if (err) *err = err_msg; + return C_ERR; } return C_OK; } @@ -2880,6 +2898,35 @@ int clientSetNameOrReply(client *c, robj *name) { return result; } +/* Set client or connection related info */ +void clientSetinfoCommand(client *c) { + sds attr = c->argv[2]->ptr; + robj *valob = c->argv[3]; + sds val = valob->ptr; + robj **destvar = NULL; + if (!strcasecmp(attr,"lib-name")) { + destvar = &c->lib_name; + } else if (!strcasecmp(attr,"lib-ver")) { + destvar = &c->lib_ver; + } else { + addReplyStatusFormat(c,"Unrecognized option '%s'", attr); + return; + } + + if (validateClientAttr(val)==C_ERR) { + addReplyStatusFormat(c, + "%s cannot contain spaces, newlines or special characters.", attr); + return; + } + if (*destvar) decrRefCount(*destvar); + if (sdslen(val)) { + *destvar = valob; + incrRefCount(valob); + } else + *destvar = NULL; + addReply(c,shared.ok); +} + /* Reset the client state to resemble a newly connected client. */ void resetCommand(client *c) { diff --git a/src/server.h b/src/server.h index 0cc15f3e3..1a68071f1 100644 --- a/src/server.h +++ b/src/server.h @@ -1151,6 +1151,8 @@ typedef struct client { int resp; /* RESP protocol version. Can be 2 or 3. */ redisDb *db; /* Pointer to currently SELECTed DB. */ robj *name; /* As set by CLIENT SETNAME. */ + robj *lib_name; /* The client library name as set by CLIENT SETINFO. */ + robj *lib_ver; /* The client library version as set by CLIENT SETINFO. */ sds querybuf; /* Buffer we use to accumulate client queries. */ size_t qb_pos; /* The position we have read in querybuf. */ size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size. */ @@ -3639,6 +3641,7 @@ void objectCommand(client *c); void memoryCommand(client *c); void clientCommand(client *c); void helloCommand(client *c); +void clientSetinfoCommand(client *c); void evalCommand(client *c); void evalRoCommand(client *c); void evalShaCommand(client *c); diff --git a/tests/unit/introspection.tcl b/tests/unit/introspection.tcl index 10d3a15e9..c8bc3eb89 100644 --- a/tests/unit/introspection.tcl +++ b/tests/unit/introspection.tcl @@ -325,6 +325,31 @@ start_server {tags {"introspection"}} { } } + test {CLIENT SETINFO can set a library name to this connection} { + r CLIENT SETINFO lib-name redis.py + r CLIENT SETINFO lib-ver 1.2.3 + r client info + } {*lib-name=redis.py lib-ver=1.2.3*} + + test {CLIENT SETINFO invalid args} { + assert_error {*wrong number of arguments*} {r CLIENT SETINFO lib-name} + assert_match {*cannot contain spaces*} [r CLIENT SETINFO lib-name "redis py"] + assert_match {*newlines*} [r CLIENT SETINFO lib-name "redis.py\n"] + assert_match {*Unrecognized*} [r CLIENT SETINFO badger hamster] + # test that all of these didn't affect the previously set values + r client info + } {*lib-name=redis.py lib-ver=1.2.3*} + + test {RESET doesn NOT clean library name} { + r reset + r client info + } {*lib-name=redis.py*} + + test {CLIENT SETINFO can clear library name} { + r CLIENT SETINFO lib-name "" + r client info + } {*lib-name= *} + test {CONFIG save params special case handled properly} { # No "save" keyword - defaults should apply start_server {config "minimal.conf"} {