diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:34:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:34:10 +0000 |
commit | e4ba6dbc3f1e76890b22773807ea37fe8fa2b1bc (patch) | |
tree | 68cb5ef9081156392f1dd62a00c6ccc1451b93df /wsutil/wslog.c | |
parent | Initial commit. (diff) | |
download | wireshark-e4ba6dbc3f1e76890b22773807ea37fe8fa2b1bc.tar.xz wireshark-e4ba6dbc3f1e76890b22773807ea37fe8fa2b1bc.zip |
Adding upstream version 4.2.2.upstream/4.2.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'wsutil/wslog.c')
-rw-r--r-- | wsutil/wslog.c | 1442 |
1 files changed, 1442 insertions, 0 deletions
diff --git a/wsutil/wslog.c b/wsutil/wslog.c new file mode 100644 index 00000000..e181e5ed --- /dev/null +++ b/wsutil/wslog.c @@ -0,0 +1,1442 @@ +/* + * Copyright 2021, João Valverde <j@v6e.pt> + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "config.h" + +#include "wslog.h" + +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <time.h> +/* Because ws_assert() dependes on ws_error() we do not use it + * here and fall back on assert() instead. */ +#include <assert.h> +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif +#ifdef _WIN32 +#include <process.h> +#include <windows.h> +#include <conio.h> +#endif + +#include "file_util.h" +#include "time_util.h" +#include "to_str.h" +#include "strtoi.h" +#ifdef _WIN32 +#include "console_win32.h" +#endif + +#define ASSERT(expr) assert(expr) + +/* Runtime log level. */ +#define ENV_VAR_LEVEL "WIRESHARK_LOG_LEVEL" + +/* Log domains enabled/disabled. */ +#define ENV_VAR_DOMAIN "WIRESHARK_LOG_DOMAIN" + +/* Alias "domain" and "domains". */ +#define ENV_VAR_DOMAIN_S "WIRESHARK_LOG_DOMAINS" + +/* Log level that generates a trap and aborts. Can be "critical" + * or "warning". */ +#define ENV_VAR_FATAL "WIRESHARK_LOG_FATAL" + +/* Log domains that are fatal. */ +#define ENV_VAR_FATAL_DOMAIN "WIRESHARK_LOG_FATAL_DOMAIN" + +/* Alias "domain" and "domains". */ +#define ENV_VAR_FATAL_DOMAIN_S "WIRESHARK_LOG_FATAL_DOMAINS" + +/* Domains that will produce debug output, regardless of log level or + * domain filter. */ +#define ENV_VAR_DEBUG "WIRESHARK_LOG_DEBUG" + +/* Domains that will produce noisy output, regardless of log level or + * domain filter. */ +#define ENV_VAR_NOISY "WIRESHARK_LOG_NOISY" + +#define DEFAULT_LOG_LEVEL LOG_LEVEL_MESSAGE + +#define DEFAULT_PROGNAME "PID" + +#define DOMAIN_UNDEFED(domain) ((domain) == NULL || *(domain) == '\0') +#define DOMAIN_DEFINED(domain) (!DOMAIN_UNDEFED(domain)) + +/* + * Note: I didn't measure it but I assume using a string array is faster than + * a GHashTable for small number N of domains. + */ +typedef struct { + char **domainv; + bool positive; /* positive or negative match */ + enum ws_log_level min_level; /* for level filters */ +} log_filter_t; + + +/* If the module is not initialized by calling ws_log_init() all messages + * will be printed regardless of log level. This is a feature, not a bug. */ +static enum ws_log_level current_log_level = LOG_LEVEL_NONE; + +static bool stdout_color_enabled = false; + +static bool stderr_color_enabled = false; + +/* Use stdout for levels "info" and below, for backward compatibility + * with GLib. */ +static bool stdout_logging_enabled = false; + +static const char *registered_progname = DEFAULT_PROGNAME; + +/* List of domains to filter. */ +static log_filter_t *domain_filter = NULL; + +/* List of domains to output debug level unconditionally. */ +static log_filter_t *debug_filter = NULL; + +/* List of domains to output noisy level unconditionally. */ +static log_filter_t *noisy_filter = NULL; + +/* List of domains that are fatal. */ +static log_filter_t *fatal_filter = NULL; + +static ws_log_writer_cb *registered_log_writer = NULL; + +static void *registered_log_writer_data = NULL; + +static ws_log_writer_free_data_cb *registered_log_writer_data_free = NULL; + +static FILE *custom_log = NULL; + +static enum ws_log_level fatal_log_level = LOG_LEVEL_ERROR; + +#ifdef WS_DEBUG +static bool init_complete = false; +#endif + +ws_log_console_open_pref ws_log_console_open = LOG_CONSOLE_OPEN_NEVER; + + +static void print_err(void (*vcmdarg_err)(const char *, va_list ap), + int exit_failure, + const char *fmt, ...) G_GNUC_PRINTF(3,4); + +static void ws_log_cleanup(void); + + +const char *ws_log_level_to_string(enum ws_log_level level) +{ + switch (level) { + case LOG_LEVEL_NONE: + return "(zero)"; + case LOG_LEVEL_ECHO: + return "ECHO"; + case LOG_LEVEL_ERROR: + return "ERROR"; + case LOG_LEVEL_CRITICAL: + return "CRITICAL"; + case LOG_LEVEL_WARNING: + return "WARNING"; + case LOG_LEVEL_MESSAGE: + return "MESSAGE"; + case LOG_LEVEL_INFO: + return "INFO"; + case LOG_LEVEL_DEBUG: + return "DEBUG"; + case LOG_LEVEL_NOISY: + return "NOISY"; + default: + return "(BOGUS LOG LEVEL)"; + } +} + + +static enum ws_log_level string_to_log_level(const char *str_level) +{ + if (!str_level) + return LOG_LEVEL_NONE; + + if (g_ascii_strcasecmp(str_level, "noisy") == 0) + return LOG_LEVEL_NOISY; + else if (g_ascii_strcasecmp(str_level, "debug") == 0) + return LOG_LEVEL_DEBUG; + else if (g_ascii_strcasecmp(str_level, "info") == 0) + return LOG_LEVEL_INFO; + else if (g_ascii_strcasecmp(str_level, "message") == 0) + return LOG_LEVEL_MESSAGE; + else if (g_ascii_strcasecmp(str_level, "warning") == 0) + return LOG_LEVEL_WARNING; + else if (g_ascii_strcasecmp(str_level, "critical") == 0) + return LOG_LEVEL_CRITICAL; + else if (g_ascii_strcasecmp(str_level, "error") == 0) + return LOG_LEVEL_ERROR; + else if (g_ascii_strcasecmp(str_level, "echo") == 0) + return LOG_LEVEL_ECHO; + else + return LOG_LEVEL_NONE; +} + + +WS_RETNONNULL +static inline const char *domain_to_string(const char *domain) +{ + return DOMAIN_UNDEFED(domain) ? "(none)" : domain; +} + + +static inline bool filter_contains(log_filter_t *filter, + const char *domain) +{ + if (filter == NULL || DOMAIN_UNDEFED(domain)) + return false; + + for (char **domv = filter->domainv; *domv != NULL; domv++) { + if (g_ascii_strcasecmp(*domv, domain) == 0) { + return true; + } + } + return false; +} + + +static inline bool level_filter_matches(log_filter_t *filter, + const char *domain, + enum ws_log_level level, + bool *active_ptr) +{ + if (filter == NULL || DOMAIN_UNDEFED(domain)) + return false; + + if (!filter_contains(filter, domain)) + return false; + + if (filter->positive) { + if (active_ptr) + *active_ptr = level >= filter->min_level; + return true; + } + + /* negative match */ + if (level <= filter->min_level) { + if (active_ptr) + *active_ptr = false; + return true; + } + + return false; +} + + +static inline void +get_timestamp(struct timespec *ts) +{ + bool ok = false; + +#if defined(HAVE_CLOCK_GETTIME) + ok = (clock_gettime(CLOCK_REALTIME, ts) == 0); +#elif defined(HAVE_TIMESPEC_GET) + ok = (timespec_get(ts, TIME_UTC) == TIME_UTC); +#endif + if (ok) + return; + + /* Fall back on time(). */ + ts->tv_sec = time(NULL); + ts->tv_nsec = -1; +} + + +static inline void fill_manifest(ws_log_manifest_t *mft) +{ + struct timespec ts; + get_timestamp(&ts); + ws_localtime_r(&ts.tv_sec, &mft->tstamp_secs); + mft->nanosecs = ts.tv_nsec; + mft->pid = getpid(); +} + + +static inline bool msg_is_active(const char *domain, enum ws_log_level level, + ws_log_manifest_t *mft) +{ + bool is_active = ws_log_msg_is_active(domain, level); + if (is_active) + fill_manifest(mft); + return is_active; +} + + +bool ws_log_msg_is_active(const char *domain, enum ws_log_level level) +{ + /* + * Higher numerical levels have higher priority. Critical and above + * are always enabled. + */ + if (level >= LOG_LEVEL_CRITICAL) + return true; + + /* + * Check if the level has been configured as fatal. + */ + if (level >= fatal_log_level) + return true; + + /* + * Check if the domain has been configured as fatal. + */ + if (DOMAIN_DEFINED(domain) && fatal_filter != NULL) { + if (filter_contains(fatal_filter, domain) && fatal_filter->positive) { + return true; + } + } + + /* + * The debug/noisy filter overrides the other parameters. + */ + if (DOMAIN_DEFINED(domain)) { + bool active; + + if (level_filter_matches(noisy_filter, domain, level, &active)) + return active; + if (level_filter_matches(debug_filter, domain, level, &active)) + return active; + } + + /* + * If the priority is lower than the current minimum drop the + * message. + */ + if (level < current_log_level) + return false; + + /* + * If we don't have domain filtering enabled we are done. + */ + if (domain_filter == NULL) + return true; + + /* + * We have a filter but we don't use it with the undefined domain, + * pretty much every permanent call to ws_log should be using a + * chosen domain. + */ + if (DOMAIN_UNDEFED(domain)) + return true; + + /* Check if the domain filter matches. */ + if (filter_contains(domain_filter, domain)) + return domain_filter->positive; + + /* We have a domain filter but it didn't match. */ + return !domain_filter->positive; +} + + +enum ws_log_level ws_log_get_level(void) +{ + return current_log_level; +} + + +enum ws_log_level ws_log_set_level(enum ws_log_level level) +{ + if (level <= LOG_LEVEL_NONE || level >= _LOG_LEVEL_LAST) + return LOG_LEVEL_NONE; + if (level > LOG_LEVEL_CRITICAL) + level = LOG_LEVEL_CRITICAL; + + current_log_level = level; + return current_log_level; +} + + +enum ws_log_level ws_log_set_level_str(const char *str_level) +{ + enum ws_log_level level; + + level = string_to_log_level(str_level); + return ws_log_set_level(level); +} + + +static const char *opt_level = "--log-level"; +static const char *opt_domain = "--log-domain"; +/* Alias "domain" and "domains". */ +static const char *opt_domain_s = "--log-domains"; +static const char *opt_file = "--log-file"; +static const char *opt_fatal = "--log-fatal"; +static const char *opt_fatal_domain = "--log-fatal-domain"; +/* Alias "domain" and "domains". */ +static const char *opt_fatal_domain_s = "--log-fatal-domains"; +static const char *opt_debug = "--log-debug"; +static const char *opt_noisy = "--log-noisy"; + + +static void print_err(void (*vcmdarg_err)(const char *, va_list ap), + int exit_failure, + const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + if (vcmdarg_err) + vcmdarg_err(fmt, ap); + else + vfprintf(stderr, fmt, ap); + va_end(ap); + if (exit_failure != LOG_ARGS_NOEXIT) + exit(exit_failure); +} + + +/* + * This tries to convert old log level preference to a wslog + * configuration. The string must start with "console.log.level:" + * It receives an argv for { '-o', 'console.log.level:nnn', ...} or + * { '-oconsole.log.level:nnn', ...}. + */ +static void +parse_console_compat_option(char *argv[], + void (*vcmdarg_err)(const char *, va_list ap), + int exit_failure) +{ + const char *mask_str; + uint32_t mask; + enum ws_log_level level; + + ASSERT(argv != NULL); + + if (argv[0] == NULL) + return; + + if (strcmp(argv[0], "-o") == 0) { + if (argv[1] == NULL || + !g_str_has_prefix(argv[1], "console.log.level:")) { + /* Not what we were looking for. */ + return; + } + mask_str = argv[1] + strlen("console.log.level:"); + } + else if (g_str_has_prefix(argv[0], "-oconsole.log.level:")) { + mask_str = argv[0] + strlen("-oconsole.log.level:"); + } + else { + /* Not what we were looking for. */ + return; + } + + print_err(vcmdarg_err, LOG_ARGS_NOEXIT, + "Option 'console.log.level' is deprecated, consult '--help' " + "for diagnostic message options."); + + if (*mask_str == '\0') { + print_err(vcmdarg_err, exit_failure, + "Missing value to 'console.log.level' option."); + return; + } + + if (!ws_basestrtou32(mask_str, NULL, &mask, 10)) { + print_err(vcmdarg_err, exit_failure, + "%s is not a valid decimal number.", mask_str); + return; + } + + /* + * The lowest priority bit in the mask defines the level. + */ + if (mask & G_LOG_LEVEL_DEBUG) + level = LOG_LEVEL_DEBUG; + else if (mask & G_LOG_LEVEL_INFO) + level = LOG_LEVEL_INFO; + else if (mask & G_LOG_LEVEL_MESSAGE) + level = LOG_LEVEL_MESSAGE; + else if (mask & G_LOG_LEVEL_WARNING) + level = LOG_LEVEL_WARNING; + else if (mask & G_LOG_LEVEL_CRITICAL) + level = LOG_LEVEL_CRITICAL; + else if (mask & G_LOG_LEVEL_ERROR) + level = LOG_LEVEL_ERROR; + else + level = LOG_LEVEL_NONE; + + if (level == LOG_LEVEL_NONE) { + /* Some values (like zero) might not contain any meaningful bits. + * Throwing an error in that case seems appropriate. */ + print_err(vcmdarg_err, exit_failure, + "Value %s is not a valid log mask.", mask_str); + return; + } + + ws_log_set_level(level); +} + +/* Match "arg_name=value" or "arg_name value" to opt_name. */ +static bool optequal(const char *arg, const char *opt) +{ + ASSERT(arg); + ASSERT(opt); +#define ARGEND(arg) (*(arg) == '\0' || *(arg) == ' ' || *(arg) == '=') + + while (!ARGEND(arg) && *opt != '\0') { + if (*arg != *opt) { + return false; + } + arg += 1; + opt += 1; + } + if (ARGEND(arg) && *opt == '\0') { + return true; + } + return false; +} + +int ws_log_parse_args(int *argc_ptr, char *argv[], + void (*vcmdarg_err)(const char *, va_list ap), + int exit_failure) +{ + char **ptr = argv; + int count = *argc_ptr; + int ret = 0; + size_t optlen; + const char *option, *value; + int extra; + + if (argc_ptr == NULL || argv == NULL) + return -1; + +#ifdef WS_DEBUG + /* Assert ws_log_init() was called before ws_log_parse_args(). */ + ASSERT(init_complete); +#endif + + /* Configure from command line. */ + + while (*ptr != NULL) { + if (optequal(*ptr, opt_level)) { + option = opt_level; + optlen = strlen(opt_level); + } + else if (optequal(*ptr, opt_domain)) { + option = opt_domain; + optlen = strlen(opt_domain); + } + else if (optequal(*ptr, opt_domain_s)) { + option = opt_domain; /* Alias */ + optlen = strlen(opt_domain_s); + } + else if (optequal(*ptr, opt_fatal_domain)) { + option = opt_fatal_domain; + optlen = strlen(opt_fatal_domain); + } + else if (optequal(*ptr, opt_fatal_domain_s)) { + option = opt_fatal_domain; /* Alias */ + optlen = strlen(opt_fatal_domain_s); + } + else if (optequal(*ptr, opt_file)) { + option = opt_file; + optlen = strlen(opt_file); + } + else if (optequal(*ptr, opt_fatal)) { + option = opt_fatal; + optlen = strlen(opt_fatal); + } + else if (optequal(*ptr, opt_debug)) { + option = opt_debug; + optlen = strlen(opt_debug); + } + else if (optequal(*ptr, opt_noisy)) { + option = opt_noisy; + optlen = strlen(opt_noisy); + } + else { + /* Check is we have the old '-o console.log.level' flag, + * or '-oconsole.log.level', for backward compatibility. + * Then if we do ignore it after processing and let the + * preferences module handle it later. */ + if (*(*ptr + 0) == '-' && *(*ptr + 1) == 'o') { + parse_console_compat_option(ptr, vcmdarg_err, exit_failure); + } + ptr += 1; + count -= 1; + continue; + } + + value = *ptr + optlen; + /* Two possibilities: + * --<option> <value> + * or + * --<option>=<value> + */ + if (value[0] == '\0') { + /* value is separated with blank space */ + value = *(ptr + 1); + extra = 1; + + if (value == NULL || !*value || *value == '-') { + /* If the option value after the blank starts with '-' assume + * it is another option. */ + print_err(vcmdarg_err, exit_failure, + "Option \"%s\" requires a value.\n", *ptr); + option = NULL; + extra = 0; + ret += 1; + } + } + else if (value[0] == '=') { + /* value is after equals */ + value += 1; + extra = 0; + } + else { + /* Option isn't known. */ + ptr += 1; + count -= 1; + continue; + } + + if (option == opt_level) { + if (ws_log_set_level_str(value) == LOG_LEVEL_NONE) { + print_err(vcmdarg_err, exit_failure, + "Invalid log level \"%s\".\n", value); + ret += 1; + } + } + else if (option == opt_domain) { + ws_log_set_domain_filter(value); + } + else if (option == opt_fatal_domain) { + ws_log_set_fatal_domain_filter(value); + } + else if (option == opt_file) { + if (value == NULL) { + print_err(vcmdarg_err, exit_failure, + "Option '%s' requires an argument.\n", + option); + ret += 1; + } + else { + FILE *fp = ws_fopen(value, "w"); + if (fp == NULL) { + print_err(vcmdarg_err, exit_failure, + "Error opening file '%s' for writing: %s.\n", + value, g_strerror(errno)); + ret += 1; + } + else { + ws_log_add_custom_file(fp); + } + } + } + else if (option == opt_fatal) { + if (ws_log_set_fatal_level_str(value) == LOG_LEVEL_NONE) { + print_err(vcmdarg_err, exit_failure, + "Fatal log level must be \"critical\" or " + "\"warning\", not \"%s\".\n", value); + ret += 1; + } + } + else if (option == opt_debug) { + ws_log_set_debug_filter(value); + } + else if (option == opt_noisy) { + ws_log_set_noisy_filter(value); + } + else { + /* Option value missing or invalid, do nothing. */ + } + + /* + * We found a log option. We will remove it from + * the argv by moving up the other strings in the array. This is + * so that it doesn't generate an unrecognized option + * error further along in the initialization process. + */ + /* Include the terminating NULL in the memmove. */ + memmove(ptr, ptr + 1 + extra, (count - extra) * sizeof(*ptr)); + /* No need to increment ptr here. */ + count -= (1 + extra); + *argc_ptr -= (1 + extra); + } + + return ret; +} + + +static void free_log_filter(log_filter_t **filter_ptr) +{ + if (filter_ptr == NULL || *filter_ptr == NULL) + return; + g_strfreev((*filter_ptr)->domainv); + g_free(*filter_ptr); + *filter_ptr = NULL; +} + + +static void tokenize_filter_str(log_filter_t **filter_ptr, + const char *str_filter, + enum ws_log_level min_level) +{ + const char *sep = ",;"; + bool negated = false; + log_filter_t *filter; + + ASSERT(filter_ptr); + ASSERT(*filter_ptr == NULL); + + if (str_filter == NULL) + return; + + if (str_filter[0] == '!') { + negated = true; + str_filter += 1; + } + if (*str_filter == '\0') + return; + + filter = g_new(log_filter_t, 1); + filter->domainv = g_strsplit_set(str_filter, sep, -1); + filter->positive = !negated; + filter->min_level = min_level; + *filter_ptr = filter; +} + + +void ws_log_set_domain_filter(const char *str_filter) +{ + free_log_filter(&domain_filter); + tokenize_filter_str(&domain_filter, str_filter, LOG_LEVEL_NONE); +} + + +void ws_log_set_fatal_domain_filter(const char *str_filter) +{ + free_log_filter(&fatal_filter); + tokenize_filter_str(&fatal_filter, str_filter, LOG_LEVEL_NONE); +} + + +void ws_log_set_debug_filter(const char *str_filter) +{ + free_log_filter(&debug_filter); + tokenize_filter_str(&debug_filter, str_filter, LOG_LEVEL_DEBUG); +} + + +void ws_log_set_noisy_filter(const char *str_filter) +{ + free_log_filter(&noisy_filter); + tokenize_filter_str(&noisy_filter, str_filter, LOG_LEVEL_NOISY); +} + + +enum ws_log_level ws_log_set_fatal_level(enum ws_log_level level) +{ + if (level <= LOG_LEVEL_NONE || level >= _LOG_LEVEL_LAST) + return LOG_LEVEL_NONE; + if (level > LOG_LEVEL_ERROR) + level = LOG_LEVEL_ERROR; + if (level < LOG_LEVEL_WARNING) + level = LOG_LEVEL_WARNING; + + fatal_log_level = level; + return fatal_log_level; +} + + +enum ws_log_level ws_log_set_fatal_level_str(const char *str_level) +{ + enum ws_log_level level; + + level = string_to_log_level(str_level); + return ws_log_set_fatal_level(level); +} + + +void ws_log_set_writer(ws_log_writer_cb *writer) +{ + if (registered_log_writer_data_free) + registered_log_writer_data_free(registered_log_writer_data); + + registered_log_writer = writer; + registered_log_writer_data = NULL; + registered_log_writer_data_free = NULL; +} + + +void ws_log_set_writer_with_data(ws_log_writer_cb *writer, + void *user_data, + ws_log_writer_free_data_cb *free_user_data) +{ + if (registered_log_writer_data_free) + registered_log_writer_data_free(registered_log_writer_data); + + registered_log_writer = writer; + registered_log_writer_data = user_data; + registered_log_writer_data_free = free_user_data; +} + + +static void glib_log_handler(const char *domain, GLogLevelFlags flags, + const char *message, void * user_data _U_) +{ + enum ws_log_level level; + + /* + * The highest priority bit in the mask defines the level. We + * ignore the GLib fatal log level mask and use our own fatal + * log level setting instead. + */ + + if (flags & G_LOG_LEVEL_ERROR) + level = LOG_LEVEL_ERROR; + else if (flags & G_LOG_LEVEL_CRITICAL) + level = LOG_LEVEL_CRITICAL; + else if (flags & G_LOG_LEVEL_WARNING) + level = LOG_LEVEL_WARNING; + else if (flags & G_LOG_LEVEL_MESSAGE) + level = LOG_LEVEL_MESSAGE; + else if (flags & G_LOG_LEVEL_INFO) + level = LOG_LEVEL_INFO; + else if (flags & G_LOG_LEVEL_DEBUG) + level = LOG_LEVEL_DEBUG; + else + level = LOG_LEVEL_NONE; /* Should not happen. */ + + ws_log(domain, level, "%s", message); +} + + +#ifdef _WIN32 +static void load_registry(void) +{ + LONG lResult; + DWORD ptype; + DWORD data; + DWORD data_size = sizeof(DWORD); + + lResult = RegGetValueA(HKEY_CURRENT_USER, + "Software\\Wireshark", + LOG_HKCU_CONSOLE_OPEN, + RRF_RT_REG_DWORD, + &ptype, + &data, + &data_size); + if (lResult != ERROR_SUCCESS || ptype != REG_DWORD) { + return; + } + + ws_log_console_open = (ws_log_console_open_pref)data; +} +#endif + + +/* + * We can't write to stderr in ws_log_init() because dumpcap uses stderr + * to communicate with the parent and it will block. We have to use + * vcmdarg_err to report errors. + */ +void ws_log_init(const char *progname, + void (*vcmdarg_err)(const char *, va_list ap)) +{ + const char *env; + int fd; + + if (progname != NULL) { + registered_progname = progname; + g_set_prgname(progname); + } + + ws_tzset(); + + current_log_level = DEFAULT_LOG_LEVEL; + + if ((fd = fileno(stdout)) >= 0) + stdout_color_enabled = g_log_writer_supports_color(fd); + if ((fd = fileno(stderr)) >= 0) + stderr_color_enabled = g_log_writer_supports_color(fd); + + /* Set the GLib log handler for the default domain. */ + g_log_set_handler(NULL, G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL, + glib_log_handler, NULL); + + /* Set the GLib log handler for GLib itself. */ + g_log_set_handler("GLib", G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL, + glib_log_handler, NULL); + +#ifdef _WIN32 + load_registry(); + + /* if the user wants a console to be always there, well, we should open one for him */ + if (ws_log_console_open == LOG_CONSOLE_OPEN_ALWAYS) { + create_console(); + } +#endif + + atexit(ws_log_cleanup); + + /* Configure from environment. */ + + env = g_getenv(ENV_VAR_LEVEL); + if (env != NULL) { + if (ws_log_set_level_str(env) == LOG_LEVEL_NONE) { + print_err(vcmdarg_err, LOG_ARGS_NOEXIT, + "Ignoring invalid environment value %s=\"%s\"", + ENV_VAR_LEVEL, env); + } + } + + env = g_getenv(ENV_VAR_FATAL); + if (env != NULL) { + if (ws_log_set_fatal_level_str(env) == LOG_LEVEL_NONE) { + print_err(vcmdarg_err, LOG_ARGS_NOEXIT, + "Ignoring invalid environment value %s=\"%s\"", + ENV_VAR_FATAL, env); + } + } + + /* Alias "domain" and "domains". The plural form wins. */ + if ((env = g_getenv(ENV_VAR_DOMAIN_S)) != NULL) + ws_log_set_domain_filter(env); + else if ((env = g_getenv(ENV_VAR_DOMAIN)) != NULL) + ws_log_set_domain_filter(env); + + /* Alias "domain" and "domains". The plural form wins. */ + if ((env = g_getenv(ENV_VAR_FATAL_DOMAIN_S)) != NULL) + ws_log_set_fatal_domain_filter(env); + else if ((env = g_getenv(ENV_VAR_FATAL_DOMAIN)) != NULL) + ws_log_set_fatal_domain_filter(env); + + env = g_getenv(ENV_VAR_DEBUG); + if (env != NULL) + ws_log_set_debug_filter(env); + + env = g_getenv(ENV_VAR_NOISY); + if (env != NULL) + ws_log_set_noisy_filter(env); + +#ifdef WS_DEBUG + init_complete = true; +#endif +} + + +void ws_log_init_with_writer(const char *progname, + ws_log_writer_cb *writer, + void (*vcmdarg_err)(const char *, va_list ap)) +{ + registered_log_writer = writer; + ws_log_init(progname, vcmdarg_err); +} + + +void ws_log_init_with_writer_and_data(const char *progname, + ws_log_writer_cb *writer, + void *user_data, + ws_log_writer_free_data_cb *free_user_data, + void (*vcmdarg_err)(const char *, va_list ap)) +{ + registered_log_writer_data = user_data; + registered_log_writer_data_free = free_user_data; + ws_log_init_with_writer(progname, writer, vcmdarg_err); +} + + +#define MAGENTA "\033[35m" +#define BLUE "\033[34m" +#define CYAN "\033[36m" +#define GREEN "\033[32m" +#define YELLOW "\033[33m" +#define RED "\033[31m" +#define RESET "\033[0m" + +static inline const char *level_color_on(bool enable, enum ws_log_level level) +{ + if (!enable) + return ""; + + switch (level) { + case LOG_LEVEL_NOISY: + case LOG_LEVEL_DEBUG: + return GREEN; + case LOG_LEVEL_INFO: + case LOG_LEVEL_MESSAGE: + return CYAN; + case LOG_LEVEL_WARNING: + return YELLOW; + case LOG_LEVEL_CRITICAL: + return MAGENTA; + case LOG_LEVEL_ERROR: + return RED; + case LOG_LEVEL_ECHO: + return YELLOW; + default: + break; + } + return ""; +} + +static inline const char *color_off(bool enable) +{ + return enable ? RESET : ""; +} + +#define NANOSECS_IN_MICROSEC 1000 + +/* + * We must not call anything that might log a message + * in the log handler context (GLib might log a message if we register + * our own handler for the GLib domain). + */ +static void log_write_do_work(FILE *fp, bool use_color, + struct tm *when, long nanosecs, intmax_t pid, + const char *domain, enum ws_log_level level, + const char *file, long line, const char *func, + const char *user_format, va_list user_ap) +{ + fputs(" **", fp); + +#ifdef WS_DEBUG + if (!init_complete) + fputs(" no init!", fp); +#endif + + /* Process */ + fprintf(fp, " (%s:%"PRIdMAX")", registered_progname, pid); + + /* Timestamp */ + if (when != NULL) { + fprintf(fp, " %02d:%02d:%02d", + when->tm_hour, when->tm_min, when->tm_sec); + if (nanosecs >= 0) { + fprintf(fp, ".%06ld", nanosecs / NANOSECS_IN_MICROSEC); + } + } + + /* Domain/level */ + fprintf(fp, " [%s %s%s%s]", domain_to_string(domain), + level_color_on(use_color, level), + ws_log_level_to_string(level), + color_off(use_color)); + + /* File/line */ + if (file != NULL) { + fprintf(fp, " %s", file); + if (line >= 0) { + fprintf(fp, ":%ld", line); + } + } + + /* Any formatting changes here need to be synced with ui/capture.c:capture_input_closed. */ + fputs(" --", fp); + + /* Function name */ + if (func != NULL) + fprintf(fp, " %s():", func); + + /* User message */ + fputc(' ', fp); + vfprintf(fp, user_format, user_ap); + fputc('\n', fp); + fflush(fp); +} + + +static inline FILE *console_file(enum ws_log_level level) +{ + if (level <= LOG_LEVEL_INFO && stdout_logging_enabled) + return stdout; + return stderr; +} + + +static inline bool console_color_enabled(enum ws_log_level level) +{ + if (level <= LOG_LEVEL_INFO && stdout_logging_enabled) + return stdout_color_enabled; + return stderr_color_enabled; +} + + +static void log_write_fatal_msg(FILE *fp, intmax_t pid, const char *msg) +{ + /* Process */ + fprintf(fp, " ** (%s:%"PRIdMAX") %s", registered_progname, pid, msg); +} + + +/* + * We must not call anything that might log a message + * in the log handler context (GLib might log a message if we register + * our own handler for the GLib domain). + */ +static void log_write_dispatch(const char *domain, enum ws_log_level level, + const char *file, long line, const char *func, + ws_log_manifest_t *mft, + const char *user_format, va_list user_ap) +{ + bool fatal_event = false; + const char *fatal_msg = NULL; + va_list user_ap_copy; + + if (level >= fatal_log_level && level != LOG_LEVEL_ECHO) { + fatal_event = true; + fatal_msg = "Aborting on fatal log level exception\n"; + } + else if (fatal_filter != NULL) { + if (filter_contains(fatal_filter, domain) && fatal_filter->positive) { + fatal_event = true; + fatal_msg = "Aborting on fatal log domain exception\n"; + } + } + +#ifdef _WIN32 + if (ws_log_console_open != LOG_CONSOLE_OPEN_NEVER) { + create_console(); + } +#endif /* _WIN32 */ + + if (custom_log) { + va_copy(user_ap_copy, user_ap); + log_write_do_work(custom_log, false, + &mft->tstamp_secs, mft->nanosecs, mft->pid, + domain, level, file, line, func, + user_format, user_ap_copy); + va_end(user_ap_copy); + if (fatal_msg) { + log_write_fatal_msg(custom_log, mft->pid, fatal_msg); + } + } + + if (registered_log_writer) { + registered_log_writer(domain, level, file, line, func, fatal_msg, mft, + user_format, user_ap, registered_log_writer_data); + } + else { + log_write_do_work(console_file(level), console_color_enabled(level), + &mft->tstamp_secs, mft->nanosecs, mft->pid, + domain, level, file, line, func, + user_format, user_ap); + if (fatal_msg) { + log_write_fatal_msg(console_file(level), mft->pid, fatal_msg); + } + } + +#ifdef _WIN32 + if (fatal_event && ws_log_console_open != LOG_CONSOLE_OPEN_NEVER) { + /* wait for a key press before the following error handler will terminate the program + this way the user at least can read the error message */ + printf("\n\nPress any key to exit\n"); + _getch(); + } +#endif /* _WIN32 */ + + if (fatal_event) { + abort(); + } +} + + +void ws_logv(const char *domain, enum ws_log_level level, + const char *format, va_list ap) +{ + ws_log_manifest_t mft; + if (!msg_is_active(domain, level, &mft)) + return; + + log_write_dispatch(domain, level, NULL, -1, NULL, &mft, format, ap); +} + + +void ws_logv_full(const char *domain, enum ws_log_level level, + const char *file, long line, const char *func, + const char *format, va_list ap) +{ + ws_log_manifest_t mft; + if (!msg_is_active(domain, level, &mft)) + return; + + log_write_dispatch(domain, level, file, line, func, &mft, format, ap); +} + + +void ws_log(const char *domain, enum ws_log_level level, + const char *format, ...) +{ + ws_log_manifest_t mft; + if (!msg_is_active(domain, level, &mft)) + return; + + va_list ap; + + va_start(ap, format); + log_write_dispatch(domain, level, NULL, -1, NULL, &mft, format, ap); + va_end(ap); +} + + +void ws_log_full(const char *domain, enum ws_log_level level, + const char *file, long line, const char *func, + const char *format, ...) +{ + ws_log_manifest_t mft; + if (!msg_is_active(domain, level, &mft)) + return; + + va_list ap; + + va_start(ap, format); + log_write_dispatch(domain, level, file, line, func, &mft, format, ap); + va_end(ap); +} + + +void ws_log_fatal_full(const char *domain, enum ws_log_level level, + const char *file, long line, const char *func, + const char *format, ...) +{ + ws_log_manifest_t mft; + va_list ap; + + fill_manifest(&mft); + va_start(ap, format); + log_write_dispatch(domain, level, file, line, func, &mft, format, ap); + va_end(ap); + abort(); +} + + +void ws_log_write_always_full(const char *domain, enum ws_log_level level, + const char *file, long line, const char *func, + const char *format, ...) +{ + ws_log_manifest_t mft; + va_list ap; + + fill_manifest(&mft); + va_start(ap, format); + log_write_dispatch(domain, level, file, line, func, &mft, format, ap); + va_end(ap); +} + + +static void +append_trailer(const char *src, size_t src_length, wmem_strbuf_t *display, wmem_strbuf_t *underline) +{ + gunichar ch; + size_t hex_len; + + while (src_length > 0) { + ch = g_utf8_get_char_validated(src, src_length); + if (ch == (gunichar)-1 || ch == (gunichar)-2) { + wmem_strbuf_append_hex(display, *src); + wmem_strbuf_append_c_count(underline, '^', 4); + src += 1; + src_length -= 1; + } + else { + if (g_unichar_isprint(ch)) { + wmem_strbuf_append_unichar(display, ch); + wmem_strbuf_append_c_count(underline, ' ', 1); + } + else { + hex_len = wmem_strbuf_append_hex_unichar(display, ch); + wmem_strbuf_append_c_count(underline, ' ', hex_len); + } + const char *tmp = g_utf8_next_char(src); + src_length -= tmp - src; + src = tmp; + } + } +} + + +static char * +make_utf8_display(const char *src, size_t src_length, size_t good_length) +{ + wmem_strbuf_t *display; + wmem_strbuf_t *underline; + gunichar ch; + size_t hex_len; + + display = wmem_strbuf_create(NULL); + underline = wmem_strbuf_create(NULL); + + for (const char *s = src; s < src + good_length; s = g_utf8_next_char(s)) { + ch = g_utf8_get_char(s); + + if (g_unichar_isprint(ch)) { + wmem_strbuf_append_unichar(display, ch); + wmem_strbuf_append_c(underline, ' '); + } + else { + hex_len = wmem_strbuf_append_hex_unichar(display, ch); + wmem_strbuf_append_c_count(underline, ' ', hex_len); + } + } + + append_trailer(&src[good_length], src_length - good_length, display, underline); + + wmem_strbuf_append_c(display, '\n'); + wmem_strbuf_append(display, underline->str); + wmem_strbuf_destroy(underline); + + return wmem_strbuf_finalize(display); +} + + +void ws_log_utf8_full(const char *domain, enum ws_log_level level, + const char *file, long line, const char *func, + const char *string, ssize_t _length, const char *endptr) +{ + if (!ws_log_msg_is_active(domain, level)) + return; + + char *display; + size_t length; + size_t good_length; + + if (_length < 0) + length = strlen(string); + else + length = _length; + + if (endptr == NULL || endptr < string) { + /* Find the pointer to the first invalid byte. */ + if (g_utf8_validate(string, length, &endptr)) { + /* Valid string - should not happen. */ + return; + } + } + good_length = endptr - string; + + display = make_utf8_display(string, length, good_length); + + ws_log_write_always_full(domain, level, file, line, func, + "Invalid UTF-8 at address %p offset %zu (length = %zu):\n%s", + string, good_length, length, display); + + g_free(display); +} + + +void ws_log_buffer_full(const char *domain, enum ws_log_level level, + const char *file, long line, const char *func, + const uint8_t *ptr, size_t size, size_t max_bytes_len, + const char *msg) +{ + if (!ws_log_msg_is_active(domain, level)) + return; + + char *bufstr = bytes_to_str_maxlen(NULL, ptr, size, max_bytes_len); + + if (G_UNLIKELY(msg == NULL)) + ws_log_write_always_full(domain, level, file, line, func, + "<buffer:%p>: %s (%zu bytes)", + ptr, bufstr, size); + else + ws_log_write_always_full(domain, level, file, line, func, + "%s: %s (%zu bytes)", + msg, bufstr, size); + wmem_free(NULL, bufstr); +} + + +void ws_log_file_writer(FILE *fp, const char *domain, enum ws_log_level level, + const char *file, long line, const char *func, + ws_log_manifest_t *mft, + const char *user_format, va_list user_ap) +{ + log_write_do_work(fp, false, + &mft->tstamp_secs, mft->nanosecs, mft->pid, + domain, level, file, line, func, + user_format, user_ap); +} + + +void ws_log_console_writer(const char *domain, enum ws_log_level level, + const char *file, long line, const char *func, + ws_log_manifest_t *mft, + const char *user_format, va_list user_ap) +{ + log_write_do_work(console_file(level), console_color_enabled(level), + &mft->tstamp_secs, mft->nanosecs, mft->pid, + domain, level, file, line, func, + user_format, user_ap); +} + + +WS_DLL_PUBLIC +void ws_log_console_writer_set_use_stdout(bool use_stdout) +{ + stdout_logging_enabled = use_stdout; +} + + +static void ws_log_cleanup(void) +{ + if (registered_log_writer_data_free) { + registered_log_writer_data_free(registered_log_writer_data); + registered_log_writer_data = NULL; + } + if (custom_log) { + fclose(custom_log); + custom_log = NULL; + } + free_log_filter(&domain_filter); + free_log_filter(&debug_filter); + free_log_filter(&noisy_filter); + free_log_filter(&fatal_filter); +} + + +void ws_log_add_custom_file(FILE *fp) +{ + if (custom_log != NULL) { + fclose(custom_log); + } + custom_log = fp; +} + + +#define USAGE_LEVEL \ + "sets the active log level (\"critical\", \"warning\", etc.)" + +#define USAGE_FATAL \ + "sets level to abort the program (\"critical\" or \"warning\")" + +#define USAGE_DOMAINS \ + "comma-separated list of the active log domains" + +#define USAGE_FATAL_DOMAINS \ + "list of domains that cause the program to abort" + +#define USAGE_DEBUG \ + "list of domains with \"debug\" level" + +#define USAGE_NOISY \ + "list of domains with \"noisy\" level" + +#define USAGE_FILE \ + "file to output messages to (in addition to stderr)" + +void ws_log_print_usage(FILE *fp) +{ + fprintf(fp, "Diagnostic output:\n"); + fprintf(fp, " --log-level <level> " USAGE_LEVEL "\n"); + fprintf(fp, " --log-fatal <level> " USAGE_FATAL "\n"); + fprintf(fp, " --log-domains <[!]list> " USAGE_DOMAINS "\n"); + fprintf(fp, " --log-fatal-domains <list>\n"); + fprintf(fp, " " USAGE_FATAL_DOMAINS "\n"); + fprintf(fp, " --log-debug <[!]list> " USAGE_DEBUG "\n"); + fprintf(fp, " --log-noisy <[!]list> " USAGE_NOISY "\n"); + fprintf(fp, " --log-file <path> " USAGE_FILE "\n"); +} |