Extract functions used in tests to a separate file 65/317465/1
authorMichal Bloch <m.bloch@samsung.com>
Thu, 29 Aug 2024 12:01:15 +0000 (14:01 +0200)
committerMichal Bloch <m.bloch@samsung.com>
Tue, 10 Sep 2024 12:45:26 +0000 (14:45 +0200)
Previously they were under an #ifdef macro,
which some static analyzer complained about.

Change-Id: I2ed323bb036267ce3ecfdde646dff63502353cd1

Makefile.am
src/logctl/logctl.c
src/logctl/logctl_functions.c [new file with mode: 0644]
src/logctl/logctl_functions.h [new file with mode: 0644]

index 85735ab..7218dd7 100644 (file)
@@ -240,6 +240,7 @@ dlogctl_SOURCES = \
        src/shared/ptrs_list.c \
        src/shared/hash.c \
        src/shared/loglimiter.c \
+       src/logctl/logctl_functions.c \
        src/logctl/logctl.c
 
 bin_PROGRAMS += dlog_cleanup
@@ -723,7 +724,7 @@ src_tests_queued_entry_monotonic_pos_SOURCES = src/tests/queued_entry_pos.c src/
 src_tests_queued_entry_monotonic_pos_CFLAGS = $(check_CFLAGS) -DUSE_ANDROID_MONOTONIC
 src_tests_queued_entry_monotonic_pos_LDFLAGS = $(AM_LDFLAGS)
 
-src_tests_logctl_SOURCES = src/tests/logctl.c src/logctl/logctl.c src/shared/logcommon.c src/shared/logconfig.c src/shared/parsers.c src/shared/buffer_traits.c
+src_tests_logctl_SOURCES = src/tests/logctl.c src/logctl/logctl_functions.c src/shared/logcommon.c src/shared/logconfig.c src/shared/parsers.c src/shared/buffer_traits.c
 src_tests_logctl_CFLAGS = $(check_CFLAGS)
 src_tests_logctl_LDFLAGS = $(AM_LDFLAGS) -Wl,--wrap=calloc,--wrap=asprintf,--wrap=open,--wrap=open64,--wrap=fdopen,--wrap=fdopen64,--wrap=mkstemp,--wrap=mkstemp64,--wrap=fchmod,--wrap=rename
 
index 15706ab..3c49af4 100644 (file)
-// C
-#include <assert.h>
-#include <ctype.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-// POSIX
-#include <getopt.h>
-#include <unistd.h>
-#include <sys/file.h>
-#include <sys/stat.h>
-
 // DLog
 #include <buffer_config.h>
 #include <buffer_traits.h>
 #include <dynamic_config.h>
 #include <extra_sinks.h>
-#include <logconfig.h>
-#include <loglimiter.h>
 
-#include "log_compressed_storage.h"
-#include "fdi_pipe.h"
-#include "fdi_pipe_internal.h"
+#include "logctl_functions.h"
 #include "logctl.h"
