fda8556a39
This fixes a nasty bug that manifests on windows when building without precompiled headers. util/stringops.h used to silently replace strdup with a macro that's compatible with mem_free(). This header would typically be included everywhere due to PCH, but without it the strdup from libc would sometimes be in scope. On most platforms mem_free() is equivalent to free(), but not on windows, because we have to use _aligned_free() there. Attempting to mem_free() the result of a libc strdup() would segfault in such a configuration. Avoid the footgun by banning strdup() entirely. Maybe redefining libc names isn't such a great idea, who knew?
820 lines
18 KiB
C
820 lines
18 KiB
C
/*
|
|
* This software is licensed under the terms of the MIT License.
|
|
* See COPYING for further information.
|
|
* ---
|
|
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
|
|
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
|
|
*/
|
|
|
|
#include "log.h"
|
|
|
|
#include "coroutine/cotask.h"
|
|
#include "list.h"
|
|
#include "random.h"
|
|
#include "thread.h"
|
|
#include "util.h"
|
|
#include "util/env.h"
|
|
#include "util/io.h"
|
|
#include "util/strbuf.h"
|
|
#include "util/stringops.h"
|
|
|
|
#include <SDL_bits.h>
|
|
#include <SDL_mutex.h>
|
|
|
|
typedef struct Logger {
|
|
LIST_INTERFACE(struct Logger);
|
|
|
|
FormatterObj formatter;
|
|
SDL_RWops *out;
|
|
uint levels;
|
|
} Logger;
|
|
|
|
typedef struct QueuedLogEntry {
|
|
LIST_INTERFACE(struct QueuedLogEntry);
|
|
LogEntry e;
|
|
char message[];
|
|
} QueuedLogEntry;
|
|
|
|
typedef struct LogFilterEntry {
|
|
struct {
|
|
char *module;
|
|
char *func;
|
|
} patterns;
|
|
LogLevelDiff diff;
|
|
} LogFilterEntry;
|
|
|
|
static struct {
|
|
Logger *outputs;
|
|
size_t num_outputs;
|
|
SDL_mutex *mutex;
|
|
uint enabled_log_levels;
|
|
|
|
struct {
|
|
StringBuffer pre_format;
|
|
StringBuffer format;
|
|
} buffers;
|
|
|
|
struct {
|
|
Thread *thread;
|
|
SDL_mutex *mutex;
|
|
SDL_cond *cond;
|
|
LIST_ANCHOR(QueuedLogEntry) queue;
|
|
int shutdown;
|
|
} queue;
|
|
|
|
DYNAMIC_ARRAY(LogFilterEntry) filters;
|
|
|
|
bool initialized;
|
|
|
|
#ifdef LOG_FATAL_MSGBOX
|
|
char *err_appendix;
|
|
#endif
|
|
} logging;
|
|
|
|
#define INIT_BUF_SIZE 0x100
|
|
#define MAX_BUF_SIZE 0x10000
|
|
|
|
static const char *level_prefix_map[] = {
|
|
[_LOG_DEBUG_ID] = "D",
|
|
[_LOG_INFO_ID] = "I",
|
|
[_LOG_WARN_ID] = "W",
|
|
[_LOG_ERROR_ID] = "E",
|
|
[_LOG_FATAL_ID] = "F",
|
|
};
|
|
|
|
static const char *level_name_map[] = {
|
|
[_LOG_DEBUG_ID] = "DEBUG",
|
|
[_LOG_INFO_ID] = "INFO",
|
|
[_LOG_WARN_ID] = "WARNING",
|
|
[_LOG_ERROR_ID] = "ERROR",
|
|
[_LOG_FATAL_ID] = "FATAL",
|
|
};
|
|
|
|
attr_unused
|
|
static const char *level_ansi_style_map[] = {
|
|
[_LOG_DEBUG_ID] = "\x1b[1;35m",
|
|
[_LOG_INFO_ID] = "\x1b[1;32m",
|
|
[_LOG_WARN_ID] = "\x1b[1;33m",
|
|
[_LOG_ERROR_ID] = "\x1b[1;31m",
|
|
[_LOG_FATAL_ID] = "\x1b[1;31m",
|
|
};
|
|
|
|
#define INDEX_MAP(map, lvl) \
|
|
uint idx = SDL_MostSignificantBitIndex32(lvl); \
|
|
assert_nolog(idx < sizeof(map) / sizeof(*map)); \
|
|
return map[idx];
|
|
|
|
static const char* level_prefix(LogLevel lvl) { INDEX_MAP(level_prefix_map, lvl) }
|
|
static const char* level_name(LogLevel lvl) { INDEX_MAP(level_name_map, lvl) }
|
|
attr_unused static const char* level_ansi_style_code(LogLevel lvl) { INDEX_MAP(level_ansi_style_map, lvl) }
|
|
|
|
static void log_queue_shutdown_internal(bool force_sync);
|
|
|
|
noreturn static void log_abort(const char *msg) {
|
|
#ifdef LOG_FATAL_MSGBOX
|
|
static const char *const title = "Taisei: fatal error";
|
|
|
|
if(msg) {
|
|
if(logging.err_appendix) {
|
|
char *m = strfmt("%s\n\n%s", msg, logging.err_appendix);
|
|
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, m, NULL);
|
|
mem_free(m);
|
|
} else {
|
|
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, msg, NULL);
|
|
}
|
|
} else if(logging.err_appendix) {
|
|
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, logging.err_appendix, NULL);
|
|
}
|
|
#endif
|
|
|
|
log_queue_shutdown_internal(true);
|
|
log_shutdown();
|
|
|
|
// abort() doesn't clean up, but it lets us get a backtrace, which is more useful
|
|
abort();
|
|
}
|
|
|
|
void log_set_gui_error_appendix(const char *message) {
|
|
#ifdef LOG_FATAL_MSGBOX
|
|
stralloc(&logging.err_appendix, message);
|
|
#endif
|
|
}
|
|
|
|
static void add_debug_info(StringBuffer *buf) {
|
|
IF_DEBUG({
|
|
DebugInfo *debug_info = get_debug_info();
|
|
DebugInfo *debug_meta = get_debug_meta();
|
|
|
|
strbuf_printf(buf,
|
|
"\n\n\n"
|
|
"Debug info: %s:%i:%s\n"
|
|
"Debug info set at: %s:%i:%s\n"
|
|
"Note: debug info may not be relevant to this issue\n",
|
|
debug_info->file, debug_info->line, debug_info->func,
|
|
debug_meta->file, debug_meta->line, debug_meta->func
|
|
);
|
|
});
|
|
}
|
|
|
|
#define MODULE_BUFFER_SIZE 128
|
|
|
|
static size_t modname(const char *filename, size_t filename_len, char *mod) {
|
|
size_t mlen = filename_len;
|
|
|
|
char *dot = memchr(filename, '.', mlen);
|
|
if(dot) {
|
|
mlen = dot - filename;
|
|
}
|
|
|
|
if(mlen >= MODULE_BUFFER_SIZE) {
|
|
mlen = MODULE_BUFFER_SIZE - 1;
|
|
}
|
|
|
|
memcpy(mod, filename, mlen);
|
|
mod[mlen] = 0;
|
|
|
|
#ifdef _WIN32
|
|
for(char *c = mod; c < mod + mlen; ++c) {
|
|
if(*c == '\\') {
|
|
*c = '/';
|
|
}
|
|
}
|
|
#endif
|
|
|
|
char *sep = memrchr(mod, '/', mlen);
|
|
|
|
if(sep) {
|
|
// simplify "foobar/foobar" into foobar and "foo/bar/foo_bar" into "foo/bar"
|
|
|
|
char pfx[sep - mod + 1];
|
|
int i;
|
|
for(i = 0; i < ARRAY_SIZE(pfx) - 1; ++i) {
|
|
char c = mod[i];
|
|
if(c == '/') {
|
|
c = '_';
|
|
}
|
|
pfx[i] = c;
|
|
}
|
|
pfx[i] = 0;
|
|
|
|
size_t tlen = strlen(sep + 1);
|
|
|
|
if(i == tlen && !memcmp(pfx, sep + 1, tlen)) {
|
|
mlen = tlen;
|
|
mod[mlen] = 0;
|
|
}
|
|
}
|
|
|
|
return mlen;
|
|
}
|
|
|
|
static void filter_entry_apply(const LogEntry *entry, const LogFilterEntry *f, LogLevelDiff *diff) {
|
|
if(f->patterns.module && !strmatch(f->patterns.module, entry->module)) {
|
|
return;
|
|
}
|
|
|
|
if(f->patterns.func && !strmatch(f->patterns.func, entry->func)) {
|
|
return;
|
|
}
|
|
|
|
*diff = log_merge_level_diff(*diff, f->diff);
|
|
}
|
|
|
|
static LogLevel filter_entry(LogEntry *entry) {
|
|
LogLevelDiff d = {};
|
|
|
|
dynarray_foreach_elem(&logging.filters, LogFilterEntry *f, {
|
|
filter_entry_apply(entry, f, &d);
|
|
});
|
|
|
|
return log_apply_level_diff(LOG_ALL, d);
|
|
}
|
|
|
|
static void log_dispatch(LogEntry *entry) {
|
|
StringBuffer *fmt_buf = &logging.buffers.format;
|
|
|
|
bool init = false;
|
|
LogLevel filter_lvlmask = LOG_ALL;
|
|
|
|
char mbuf[MODULE_BUFFER_SIZE];
|
|
|
|
for(Logger *l = logging.outputs; l; l = l->next) {
|
|
if(l->levels & entry->level) {
|
|
if(!init) {
|
|
modname(entry->file, strlen(entry->file), mbuf);
|
|
|
|
DIAGNOSTIC_GCC(push)
|
|
DIAGNOSTIC_GCC(ignored "-Wpragmas")
|
|
DIAGNOSTIC_GCC(ignored "-Wdangling-pointer")
|
|
// The formatter is not expected to leak this out of the the stack frame.
|
|
entry->module = mbuf;
|
|
DIAGNOSTIC_GCC(pop)
|
|
|
|
filter_lvlmask = filter_entry(entry);
|
|
init = true;
|
|
}
|
|
|
|
if(!((entry->level & filter_lvlmask) | (l->levels & LOG_UNFILTERED_BIT))) {
|
|
continue;
|
|
}
|
|
|
|
strbuf_clear(fmt_buf);
|
|
size_t slen = l->formatter.format(&l->formatter, fmt_buf, entry);
|
|
assert_nolog(fmt_buf->buf_size >= slen);
|
|
SDL_RWwrite(l->out, fmt_buf->start, 1, slen);
|
|
}
|
|
}
|
|
|
|
if(entry->thread) {
|
|
thread_decref(entry->thread);
|
|
}
|
|
}
|
|
|
|
static void log_dispatch_async(LogEntry *entry) {
|
|
for(Logger *l = logging.outputs; l; l = l->next) {
|
|
if(l->levels & entry->level) {
|
|
size_t msg_len = strlen(entry->message);
|
|
auto qle = ALLOC_FLEX(QueuedLogEntry, msg_len + 1);
|
|
memcpy(&qle->e, entry, sizeof(*entry));
|
|
memcpy(qle->message, entry->message, msg_len);
|
|
qle->e.message = qle->message;
|
|
SDL_LockMutex(logging.queue.mutex);
|
|
alist_append(&logging.queue.queue, qle);
|
|
SDL_CondBroadcast(logging.queue.cond);
|
|
SDL_UnlockMutex(logging.queue.mutex);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void *sync_logger(List **loggers, List *logger, void *arg) {
|
|
Logger *l = (Logger*)logger;
|
|
SDL_RWsync(l->out);
|
|
return NULL;
|
|
}
|
|
|
|
static void log_internal(LogLevel lvl, const char *funcname, const char *filename, uint line, const char *fmt, va_list args) {
|
|
assert(fmt[strlen(fmt)-1] != '\n');
|
|
|
|
bool noabort = lvl & LOG_NOABORT_BIT;
|
|
lvl &= logging.enabled_log_levels;
|
|
|
|
if(lvl == LOG_NONE) {
|
|
return;
|
|
}
|
|
|
|
SDL_LockMutex(logging.mutex);
|
|
|
|
StringBuffer *buf = &logging.buffers.pre_format;
|
|
strbuf_clear(buf);
|
|
|
|
va_list args_copy;
|
|
va_copy(args_copy, args);
|
|
attr_unused int slen = strbuf_vprintf(buf, fmt, args_copy);
|
|
va_end(args_copy);
|
|
assert_nolog(slen >= 0);
|
|
|
|
if(lvl & LOG_FATAL) {
|
|
add_debug_info(buf);
|
|
}
|
|
|
|
LogEntry entry = {
|
|
.message = buf->start,
|
|
.file = filename,
|
|
.func = funcname,
|
|
.line = line,
|
|
.level = lvl,
|
|
.time = SDL_GetTicks(),
|
|
.thread = thread_get_current(),
|
|
.thread_id = thread_get_current_id(),
|
|
};
|
|
|
|
CoTask *task = cotask_active();
|
|
|
|
if(task) {
|
|
entry.task = cotask_get_name(task);
|
|
entry.task_id = cotask_box(task).unique_id;
|
|
}
|
|
|
|
if(entry.thread) {
|
|
thread_incref(entry.thread);
|
|
}
|
|
|
|
if(logging.queue.thread) {
|
|
log_dispatch_async(&entry);
|
|
} else {
|
|
log_dispatch(&entry);
|
|
}
|
|
|
|
if(lvl & LOG_FATAL) {
|
|
if(noabort) {
|
|
// Will likely abort externally (e.g. assertion failure), so sync everything now.
|
|
list_foreach(&logging.outputs, sync_logger, NULL);
|
|
} else {
|
|
log_abort(entry.message);
|
|
}
|
|
}
|
|
|
|
SDL_UnlockMutex(logging.mutex);
|
|
}
|
|
|
|
void _taisei_log(LogLevel lvl, const char *funcname, const char *filename, uint line, const char *fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
log_internal(lvl, funcname, filename, line, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void _taisei_log_fatal(LogLevel lvl, const char *funcname, const char *filename, uint line, const char *fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
log_internal(lvl, funcname, filename, line, fmt, args);
|
|
va_end(args);
|
|
|
|
// should usually not get here, log_internal will abort earlier if lvl is LOG_FATAL
|
|
// that is unless LOG_FATAL is disabled for some reason
|
|
log_abort(NULL);
|
|
}
|
|
|
|
static void *delete_logger(List **loggers, List *logger, void *arg) {
|
|
Logger *l = (Logger*)logger;
|
|
|
|
if(l->formatter.free != NULL) {
|
|
l->formatter.free(&l->formatter);
|
|
}
|
|
|
|
SDL_RWsync(l->out);
|
|
SDL_RWclose(l->out);
|
|
mem_free(list_unlink(loggers, logger));
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void *log_queue_thread(void *a) {
|
|
SDL_mutex *mtx = logging.queue.mutex;
|
|
SDL_cond *cond = logging.queue.cond;
|
|
|
|
SDL_LockMutex(mtx);
|
|
|
|
for(;;) {
|
|
SDL_CondWait(cond, mtx);
|
|
QueuedLogEntry *qle;
|
|
|
|
while((qle = alist_pop(&logging.queue.queue)) && logging.queue.shutdown < 2) {
|
|
SDL_UnlockMutex(mtx);
|
|
log_dispatch(&qle->e);
|
|
mem_free(qle);
|
|
SDL_LockMutex(mtx);
|
|
}
|
|
|
|
SDL_CondBroadcast(cond);
|
|
|
|
if(logging.queue.shutdown) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
SDL_UnlockMutex(mtx);
|
|
return NULL;
|
|
}
|
|
|
|
static void log_queue_init(void) {
|
|
if(!env_get("TAISEI_LOG_ASYNC", true)) {
|
|
return;
|
|
}
|
|
|
|
if(!(logging.queue.cond = SDL_CreateCond())) {
|
|
log_sdl_error(LOG_ERROR, "SDL_CreateCond");
|
|
return;
|
|
}
|
|
|
|
if(!(logging.queue.mutex = SDL_CreateMutex())) {
|
|
log_sdl_error(LOG_ERROR, "SDL_CreateMutex");
|
|
return;
|
|
}
|
|
|
|
logging.queue.thread = thread_create("Log queue", log_queue_thread, NULL, THREAD_PRIO_LOW);
|
|
|
|
if(!logging.queue.thread) {
|
|
log_error("Failed to create log queue thread");
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void log_queue_shutdown_internal(bool force_sync) {
|
|
if(!logging.queue.thread) {
|
|
return;
|
|
}
|
|
|
|
SDL_LockMutex(logging.queue.mutex);
|
|
logging.queue.shutdown = env_get("TAISEI_LOG_ASYNC_FAST_SHUTDOWN", false) && !force_sync ? 2 : 1;
|
|
SDL_CondBroadcast(logging.queue.cond);
|
|
SDL_UnlockMutex(logging.queue.mutex);
|
|
thread_wait(logging.queue.thread);
|
|
logging.queue.thread = NULL;
|
|
SDL_DestroyMutex(logging.queue.mutex);
|
|
logging.queue.mutex = NULL;
|
|
SDL_DestroyCond(logging.queue.cond);
|
|
logging.queue.cond = NULL;
|
|
}
|
|
|
|
void log_queue_shutdown(void) {
|
|
log_queue_shutdown_internal(false);
|
|
}
|
|
|
|
void log_init(LogLevel lvls) {
|
|
logging.enabled_log_levels = lvls;
|
|
logging.mutex = SDL_CreateMutex();
|
|
log_queue_init();
|
|
logging.initialized = true;
|
|
}
|
|
|
|
void log_shutdown(void) {
|
|
logging.initialized = false;
|
|
log_queue_shutdown_internal(false);
|
|
list_foreach(&logging.outputs, delete_logger, NULL);
|
|
SDL_DestroyMutex(logging.mutex);
|
|
strbuf_free(&logging.buffers.pre_format);
|
|
strbuf_free(&logging.buffers.format);
|
|
|
|
log_remove_filters();
|
|
dynarray_free_data(&logging.filters);
|
|
|
|
#ifdef LOG_FATAL_MSGBOX
|
|
mem_free(logging.err_appendix);
|
|
#endif
|
|
|
|
memset(&logging, 0, sizeof(logging));
|
|
}
|
|
|
|
void log_sync(bool flush) {
|
|
SDL_LockMutex(logging.queue.mutex);
|
|
|
|
while(logging.queue.queue.first) {
|
|
SDL_CondWait(logging.queue.cond, logging.queue.mutex);
|
|
}
|
|
|
|
if(flush) {
|
|
list_foreach(&logging.outputs, sync_logger, NULL);
|
|
}
|
|
|
|
SDL_UnlockMutex(logging.queue.mutex);
|
|
}
|
|
|
|
bool log_initialized(void) {
|
|
return logging.initialized;
|
|
}
|
|
|
|
void log_add_output(LogLevel levels, SDL_RWops *output, Formatter *formatter) {
|
|
if(!output) {
|
|
return;
|
|
}
|
|
|
|
if(!(levels & logging.enabled_log_levels)) {
|
|
SDL_RWclose(output);
|
|
return;
|
|
}
|
|
|
|
auto l = ALLOC(Logger, { .levels = levels, .out = output });
|
|
formatter(&l->formatter, l->out);
|
|
assert(l->formatter.format != NULL);
|
|
|
|
list_append(&logging.outputs, l);
|
|
}
|
|
|
|
static LogLevel chr2lvl(char c) {
|
|
c = toupper(c);
|
|
|
|
for(int i = 0; i < sizeof(level_prefix_map) / sizeof(char*); ++i) {
|
|
if(c == level_prefix_map[i][0]) {
|
|
return (1 << i);
|
|
} else if(c == 'A') {
|
|
return LOG_ALL;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
LogLevelDiff log_parse_level_diff(const char *lvlmod) {
|
|
LogLevelDiff d = {};
|
|
|
|
bool enable = true;
|
|
|
|
for(const char *c = lvlmod; *c; ++c) {
|
|
if(*c == '+') {
|
|
enable = true;
|
|
} else if(*c == '-') {
|
|
enable = false;
|
|
} else {
|
|
char l = chr2lvl(*c);
|
|
d.diff[enable] |= l;
|
|
d.diff[!enable] &= ~l;
|
|
}
|
|
}
|
|
|
|
assert((d.removed & d.added) == 0);
|
|
return d;
|
|
}
|
|
|
|
LogLevelDiff log_merge_level_diff(LogLevelDiff lower, LogLevelDiff upper) {
|
|
assert((lower.removed & lower.added) == 0);
|
|
assert((upper.removed & upper.added) == 0);
|
|
lower.added = (lower.added & ~upper.removed) | upper.added;
|
|
lower.removed = (lower.removed & ~upper.added) | upper.removed;
|
|
assert((lower.removed & lower.added) == 0);
|
|
return lower;
|
|
}
|
|
|
|
LogLevel log_apply_level_diff(LogLevel lvls, LogLevelDiff diff) {
|
|
assert((diff.removed & diff.added) == 0);
|
|
return (lvls & ~diff.removed) | diff.added;
|
|
}
|
|
|
|
LogLevel log_parse_levels(LogLevel lvls, const char *lvlmod) {
|
|
return log_apply_level_diff(lvls, log_parse_level_diff(lvlmod));
|
|
}
|
|
|
|
static inline int thread_color(uint64_t tid) {
|
|
uint64_t hash = splitmix64(&tid);
|
|
return 0x10 + hash % 0xD8;
|
|
}
|
|
|
|
static const char *thread_name(LogEntry *entry, size_t tmpsize, char tmpbuf[tmpsize]) {
|
|
if(entry->thread) {
|
|
return thread_get_name(entry->thread);
|
|
} else {
|
|
snprintf(tmpbuf, tmpsize, "%llx", (unsigned long long)entry->thread_id);
|
|
return tmpbuf;
|
|
}
|
|
}
|
|
|
|
typedef struct ConsoleFormatterData {
|
|
struct {
|
|
bool timestamps;
|
|
bool context;
|
|
bool color;
|
|
} cfg;
|
|
|
|
struct {
|
|
int max_context_len;
|
|
} state;
|
|
} ConsoleFormatterData;
|
|
|
|
#define C(x) (fdata->cfg.color ? (x) : 0)
|
|
|
|
static int log_fmtconsole_format_context(
|
|
ConsoleFormatterData *fdata,
|
|
StringBuffer *buf,
|
|
LogEntry *entry
|
|
) {
|
|
char tmp[32];
|
|
int txtlen = 0;
|
|
int clrlen = 0;
|
|
|
|
if(entry->thread_id != thread_get_main_id()) {
|
|
clrlen += C(strbuf_printf(buf, "\x1b[38;5;%im", thread_color(entry->thread_id)));
|
|
txtlen += strbuf_printf(buf, "%-18s ", thread_name(entry, sizeof(tmp), tmp));
|
|
clrlen += C(strbuf_cat(buf, "\x1b[1;30m"));
|
|
}
|
|
|
|
if(entry->task_id != 0) {
|
|
clrlen += C(strbuf_cat(buf, "\x1b[1;35m"));
|
|
txtlen += strbuf_cat(buf, entry->task ?: "<unknown task>");
|
|
clrlen += C(strbuf_cat(buf, "\x1b[1;30m"));
|
|
txtlen += strbuf_cat(buf, "[");
|
|
clrlen += C(strbuf_cat(buf, "\x1b[1;37m"));
|
|
txtlen += strbuf_printf(buf, "%x", entry->task_id);
|
|
clrlen += C(strbuf_cat(buf, "\x1b[1;30m"));
|
|
txtlen += strbuf_cat(buf, "] ");
|
|
}
|
|
|
|
if(txtlen) {
|
|
if(fdata->state.max_context_len < txtlen) {
|
|
fdata->state.max_context_len = txtlen;
|
|
} else {
|
|
static char spaces[] = " ";
|
|
size_t n = min(sizeof(spaces) - 1, fdata->state.max_context_len - txtlen);
|
|
txtlen += strbuf_ncat(buf, n, spaces);
|
|
}
|
|
}
|
|
|
|
return txtlen + clrlen;
|
|
}
|
|
|
|
static int log_fmtconsole_format(FormatterObj *obj, StringBuffer *buf, LogEntry *entry) {
|
|
ConsoleFormatterData *fdata = obj->data;
|
|
int len = 0;
|
|
|
|
if(fdata->cfg.timestamps) {
|
|
len += strbuf_printf(buf, "\x1b[1;30m%-9d ", entry->time);
|
|
}
|
|
|
|
if(fdata->cfg.context) {
|
|
len += log_fmtconsole_format_context(fdata, buf, entry);
|
|
}
|
|
|
|
len += C(strbuf_cat(buf, level_ansi_style_code(entry->level)));
|
|
len += strbuf_cat(buf, level_prefix(entry->level));
|
|
len += C(strbuf_cat(buf, "\x1b[1;30m"));
|
|
len += strbuf_cat(buf, ": ");
|
|
|
|
len += C(strbuf_cat(buf, "\x1b[1;36m"));
|
|
len += strbuf_cat(buf, entry->module);
|
|
len += C(strbuf_cat(buf, "\x1b[1;30m"));
|
|
len += strbuf_cat(buf, ">");
|
|
len += C(strbuf_cat(buf, "\x1b[1;34m"));
|
|
len += strbuf_cat(buf, entry->func);
|
|
len += C(strbuf_cat(buf, "\x1b[1;30m"));
|
|
len += strbuf_cat(buf, ": ");
|
|
len += C(strbuf_cat(buf, "\x1b[0m"));
|
|
len += strbuf_cat(buf, entry->message);
|
|
len += strbuf_cat(buf, "\n");
|
|
|
|
return len;
|
|
}
|
|
|
|
#undef C
|
|
|
|
static void log_fmtconsole_free(FormatterObj *fobj) {
|
|
mem_free(fobj->data);
|
|
}
|
|
|
|
#ifdef TAISEI_BUILDCONF_HAVE_POSIX
|
|
#include <unistd.h>
|
|
|
|
static bool output_supports_ansi_sequences(const SDL_RWops *output) {
|
|
if(!strcmp(env_get("TERM", "dumb"), "dumb")) {
|
|
return false;
|
|
}
|
|
|
|
if(output->type != SDL_RWOPS_STDFILE) {
|
|
return false;
|
|
}
|
|
|
|
return isatty(fileno(output->hidden.stdio.fp));
|
|
}
|
|
#else
|
|
static bool output_supports_ansi_sequences(const SDL_RWops *output) {
|
|
// TODO: Handle it for windows. The windows console supports ANSI escapes, but requires special setup.
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
void log_formatter_console(FormatterObj *obj, const SDL_RWops *output) {
|
|
obj->data = ALLOC(ConsoleFormatterData, {
|
|
.cfg = {
|
|
.color = (
|
|
env_get("TAISEI_LOG_CONSOLE_COLOR", true) &&
|
|
output_supports_ansi_sequences(output)
|
|
),
|
|
.context = env_get("TAISEI_LOG_CONSOLE_CONTEXT", true),
|
|
.timestamps = env_get("TAISEI_LOG_CONSOLE_TIME", false),
|
|
}
|
|
});
|
|
obj->format = log_fmtconsole_format;
|
|
obj->free = log_fmtconsole_free;
|
|
}
|
|
|
|
static int log_fmtfile_format(FormatterObj *obj, StringBuffer *buf, LogEntry *entry) {
|
|
char tmp[32];
|
|
int len = 0;
|
|
|
|
len += strbuf_printf(buf, "%d ", entry->time);
|
|
|
|
if(entry->thread_id != thread_get_main_id()) {
|
|
len += strbuf_printf(
|
|
buf,
|
|
"{%s} ",
|
|
thread_name(entry, sizeof(tmp), tmp)
|
|
);
|
|
}
|
|
|
|
if(entry->task_id != 0) {
|
|
len += strbuf_printf(
|
|
buf,
|
|
"%s[%x] ",
|
|
entry->task ?: "<unknown task>", entry->task_id
|
|
);
|
|
}
|
|
|
|
len += strbuf_printf(
|
|
buf,
|
|
"%s %s %s: %s [%s:%i]\n",
|
|
level_name(entry->level),
|
|
entry->module,
|
|
entry->func,
|
|
entry->message,
|
|
entry->file,
|
|
entry->line
|
|
);
|
|
|
|
return len;
|
|
}
|
|
|
|
void log_formatter_file(FormatterObj *obj, const SDL_RWops *output) {
|
|
obj->format = log_fmtfile_format;
|
|
}
|
|
|
|
static char *copy_pattern(const char *p) {
|
|
const char *orig = p;
|
|
|
|
if(!p) {
|
|
return NULL;
|
|
}
|
|
|
|
while(*p == '*') {
|
|
++p;
|
|
}
|
|
|
|
if(!*p) {
|
|
return NULL;
|
|
}
|
|
|
|
return mem_strdup(orig);
|
|
}
|
|
|
|
void log_add_filter(LogLevelDiff diff, const char *pmod, const char *pfunc) {
|
|
dynarray_append(&logging.filters, {
|
|
.patterns.module = copy_pattern(pmod),
|
|
.patterns.func = copy_pattern(pfunc),
|
|
.diff = diff,
|
|
});
|
|
}
|
|
|
|
bool log_add_filter_string(const char *fstr) {
|
|
char buf[strlen(fstr) + 1];
|
|
memcpy(buf, fstr, sizeof(buf));
|
|
char *pbuf = buf;
|
|
|
|
char *levelmod = strtok_r(NULL, " \t\r\n", &pbuf);
|
|
if(!levelmod) {
|
|
goto error;
|
|
}
|
|
|
|
char *pmodule = strtok_r(NULL, " \t\r\n", &pbuf);
|
|
if(!pmodule) {
|
|
goto error;
|
|
}
|
|
|
|
char *pfunc = strtok_r(NULL, " \t\r\n", &pbuf);
|
|
if(!pfunc) {
|
|
goto error;
|
|
}
|
|
|
|
log_add_filter(log_parse_level_diff(levelmod), pmodule, pfunc);
|
|
return true;
|
|
|
|
error:
|
|
log_error("Malformed filter string");
|
|
return false;
|
|
}
|
|
|
|
void log_remove_filters(void) {
|
|
dynarray_foreach_elem(&logging.filters, LogFilterEntry *f, {
|
|
mem_free(f->patterns.module);
|
|
mem_free(f->patterns.func);
|
|
});
|
|
|
|
logging.filters.num_elements = 0;
|
|
}
|