X-Git-Url: https://git.octo.it/?p=rrdtool.git;a=blobdiff_plain;f=src%2Frrd_daemon.c;h=28912c5358f8d17214b34d7c681fe8bcf714b69b;hp=0816526bd43a808e549dc4c68b1b4f04aa7dbf82;hb=3c3effad5f54f8c73260068e3ec32c56684243bb;hpb=a12627275ff8487174cbb907a066f62a00b6ae44 diff --git a/src/rrd_daemon.c b/src/rrd_daemon.c index 0816526..28912c5 100644 --- a/src/rrd_daemon.c +++ b/src/rrd_daemon.c @@ -101,10 +101,32 @@ /* * Types */ +typedef enum +{ + PRIV_LOW, + PRIV_HIGH +} socket_privilege; + +typedef enum { RESP_ERR = -1, RESP_OK = 0 } response_code; + struct listen_socket_s { int fd; - char path[PATH_MAX + 1]; + char addr[PATH_MAX + 1]; + int family; + socket_privilege privilege; + + /* state for BATCH processing */ + time_t batch_start; + int batch_cmd; + + /* buffered IO */ + char *rbuf; + off_t next_cmd; + off_t next_read; + + char *wbuf; + ssize_t wbuf_len; }; typedef struct listen_socket_s listen_socket_t; @@ -116,10 +138,12 @@ struct cache_item_s char **values; int values_num; time_t last_flush_time; + time_t last_update_stamp; #define CI_FLAGS_IN_TREE (1<<0) #define CI_FLAGS_IN_QUEUE (1<<1) int flags; - + pthread_cond_t flushed; + cache_item_t *prev; cache_item_t *next; }; @@ -141,11 +165,13 @@ typedef enum queue_side_e queue_side_t; /* max length of socket command or response */ #define CMD_MAX 4096 +#define RBUF_SIZE (CMD_MAX*2) /* * Variables */ static int stay_foreground = 0; +static uid_t daemon_uid; static listen_socket_t *listen_fds = NULL; static size_t listen_fds_num = 0; @@ -165,15 +191,16 @@ static cache_item_t *cache_queue_tail = NULL; static pthread_mutex_t cache_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t cache_cond = PTHREAD_COND_INITIALIZER; -static pthread_cond_t flush_cond = PTHREAD_COND_INITIALIZER; - static int config_write_interval = 300; static int config_write_jitter = 0; static int config_flush_interval = 3600; +static int config_flush_at_shutdown = 0; static char *config_pid_file = NULL; static char *config_base_dir = NULL; +static size_t _config_base_dir_len = 0; +static int config_write_base_only = 0; -static char **config_listen_address_list = NULL; +static listen_socket_t **config_listen_address_list = NULL; static int config_listen_address_list_len = 0; static uint64_t stats_queue_length = 0; @@ -197,45 +224,96 @@ static void journal_rotate(void); /* * Functions */ -static void sig_int_handler (int s __attribute__((unused))) /* {{{ */ +static void sig_common (const char *sig) /* {{{ */ { - RRDD_LOG(LOG_NOTICE, "caught SIGINT"); + RRDD_LOG(LOG_NOTICE, "caught SIG%s", sig); do_shutdown++; pthread_cond_broadcast(&cache_cond); +} /* }}} void sig_common */ + +static void sig_int_handler (int s __attribute__((unused))) /* {{{ */ +{ + sig_common("INT"); } /* }}} void sig_int_handler */ static void sig_term_handler (int s __attribute__((unused))) /* {{{ */ { - RRDD_LOG(LOG_NOTICE, "caught SIGTERM"); - do_shutdown++; - pthread_cond_broadcast(&cache_cond); + sig_common("TERM"); } /* }}} void sig_term_handler */ -static int write_pidfile (void) /* {{{ */ +static void sig_usr1_handler (int s __attribute__((unused))) /* {{{ */ +{ + config_flush_at_shutdown = 1; + sig_common("USR1"); +} /* }}} void sig_usr1_handler */ + +static void sig_usr2_handler (int s __attribute__((unused))) /* {{{ */ +{ + config_flush_at_shutdown = 0; + sig_common("USR2"); +} /* }}} void sig_usr2_handler */ + +static void install_signal_handlers(void) /* {{{ */ +{ + /* These structures are static, because `sigaction' behaves weird if the are + * overwritten.. */ + static struct sigaction sa_int; + static struct sigaction sa_term; + static struct sigaction sa_pipe; + static struct sigaction sa_usr1; + static struct sigaction sa_usr2; + + /* Install signal handlers */ + memset (&sa_int, 0, sizeof (sa_int)); + sa_int.sa_handler = sig_int_handler; + sigaction (SIGINT, &sa_int, NULL); + + memset (&sa_term, 0, sizeof (sa_term)); + sa_term.sa_handler = sig_term_handler; + sigaction (SIGTERM, &sa_term, NULL); + + memset (&sa_pipe, 0, sizeof (sa_pipe)); + sa_pipe.sa_handler = SIG_IGN; + sigaction (SIGPIPE, &sa_pipe, NULL); + + memset (&sa_pipe, 0, sizeof (sa_usr1)); + sa_usr1.sa_handler = sig_usr1_handler; + sigaction (SIGUSR1, &sa_usr1, NULL); + + memset (&sa_usr2, 0, sizeof (sa_usr2)); + sa_usr2.sa_handler = sig_usr2_handler; + sigaction (SIGUSR2, &sa_usr2, NULL); + +} /* }}} void install_signal_handlers */ + +static int open_pidfile(void) /* {{{ */ { - pid_t pid; - char *file; int fd; - FILE *fh; + char *file; - pid = getpid (); - file = (config_pid_file != NULL) ? config_pid_file : LOCALSTATEDIR "/run/rrdcached.pid"; fd = open(file, O_CREAT|O_EXCL|O_WRONLY, S_IRUSR|S_IRGRP|S_IROTH); if (fd < 0) - { - RRDD_LOG(LOG_ERR, "FATAL: cannot create '%s' (%s)", - file, rrd_strerror(errno)); - return (-1); - } + fprintf(stderr, "FATAL: cannot create '%s' (%s)\n", + file, rrd_strerror(errno)); + + return(fd); +} /* }}} static int open_pidfile */ + +static int write_pidfile (int fd) /* {{{ */ +{ + pid_t pid; + FILE *fh; + + pid = getpid (); fh = fdopen (fd, "w"); if (fh == NULL) { - RRDD_LOG (LOG_ERR, "write_pidfile: Opening `%s' failed.", file); + RRDD_LOG (LOG_ERR, "write_pidfile: fdopen() failed."); close(fd); return (-1); } @@ -261,88 +339,177 @@ static int remove_pidfile (void) /* {{{ */ return (errno); } /* }}} int remove_pidfile */ -static ssize_t sread (int fd, void *buffer_void, size_t buffer_size) /* {{{ */ +static char *next_cmd (listen_socket_t *sock, ssize_t *len) /* {{{ */ { - char *buffer; - size_t buffer_used; - size_t buffer_free; - ssize_t status; + char *eol; - buffer = (char *) buffer_void; - buffer_used = 0; - buffer_free = buffer_size; + eol = memchr(sock->rbuf + sock->next_cmd, '\n', + sock->next_read - sock->next_cmd); - while (buffer_free > 0) + if (eol == NULL) { - status = read (fd, buffer + buffer_used, buffer_free); - if ((status < 0) && ((errno == EAGAIN) || (errno == EINTR))) - continue; + /* no commands left, move remainder back to front of rbuf */ + memmove(sock->rbuf, sock->rbuf + sock->next_cmd, + sock->next_read - sock->next_cmd); + sock->next_read -= sock->next_cmd; + sock->next_cmd = 0; + *len = 0; + return NULL; + } + else + { + char *cmd = sock->rbuf + sock->next_cmd; + *eol = '\0'; - if (status < 0) - return (-1); + sock->next_cmd = eol - sock->rbuf + 1; - if (status == 0) - return (0); - - assert ((0 > status) || (buffer_free >= (size_t) status)); + if (eol > sock->rbuf && *(eol-1) == '\r') + *(--eol) = '\0'; /* handle "\r\n" EOL */ - buffer_free = buffer_free - status; - buffer_used = buffer_used + status; + *len = eol - cmd; - if (buffer[buffer_used - 1] == '\n') - break; + return cmd; } - assert (buffer_used > 0); + /* NOTREACHED */ + assert(1==0); +} + +/* add the characters directly to the write buffer */ +static int add_to_wbuf(listen_socket_t *sock, char *str, size_t len) /* {{{ */ +{ + char *new_buf; + + assert(sock != NULL); - if (buffer[buffer_used - 1] != '\n') + new_buf = realloc(sock->wbuf, sock->wbuf_len + len + 1); + if (new_buf == NULL) { - errno = ENOBUFS; - return (-1); + RRDD_LOG(LOG_ERR, "add_to_wbuf: realloc failed"); + return -1; } - buffer[buffer_used - 1] = 0; + strncpy(new_buf + sock->wbuf_len, str, len + 1); - /* Fix network line endings. */ - if ((buffer_used > 1) && (buffer[buffer_used - 2] == '\r')) + sock->wbuf = new_buf; + sock->wbuf_len += len; + + return 0; +} /* }}} static int add_to_wbuf */ + +/* add the text to the "extra" info that's sent after the status line */ +static int add_response_info(listen_socket_t *sock, char *fmt, ...) /* {{{ */ +{ + va_list argp; + char buffer[CMD_MAX]; + int len; + + if (sock == NULL) return 0; /* journal replay mode */ + if (sock->batch_start) return 0; /* no extra info returned when in BATCH */ + + va_start(argp, fmt); +#ifdef HAVE_VSNPRINTF + len = vsnprintf(buffer, sizeof(buffer)-1, fmt, argp); +#else + len = vsprintf(buffer, fmt, argp); +#endif + va_end(argp); + if (len < 0) { - buffer_used--; - buffer[buffer_used - 1] = 0; + RRDD_LOG(LOG_ERR, "add_response_info: vnsprintf failed"); + return -1; } - return (buffer_used); -} /* }}} ssize_t sread */ + return add_to_wbuf(sock, buffer, len); +} /* }}} static int add_response_info */ -static ssize_t swrite (int fd, const void *buf, size_t count) /* {{{ */ +static int count_lines(char *str) /* {{{ */ { - const char *ptr; - size_t nleft; - ssize_t status; + int lines = 0; - /* special case for journal replay */ - if (fd < 0) return 0; + if (str != NULL) + { + while ((str = strchr(str, '\n')) != NULL) + { + ++lines; + ++str; + } + } - ptr = (const char *) buf; - nleft = count; + return lines; +} /* }}} static int count_lines */ - while (nleft > 0) +/* send the response back to the user. + * returns 0 on success, -1 on error + * write buffer is always zeroed after this call */ +static int send_response (listen_socket_t *sock, response_code rc, + char *fmt, ...) /* {{{ */ +{ + va_list argp; + char buffer[CMD_MAX]; + int lines; + ssize_t wrote; + int rclen, len; + + if (sock == NULL) return rc; /* journal replay mode */ + + if (sock->batch_start) { - status = write (fd, (const void *) ptr, nleft); + if (rc == RESP_OK) + return rc; /* no response on success during BATCH */ + lines = sock->batch_cmd; + } + else if (rc == RESP_OK) + lines = count_lines(sock->wbuf); + else + lines = -1; + + rclen = sprintf(buffer, "%d ", lines); + va_start(argp, fmt); +#ifdef HAVE_VSNPRINTF + len = vsnprintf(buffer+rclen, sizeof(buffer)-rclen-1, fmt, argp); +#else + len = vsprintf(buffer+rclen, fmt, argp); +#endif + va_end(argp); + if (len < 0) + return -1; - if ((status < 0) && ((errno == EAGAIN) || (errno == EINTR))) - continue; + len += rclen; - if (status < 0) - return (status); + /* append the result to the wbuf, don't write to the user */ + if (sock->batch_start) + return add_to_wbuf(sock, buffer, len); - nleft -= status; - ptr += status; + /* first write must be complete */ + if (len != write(sock->fd, buffer, len)) + { + RRDD_LOG(LOG_INFO, "send_response: could not write status message"); + return -1; } - return (0); -} /* }}} ssize_t swrite */ + if (sock->wbuf != NULL && rc == RESP_OK) + { + wrote = 0; + while (wrote < sock->wbuf_len) + { + ssize_t wb = write(sock->fd, sock->wbuf + wrote, sock->wbuf_len - wrote); + if (wb <= 0) + { + RRDD_LOG(LOG_INFO, "send_response: could not write results"); + return -1; + } + wrote += wb; + } + } + + free(sock->wbuf); sock->wbuf = NULL; + sock->wbuf_len = 0; -static void _wipe_ci_values(cache_item_t *ci, time_t when) + return 0; +} /* }}} */ + +static void wipe_ci_values(cache_item_t *ci, time_t when) { ci->values = NULL; ci->values_num = 0; @@ -350,10 +517,58 @@ static void _wipe_ci_values(cache_item_t *ci, time_t when) ci->last_flush_time = when; if (config_write_jitter > 0) ci->last_flush_time += (random() % config_write_jitter); - - ci->flags &= ~(CI_FLAGS_IN_QUEUE); } +/* remove_from_queue + * remove a "cache_item_t" item from the queue. + * must hold 'cache_lock' when calling this + */ +static void remove_from_queue(cache_item_t *ci) /* {{{ */ +{ + if (ci == NULL) return; + + if (ci->prev == NULL) + cache_queue_head = ci->next; /* reset head */ + else + ci->prev->next = ci->next; + + if (ci->next == NULL) + cache_queue_tail = ci->prev; /* reset the tail */ + else + ci->next->prev = ci->prev; + + ci->next = ci->prev = NULL; + ci->flags &= ~CI_FLAGS_IN_QUEUE; +} /* }}} static void remove_from_queue */ + +/* remove an entry from the tree and free all its resources. + * must hold 'cache lock' while calling this. + * returns 0 on success, otherwise errno */ +static int forget_file(const char *file) +{ + cache_item_t *ci; + + ci = g_tree_lookup(cache_tree, file); + if (ci == NULL) + return ENOENT; + + g_tree_remove (cache_tree, file); + remove_from_queue(ci); + + for (int i=0; i < ci->values_num; i++) + free(ci->values[i]); + + free (ci->values); + free (ci->file); + + /* in case anyone is waiting */ + pthread_cond_broadcast(&ci->flushed); + + free (ci); + + return 0; +} /* }}} static int forget_file */ + /* * enqueue_cache_item: * `cache_lock' must be acquired before calling this function! @@ -361,8 +576,6 @@ static void _wipe_ci_values(cache_item_t *ci, time_t when) static int enqueue_cache_item (cache_item_t *ci, /* {{{ */ queue_side_t side) { - int did_insert = 0; - if (ci == NULL) return (-1); @@ -371,66 +584,47 @@ static int enqueue_cache_item (cache_item_t *ci, /* {{{ */ if (side == HEAD) { - if ((ci->flags & CI_FLAGS_IN_QUEUE) == 0) - { - assert (ci->next == NULL); - ci->next = cache_queue_head; - cache_queue_head = ci; + if (cache_queue_head == ci) + return 0; - if (cache_queue_tail == NULL) - cache_queue_tail = cache_queue_head; + /* remove from the double linked list */ + if (ci->flags & CI_FLAGS_IN_QUEUE) + remove_from_queue(ci); - did_insert = 1; - } - else if (cache_queue_head == ci) - { - /* do nothing */ - } - else /* enqueued, but not first entry */ - { - cache_item_t *prev; + ci->prev = NULL; + ci->next = cache_queue_head; + if (ci->next != NULL) + ci->next->prev = ci; + cache_queue_head = ci; - /* find previous entry */ - for (prev = cache_queue_head; prev != NULL; prev = prev->next) - if (prev->next == ci) - break; - assert (prev != NULL); - - /* move to the front */ - prev->next = ci->next; - ci->next = cache_queue_head; - cache_queue_head = ci; - - /* check if we need to adapt the tail */ - if (cache_queue_tail == ci) - cache_queue_tail = prev; - } + if (cache_queue_tail == NULL) + cache_queue_tail = cache_queue_head; } else /* (side == TAIL) */ { /* We don't move values back in the list.. */ - if ((ci->flags & CI_FLAGS_IN_QUEUE) != 0) + if (ci->flags & CI_FLAGS_IN_QUEUE) return (0); assert (ci->next == NULL); + assert (ci->prev == NULL); + + ci->prev = cache_queue_tail; if (cache_queue_tail == NULL) cache_queue_head = ci; else cache_queue_tail->next = ci; - cache_queue_tail = ci; - did_insert = 1; + cache_queue_tail = ci; } ci->flags |= CI_FLAGS_IN_QUEUE; - if (did_insert) - { - pthread_mutex_lock (&stats_lock); - stats_queue_length++; - pthread_mutex_unlock (&stats_lock); - } + pthread_cond_broadcast(&cache_cond); + pthread_mutex_lock (&stats_lock); + stats_queue_length++; + pthread_mutex_unlock (&stats_lock); return (0); } /* }}} int enqueue_cache_item */ @@ -499,7 +693,7 @@ static int flush_old_values (int max_age) if (max_age > 0) cfd.abs_timeout = cfd.now - max_age; else - cfd.abs_timeout = cfd.now + 1; + cfd.abs_timeout = cfd.now + 2*config_write_jitter + 1; /* `tree_callback_flush' will return the keys of all values that haven't * been touched in the last `config_flush_interval' seconds in `cfd'. @@ -509,26 +703,10 @@ static int flush_old_values (int max_age) for (k = 0; k < cfd.keys_num; k++) { - cache_item_t *ci; - - /* This must not fail. */ - ci = (cache_item_t *) g_tree_lookup (cache_tree, cfd.keys[k]); - assert (ci != NULL); - - /* If we end up here with values available, something's seriously - * messed up. */ - assert (ci->values_num == 0); - - /* Remove the node from the tree */ - g_tree_remove (cache_tree, cfd.keys[k]); - cfd.keys[k] = NULL; - - /* Now free and clean up `ci'. */ - free (ci->file); - ci->file = NULL; - free (ci); - ci = NULL; - } /* for (k = 0; k < cfd.keys_num; k++) */ + /* should never fail, since we have held the cache_lock + * the entire time */ + assert( forget_file(cfd.keys[k]) == 0 ); + } if (cfd.keys != NULL) { @@ -543,6 +721,7 @@ static void *queue_thread_main (void *args __attribute__((unused))) /* {{{ */ { struct timeval now; struct timespec next_flush; + int final_flush = 0; /* make sure we only flush once on shutdown */ gettimeofday (&now, NULL); next_flush.tv_sec = now.tv_sec + config_flush_interval; @@ -580,8 +759,9 @@ static void *queue_thread_main (void *args __attribute__((unused))) /* {{{ */ } /* Now, check if there's something to store away. If not, wait until - * something comes in or it's time to do the cache flush. */ - if (cache_queue_head == NULL) + * something comes in or it's time to do the cache flush. if we are + * shutting down, do not wait around. */ + if (cache_queue_head == NULL && !do_shutdown) { status = pthread_cond_timedwait (&cache_cond, &cache_lock, &next_flush); if ((status != 0) && (status != ETIMEDOUT)) @@ -591,9 +771,14 @@ static void *queue_thread_main (void *args __attribute__((unused))) /* {{{ */ } } - /* We're about to shut down, so lets flush the entire tree. */ - if ((do_shutdown != 0) && (cache_queue_head == NULL)) - flush_old_values (/* max age = */ -1); + /* We're about to shut down */ + if (do_shutdown != 0 && !final_flush++) + { + if (config_flush_at_shutdown) + flush_old_values (-1); /* flush everything */ + else + break; + } /* Check if a value has arrived. This may be NULL if we timed out or there * was an interrupt such as a signal. */ @@ -616,12 +801,8 @@ static void *queue_thread_main (void *args __attribute__((unused))) /* {{{ */ values = ci->values; values_num = ci->values_num; - _wipe_ci_values(ci, time(NULL)); - - cache_queue_head = ci->next; - if (cache_queue_head == NULL) - cache_queue_tail = NULL; - ci->next = NULL; + wipe_ci_values(ci, time(NULL)); + remove_from_queue(ci); pthread_mutex_lock (&stats_lock); assert (stats_queue_length > 0); @@ -640,6 +821,7 @@ static void *queue_thread_main (void *args __attribute__((unused))) /* {{{ */ } journal_write("wrote", file); + pthread_cond_broadcast(&ci->flushed); for (i = 0; i < values_num; i++) free (values[i]); @@ -656,16 +838,24 @@ static void *queue_thread_main (void *args __attribute__((unused))) /* {{{ */ } pthread_mutex_lock (&cache_lock); - pthread_cond_broadcast (&flush_cond); - /* We're about to shut down, so lets flush the entire tree. */ - if ((do_shutdown != 0) && (cache_queue_head == NULL)) - flush_old_values (/* max age = */ -1); + /* We're about to shut down */ + if (do_shutdown != 0 && !final_flush++) + { + if (config_flush_at_shutdown) + flush_old_values (-1); /* flush everything */ + else + break; + } } /* while ((do_shutdown == 0) || (cache_queue_head != NULL)) */ pthread_mutex_unlock (&cache_lock); - assert(cache_queue_head == NULL); - RRDD_LOG(LOG_INFO, "clean shutdown; all RRDs flushed"); + if (config_flush_at_shutdown) + { + assert(cache_queue_head == NULL); + RRDD_LOG(LOG_INFO, "clean shutdown; all RRDs flushed"); + } + journal_done(); return (NULL); @@ -734,6 +924,52 @@ static int buffer_get_field (char **buffer_ret, /* {{{ */ return (0); } /* }}} int buffer_get_field */ +/* if we're restricting writes to the base directory, + * check whether the file falls within the dir + * returns 1 if OK, otherwise 0 + */ +static int check_file_access (const char *file, listen_socket_t *sock) /* {{{ */ +{ + assert(file != NULL); + + if (!config_write_base_only + || sock == NULL /* journal replay */ + || config_base_dir == NULL) + return 1; + + if (strstr(file, "../") != NULL) goto err; + + /* relative paths without "../" are ok */ + if (*file != '/') return 1; + + /* file must be of the format base + "/" + <1+ char filename> */ + if (strlen(file) < _config_base_dir_len + 2) goto err; + if (strncmp(file, config_base_dir, _config_base_dir_len) != 0) goto err; + if (*(file + _config_base_dir_len) != '/') goto err; + + return 1; + +err: + if (sock != NULL && sock->fd >= 0) + send_response(sock, RESP_ERR, "%s\n", rrd_strerror(EACCES)); + + return 0; +} /* }}} static int check_file_access */ + +/* returns 1 if we have the required privilege level, + * otherwise issue an error to the user on sock */ +static int has_privilege (listen_socket_t *sock, /* {{{ */ + socket_privilege priv) +{ + if (sock == NULL) /* journal replay */ + return 1; + + if (sock->privilege >= priv) + return 1; + + return send_response(sock, RESP_ERR, "%s\n", rrd_strerror(EACCES)); +} /* }}} static int has_privilege */ + static int flush_file (const char *filename) /* {{{ */ { cache_item_t *ci; @@ -747,135 +983,157 @@ static int flush_file (const char *filename) /* {{{ */ return (ENOENT); } - /* Enqueue at head */ - enqueue_cache_item (ci, HEAD); - pthread_cond_signal (&cache_cond); - - while ((ci->flags & CI_FLAGS_IN_QUEUE) != 0) + if (ci->values_num > 0) { - ci = NULL; + /* Enqueue at head */ + enqueue_cache_item (ci, HEAD); + pthread_cond_wait(&ci->flushed, &cache_lock); + } - pthread_cond_wait (&flush_cond, &cache_lock); + /* DO NOT DO ANYTHING WITH ci HERE!! The entry + * may have been purged during our cond_wait() */ - ci = g_tree_lookup (cache_tree, filename); - if (ci == NULL) - { - RRDD_LOG (LOG_ERR, "flush_file: Tree node went away " - "while waiting for flush."); - pthread_mutex_unlock (&cache_lock); - return (-1); - } - } + pthread_mutex_unlock(&cache_lock); - pthread_mutex_unlock (&cache_lock); return (0); } /* }}} int flush_file */ -static int handle_request_help (int fd, /* {{{ */ +static int handle_request_help (listen_socket_t *sock, /* {{{ */ char *buffer, size_t buffer_size) { int status; char **help_text; - size_t help_text_len; char *command; - size_t i; - char *help_help[] = - { - "4 Command overview\n", - "FLUSH \n", - "HELP []\n", - "UPDATE [ ...]\n", + char *help_help[2] = + { + "Command overview\n" + , + "HELP []\n" + "FLUSH \n" + "FLUSHALL\n" + "PENDING \n" + "FORGET \n" + "UPDATE [ ...]\n" + "BATCH\n" "STATS\n" }; - size_t help_help_len = sizeof (help_help) / sizeof (help_help[0]); - char *help_flush[] = + char *help_flush[2] = { - "4 Help for FLUSH\n", - "Usage: FLUSH \n", - "\n", - "Adds the given filename to the head of the update queue and returns\n", + "Help for FLUSH\n" + , + "Usage: FLUSH \n" + "\n" + "Adds the given filename to the head of the update queue and returns\n" "after is has been dequeued.\n" }; - size_t help_flush_len = sizeof (help_flush) / sizeof (help_flush[0]); - char *help_update[] = + char *help_flushall[2] = + { + "Help for FLUSHALL\n" + , + "Usage: FLUSHALL\n" + "\n" + "Triggers writing of all pending updates. Returns immediately.\n" + }; + + char *help_pending[2] = + { + "Help for PENDING\n" + , + "Usage: PENDING \n" + "\n" + "Shows any 'pending' updates for a file, in order.\n" + "The updates shown have not yet been written to the underlying RRD file.\n" + }; + + char *help_forget[2] = { - "9 Help for UPDATE\n", + "Help for FORGET\n" + , + "Usage: FORGET \n" + "\n" + "Removes the file completely from the cache.\n" + "Any pending updates for the file will be lost.\n" + }; + + char *help_update[2] = + { + "Help for UPDATE\n" + , "Usage: UPDATE [ ...]\n" - "\n", - "Adds the given file to the internal cache if it is not yet known and\n", - "appends the given value(s) to the entry. See the rrdcached(1) manpage\n", - "for details.\n", - "\n", - "Each has the following form:\n", - " =