-#include "logpipe.h"
-
-int get_is_pipe(struct log_config *conf, bool *out)
-{
-       const char *const backend = log_config_claim_backend(conf);
-       if (!backend)
-               return -ENOKEY;
-
-       *out = strcmp(backend, "pipe") == 0;
-       return 0;
-}
-
-bool parse_options(int argc, const char **argv, struct parsed_params *params, int *exit_value)
-{
-       assert(argv);
-       assert(params);
-       assert(params->action == ACTION_NONE);
-       assert(!params->value);
-       assert(!params->tag);
-       assert(exit_value);
-
-       params->tag = "*";
-       params->prio = '\0';
-       params->pid = 0;
-       params->plog_buffers_bitset = 0;
-
-       bool use_global_action = true; // if neither tag, pid nor priority is specified
-
-       /* skips a part of the help message */
-       bool show_dynamic_conf_msg = true;
-
-#define SET_ACTION(x) do { \
-       if (params->action != x && params->action != ACTION_NONE) \
-               goto failure; \
-       params->action = x; \
-} while (false)
-
-       for (;;) {
-
-               static const struct option long_options[] = {
-                       {"get"     ,       no_argument, NULL, 'g'},
-                       {"set"     , required_argument, NULL, 's'},
-                       {"tag"     , required_argument, NULL, 't'},
-                       {"priority", required_argument, NULL, 'p'},
-                       {"help"    ,       no_argument, NULL, 'h'},
-                       {"buffer"  , required_argument, NULL, 'b'},
-                       {"clear"   ,       no_argument, NULL, 'c'},
-                       {"enable"  ,       no_argument, NULL,   0},
-                       {"disable" ,       no_argument, NULL,   1},
-                       {"pid"     , required_argument, NULL,   2},
-                       {"enable-stdout",  no_argument, NULL,   3},
-                       {"disable-stdout", no_argument, NULL,   4},
-                       {"compression-resize", required_argument, NULL, 5},
-                       {"compression-name", required_argument, NULL, 6},
-                       {0}
-               };
-
-               int long_index;
-               int opt = getopt_long(argc, (char **) argv, "chb:gs:t:p:", long_options, &long_index);
-               if (opt < 0)
-                       break;
-
-               switch (opt) {
-               case 0:
-                       SET_ACTION(ACTION_PLOG);
-                       params->plog_enable = true;
-                       break;
-               case 1:
-                       SET_ACTION(ACTION_PLOG);
-                       params->plog_enable = false;
-                       break;
-               case 2: {
-                       const int value_num = atoi(optarg);
-                       if (value_num <= 0)
-                               goto failure;
-                       params->pid = value_num;
-                       use_global_action = false;
-                       break;
-               }
-               case 3:
-                       SET_ACTION(ACTION_STDOUT);
-                       params->plog_enable = true;
-                       break;
-               case 4:
-                       SET_ACTION(ACTION_STDOUT);
-                       params->plog_enable = false;
-                       break;
-               case 5: {
-                       const int capacity = atoi(optarg);
-                       if (capacity <= 0)
-                               goto failure;
-                       params->capacity = capacity;
-                       SET_ACTION(ACTION_RESIZE);
-                       show_dynamic_conf_msg = false;
-                       break;
-               } case 6: {
-                       params->storage_name = optarg;
-                       SET_ACTION(ACTION_RESIZE);
-                       show_dynamic_conf_msg = false;
-                       break;
-               } case 'b': {
-                       const int buf_id = sink_id_by_name(optarg);
-                       if (buf_id == LOG_ID_INVALID)
-                               goto failure;
-
-                       params->plog_buffers_bitset |= 1 << buf_id;
-                       break;
-               } case 'h':
-                       *exit_value = EXIT_SUCCESS;
-                       goto print_help;
-               case 'g':
-                       SET_ACTION(ACTION_GET);
-                       break;
-               case 'c':
-                       SET_ACTION(ACTION_CLEAR);
-                       params->value = NULL;
-                       break;
-               case 's': {
-                       const int value_num = atoi(optarg);
-                       if (strcmp(optarg, "allow") && strcmp(optarg, "deny") && (value_num <= 0 || value_num >= __LOG_LIMITER_LIMIT_MAX))
-                               goto failure;
-                       params->value = optarg;
-                       SET_ACTION(ACTION_SET);
-                       break;
-               } case 't': {
-                       params->tag = optarg;
-                       use_global_action = false;
-                       break;
-               } case 'p':
-                       params->prio = toupper(*optarg);
-                       if (!strchr("DVIWEFS*", params->prio))
-                               goto failure;
-                       use_global_action = false;
-                       break;
-
-               default:
-                       break;
-               }
-       }
-
-#undef SET_ACTION
-
-       if (params->pid != 0 && (params->prio != '\0' || strcmp(params->tag, "*") != 0))
-               goto failure;
-
-       if (params->plog_buffers_bitset != 0 && params->action != ACTION_PLOG && params->action != ACTION_STDOUT)
-               goto failure;
-
-       if (params->action == ACTION_NONE) {
-               *exit_value = EXIT_SUCCESS;
-               goto print_help;
-       }
-
-       switch (params->action) {
-       case ACTION_GET:
-               if (use_global_action)
-                       params->action = ACTION_DUMP;
-               break;
-       case ACTION_CLEAR:
-               if (params->pid != 0)
-                       params->action = ACTION_SET_PID;
-               else if (!use_global_action)
-                       params->action = ACTION_SET;
-               break;
-       case ACTION_SET:
-               if (params->pid != 0)
-                       params->action = ACTION_SET_PID;
-               break;
-       default:
-               break;
-       }
-
-       if (params->action == ACTION_SET && params->prio == '\0')
-               params->prio = '*';
-
-       return true;
-
-failure:
-       *exit_value = EXIT_FAILURE;
-
-print_help:
-       printf("Usage: %s [-t/--tag tag] [-p/--priority priority] [--pid pid] (-g/--get | -s/--set value)\n"
-               "\ttag: defaults to *\n"
-               "\tpriority: F[atal], E[rror], W[arning], I[nfo], V[erbose], D[ebug] S[ilent] * (all, default)\n"
-               "\tpid: integer larger than 0, mutually exclusive with --tag and --priority\n"
-               "\t-g/--get: get current limit for given tag/priority; if neither given, dump the whole filterset\n"
-               "\t-s/--set value: set a new limit to value (\"allow\", \"deny\", or number (0; %d) - allowed logs per minute)\n"
-               "\t-c/--clear: clear the limit for given tag/priority; if neither given, clear the whole filterset\n"
-               "Note: static filters are shadowed by dynamic ones but cannot be overridden at runtime\n\n"
-               "       %s [-b/--buffer bufname] [-b ...] (--enable | --disable)\n"
-               "\tbufname: main system radio apps (default: all of them)\n"
-               "\t-b/--buffer bufname: pick given buffer for plog control\n"
-               "\t--enable/--disable: control platform logging for chosen buffers\n"
-               "\t--enable-stdout/--disable-stdout: control stdout redirection for chosen buffers\n"
-               "\t--compression-name: resizes given named compression buffer. Must be used with --compression-resize\n"
-               "\t--compression-resize: sets compression buffer to given size, in bytes.\n"
-               "\n"
-               , argv[0]
-               , __LOG_LIMITER_LIMIT_MAX
-               , argv[0]
-       );
-
-       if (show_dynamic_conf_msg) {
-               printf("NOTE: to use dlogctl, dynamic dlog config has to be enabled!\n"
-               "Define the %s config entry to a *DIRECTORY* path.\n"
-               "ALSO: filters won't work for the APPS buffer (whence most spam comes)\n"
-               "unless you set `limiter_apply_to_all_buffers` in the config to 1!\n"
-               "The filters are also currently per process; this may change in the future.\n\n"
-               , DYNAMIC_CONFIG_CONF_KEY
-               );
-       }
-
-       return false;
-}
-
-/**
- * @brief Copy file, except modify some lines
- * @details Copies a file linewise to another, except ensures lines starting with given prefixes have certain follow-up
- * @param[in] in Copy from this file
- * @param[in] out Copy into this file
- * @param[in] kv A set of key-value pairs, where lines which do not match any of the prefixes are copied verbatim, while
- *               lines that do are ignored. For each key where the value is not NULL, a line with the key as the prefix
- *               and the value as the suffix is then appended to the resulting file.
- *
- * @notes Usually the keys will contain a '=' at the end so as to only replace exact matches in a config file,
- *        and keys without '=' at the end should usually have the value NULL to keep the config format valid
- *
- * @example Consider a file with the contents:
- *   FOO=BAR
- *   FOOOO=BAZ
- *   ABC=XYZ
- *   ABCABC=QQQ
- *   ABC=ZYX
- *
- * And a set of kv pairs:
- *   {"FOO", NULL}
- *   {"ABC=", "YYY"}
- *   {"QWER=", "ASDF"}
- *
- * Thus:
- *   FOO matches both FOO=BAR and FOOOO=BAZ. Since the value is NULL, both these lines are removed.
- *   ABC= matches both ABC=XYZ and ABC=ZYX, these lines are not copied. Instead, ABC=YYY is added
- *     behind any lines that were not matched.
- *   ABCABC is not matched by any kv pair, so it is copied verbatim.
- *   The QWER= prefix is not matched by any line, so QWER=ASDF is appended to the end.
- *
- * The resulting file will therefore look like:
- *  ABCABC=QQQ
- *  ABC=YYY
- *  QWER=ASDF
- *
- */
-int copy_except(FILE *in, FILE *out, const struct keys_values *kv)
-{
-       __attribute__((cleanup(free_ptr))) size_t *const key_len = calloc(kv->n, sizeof *key_len);
-       if (!key_len)
-               return -ENOMEM;
-
-       for (size_t i = 0; i < kv->n; ++i)
-               key_len[i] = strlen(kv->keys[i]);
-
-       char line[MAX_CONF_KEY_LEN + MAX_CONF_VAL_LEN + 2]; // +2 for delimiter and terminator: "K=V\0"
-       while (fgets(line, sizeof line, in)) {
-               size_t i = 0;
-               while (i < kv->n && strncmp(line, kv->keys[i], key_len[i]))
-                       ++i;
-               if (i == kv->n)
-                       fputs(line, out);
-       }
-
-       for (size_t i = 0; i < kv->n; ++i)
-               if (kv->values[i])
-                       fprintf(out, "%s%s\n", kv->keys[i], kv->values[i]);
-
-       return 0;
-}
-
-int copy_file_with_exceptions(const char *config_path, const struct keys_values *kv)
-{
-       const int configfd = open(config_path, O_RDONLY | O_CREAT, 0644);
-       if (configfd < 0) {
-               ERR("configfd open failed: %m");
-               return EXIT_FAILURE;
-       }
-
-       int r = flock(configfd, LOCK_EX);
-       if (r < 0) {
-               ERR("flock failed: %m");
-               close(configfd);
-               return EXIT_FAILURE;
-       }
-
-       __attribute__((cleanup(close_FILE))) FILE *const configfile = fdopen(configfd, "r");
-       if (!configfile) {
-               ERR("configfile fdopen failed: %m");
-               close(configfd);
-               return EXIT_FAILURE;
-       }
-
-       __attribute__((cleanup(free_ptr))) char *tempname = NULL;
-       r = asprintf(&tempname, "%sXXXXXX", config_path);
-       if (r < 0) {
-               tempname = NULL;
-               ERR("asprintf tempname failed: %m\n");
-               return EXIT_FAILURE;
-       }
-
-       /* In older libc versions, umask(077) needed to be called
-        * before mkstemp() to prevent a potential race condition
-        * vulnerability. POSIX-2008 requires mkstemp() to create
-        * the file with rights 600, but our static analysis tools
-        * still complain about this and in this case it was safe
-        * to just do this and silence them. In the general case,
-        * doing this is unsafe as umask() is not thread-aware but
-        * logctl is single-threaded anyway. */
-       mode_t old = umask(077);
-       const int tempfd = mkstemp(tempname);
-       umask(old);
-       if (tempfd < 0) {
-               ERR("mkstemp failed %m\n");
-               return EXIT_FAILURE;
-       }
-
-       __attribute__((cleanup(close_FILE))) FILE *const tempfile = fdopen(tempfd, "w");
-       if (!tempfile) {
-               ERR("tmpfile fdopen failed: %s %m\n", tempname);
-               goto remove_temp;
-       }
-
-       if (fchmod(tempfd, 0644) < 0) {
-               ERR("fchmod failed on %s %m\n", tempname);
-               goto remove_temp;
-       }
-
-       if (copy_except(configfile, tempfile, kv) < 0) {
-               ERR("copy_except failed\n");
-               goto remove_temp;
-       }
-
-       if (rename(tempname, config_path) < 0) {
-               ERR("rename failed: %m\n");
-               goto remove_temp;
-       }
-
-       return EXIT_SUCCESS;
-
-remove_temp:
-       unlink(tempname);
-
-       return EXIT_FAILURE;
-}
-
-int handle_set(const struct parsed_params *params, const char *config_path, struct log_config *conf)
-{
-       (void) conf;
-       __attribute__((cleanup(free_ptr))) char *key;
-       if (asprintf(&key, "limiter|%s|%c=", params->tag, params->prio) < 0) {
-               key = NULL;
-               ERR("asprintf key failed: %m\n");
-               return EXIT_FAILURE;
-       }
-       const char *value = params->value;
-
-       const struct keys_values kv = {
-               .keys = (const char **)&key,
-               .values = &value,
-               .n = 1,
-       };
-
-       return copy_file_with_exceptions(config_path, &kv);
-}
 
