xref: /redis-3.2.3/src/config.c (revision 5e1d2d30)
1 #include "redis.h"
2 
3 /*-----------------------------------------------------------------------------
4  * Config file parsing
5  *----------------------------------------------------------------------------*/
6 
7 int yesnotoi(char *s) {
8     if (!strcasecmp(s,"yes")) return 1;
9     else if (!strcasecmp(s,"no")) return 0;
10     else return -1;
11 }
12 
13 void appendServerSaveParams(time_t seconds, int changes) {
14     server.saveparams = zrealloc(server.saveparams,sizeof(struct saveparam)*(server.saveparamslen+1));
15     server.saveparams[server.saveparamslen].seconds = seconds;
16     server.saveparams[server.saveparamslen].changes = changes;
17     server.saveparamslen++;
18 }
19 
20 void resetServerSaveParams() {
21     zfree(server.saveparams);
22     server.saveparams = NULL;
23     server.saveparamslen = 0;
24 }
25 
26 /* I agree, this is a very rudimental way to load a configuration...
27    will improve later if the config gets more complex */
28 void loadServerConfig(char *filename) {
29     FILE *fp;
30     char buf[REDIS_CONFIGLINE_MAX+1], *err = NULL;
31     int linenum = 0;
32     sds line = NULL;
33 
34     if (filename[0] == '-' && filename[1] == '\0')
35         fp = stdin;
36     else {
37         if ((fp = fopen(filename,"r")) == NULL) {
38             redisLog(REDIS_WARNING, "Fatal error, can't open config file '%s'", filename);
39             exit(1);
40         }
41     }
42 
43     while(fgets(buf,REDIS_CONFIGLINE_MAX+1,fp) != NULL) {
44         sds *argv;
45         int argc, j;
46 
47         linenum++;
48         line = sdsnew(buf);
49         line = sdstrim(line," \t\r\n");
50 
51         /* Skip comments and blank lines*/
52         if (line[0] == '#' || line[0] == '\0') {
53             sdsfree(line);
54             continue;
55         }
56 
57         /* Split into arguments */
58         argv = sdssplitargs(line,&argc);
59         sdstolower(argv[0]);
60 
61         /* Execute config directives */
62         if (!strcasecmp(argv[0],"timeout") && argc == 2) {
63             server.maxidletime = atoi(argv[1]);
64             if (server.maxidletime < 0) {
65                 err = "Invalid timeout value"; goto loaderr;
66             }
67         } else if (!strcasecmp(argv[0],"port") && argc == 2) {
68             server.port = atoi(argv[1]);
69             if (server.port < 1 || server.port > 65535) {
70                 err = "Invalid port"; goto loaderr;
71             }
72         } else if (!strcasecmp(argv[0],"bind") && argc == 2) {
73             server.bindaddr = zstrdup(argv[1]);
74         } else if (!strcasecmp(argv[0],"unixsocket") && argc == 2) {
75             server.unixsocket = zstrdup(argv[1]);
76         } else if (!strcasecmp(argv[0],"save") && argc == 3) {
77             int seconds = atoi(argv[1]);
78             int changes = atoi(argv[2]);
79             if (seconds < 1 || changes < 0) {
80                 err = "Invalid save parameters"; goto loaderr;
81             }
82             appendServerSaveParams(seconds,changes);
83         } else if (!strcasecmp(argv[0],"dir") && argc == 2) {
84             if (chdir(argv[1]) == -1) {
85                 redisLog(REDIS_WARNING,"Can't chdir to '%s': %s",
86                     argv[1], strerror(errno));
87                 exit(1);
88             }
89         } else if (!strcasecmp(argv[0],"loglevel") && argc == 2) {
90             if (!strcasecmp(argv[1],"debug")) server.verbosity = REDIS_DEBUG;
91             else if (!strcasecmp(argv[1],"verbose")) server.verbosity = REDIS_VERBOSE;
92             else if (!strcasecmp(argv[1],"notice")) server.verbosity = REDIS_NOTICE;
93             else if (!strcasecmp(argv[1],"warning")) server.verbosity = REDIS_WARNING;
94             else {
95                 err = "Invalid log level. Must be one of debug, notice, warning";
96                 goto loaderr;
97             }
98         } else if (!strcasecmp(argv[0],"logfile") && argc == 2) {
99             FILE *logfp;
100 
101             server.logfile = zstrdup(argv[1]);
102             if (!strcasecmp(server.logfile,"stdout")) {
103                 zfree(server.logfile);
104                 server.logfile = NULL;
105             }
106             if (server.logfile) {
107                 /* Test if we are able to open the file. The server will not
108                  * be able to abort just for this problem later... */
109                 logfp = fopen(server.logfile,"a");
110                 if (logfp == NULL) {
111                     err = sdscatprintf(sdsempty(),
112                         "Can't open the log file: %s", strerror(errno));
113                     goto loaderr;
114                 }
115                 fclose(logfp);
116             }
117         } else if (!strcasecmp(argv[0],"syslog-enabled") && argc == 2) {
118             if ((server.syslog_enabled = yesnotoi(argv[1])) == -1) {
119                 err = "argument must be 'yes' or 'no'"; goto loaderr;
120             }
121         } else if (!strcasecmp(argv[0],"syslog-ident") && argc == 2) {
122             if (server.syslog_ident) zfree(server.syslog_ident);
123             server.syslog_ident = zstrdup(argv[1]);
124         } else if (!strcasecmp(argv[0],"syslog-facility") && argc == 2) {
125             struct {
126                 const char     *name;
127                 const int       value;
128             } validSyslogFacilities[] = {
129                 {"user",    LOG_USER},
130                 {"local0",  LOG_LOCAL0},
131                 {"local1",  LOG_LOCAL1},
132                 {"local2",  LOG_LOCAL2},
133                 {"local3",  LOG_LOCAL3},
134                 {"local4",  LOG_LOCAL4},
135                 {"local5",  LOG_LOCAL5},
136                 {"local6",  LOG_LOCAL6},
137                 {"local7",  LOG_LOCAL7},
138                 {NULL, 0}
139             };
140             int i;
141 
142             for (i = 0; validSyslogFacilities[i].name; i++) {
143                 if (!strcasecmp(validSyslogFacilities[i].name, argv[1])) {
144                     server.syslog_facility = validSyslogFacilities[i].value;
145                     break;
146                 }
147             }
148 
149             if (!validSyslogFacilities[i].name) {
150                 err = "Invalid log facility. Must be one of USER or between LOCAL0-LOCAL7";
151                 goto loaderr;
152             }
153         } else if (!strcasecmp(argv[0],"databases") && argc == 2) {
154             server.dbnum = atoi(argv[1]);
155             if (server.dbnum < 1) {
156                 err = "Invalid number of databases"; goto loaderr;
157             }
158         } else if (!strcasecmp(argv[0],"include") && argc == 2) {
159             loadServerConfig(argv[1]);
160         } else if (!strcasecmp(argv[0],"maxclients") && argc == 2) {
161             server.maxclients = atoi(argv[1]);
162         } else if (!strcasecmp(argv[0],"maxmemory") && argc == 2) {
163             server.maxmemory = memtoll(argv[1],NULL);
164         } else if (!strcasecmp(argv[0],"maxmemory-policy") && argc == 2) {
165             if (!strcasecmp(argv[1],"volatile-lru")) {
166                 server.maxmemory_policy = REDIS_MAXMEMORY_VOLATILE_LRU;
167             } else if (!strcasecmp(argv[1],"volatile-random")) {
168                 server.maxmemory_policy = REDIS_MAXMEMORY_VOLATILE_RANDOM;
169             } else if (!strcasecmp(argv[1],"volatile-ttl")) {
170                 server.maxmemory_policy = REDIS_MAXMEMORY_VOLATILE_TTL;
171             } else if (!strcasecmp(argv[1],"allkeys-lru")) {
172                 server.maxmemory_policy = REDIS_MAXMEMORY_ALLKEYS_LRU;
173             } else if (!strcasecmp(argv[1],"allkeys-random")) {
174                 server.maxmemory_policy = REDIS_MAXMEMORY_ALLKEYS_RANDOM;
175             } else if (!strcasecmp(argv[1],"noeviction")) {
176                 server.maxmemory_policy = REDIS_MAXMEMORY_NO_EVICTION;
177             } else {
178                 err = "Invalid maxmemory policy";
179                 goto loaderr;
180             }
181         } else if (!strcasecmp(argv[0],"maxmemory-samples") && argc == 2) {
182             server.maxmemory_samples = atoi(argv[1]);
183             if (server.maxmemory_samples <= 0) {
184                 err = "maxmemory-samples must be 1 or greater";
185                 goto loaderr;
186             }
187         } else if (!strcasecmp(argv[0],"slaveof") && argc == 3) {
188             server.masterhost = sdsnew(argv[1]);
189             server.masterport = atoi(argv[2]);
190             server.replstate = REDIS_REPL_CONNECT;
191         } else if (!strcasecmp(argv[0],"masterauth") && argc == 2) {
192         	server.masterauth = zstrdup(argv[1]);
193         } else if (!strcasecmp(argv[0],"slave-serve-stale-data") && argc == 2) {
194             if ((server.repl_serve_stale_data = yesnotoi(argv[1])) == -1) {
195                 err = "argument must be 'yes' or 'no'"; goto loaderr;
196             }
197         } else if (!strcasecmp(argv[0],"glueoutputbuf")) {
198             redisLog(REDIS_WARNING, "Deprecated configuration directive: \"%s\"", argv[0]);
199         } else if (!strcasecmp(argv[0],"rdbcompression") && argc == 2) {
200             if ((server.rdbcompression = yesnotoi(argv[1])) == -1) {
201                 err = "argument must be 'yes' or 'no'"; goto loaderr;
202             }
203         } else if (!strcasecmp(argv[0],"activerehashing") && argc == 2) {
204             if ((server.activerehashing = yesnotoi(argv[1])) == -1) {
205                 err = "argument must be 'yes' or 'no'"; goto loaderr;
206             }
207         } else if (!strcasecmp(argv[0],"daemonize") && argc == 2) {
208             if ((server.daemonize = yesnotoi(argv[1])) == -1) {
209                 err = "argument must be 'yes' or 'no'"; goto loaderr;
210             }
211         } else if (!strcasecmp(argv[0],"appendonly") && argc == 2) {
212             if ((server.appendonly = yesnotoi(argv[1])) == -1) {
213                 err = "argument must be 'yes' or 'no'"; goto loaderr;
214             }
215         } else if (!strcasecmp(argv[0],"appendfilename") && argc == 2) {
216             zfree(server.appendfilename);
217             server.appendfilename = zstrdup(argv[1]);
218         } else if (!strcasecmp(argv[0],"no-appendfsync-on-rewrite")
219                    && argc == 2) {
220             if ((server.no_appendfsync_on_rewrite= yesnotoi(argv[1])) == -1) {
221                 err = "argument must be 'yes' or 'no'"; goto loaderr;
222             }
223         } else if (!strcasecmp(argv[0],"appendfsync") && argc == 2) {
224             if (!strcasecmp(argv[1],"no")) {
225                 server.appendfsync = APPENDFSYNC_NO;
226             } else if (!strcasecmp(argv[1],"always")) {
227                 server.appendfsync = APPENDFSYNC_ALWAYS;
228             } else if (!strcasecmp(argv[1],"everysec")) {
229                 server.appendfsync = APPENDFSYNC_EVERYSEC;
230             } else {
231                 err = "argument must be 'no', 'always' or 'everysec'";
232                 goto loaderr;
233             }
234         } else if (!strcasecmp(argv[0],"requirepass") && argc == 2) {
235             server.requirepass = zstrdup(argv[1]);
236         } else if (!strcasecmp(argv[0],"pidfile") && argc == 2) {
237             zfree(server.pidfile);
238             server.pidfile = zstrdup(argv[1]);
239         } else if (!strcasecmp(argv[0],"dbfilename") && argc == 2) {
240             zfree(server.dbfilename);
241             server.dbfilename = zstrdup(argv[1]);
242         } else if (!strcasecmp(argv[0],"diskstore-enabled") && argc == 2) {
243             if ((server.ds_enabled = yesnotoi(argv[1])) == -1) {
244                 err = "argument must be 'yes' or 'no'"; goto loaderr;
245             }
246         } else if (!strcasecmp(argv[0],"diskstore-path") && argc == 2) {
247             sdsfree(server.ds_path);
248             server.ds_path = sdsnew(argv[1]);
249         } else if (!strcasecmp(argv[0],"cache-max-memory") && argc == 2) {
250             server.cache_max_memory = memtoll(argv[1],NULL);
251         } else if (!strcasecmp(argv[0],"cache-flush-delay") && argc == 2) {
252             server.cache_flush_delay = atoi(argv[1]);
253             if (server.cache_flush_delay < 0) server.cache_flush_delay = 0;
254         } else if (!strcasecmp(argv[0],"hash-max-zipmap-entries") && argc == 2) {
255             server.hash_max_zipmap_entries = memtoll(argv[1], NULL);
256         } else if (!strcasecmp(argv[0],"hash-max-zipmap-value") && argc == 2) {
257             server.hash_max_zipmap_value = memtoll(argv[1], NULL);
258         } else if (!strcasecmp(argv[0],"list-max-ziplist-entries") && argc == 2){
259             server.list_max_ziplist_entries = memtoll(argv[1], NULL);
260         } else if (!strcasecmp(argv[0],"list-max-ziplist-value") && argc == 2) {
261             server.list_max_ziplist_value = memtoll(argv[1], NULL);
262         } else if (!strcasecmp(argv[0],"set-max-intset-entries") && argc == 2) {
263             server.set_max_intset_entries = memtoll(argv[1], NULL);
264         } else if (!strcasecmp(argv[0],"rename-command") && argc == 3) {
265             struct redisCommand *cmd = lookupCommand(argv[1]);
266             int retval;
267 
268             if (!cmd) {
269                 err = "No such command in rename-command";
270                 goto loaderr;
271             }
272 
273             /* If the target command name is the emtpy string we just
274              * remove it from the command table. */
275             retval = dictDelete(server.commands, argv[1]);
276             redisAssert(retval == DICT_OK);
277 
278             /* Otherwise we re-add the command under a different name. */
279             if (sdslen(argv[2]) != 0) {
280                 sds copy = sdsdup(argv[2]);
281 
282                 retval = dictAdd(server.commands, copy, cmd);
283                 if (retval != DICT_OK) {
284                     sdsfree(copy);
285                     err = "Target command name already exists"; goto loaderr;
286                 }
287             }
288         } else {
289             err = "Bad directive or wrong number of arguments"; goto loaderr;
290         }
291         for (j = 0; j < argc; j++)
292             sdsfree(argv[j]);
293         zfree(argv);
294         sdsfree(line);
295     }
296     if (fp != stdin) fclose(fp);
297     return;
298 
299 loaderr:
300     fprintf(stderr, "\n*** FATAL CONFIG FILE ERROR ***\n");
301     fprintf(stderr, "Reading the configuration file, at line %d\n", linenum);
302     fprintf(stderr, ">>> '%s'\n", line);
303     fprintf(stderr, "%s\n", err);
304     exit(1);
305 }
306 
307 /*-----------------------------------------------------------------------------
308  * CONFIG command for remote configuration
309  *----------------------------------------------------------------------------*/
310 
311 void configSetCommand(redisClient *c) {
312     robj *o;
313     long long ll;
314     redisAssert(c->argv[2]->encoding == REDIS_ENCODING_RAW);
315     redisAssert(c->argv[3]->encoding == REDIS_ENCODING_RAW);
316     o = c->argv[3];
317 
318     if (!strcasecmp(c->argv[2]->ptr,"dbfilename")) {
319         zfree(server.dbfilename);
320         server.dbfilename = zstrdup(o->ptr);
321     } else if (!strcasecmp(c->argv[2]->ptr,"requirepass")) {
322         zfree(server.requirepass);
323         server.requirepass = zstrdup(o->ptr);
324     } else if (!strcasecmp(c->argv[2]->ptr,"masterauth")) {
325         zfree(server.masterauth);
326         server.masterauth = zstrdup(o->ptr);
327     } else if (!strcasecmp(c->argv[2]->ptr,"maxmemory")) {
328         if (getLongLongFromObject(o,&ll) == REDIS_ERR ||
329             ll < 0) goto badfmt;
330         server.maxmemory = ll;
331         if (server.maxmemory) freeMemoryIfNeeded();
332     } else if (!strcasecmp(c->argv[2]->ptr,"maxmemory-policy")) {
333         if (!strcasecmp(o->ptr,"volatile-lru")) {
334             server.maxmemory_policy = REDIS_MAXMEMORY_VOLATILE_LRU;
335         } else if (!strcasecmp(o->ptr,"volatile-random")) {
336             server.maxmemory_policy = REDIS_MAXMEMORY_VOLATILE_RANDOM;
337         } else if (!strcasecmp(o->ptr,"volatile-ttl")) {
338             server.maxmemory_policy = REDIS_MAXMEMORY_VOLATILE_TTL;
339         } else if (!strcasecmp(o->ptr,"allkeys-lru")) {
340             server.maxmemory_policy = REDIS_MAXMEMORY_ALLKEYS_LRU;
341         } else if (!strcasecmp(o->ptr,"allkeys-random")) {
342             server.maxmemory_policy = REDIS_MAXMEMORY_ALLKEYS_RANDOM;
343         } else if (!strcasecmp(o->ptr,"noeviction")) {
344             server.maxmemory_policy = REDIS_MAXMEMORY_NO_EVICTION;
345         } else {
346             goto badfmt;
347         }
348     } else if (!strcasecmp(c->argv[2]->ptr,"maxmemory-samples")) {
349         if (getLongLongFromObject(o,&ll) == REDIS_ERR ||
350             ll <= 0) goto badfmt;
351         server.maxmemory_samples = ll;
352     } else if (!strcasecmp(c->argv[2]->ptr,"timeout")) {
353         if (getLongLongFromObject(o,&ll) == REDIS_ERR ||
354             ll < 0 || ll > LONG_MAX) goto badfmt;
355         server.maxidletime = ll;
356     } else if (!strcasecmp(c->argv[2]->ptr,"appendfsync")) {
357         if (!strcasecmp(o->ptr,"no")) {
358             server.appendfsync = APPENDFSYNC_NO;
359         } else if (!strcasecmp(o->ptr,"everysec")) {
360             server.appendfsync = APPENDFSYNC_EVERYSEC;
361         } else if (!strcasecmp(o->ptr,"always")) {
362             server.appendfsync = APPENDFSYNC_ALWAYS;
363         } else {
364             goto badfmt;
365         }
366     } else if (!strcasecmp(c->argv[2]->ptr,"no-appendfsync-on-rewrite")) {
367         int yn = yesnotoi(o->ptr);
368 
369         if (yn == -1) goto badfmt;
370         server.no_appendfsync_on_rewrite = yn;
371     } else if (!strcasecmp(c->argv[2]->ptr,"appendonly")) {
372         int old = server.appendonly;
373         int new = yesnotoi(o->ptr);
374 
375         if (new == -1) goto badfmt;
376         if (old != new) {
377             if (new == 0) {
378                 stopAppendOnly();
379             } else {
380                 if (startAppendOnly() == REDIS_ERR) {
381                     addReplyError(c,
382                         "Unable to turn on AOF. Check server logs.");
383                     return;
384                 }
385             }
386         }
387     } else if (!strcasecmp(c->argv[2]->ptr,"save")) {
388         int vlen, j;
389         sds *v = sdssplitlen(o->ptr,sdslen(o->ptr)," ",1,&vlen);
390 
391         /* Perform sanity check before setting the new config:
392          * - Even number of args
393          * - Seconds >= 1, changes >= 0 */
394         if (vlen & 1) {
395             sdsfreesplitres(v,vlen);
396             goto badfmt;
397         }
398         for (j = 0; j < vlen; j++) {
399             char *eptr;
400             long val;
401 
402             val = strtoll(v[j], &eptr, 10);
403             if (eptr[0] != '\0' ||
404                 ((j & 1) == 0 && val < 1) ||
405                 ((j & 1) == 1 && val < 0)) {
406                 sdsfreesplitres(v,vlen);
407                 goto badfmt;
408             }
409         }
410         /* Finally set the new config */
411         resetServerSaveParams();
412         for (j = 0; j < vlen; j += 2) {
413             time_t seconds;
414             int changes;
415 
416             seconds = strtoll(v[j],NULL,10);
417             changes = strtoll(v[j+1],NULL,10);
418             appendServerSaveParams(seconds, changes);
419         }
420         sdsfreesplitres(v,vlen);
421     } else if (!strcasecmp(c->argv[2]->ptr,"slave-serve-stale-data")) {
422         int yn = yesnotoi(o->ptr);
423 
424         if (yn == -1) goto badfmt;
425         server.repl_serve_stale_data = yn;
426     } else {
427         addReplyErrorFormat(c,"Unsupported CONFIG parameter: %s",
428             (char*)c->argv[2]->ptr);
429         return;
430     }
431     addReply(c,shared.ok);
432     return;
433 
434 badfmt: /* Bad format errors */
435     addReplyErrorFormat(c,"Invalid argument '%s' for CONFIG SET '%s'",
436             (char*)o->ptr,
437             (char*)c->argv[2]->ptr);
438 }
439 
440 void configGetCommand(redisClient *c) {
441     robj *o = c->argv[2];
442     void *replylen = addDeferredMultiBulkLength(c);
443     char *pattern = o->ptr;
444     char buf[128];
445     int matches = 0;
446     redisAssert(o->encoding == REDIS_ENCODING_RAW);
447 
448     if (stringmatch(pattern,"dbfilename",0)) {
449         addReplyBulkCString(c,"dbfilename");
450         addReplyBulkCString(c,server.dbfilename);
451         matches++;
452     }
453     if (stringmatch(pattern,"requirepass",0)) {
454         addReplyBulkCString(c,"requirepass");
455         addReplyBulkCString(c,server.requirepass);
456         matches++;
457     }
458     if (stringmatch(pattern,"masterauth",0)) {
459         addReplyBulkCString(c,"masterauth");
460         addReplyBulkCString(c,server.masterauth);
461         matches++;
462     }
463     if (stringmatch(pattern,"maxmemory",0)) {
464         ll2string(buf,sizeof(buf),server.maxmemory);
465         addReplyBulkCString(c,"maxmemory");
466         addReplyBulkCString(c,buf);
467         matches++;
468     }
469     if (stringmatch(pattern,"maxmemory-policy",0)) {
470         char *s;
471 
472         switch(server.maxmemory_policy) {
473         case REDIS_MAXMEMORY_VOLATILE_LRU: s = "volatile-lru"; break;
474         case REDIS_MAXMEMORY_VOLATILE_TTL: s = "volatile-ttl"; break;
475         case REDIS_MAXMEMORY_VOLATILE_RANDOM: s = "volatile-random"; break;
476         case REDIS_MAXMEMORY_ALLKEYS_LRU: s = "allkeys-lru"; break;
477         case REDIS_MAXMEMORY_ALLKEYS_RANDOM: s = "allkeys-random"; break;
478         case REDIS_MAXMEMORY_NO_EVICTION: s = "noeviction"; break;
479         default: s = "unknown"; break; /* too harmless to panic */
480         }
481         addReplyBulkCString(c,"maxmemory-policy");
482         addReplyBulkCString(c,s);
483         matches++;
484     }
485     if (stringmatch(pattern,"maxmemory-samples",0)) {
486         ll2string(buf,sizeof(buf),server.maxmemory_samples);
487         addReplyBulkCString(c,"maxmemory-samples");
488         addReplyBulkCString(c,buf);
489         matches++;
490     }
491     if (stringmatch(pattern,"timeout",0)) {
492         ll2string(buf,sizeof(buf),server.maxidletime);
493         addReplyBulkCString(c,"timeout");
494         addReplyBulkCString(c,buf);
495         matches++;
496     }
497     if (stringmatch(pattern,"appendonly",0)) {
498         addReplyBulkCString(c,"appendonly");
499         addReplyBulkCString(c,server.appendonly ? "yes" : "no");
500         matches++;
501     }
502     if (stringmatch(pattern,"no-appendfsync-on-rewrite",0)) {
503         addReplyBulkCString(c,"no-appendfsync-on-rewrite");
504         addReplyBulkCString(c,server.no_appendfsync_on_rewrite ? "yes" : "no");
505         matches++;
506     }
507     if (stringmatch(pattern,"appendfsync",0)) {
508         char *policy;
509 
510         switch(server.appendfsync) {
511         case APPENDFSYNC_NO: policy = "no"; break;
512         case APPENDFSYNC_EVERYSEC: policy = "everysec"; break;
513         case APPENDFSYNC_ALWAYS: policy = "always"; break;
514         default: policy = "unknown"; break; /* too harmless to panic */
515         }
516         addReplyBulkCString(c,"appendfsync");
517         addReplyBulkCString(c,policy);
518         matches++;
519     }
520     if (stringmatch(pattern,"save",0)) {
521         sds buf = sdsempty();
522         int j;
523 
524         for (j = 0; j < server.saveparamslen; j++) {
525             buf = sdscatprintf(buf,"%ld %d",
526                     server.saveparams[j].seconds,
527                     server.saveparams[j].changes);
528             if (j != server.saveparamslen-1)
529                 buf = sdscatlen(buf," ",1);
530         }
531         addReplyBulkCString(c,"save");
532         addReplyBulkCString(c,buf);
533         sdsfree(buf);
534         matches++;
535     }
536     if (stringmatch(pattern,"slave-serve-stale-data",0)) {
537         addReplyBulkCString(c,"slave-serve-stale-data");
538         addReplyBulkCString(c,server.repl_serve_stale_data ? "yes" : "no");
539         matches++;
540     }
541     setDeferredMultiBulkLength(c,replylen,matches*2);
542 }
543 
544 void configCommand(redisClient *c) {
545     if (!strcasecmp(c->argv[1]->ptr,"set")) {
546         if (c->argc != 4) goto badarity;
547         configSetCommand(c);
548     } else if (!strcasecmp(c->argv[1]->ptr,"get")) {
549         if (c->argc != 3) goto badarity;
550         configGetCommand(c);
551     } else if (!strcasecmp(c->argv[1]->ptr,"resetstat")) {
552         if (c->argc != 2) goto badarity;
553         server.stat_keyspace_hits = 0;
554         server.stat_keyspace_misses = 0;
555         server.stat_numcommands = 0;
556         server.stat_numconnections = 0;
557         server.stat_expiredkeys = 0;
558         addReply(c,shared.ok);
559     } else {
560         addReplyError(c,
561             "CONFIG subcommand must be one of GET, SET, RESETSTAT");
562     }
563     return;
564 
565 badarity:
566     addReplyErrorFormat(c,"Wrong number of arguments for CONFIG %s",
567         (char*) c->argv[1]->ptr);
568 }
569