-int handle_set_pid(const struct parsed_params *params, const char *config_path, struct log_config *conf)
-{
-       (void) conf;
-
-       __attribute__((cleanup(free_ptr))) char *key;
-       if (asprintf(&key, "pidlimit|%d=", params->pid) < 0) {
-               key = NULL;
-               ERR("asprintf key failed: %m\n");
-               return EXIT_FAILURE;
-       }
-       const char *value = params->value;
-
-       const struct keys_values kv = {
-               .keys = (const char **)&key,
-               .values = &value,
-               .n = 1,
-       };
-
-       return copy_file_with_exceptions(config_path, &kv);
-}
-
-int handle_clear(const struct parsed_params *params, const char *config_path, struct log_config *conf)
-{
-       (void) params;
-       (void) conf;
-       const char *key[] = { "limiter|", "pidlimit|" }; // note: no '=' at the end, so as to match all rules
-       const char *value[] = { NULL, NULL }; // removal
-
-       const struct keys_values kv = {
-               .keys = key,
-               .values = value,
-               .n = 2,
-       };
-
-       return copy_file_with_exceptions(config_path, &kv);
-}
-
-int handle_plog(const struct parsed_params *params, const char *config_path, struct log_config *conf)
-{
-       (void) conf;
-       const int buffers_bitset = params->plog_buffers_bitset ?: ((1 << SINKS_MAX) - 1);
-
-       int count = 0;
-       for (int i = 0; i < SINKS_MAX; ++i)
-               if (buffers_bitset & (1 << i))
-                       ++count;
-       assert(count <= SINKS_MAX);
-
-       const char *key_arr[SINKS_MAX];
-       const char *value_arr[SINKS_MAX];
-
-       const int BUF_SIZE = SINKS_MAX * 20;
-       char buf[BUF_SIZE];
-       int buf_pos = 0;
-       int real_count = 0;
-       for (int i = 0; i < SINKS_MAX; ++i) {
-               if (!(buffers_bitset & (1 << i)))
-                       continue;
-
-               const int r = snprintf(buf + buf_pos, BUF_SIZE - buf_pos, "enable_%s=", log_name_by_id((log_id_t)i));
-               if (r < 0) {
-                       ERR("snprintf bufname %d failed: %m\n", i);
-                       return EXIT_FAILURE;
-               }
-               assert(r < BUF_SIZE - buf_pos);
-               key_arr[real_count] = buf + buf_pos;
-               buf_pos += r + 1; // Also go past NULL
-
-               value_arr[real_count] = params->plog_enable ? "1" : "0";
-               ++real_count;
-       }
-       assert(real_count == count);
-
-       const struct keys_values kv = {
-               .keys = key_arr,
-               .values = value_arr,
-               .n = count,
-       };
-
-       return copy_file_with_exceptions(config_path, &kv);
-}
-
-int handle_stdout_one_pipe(log_id_t id, struct log_config *conf, bool enabled)
-{
-       assert(conf);
-
-       char conf_key[MAX_CONF_KEY_LEN];
-       snprintf(conf_key, sizeof(conf_key), "%s_ctl_sock", log_name_by_id(id));
-
-       const char *sock_path = log_config_get(conf, conf_key);
-       if (!sock_path)
-               return -ENOENT;
-
-       __attribute__((cleanup(close_fd))) int sock_fd = connect_sock(sock_path);
-       if (sock_fd < 0)
-               return sock_fd;
-
-       char enabled_char = (char)enabled;
-
-       int r = send_dlog_request(sock_fd, DLOG_REQ_GLOBAL_ENABLE_DISABLE_STDOUT, &enabled_char, sizeof(enabled_char));
-       if (r < 0)
-               return r;
-
-       return 0;
-}
-
-int handle_stdout_one_nonpipe(log_id_t id, struct log_config *conf, bool enabled)
-{
-       assert(conf);
-
-       return -ENOTSUP;
-}
-
-int handle_stdout(const struct parsed_params *params, const char *config_path, struct log_config *conf)
-{
-       assert(params);
-       assert(conf);
-       (void)config_path;
-
-       const int buffers_bitset = params->plog_buffers_bitset ?: ((1 << LOG_ID_MAX) - 1);
-
-       bool is_pipe;
-       int r = get_is_pipe(conf, &is_pipe);
-       if (r < 0)
-               return r;
-
-       for (int i = 0; i < LOG_ID_MAX; ++i) {
-               if (!(buffers_bitset & (1 << i)))
-                       continue;
-
-               r = (is_pipe ? handle_stdout_one_pipe : handle_stdout_one_nonpipe)(i, conf, params->plog_enable);
-               if (r < 0)
-                       return r;
-       }
-
-       return 0;
-}
-
-int handle_resize(const struct parsed_params *params, const char *config_path, struct log_config *conf)
-{
-       assert(params);
-       assert(conf);
-       (void)config_path;
-
-       const unsigned int capacity = params->capacity;
-       int request = DLOG_REQ_CHANGE_COMPRESSION_SIZE;
-
-       char conf_key[MAX_CONF_KEY_LEN];
-       snprintf(conf_key, sizeof(conf_key), "main_ctl_sock");
-       strtok(conf_key, "\n");
-
-       const char *sock_path = log_config_get(conf, conf_key);
-       if (!sock_path) {
-               fprintf(stderr, "Couldn't get sock path\n");
-               return -ENOENT;
-       }
-
-       __attribute__((cleanup(close_fd))) int sock_fd = connect_sock(sock_path);
-       if (sock_fd < 0) {
-               fprintf(stderr, "Couldn't connect to the sock\n");
-               return sock_fd;
-       }
-
-       size_t data_size = sizeof(unsigned int) + strlen(params->storage_name) + 1;
-       if (data_size > MAX_LOGGER_REQUEST_LEN) {
-               fprintf(stderr, "Request data is too big\n");
-               return -ENOMEM;
-       }
-
-       __attribute__((cleanup(free_ptr))) char *data = malloc(data_size);
-       if (!data) {
-               fprintf(stderr, "Insufficient memory\n");
-               return -ENOMEM;
-       }
-       memcpy(data, &capacity, sizeof(unsigned int));
-       memcpy(data + sizeof(unsigned int), params->storage_name, strlen(params->storage_name) + 1);
-
-       return send_dlog_request(sock_fd, request, (void *)data, data_size);
-}
-
-int get_stdout_one_pipe(log_id_t id, struct log_config *conf, bool *out_enabled)
-{
-       assert(conf);
-       assert(out_enabled);
-
-       char conf_key[MAX_CONF_KEY_LEN];
-       snprintf(conf_key, sizeof(conf_key), "%s_ctl_sock", log_name_by_id(id));
-
-       const char *sock_path = log_config_get(conf, conf_key);
-       if (!sock_path)
-               return -ENOENT;
-
-       __attribute__((cleanup(close_fd))) int sock_fd = connect_sock(sock_path);
-       if (sock_fd < 0)
-               return sock_fd;
-
-       int r = send_dlog_request(sock_fd, DLOG_REQ_GET_STDOUT, NULL, 0);
-       if (r < 0)
-               return r;
-
-       __attribute__((cleanup(free_ptr))) char *enabled_char = NULL;
-       int recv_size;
-       r = recv_dlog_reply(sock_fd, DLOG_REQ_GET_STDOUT, (void **)&enabled_char, &recv_size);
-       if (r < 0)
-               return r;
-       if (recv_size != sizeof(char))
-               return -EINVAL;
-
-       *out_enabled = *enabled_char != '\0';
-       return 0;
-}
-
-int get_stdout_one_nonpipe(log_id_t id, struct log_config *conf, bool *out_enabled)
-{
-       assert(conf);
-
-       return -ENOTSUP;
-}
-
-#ifndef UNIT_TEST
 static void print_limit(int limit, const char *tag, char prio, bool *shadowed, bool dynamic)
 {
        const char *const shadow_str = *shadowed ? "[shadowed] " : "";
@@ -883,4 +274,4 @@ int main(int argc, const char **argv)
 
        return exit_code;
 }
-#endif // !UNIT_TEST
+
diff --git a/src/logctl/logctl_functions.c b/src/logctl/logctl_functions.c
new file mode 100644 (file)
index 0000000..40fe8f2
--- /dev/null
@@ -0,0 +1,602 @@
+#include <buffer_traits.h>
+#include <extra_sinks.h>
+#include <dynamic_config.h>
+#include "logctl_functions.h"
+#include "logctl.h"
+
+#include <ctype.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <sys/file.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+int get_is_pipe(struct log_config *conf, bool *out)
+{
+       const char *const backend = log_config_claim_backend(conf);
+       if (!backend)
+               return -ENOKEY;
+
+       *out = strcmp(backend, "pipe") == 0;
+       return 0;
+}
+
+bool parse_options(int argc, const char **argv, struct parsed_params *params, int *exit_value)
+{
+       assert(argv);
+       assert(params);
+       assert(params->action == ACTION_NONE);
+       assert(!params->value);
+       assert(!params->tag);
+       assert(exit_value);
+
+       params->tag = "*";
+       params->prio = '\0';
+       params->pid = 0;
+       params->plog_buffers_bitset = 0;
+
+       bool use_global_action = true; // if neither tag, pid nor priority is specified
+
+       /* skips a part of the help message */
+       bool show_dynamic_conf_msg = true;
+
+#define SET_ACTION(x) do { \
+       if (params->action != x && params->action != ACTION_NONE) \
+               goto failure; \
+       params->action = x; \
+} while (false)
+
+       for (;;) {
+
+               static const struct option long_options[] = {
+                       {"get"     ,       no_argument, NULL, 'g'},
+                       {"set"     , required_argument, NULL, 's'},
+                       {"tag"     , required_argument, NULL, 't'},
+                       {"priority", required_argument, NULL, 'p'},
+                       {"help"    ,       no_argument, NULL, 'h'},
+                       {"buffer"  , required_argument, NULL, 'b'},
+                       {"clear"   ,       no_argument, NULL, 'c'},
+                       {"enable"  ,       no_argument, NULL,   0},
+                       {"disable" ,       no_argument, NULL,   1},
+                       {"pid"     , required_argument, NULL,   2},
+                       {"enable-stdout",  no_argument, NULL,   3},
+                       {"disable-stdout", no_argument, NULL,   4},
+                       {"compression-resize", required_argument, NULL, 5},
+                       {"compression-name", required_argument, NULL, 6},
+                       {0}
+               };
+
+               int long_index;
+               int opt = getopt_long(argc, (char **) argv, "chb:gs:t:p:", long_options, &long_index);
+               if (opt < 0)
+                       break;
+
+               switch (opt) {
+               case 0:
+                       SET_ACTION(ACTION_PLOG);
+                       params->plog_enable = true;
+                       break;
+               case 1:
+                       SET_ACTION(ACTION_PLOG);
+                       params->plog_enable = false;
+                       break;
+               case 2: {
+                       const int value_num = atoi(optarg);
+                       if (value_num <= 0)
+                               goto failure;
+                       params->pid = value_num;
+                       use_global_action = false;
+                       break;
+               }
+               case 3:
+                       SET_ACTION(ACTION_STDOUT);
+                       params->plog_enable = true;
+                       break;
+               case 4:
+                       SET_ACTION(ACTION_STDOUT);
+                       params->plog_enable = false;
+                       break;
+               case 5: {
+                       const int capacity = atoi(optarg);
+                       if (capacity <= 0)
+                               goto failure;
+                       params->capacity = capacity;
+                       SET_ACTION(ACTION_RESIZE);
+                       show_dynamic_conf_msg = false;
+                       break;
+               } case 6: {
+                       params->storage_name = optarg;
+                       SET_ACTION(ACTION_RESIZE);
+                       show_dynamic_conf_msg = false;
+                       break;
+               } case 'b': {
+                       const int buf_id = sink_id_by_name(optarg);
+                       if (buf_id == LOG_ID_INVALID)
+                               goto failure;
+
+                       params->plog_buffers_bitset |= 1 << buf_id;
+                       break;
+               } case 'h':
+                       *exit_value = EXIT_SUCCESS;
+                       goto print_help;
+               case 'g':
+                       SET_ACTION(ACTION_GET);
+                       break;
+               case 'c':
+                       SET_ACTION(ACTION_CLEAR);
+                       params->value = NULL;
+                       break;
+               case 's': {
+                       const int value_num = atoi(optarg);
+                       if (strcmp(optarg, "allow") && strcmp(optarg, "deny") && (value_num <= 0 || value_num >= __LOG_LIMITER_LIMIT_MAX))
+                               goto failure;
+                       params->value = optarg;
+                       SET_ACTION(ACTION_SET);
+                       break;
+               } case 't': {
+                       params->tag = optarg;
+                       use_global_action = false;
+                       break;
+               } case 'p':
+                       params->prio = toupper(*optarg);
+                       if (!strchr("DVIWEFS*", params->prio))
+                               goto failure;
+                       use_global_action = false;
+                       break;
+
+               default:
+                       break;
+               }
+       }
+
+#undef SET_ACTION
+
+       if (params->pid != 0 && (params->prio != '\0' || strcmp(params->tag, "*") != 0))
+               goto failure;
+
+       if (params->plog_buffers_bitset != 0 && params->action != ACTION_PLOG && params->action != ACTION_STDOUT)
+               goto failure;
+
+       if (params->action == ACTION_NONE) {
+               *exit_value = EXIT_SUCCESS;
+               goto print_help;
+       }
+
+       switch (params->action) {
+       case ACTION_GET:
+               if (use_global_action)
+                       params->action = ACTION_DUMP;
+               break;
+       case ACTION_CLEAR:
+               if (params->pid != 0)
+                       params->action = ACTION_SET_PID;
+               else if (!use_global_action)
+                       params->action = ACTION_SET;
+               break;
+       case ACTION_SET:
+               if (params->pid != 0)
+                       params->action = ACTION_SET_PID;
+               break;
+       default:
+               break;
+       }
+
+       if (params->action == ACTION_SET && params->prio == '\0')
+               params->prio = '*';
+
+       return true;
+
+failure:
+       *exit_value = EXIT_FAILURE;
+
+print_help:
+       printf("Usage: %s [-t/--tag tag] [-p/--priority priority] [--pid pid] (-g/--get | -s/--set value)\n"
+               "\ttag: defaults to *\n"
+               "\tpriority: F[atal], E[rror], W[arning], I[nfo], V[erbose], D[ebug] S[ilent] * (all, default)\n"
+               "\tpid: integer larger than 0, mutually exclusive with --tag and --priority\n"
+               "\t-g/--get: get current limit for given tag/priority; if neither given, dump the whole filterset\n"
+               "\t-s/--set value: set a new limit to value (\"allow\", \"deny\", or number (0; %d) - allowed logs per minute)\n"
+               "\t-c/--clear: clear the limit for given tag/priority; if neither given, clear the whole filterset\n"
+               "Note: static filters are shadowed by dynamic ones but cannot be overridden at runtime\n\n"
+               "       %s [-b/--buffer bufname] [-b ...] (--enable | --disable)\n"
+               "\tbufname: main system radio apps (default: all of them)\n"
+               "\t-b/--buffer bufname: pick given buffer for plog control\n"
+               "\t--enable/--disable: control platform logging for chosen buffers\n"
+               "\t--enable-stdout/--disable-stdout: control stdout redirection for chosen buffers\n"
+               "\t--compression-name: resizes given named compression buffer. Must be used with --compression-resize\n"
+               "\t--compression-resize: sets compression buffer to given size, in bytes.\n"
+               "\n"
+               , argv[0]
+               , __LOG_LIMITER_LIMIT_MAX
+               , argv[0]
+       );
+
+       if (show_dynamic_conf_msg) {
+               printf("NOTE: to use dlogctl, dynamic dlog config has to be enabled!\n"
+               "Define the %s config entry to a *DIRECTORY* path.\n"
+               "ALSO: filters won't work for the APPS buffer (whence most spam comes)\n"
+               "unless you set `limiter_apply_to_all_buffers` in the config to 1!\n"
+               "The filters are also currently per process; this may change in the future.\n\n"
+               , DYNAMIC_CONFIG_CONF_KEY
+               );
+       }
+
+       return false;
+}
+
+/**
+ * @brief Copy file, except modify some lines
+ * @details Copies a file linewise to another, except ensures lines starting with given prefixes have certain follow-up
+ * @param[in] in Copy from this file
+ * @param[in] out Copy into this file
+ * @param[in] kv A set of key-value pairs, where lines which do not match any of the prefixes are copied verbatim, while
+ *               lines that do are ignored. For each key where the value is not NULL, a line with the key as the prefix
+ *               and the value as the suffix is then appended to the resulting file.
+ *
+ * @notes Usually the keys will contain a '=' at the end so as to only replace exact matches in a config file,
+ *        and keys without '=' at the end should usually have the value NULL to keep the config format valid
+ *
+ * @example Consider a file with the contents:
+ *   FOO=BAR
+ *   FOOOO=BAZ
+ *   ABC=XYZ
+ *   ABCABC=QQQ
+ *   ABC=ZYX
+ *
+ * And a set of kv pairs:
+ *   {"FOO", NULL}
+ *   {"ABC=", "YYY"}
+ *   {"QWER=", "ASDF"}
+ *
+ * Thus:
+ *   FOO matches both FOO=BAR and FOOOO=BAZ. Since the value is NULL, both these lines are removed.
+ *   ABC= matches both ABC=XYZ and ABC=ZYX, these lines are not copied. Instead, ABC=YYY is added
+ *     behind any lines that were not matched.
+ *   ABCABC is not matched by any kv pair, so it is copied verbatim.
+ *   The QWER= prefix is not matched by any line, so QWER=ASDF is appended to the end.
+ *
+ * The resulting file will therefore look like:
+ *  ABCABC=QQQ
+ *  ABC=YYY
+ *  QWER=ASDF
+ *
+ */
+int copy_except(FILE *in, FILE *out, const struct keys_values *kv)
+{
+       __attribute__((cleanup(free_ptr))) size_t *const key_len = calloc(kv->n, sizeof *key_len);
+       if (!key_len)
+               return -ENOMEM;
+
+       for (size_t i = 0; i < kv->n; ++i)
+               key_len[i] = strlen(kv->keys[i]);
+
+       char line[MAX_CONF_KEY_LEN + MAX_CONF_VAL_LEN + 2]; // +2 for delimiter and terminator: "K=V\0"
+       while (fgets(line, sizeof line, in)) {
+               size_t i = 0;
+               while (i < kv->n && strncmp(line, kv->keys[i], key_len[i]))
+                       ++i;
+               if (i == kv->n)
+                       fputs(line, out);
+       }
+
+       for (size_t i = 0; i < kv->n; ++i)
+               if (kv->values[i])
+                       fprintf(out, "%s%s\n", kv->keys[i], kv->values[i]);
+
+       return 0;
+}
+
+int copy_file_with_exceptions(const char *config_path, const struct keys_values *kv)
+{
+       const int configfd = open(config_path, O_RDONLY | O_CREAT, 0644);
+       if (configfd < 0) {
+               ERR("configfd open failed: %m");
+               return EXIT_FAILURE;
+       }
+
+       int r = flock(configfd, LOCK_EX);
+       if (r < 0) {
+               ERR("flock failed: %m");
+               close(configfd);
+               return EXIT_FAILURE;
+       }
+
+       __attribute__((cleanup(close_FILE))) FILE *const configfile = fdopen(configfd, "r");
+       if (!configfile) {
+               ERR("configfile fdopen failed: %m");
+               close(configfd);
+               return EXIT_FAILURE;
+       }
+
+       __attribute__((cleanup(free_ptr))) char *tempname = NULL;
+       r = asprintf(&tempname, "%sXXXXXX", config_path);
+       if (r < 0) {
+               tempname = NULL;
+               ERR("asprintf tempname failed: %m\n");
+               return EXIT_FAILURE;
+       }
+
+       /* In older libc versions, umask(077) needed to be called
+        * before mkstemp() to prevent a potential race condition
+        * vulnerability. POSIX-2008 requires mkstemp() to create
+        * the file with rights 600, but our static analysis tools
+        * still complain about this and in this case it was safe
+        * to just do this and silence them. In the general case,
+        * doing this is unsafe as umask() is not thread-aware but
+        * logctl is single-threaded anyway. */
+       mode_t old = umask(077);
+       const int tempfd = mkstemp(tempname);
+       umask(old);
+       if (tempfd < 0) {
+               ERR("mkstemp failed %m\n");
+               return EXIT_FAILURE;
+       }
+
+       __attribute__((cleanup(close_FILE))) FILE *const tempfile = fdopen(tempfd, "w");
+       if (!tempfile) {
+               ERR("tmpfile fdopen failed: %s %m\n", tempname);
+               goto remove_temp;
+       }
+
+       if (fchmod(tempfd, 0644) < 0) {
+               ERR("fchmod failed on %s %m\n", tempname);
+               goto remove_temp;
+       }
+
+       if (copy_except(configfile, tempfile, kv) < 0) {
+               ERR("copy_except failed\n");
+               goto remove_temp;
+       }
+
+       if (rename(tempname, config_path) < 0) {
+               ERR("rename failed: %m\n");
+               goto remove_temp;
+       }
+
+       return EXIT_SUCCESS;
+
+remove_temp:
+       unlink(tempname);
+
+       return EXIT_FAILURE;
+}
+
+int handle_set(const struct parsed_params *params, const char *config_path, struct log_config *conf)
+{
+       (void) conf;
+       __attribute__((cleanup(free_ptr))) char *key;
+       if (asprintf(&key, "limiter|%s|%c=", params->tag, params->prio) < 0) {
+               key = NULL;
+               ERR("asprintf key failed: %m\n");
+               return EXIT_FAILURE;
+       }
+       const char *value = params->value;
+
+       const struct keys_values kv = {
+               .keys = (const char **)&key,
+               .values = &value,
+               .n = 1,
+       };
+
+       return copy_file_with_exceptions(config_path, &kv);
+}
+
+int handle_set_pid(const struct parsed_params *params, const char *config_path, struct log_config *conf)
+{
+       (void) conf;
+
+       __attribute__((cleanup(free_ptr))) char *key;
+       if (asprintf(&key, "pidlimit|%d=", params->pid) < 0) {
+               key = NULL;
+               ERR("asprintf key failed: %m\n");
+               return EXIT_FAILURE;
+       }
+       const char *value = params->value;
+
+       const struct keys_values kv = {
+               .keys = (const char **)&key,
+               .values = &value,
+               .n = 1,
+       };
+
+       return copy_file_with_exceptions(config_path, &kv);
+}
+
+int handle_clear(const struct parsed_params *params, const char *config_path, struct log_config *conf)
+{
+       (void) params;
+       (void) conf;
+       const char *key[] = { "limiter|", "pidlimit|" }; // note: no '=' at the end, so as to match all rules
+       const char *value[] = { NULL, NULL }; // removal
+
+       const struct keys_values kv = {
+               .keys = key,
+               .values = value,
+               .n = 2,
+       };
+
+       return copy_file_with_exceptions(config_path, &kv);
+}
+
+int handle_plog(const struct parsed_params *params, const char *config_path, struct log_config *conf)
+{
+       (void) conf;
+       const int buffers_bitset = params->plog_buffers_bitset ?: ((1 << SINKS_MAX) - 1);
+
+       int count = 0;
+       for (int i = 0; i < SINKS_MAX; ++i)
+               if (buffers_bitset & (1 << i))
+                       ++count;
+       assert(count <= SINKS_MAX);
+
+       const char *key_arr[SINKS_MAX];
+       const char *value_arr[SINKS_MAX];
+
+       const int BUF_SIZE = SINKS_MAX * 20;
+       char buf[BUF_SIZE];
+       int buf_pos = 0;
+       int real_count = 0;
+       for (int i = 0; i < SINKS_MAX; ++i) {
+               if (!(buffers_bitset & (1 << i)))
+                       continue;
+
+               const int r = snprintf(buf + buf_pos, BUF_SIZE - buf_pos, "enable_%s=", log_name_by_id((log_id_t)i));
+               if (r < 0) {
+                       ERR("snprintf bufname %d failed: %m\n", i);
+                       return EXIT_FAILURE;
+               }
+               assert(r < BUF_SIZE - buf_pos);
+               key_arr[real_count] = buf + buf_pos;
+               buf_pos += r + 1; // Also go past NULL
+
+               value_arr[real_count] = params->plog_enable ? "1" : "0";
+               ++real_count;
+       }
+       assert(real_count == count);
+
+       const struct keys_values kv = {
+               .keys = key_arr,
+               .values = value_arr,
+               .n = count,
+       };
+
+       return copy_file_with_exceptions(config_path, &kv);
+}
+
+int handle_stdout_one_pipe(log_id_t id, struct log_config *conf, bool enabled)
+{
+       assert(conf);
+
+       char conf_key[MAX_CONF_KEY_LEN];
+       snprintf(conf_key, sizeof(conf_key), "%s_ctl_sock", log_name_by_id(id));
+
+       const char *sock_path = log_config_get(conf, conf_key);
+       if (!sock_path)
+               return -ENOENT;
+
+       __attribute__((cleanup(close_fd))) int sock_fd = connect_sock(sock_path);
+       if (sock_fd < 0)
+               return sock_fd;
+
+       char enabled_char = (char)enabled;
+
+       int r = send_dlog_request(sock_fd, DLOG_REQ_GLOBAL_ENABLE_DISABLE_STDOUT, &enabled_char, sizeof(enabled_char));
+       if (r < 0)
+               return r;
+
+       return 0;
+}
+
+int handle_stdout_one_nonpipe(log_id_t id, struct log_config *conf, bool enabled)
+{
+       assert(conf);
+
+       return -ENOTSUP;
+}
+
+int handle_stdout(const struct parsed_params *params, const char *config_path, struct log_config *conf)
+{
+       assert(params);
+       assert(conf);
+       (void)config_path;
+
+       const int buffers_bitset = params->plog_buffers_bitset ?: ((1 << LOG_ID_MAX) - 1);
+
+       bool is_pipe;
+       int r = get_is_pipe(conf, &is_pipe);
+       if (r < 0)
+               return r;
+
+       for (int i = 0; i < LOG_ID_MAX; ++i) {
+               if (!(buffers_bitset & (1 << i)))
+                       continue;
+
+               r = (is_pipe ? handle_stdout_one_pipe : handle_stdout_one_nonpipe)(i, conf, params->plog_enable);
+               if (r < 0)
+                       return r;
+       }
+
+       return 0;
+}
+
+int handle_resize(const struct parsed_params *params, const char *config_path, struct log_config *conf)
+{
+       assert(params);
+       assert(conf);
+       (void)config_path;
+
+       const unsigned int capacity = params->capacity;
+       int request = DLOG_REQ_CHANGE_COMPRESSION_SIZE;
+
+       char conf_key[MAX_CONF_KEY_LEN];
+       snprintf(conf_key, sizeof(conf_key), "main_ctl_sock");
+       strtok(conf_key, "\n");
+
+       const char *sock_path = log_config_get(conf, conf_key);
+       if (!sock_path) {
+               fprintf(stderr, "Couldn't get sock path\n");
+               return -ENOENT;
+       }
+
+       __attribute__((cleanup(close_fd))) int sock_fd = connect_sock(sock_path);
+       if (sock_fd < 0) {
+               fprintf(stderr, "Couldn't connect to the sock\n");
+               return sock_fd;
+       }
+
+       size_t data_size = sizeof(unsigned int) + strlen(params->storage_name) + 1;
+       if (data_size > MAX_LOGGER_REQUEST_LEN) {
+               fprintf(stderr, "Request data is too big\n");
+               return -ENOMEM;
+       }
+
+       __attribute__((cleanup(free_ptr))) char *data = malloc(data_size);
+       if (!data) {
+               fprintf(stderr, "Insufficient memory\n");
+               return -ENOMEM;
+       }
+       memcpy(data, &capacity, sizeof(unsigned int));
+       memcpy(data + sizeof(unsigned int), params->storage_name, strlen(params->storage_name) + 1);
+
+       return send_dlog_request(sock_fd, request, (void *)data, data_size);
+}
+
+int get_stdout_one_pipe(log_id_t id, struct log_config *conf, bool *out_enabled)
+{
+       assert(conf);
+       assert(out_enabled);
+
+       char conf_key[MAX_CONF_KEY_LEN];
+       snprintf(conf_key, sizeof(conf_key), "%s_ctl_sock", log_name_by_id(id));
+
+       const char *sock_path = log_config_get(conf, conf_key);
+       if (!sock_path)
+               return -ENOENT;
+
+       __attribute__((cleanup(close_fd))) int sock_fd = connect_sock(sock_path);
+       if (sock_fd < 0)
+               return sock_fd;
+
+       int r = send_dlog_request(sock_fd, DLOG_REQ_GET_STDOUT, NULL, 0);
+       if (r < 0)
+               return r;
+
+       __attribute__((cleanup(free_ptr))) char *enabled_char = NULL;
+       int recv_size;
+       r = recv_dlog_reply(sock_fd, DLOG_REQ_GET_STDOUT, (void **)&enabled_char, &recv_size);
+       if (r < 0)
+               return r;
+       if (recv_size != sizeof(char))
+               return -EINVAL;
+
+       *out_enabled = *enabled_char != '\0';
+       return 0;
+}
+
+int get_stdout_one_nonpipe(log_id_t id, struct log_config *conf, bool *out_enabled)
+{
+       assert(conf);
+
+       return -ENOTSUP;
+}
+
diff --git a/src/logctl/logctl_functions.h b/src/logctl/logctl_functions.h
new file mode 100644 (file)
index 0000000..8c9ebad
--- /dev/null
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <stdio.h> // FILE
+#include "dlog-internal.h" // log_id_t
+
+struct log_config;
+struct parsed_params;
+struct keys_values;
+
+int get_is_pipe(struct log_config *conf, bool *out);
+bool parse_options(int argc, const char **argv, struct parsed_params *params, int *exit_value);
+int copy_except(FILE *in, FILE *out, const struct keys_values *kv);
+int copy_file_with_exceptions(const char *config_path, const struct keys_values *kv);
+int handle_set(const struct parsed_params *params, const char *config_path, struct log_config *conf);
+int handle_set_pid(const struct parsed_params *params, const char *config_path, struct log_config *conf);
+int handle_clear(const struct parsed_params *params, const char *config_path, struct log_config *conf);
+int handle_plog(const struct parsed_params *params, const char *config_path, struct log_config *conf);
+int handle_stdout_one_pipe(log_id_t id, struct log_config *conf, bool enabled);
+int handle_stdout_one_nonpipe(log_id_t id, struct log_config *conf, bool enabled);
+int handle_stdout(const struct parsed_params *params, const char *config_path, struct log_config *conf);
+int handle_resize(const struct parsed_params *params, const char *config_path, struct log_config *conf);
+int get_stdout_one_pipe(log_id_t id, struct log_config *conf, bool *out_enabled);
+int get_stdout_one_nonpipe(log_id_t id, struct log_config *conf, bool *out_enabled);
+