ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
log/log.c
Go to the documentation of this file.
1
7#include <ascii-chat/common.h>
9#include <ascii-chat/options/rcu.h> // For RCU-based options access
17#include <stdarg.h>
18#include <stdio.h>
19#include <stdlib.h>
20#include <string.h>
21#include <time.h>
22#include <fcntl.h>
23#include <sys/stat.h>
24#include <ctype.h>
25
26#include <ascii-chat/log/log.h>
29#include <ascii-chat/log/mmap.h>
30#include <ascii-chat/log/grep.h>
31#include <ascii-chat/log/json.h>
40
41/* Platform-specific log hook (weak, can be overridden by platform implementations) */
42__attribute__((weak)) void platform_log_hook(log_level_t level, const char *message) {
43 (void)level;
44 (void)message;
45 // Default: no-op
46}
47
48/* ============================================================================
49 * Logging System Internal State
50 * ============================================================================ */
51
52/* Log context struct - Lock-free logging with mutex-protected rotation
53 *
54 * Design: Logging itself is lock-free using atomic operations and atomic write() syscalls.
55 * Only log rotation uses a mutex, since it involves multiple file operations that must
56 * be atomic as a group (close, read tail, write temp, rename, reopen).
57 *
58 * This structure consolidates all logging system state into a single, named struct
59 * for better auditability and easier initialization/cleanup.
60 */
61#pragma GCC diagnostic push
62#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
63static struct log_system_t {
64 /* Core logging I/O and state */
65 atomic_t file; /* File descriptor (atomic for safe access) */
66 atomic_t json_file; /* JSON output file descriptor (-1 = disabled) */
67 atomic_t level; /* Log level as int for atomic ops */
68 lifecycle_t lifecycle; /* Initialization state machine */
69 char filename[LOG_MSG_BUFFER_SIZE]; /* Store filename (set once at init) */
70 atomic_t current_size; /* Track current file size */
71
72 /* Terminal output control */
73 atomic_t terminal_output_enabled; /* Control stderr output to terminal */
74 atomic_t force_stderr; /* Force all terminal logs to stderr (client mode) */
75 atomic_t terminal_locked; /* True when a thread has exclusive terminal access */
76 atomic_t terminal_owner_thread; /* Thread that owns terminal output (stored as uint64) */
77
78 /* Log level and formatting */
79 atomic_t level_manually_set; /* Track if level was set manually */
80 atomic_t flush_delay_ms; /* Delay between each buffered log flush (0 = disabled) */
81 log_template_t *format; /* Compiled log format (NULL = use default) */
82 log_template_t *format_console_only; /* Console-only format variant */
83 atomic_t has_custom_format; /* True if format was customized */
84
85 /* Terminal capability detection and color scheme */
86 terminal_capabilities_t terminal_caps; /* Detected terminal capabilities */
87 bool terminal_caps_initialized; /* Whether terminal caps were detected */
88 bool terminal_caps_detecting; /* Guard against recursion during detection */
89 compiled_color_scheme_t compiled_colors; /* Compiled color scheme for this session */
90 bool log_colorscheme_initialized; /* Whether color scheme was initialized */
91
92 /* Shutdown state management */
93 bool shutdown_saved_terminal_output; /* Saved state for log_shutdown_begin/end */
94 bool shutdown_in_progress; /* Track if shutdown phase is active */
95
96 /* Log rotation */
97 mutex_t rotation_mutex; /* Mutex for log rotation only (not for logging!) */
98 lifecycle_t rotation_mutex_lifecycle; /* Rotation mutex initialization state machine */
99
100 /* Session-specific logging (splash/status screens) */
101 atomic_ptr_t session_log_buffer; /* Pointer to registered session log buffer (can be NULL) */
102} g_log = {
103 .file = {0}, /* STDERR_FILENO (fd 2) - fd 0 is STDIN (read-only!) */
104 .json_file = {.impl = (uint64_t)-1}, /* -1 = disabled (atomic_t initialized as uint64_t -1) */
105 .level = {0}, /* DEFAULT_LOG_LEVEL */
106 .lifecycle = LIFECYCLE_INIT,
107 .filename = {0},
108 .current_size = {0},
109 .terminal_output_enabled = {1}, /* true */
110 .force_stderr = {0}, /* false */
111 .terminal_locked = {0}, /* false */
112 .terminal_owner_thread = {0},
113 .level_manually_set = {0}, /* false */
114 .flush_delay_ms = {0},
115 .format = NULL,
116 .format_console_only = NULL,
117 .has_custom_format = {0}, /* false */
118 .terminal_caps = {0},
119 .terminal_caps_initialized = false,
120 .terminal_caps_detecting = false,
121 .compiled_colors = {0},
122 .log_colorscheme_initialized = false,
123 .shutdown_saved_terminal_output = true,
124 .shutdown_in_progress = false,
125 .rotation_mutex_lifecycle = LIFECYCLE_INIT,
126 .session_log_buffer = {0}, /* NULL */
127};
128#pragma GCC diagnostic pop
129
130/* Level strings are defined in format.c (get_level_string_padded / get_level_string) */
131
132/* ============================================================================
133 * Level Strings
134 * ============================================================================
135 * The actual default log format is defined in lib/log/format.c
136 * and accessed via OPT_LOG_TEMPLATE_DEFAULT macro to support different modes.
137 */
138
150 const char *result;
151 switch (level) {
152 case LOG_INFO:
153 result = "INFO "; // 5 chars: INFO + 1 space
154 break;
155 case LOG_WARN:
156 result = "WARN "; // 5 chars: WARN + 1 space
157 break;
158 case LOG_DEV:
159 result = "DEV "; // 5 chars: DEV + 2 spaces
160 break;
161 case LOG_DEBUG:
162 result = "DEBUG"; // 5 chars: DEBUG (no padding needed)
163 break;
164 case LOG_ERROR:
165 result = "ERROR"; // 5 chars: ERROR (no padding needed)
166 break;
167 case LOG_FATAL:
168 result = "FATAL"; // 5 chars: FATAL (no padding needed)
169 break;
170 default:
171 result = "?????"; // Invalid level - return 5 question marks
172 }
173 // Verify length
174 if (strlen(result) != 5) {
175 fprintf(stderr, "ERROR: get_level_string_padded() returned non-5-char string: '%s' (len=%zu)\n", result,
176 strlen(result));
177 }
178 return result;
179}
180
181#define LOG_COLOR_COUNT 8 /* DEV, DEBUG, WARN, INFO, ERROR, FATAL, GREY, RESET */
182
183/* NOTE: Color arrays are now generated by the color scheme system in lib/ui/colors.c
184 * and stored in g_log.compiled_colors. The log_get_color_array() function returns pointers
185 * to g_log.compiled_colors.codes_16/256/truecolor based on terminal capabilities. */
186
187/* Internal error macro - uses g_log directly, only used in this file */
188#ifdef NDEBUG
189#define LOGGING_INTERNAL_ERROR(error, message, ...) \
190 do { \
191 asciichat_set_errno_with_message(error, NULL, 0, NULL, message, ##__VA_ARGS__); \
192 static const char *msg_header = "CRITICAL LOGGING SYSTEM ERROR: "; \
193 safe_fprintf(stderr, "%s %s\n", colored_string(LOG_COLOR_ERROR, msg_header), message); \
194 int _log_fd = atomic_load_int(&g_log.file); \
195 platform_write(_log_fd, msg_header, strlen(msg_header)); \
196 platform_write(_log_fd, message, strlen(message)); \
197 platform_write(_log_fd, "\n", 1); \
198 backtrace_print_simple(0); \
199 } while (0)
200#else
201#define LOGGING_INTERNAL_ERROR(error, message, ...) \
202 do { \
203 asciichat_set_errno_with_message(error, __FILE__, __LINE__, __func__, message, ##__VA_ARGS__); \
204 static const char *msg_header = "CRITICAL LOGGING SYSTEM ERROR: "; \
205 safe_fprintf(stderr, "%s %s\n", colored_string(LOG_COLOR_ERROR, msg_header), message); \
206 int _log_fd = atomic_load_int(&g_log.file); \
207 platform_write(_log_fd, msg_header, strlen(msg_header)); \
208 platform_write(_log_fd, message, strlen(message)); \
209 platform_write(_log_fd, "\n", 1); \
210 backtrace_print_simple(0); \
211 } while (0)
212#endif
213
214/* Note: g_colorscheme_mutex is defined in lib/ui/colors.c and declared in lib/ui/colors.h */
215
216size_t get_current_time_formatted(char *time_buf) {
217 /* Get wall-clock time in nanoseconds */
219 // Extract seconds and nanoseconds from total nanoseconds
220 time_t seconds = (time_t)(ts_ns / NS_PER_SEC_INT);
221 long nanoseconds = (long)(ts_ns % NS_PER_SEC_INT);
222 struct tm tm_info;
223 platform_localtime(&seconds, &tm_info);
224 // Format the time part first
225 // strftime returns 0 on error, not negative (and len is size_t/unsigned)
226 size_t len = strftime(time_buf, 32, "%H:%M:%S", &tm_info);
227 if (len == 0 || len >= 32) {
228 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format time");
229 return 0;
230 }
231
232 // Add microseconds manually (convert nanoseconds to microseconds for display)
233 long microseconds = nanoseconds / 1000;
234 if (microseconds < 0)
235 microseconds = 0;
236 if (microseconds > 999999)
237 microseconds = 999999;
238
239 int result = safe_snprintf(time_buf + len, 32 - len, ".%06ld", microseconds);
240 if (result < 0 || result >= (int)(32 - len)) {
241 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format microseconds");
242 return 0;
243 }
244
245 return len + (size_t)result;
246}
247
248char *format_message(const char *format, va_list args) {
249 if (!format) {
250 return NULL;
251 }
252
253 // First, determine the size needed
254 va_list args_copy;
255 va_copy(args_copy, args);
256 int size = safe_vsnprintf(NULL, 0, format, args_copy);
257 va_end(args_copy);
258
259 if (size < 0) {
260 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format context message");
261 return NULL;
262 }
263
264 // Allocate and format the message
265 char *message = SAFE_MALLOC(size + 1, char *);
266 int result = safe_vsnprintf(message, (size_t)size + 1, format, args);
267 if (result < 0) {
268 SAFE_FREE(message);
269 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format context message");
270 return NULL;
271 }
272
273 return message;
274}
275
286static int truncate_at_whole_line(char *buffer, int current_len, size_t max_len) {
287 if (current_len < 0 || (size_t)current_len <= max_len) {
288 return current_len; // No truncation needed
289 }
290
291 // Search backwards from max_len position for last newline
292 int truncate_pos = (int)max_len - 1;
293 while (truncate_pos > 0 && buffer[truncate_pos] != '\n') {
294 truncate_pos--;
295 }
296
297 // If we found a newline, truncate after it
298 if (truncate_pos > 0 && buffer[truncate_pos] == '\n') {
299 truncate_pos++; // Include the newline
300 // NOLINTNEXTLINE(clang-analyzer-security.ArrayBound) - truncate_pos validated by max_len
301 buffer[truncate_pos] = '\0';
302 return truncate_pos;
303 }
304
305 // No newline found, truncate at max_len to be safe
306 truncate_pos = (int)max_len - 1;
307 buffer[truncate_pos] = '\0';
308 return truncate_pos;
309}
310
318static void validate_log_message_utf8(const char *message, const char *source) {
319 if (!message || !source) {
320 return;
321 }
322
323 if (!utf8_is_valid(message)) {
324 // Use fprintf instead of log_warn to avoid infinite recursion
325 // (this function is called from log_msg, which would create a loop)
326 safe_fprintf(stderr, "[WARN] Invalid UTF-8 detected in %s\n", source);
327 safe_fprintf(stderr, "[DEBUG] Invalid UTF-8 data: %s\n", message);
328 }
329}
330
331/* ============================================================================
332 * Logging Implementation
333 * ============================================================================
334 */
335
336/* Parse LOG_LEVEL environment variable */
337static log_level_t parse_log_level_from_env(void) {
338 const char *env_level = SAFE_GETENV("LOG_LEVEL");
339 if (!env_level) {
340 return DEFAULT_LOG_LEVEL; // Default level based on build type
341 }
342
343 // Case-insensitive comparison
344 if (platform_strcasecmp(env_level, "DEV") == 0 || strcmp(env_level, "0") == 0) {
345 return LOG_DEV;
346 }
347 if (platform_strcasecmp(env_level, "DEBUG") == 0 || strcmp(env_level, "1") == 0) {
348 return LOG_DEBUG;
349 }
350 if (platform_strcasecmp(env_level, "INFO") == 0 || strcmp(env_level, "2") == 0) {
351 return LOG_INFO;
352 }
353 if (platform_strcasecmp(env_level, "WARN") == 0 || strcmp(env_level, "3") == 0) {
354 return LOG_WARN;
355 }
356 if (platform_strcasecmp(env_level, "ERROR") == 0 || strcmp(env_level, "4") == 0) {
357 return LOG_ERROR;
358 }
359 if (platform_strcasecmp(env_level, "FATAL") == 0 || strcmp(env_level, "5") == 0) {
360 return LOG_FATAL;
361 }
362
363 // Invalid value - return default
364 log_warn("Invalid LOG_LEVEL: %s", env_level);
365 return DEFAULT_LOG_LEVEL;
366}
367
368/* Log rotation function - keeps the tail (recent entries)
369 * REQUIRES: rotation_mutex must be held by caller
370 * This is the only operation that uses a mutex - regular logging is lock-free.
371 */
372static void rotate_log_locked(void) {
373 int file = atomic_load_int(&g_log.file);
374 size_t current_size = atomic_load_u64(&g_log.current_size);
375
376 if (file < 0 || file == STDERR_FILENO || strlen(g_log.filename) == 0) {
377 return;
378 }
379
380 if (current_size < MAX_LOG_SIZE) {
381 return;
382 }
383
384 platform_close(file);
385 atomic_store_int(&g_log.file, -1);
386
387 /* Open file for reading to get the tail */
388 int read_file = platform_open("log_file_read", g_log.filename, O_RDONLY, 0);
389 if (read_file < 0) {
390 log_error("Failed to open file descriptor for log rotation: fd=%d, path=%s", read_file, g_log.filename);
391 /* Fall back to regular truncation */
392 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
393 atomic_store_int(&g_log.file, fd);
394 atomic_store_u64(&g_log.current_size, 0);
395 return;
396 }
397
398 /* Seek to position where we want to start keeping data (keep last 2MB) */
399 size_t keep_size = MAX_LOG_SIZE * 2 / 3; /* Keep last 2MB of 3MB file */
400 if (current_size < keep_size) {
401 platform_close(read_file);
402 /* Fall back to truncation since we don't have enough data to rotate */
403 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
404 atomic_store_int(&g_log.file, fd);
405 atomic_store_u64(&g_log.current_size, 0);
406 return;
407 }
408 if (lseek(read_file, (off_t)(current_size - keep_size), SEEK_SET) == (off_t)-1) {
409 platform_close(read_file);
410 /* Fall back to truncation */
411 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
412 atomic_store_int(&g_log.file, fd);
413 atomic_store_u64(&g_log.current_size, 0);
414 return;
415 }
416
417 /* Skip to next line boundary to avoid partial lines */
418 char c;
419 while (platform_read(read_file, &c, 1) > 0 && c != '\n') {
420 /* Skip characters until newline */
421 }
422
423 /* Read the tail into a temporary file */
424 char temp_filename[PLATFORM_MAX_PATH_LENGTH];
425 int result = safe_snprintf(temp_filename, sizeof(temp_filename), "%s.tmp", g_log.filename);
426 if (result <= 0 || result >= (int)sizeof(temp_filename)) {
427 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format temp filename");
428 platform_close(read_file);
429 int fd = platform_open("log_file_append", g_log.filename, O_CREAT | O_RDWR | O_APPEND, FILE_PERM_PRIVATE);
430 atomic_store_int(&g_log.file, fd);
431 return;
432 }
433
434 int temp_file = platform_open("log_file_temp", temp_filename, O_CREAT | O_WRONLY | O_TRUNC, FILE_PERM_PRIVATE);
435 if (temp_file < 0) {
436 log_warn("Failed to open temporary file descriptor for log rotation: fd=%d, path=%s", temp_file, temp_filename);
437 platform_close(read_file);
438 /* Fall back to truncation */
439 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
440 atomic_store_int(&g_log.file, fd);
441 atomic_store_u64(&g_log.current_size, 0);
442 return;
443 }
444
445 /* Copy tail to temp file */
446 char buffer[8192];
447 ssize_t bytes_read;
448 size_t new_size = 0;
449 while ((bytes_read = platform_read(read_file, buffer, sizeof(buffer))) > 0) {
450 ssize_t written = platform_write(temp_file, buffer, (size_t)bytes_read);
451 if (written != bytes_read) {
452 platform_close(read_file);
453 platform_close(temp_file);
454 unlink(temp_filename);
455 /* Fall back to truncation */
456 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
457 atomic_store_int(&g_log.file, fd);
458 atomic_store_u64(&g_log.current_size, 0);
459 return;
460 }
461 new_size += (size_t)bytes_read;
462 }
463
464 platform_close(read_file);
465 platform_close(temp_file);
466
467 /* Replace original with temp file */
468 if (rename(temp_filename, g_log.filename) != 0) {
469 unlink(temp_filename); /* Clean up temp file */
470 /* Fall back to truncation */
471 int fd = platform_open("log_file_write", g_log.filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
472 atomic_store_int(&g_log.file, fd);
473 atomic_store_u64(&g_log.current_size, 0);
474 return;
475 }
476
477 /* Reopen for appending */
478 int new_fd = platform_open("log_file_append", g_log.filename, O_CREAT | O_RDWR | O_APPEND, FILE_PERM_PRIVATE);
479 if (new_fd < 0) {
480 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to reopen rotated log file: %s", g_log.filename);
481 atomic_store_int(&g_log.file, STDERR_FILENO);
482 g_log.filename[0] = '\0';
483 atomic_store_u64(&g_log.current_size, 0);
484 return;
485 }
486 atomic_store_int(&g_log.file, new_fd);
487 atomic_store_u64(&g_log.current_size, new_size);
488
489 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
491
492 char log_msg[256];
493 int log_msg_len =
494 safe_snprintf(log_msg, sizeof(log_msg), "[%s] [INFO] Log tail-rotated (kept %zu bytes)\n", time_buf, new_size);
495 if (log_msg_len > 0 && log_msg_len < (int)sizeof(log_msg)) {
496 (void)platform_write(new_fd, log_msg, (size_t)log_msg_len);
497 }
498}
499
500/* Check if rotation is needed and perform it (acquires mutex only if needed) */
501static void maybe_rotate_log(void) {
502 /* Rotation mutex not initialized - skip rotation */
503 if (!lifecycle_is_initialized(&g_log.rotation_mutex_lifecycle)) {
504 return;
505 }
506
507 /* Check mmap path first */
508 if (log_mmap_is_active()) {
509 size_t used = 0, capacity = 0;
510 if (log_mmap_get_usage(&used, &capacity) && capacity > 0) {
511 /* Rotate when 90% full to leave room for writes */
512 if (used > capacity * 9 / 10) {
513 mutex_lock(&g_log.rotation_mutex);
515 mutex_unlock(&g_log.rotation_mutex);
516 }
517 }
518 return;
519 }
520
521 /* File path: quick atomic check - avoid mutex if not needed */
522 size_t current_size = atomic_load_u64(&g_log.current_size);
523 if (current_size < MAX_LOG_SIZE) {
524 return;
525 }
526
527 /* Size exceeded - acquire mutex and rotate */
528 mutex_lock(&g_log.rotation_mutex);
529 rotate_log_locked();
530 mutex_unlock(&g_log.rotation_mutex);
531}
532
533void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap) {
534
535 // Initialize rotation mutex (only operation that uses a mutex)
536 g_log.rotation_mutex_lifecycle.sync_type = LIFECYCLE_SYNC_MUTEX;
537 g_log.rotation_mutex_lifecycle.sync.mutex = &g_log.rotation_mutex;
538 lifecycle_init(&g_log.rotation_mutex_lifecycle, "log_rotation");
539
540 // Set basic config using atomic stores
541 atomic_store_u64(&g_log.force_stderr, force_stderr);
542 bool preserve_terminal_output = atomic_load_u64(&g_log.terminal_output_enabled);
543
544 // Force logs to stderr if stdout is piped/redirected
545 // This prevents logs from contaminating piped/redirected output in snapshot/batch modes
546 // Per terminal.h: "force stderr when piped to prevent data corruption"
548 atomic_store_bool(&g_log.force_stderr, true);
549 }
550
551 // Close any existing file (atomic load/store)
552 int old_file = atomic_load_int(&g_log.file);
553 if (lifecycle_is_initialized(&g_log.lifecycle) && old_file >= 0 && old_file != STDERR_FILENO) {
554 platform_close(old_file);
555 atomic_store_int(&g_log.file, -1);
556 }
557
558 // Check LOG_LEVEL environment variable
559 const char *env_level_str = SAFE_GETENV("LOG_LEVEL");
560 if (env_level_str) {
561 atomic_store_u64(&g_log.level, (int)parse_log_level_from_env());
562 } else {
563 atomic_store_u64(&g_log.level, (int)level);
564 }
565
566 atomic_store_bool(&g_log.level_manually_set, false);
567 atomic_store_u64(&g_log.current_size, 0);
568
569 if (filename) {
570 SAFE_STRNCPY(g_log.filename, filename, sizeof(g_log.filename) - 1);
571
572 if (use_mmap) {
573 // Lock-free mmap path - writes go to mmap'd file
574 asciichat_error_t mmap_result = log_mmap_init_simple(filename, 0);
575 if (mmap_result == ASCIICHAT_OK) {
576 atomic_store_int(&g_log.file, -1); // No regular fd - using mmap for file output
577 } else {
578 // Mmap failed - use stderr only (atomic writes, lock-free)
579 if (preserve_terminal_output) {
580 safe_fprintf(stderr, "Mmap logging failed for %s, using stderr only (lock-free)\n", filename);
581 }
582 atomic_store_int(&g_log.file, STDERR_FILENO);
583 g_log.filename[0] = '\0';
584 }
585 } else {
586 // Lock-free file I/O path - uses atomic write() syscalls
587 int fd = platform_open("log_file", filename, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
588 atomic_store_int(&g_log.file, (fd >= 0) ? fd : STDERR_FILENO);
589 if (fd < 0) {
590 if (preserve_terminal_output) {
591 safe_fprintf(stderr, "Failed to open log file: %s\n", filename);
592 }
593 g_log.filename[0] = '\0';
594 }
595 }
596 } else {
597 atomic_store_int(&g_log.file, STDERR_FILENO);
598 g_log.filename[0] = '\0';
599 }
600
601 /* Initialize default log format (NULL means use mode-specific default) */
602 log_set_format(NULL, false);
603
604 /* Mark logging system as initialized (state machine transition) */
605 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
606 lifecycle_init(&g_log.lifecycle, "logging");
607 }
608
609 atomic_store_u64(&g_log.terminal_output_enabled, preserve_terminal_output);
610
611 // Reset terminal detection if needed
612 if (g_log.terminal_caps_initialized && !g_log.terminal_caps.detection_reliable) {
613 g_log.terminal_caps_initialized = false;
614 }
615
616 // Detect terminal capabilities
618
619 // NOTE: Color initialization happens separately via log_set_color_scheme()
620 // after options are parsed. Logging works without colors until then.
621 // NOTE: Grep filter initialization happens in main.c after options_init() completes.
622}
623
624void log_destroy(void) {
625 // Destroy mmap logging first (if active)
626 if (log_mmap_is_active()) {
628 }
629
630 // Cleanup grep filter
631 grep_destroy();
632
633 /* Mark logging system as shutdown FIRST (state machine transition)
634 * This signals worker threads to stop logging immediately before we free memory.
635 * This prevents TOCTOU race where a worker thread could see non-NULL format pointer
636 * after we've freed it but before lifecycle_shutdown() is called. */
637 lifecycle_shutdown(&g_log.lifecycle);
638
639 // Cleanup custom format structures: All worker threads have been signaled to stop by this point.
640 // Safe to free templates now without use-after-free concerns.
641 if (g_log.format) {
642 log_template_free(g_log.format);
643 g_log.format = NULL;
644 }
645 if (g_log.format_console_only) {
646 log_template_free(g_log.format_console_only);
647 g_log.format_console_only = NULL;
648 }
649 atomic_store_bool(&g_log.has_custom_format, false);
650
651 // Lock-free cleanup using atomic operations
652 int old_file = atomic_load_int(&g_log.file);
653 if (old_file >= 0 && old_file != STDERR_FILENO) {
654 platform_close(old_file);
655 }
656 atomic_store_int(&g_log.file, -1);
657
658 // Destroy rotation mutex
659 lifecycle_shutdown(&g_log.rotation_mutex_lifecycle);
660}
661
669void log_system_init(void) {
670
671 // Detect terminal capabilities if not already done
673
674 // Initialize color scheme based on terminal capabilities
676}
677
687 // Unregister session log buffer if registered
689}
690
692 atomic_store_u64(&g_log.level, (int)level);
693 atomic_store_bool(&g_log.level_manually_set, true);
694}
695
697 return (log_level_t)atomic_load_u64(&g_log.level);
698}
699
701 // Respect --quiet flag: if quiet is set, never enable terminal output
702 // But allow disabling even if options are unavailable (for shutdown cleanup)
703
704 // Only check quiet flag when ENABLING output to avoid deadlock with
705 // concurrent logging from other threads (options_get() can deadlock when
706 // called while other threads are actively logging due to RCU synchronization)
707 if (enabled) {
708 const options_t *opts = options_get();
709 if (opts && opts->quiet) {
710 return; // Silently ignore attempts to enable terminal output when --quiet is set
711 }
712 }
713
714 atomic_store_u64(&g_log.terminal_output_enabled, enabled);
715}
716
718 return atomic_load_u64(&g_log.terminal_output_enabled);
719}
720
722 atomic_store_u64(&g_log.force_stderr, enabled);
723}
724
726 return atomic_load_u64(&g_log.force_stderr);
727}
728
730 atomic_store_int(&g_log.json_file, fd);
731}
732
734 /* Close the current file if it's not stderr */
735 int old_file = atomic_load_int(&g_log.file);
736 if (old_file >= 0 && old_file != STDERR_FILENO) {
737 platform_close(old_file);
738 }
739 /* Redirect file output to stderr */
740 atomic_store_int(&g_log.file, STDERR_FILENO);
741 g_log.filename[0] = '\0';
742}
743
744asciichat_error_t log_set_format(const char *format_str, bool console_only) {
745 /* Free old format if it exists */
746 if (g_log.format) {
747 log_template_free(g_log.format);
748 g_log.format = NULL;
749 }
750 if (g_log.format_console_only) {
751 log_template_free(g_log.format_console_only);
752 g_log.format_console_only = NULL;
753 }
754
755 /* Use default format if NULL or empty string */
756 const char *format_to_use = (format_str && format_str[0] != '\0') ? format_str : OPT_LOG_TEMPLATE_DEFAULT;
757 bool is_custom = (format_str && format_str[0] != '\0');
758
759 /* Parse the format string (always parse, never skip) */
760 log_template_t *parsed_format = log_template_parse(format_to_use, false);
761
762 if (!parsed_format) {
763 log_error("Failed to parse log format: %s", format_to_use);
764 return SET_ERRNO(ERROR_INVALID_STATE, "Invalid log format string");
765 }
766
767 /* If console_only is true, we also need the default format for file output */
768 if (console_only && is_custom) {
770
771 if (!default_format) {
772 log_template_free(parsed_format);
773 log_error("Failed to parse default log format");
774 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to parse default format");
775 }
776 g_log.format = default_format;
777 g_log.format_console_only = parsed_format;
778 } else {
779 g_log.format = parsed_format;
780 g_log.format_console_only = NULL;
781 }
782
783 atomic_store_u64(&g_log.has_custom_format, is_custom);
784 return ASCIICHAT_OK;
785}
786
788 bool expected = atomic_load_bool(&g_log.terminal_locked);
789 while (!atomic_cas_bool(&g_log.terminal_locked, &expected, true)) {
790 // Retry if CAS failed
791 }
792 atomic_store_u64(&g_log.terminal_owner_thread, (uint64_t)asciichat_thread_self());
793 return expected;
794}
795
796void log_unlock_terminal(bool previous_state) {
797 atomic_store_u64(&g_log.terminal_locked, previous_state);
798 if (!previous_state) {
799 atomic_store_u64(&g_log.terminal_owner_thread, 0);
800 }
801}
802
803void log_set_flush_delay(unsigned int delay_ms) {
804 atomic_store_u64(&g_log.flush_delay_ms, delay_ms);
805}
806
808 // Log rotation is inherently racy without locks - best-effort only
809 // For reliable rotation, use mmap mode or external logrotate
810 int file = atomic_load_int(&g_log.file);
811 if (file >= 0 && file != STDERR_FILENO && strlen(g_log.filename) > 0) {
812 struct stat st;
813 if (fstat(file, &st) == 0 && st.st_size > MAX_LOG_SIZE) {
814 atomic_store_u64(&g_log.current_size, (size_t)st.st_size);
815 }
816 }
817}
818
819/* Helper: Write formatted log entry to file using atomic write() syscall
820 * POSIX guarantees write() is atomic for sizes <= PIPE_BUF (typically 4096 bytes)
821 * Strips ANSI escape codes from the buffer before writing to ensure clean log files.
822 */
823static void write_to_log_file_atomic(const char *buffer, int length, const char *stripped_buffer) {
824 if (length <= 0 || buffer == NULL) {
825 return;
826 }
827
828 if (length > MAX_LOG_SIZE) {
829 return;
830 }
831
832 int file = atomic_load_int(&g_log.file);
833 if (file < 0 || file == STDERR_FILENO) {
834 return;
835 }
836
837 // Use pre-stripped buffer if available (already stripped by caller), otherwise use original
838 const char *write_buf = stripped_buffer ? stripped_buffer : buffer;
839 size_t write_len = stripped_buffer ? strlen(stripped_buffer) : (size_t)length;
840
841 // Single atomic write() call - no locking needed
842 ssize_t written = platform_write(file, write_buf, write_len);
843 if (written > 0) {
844 atomic_fetch_add_u64(&g_log.current_size, (uint64_t)written);
845 maybe_rotate_log();
846 }
847}
848
849/* Helper: Format log message header using the format system
850 * Returns the number of characters written to the buffer
851 */
852static int format_log_header(char *buffer, size_t buffer_size, log_level_t level, const char *timestamp,
853 const char *file, int line, const char *func, bool use_colors, uint64_t time_nanoseconds) {
854 const log_template_t *format = g_log.format;
855 if (!format) {
856 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Log format not initialized");
857 return -1;
858 }
859
860 /* Use log_template_apply() to generate the complete formatted output
861 * Safety: reload format immediately before use. Format is never freed to avoid TOCTOU races. */
862 const log_template_t *format_check = g_log.format;
863 if (!format_check) {
864 /* Format not available - return minimal output */
865 return -1;
866 }
867
868 int written = log_template_apply(format_check, buffer, buffer_size, level, timestamp, file, line, func,
869 asciichat_thread_current_id(), "", use_colors, time_nanoseconds);
870 if (written < 0 || written >= (int)buffer_size) {
871 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format log header");
872 return -1;
873 }
874
875 return written;
876}
877
878/* Note: Terminal buffering removed for lock-free design.
879 * When terminal is locked, other threads' output goes to file only.
880 */
881
882/* Helper: Write colored log entry to terminal (lock-free)
883 * Uses atomic loads for state checks, skips output if terminal is locked by another thread
884 */
885static void write_to_terminal_atomic(log_level_t level, const char *timestamp, const char *file, int line,
886 const char *func, const char *fmt, va_list args, uint64_t time_nanoseconds) {
887 // Prevent logging during shutdown - if logging lifecycle is being destroyed,
888 // bail out to avoid use-after-free of the log template
889 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
890 return; /* Logging is being shut down, skip */
891 }
892
893 // Choose output file descriptor using unified routing logic
894 int fd = terminal_choose_log_fd(level);
895 FILE *output_stream = (fd == STDERR_FILENO) ? stderr : stdout;
896 bool use_colors = terminal_should_color_output(fd);
897
898 // Format message first (needed for status screen capture even if terminal output disabled)
899 char msg_buffer[LOG_MSG_BUFFER_SIZE];
900 va_list args_copy;
901 va_copy(args_copy, args);
902 int msg_len = safe_vsnprintf(msg_buffer, sizeof(msg_buffer), fmt, args_copy);
903 va_end(args_copy);
904
905 // Use message buffer directly as clean message
906 // Stack allocation avoids heap corruption during shutdown signal handling
907 const char *clean_msg = msg_buffer;
908
909 // Use format system to generate the complete log line
910 char colored_log_line[LOG_MSG_BUFFER_SIZE + 512];
911 char plain_log_line[LOG_MSG_BUFFER_SIZE + 512];
912
913 /* Apply format without colors first (plain version for grep matching)
914 * Load format pointer once to prevent TOCTOU race condition where another
915 * thread frees the format between our check and use. */
916 const log_template_t *format = g_log.format;
917 int plain_len = 0;
918 if (format && lifecycle_is_initialized(&g_log.lifecycle)) { /* Double-check lifecycle is still active */
919 plain_len = log_template_apply(format, plain_log_line, sizeof(plain_log_line), level, timestamp, file, line, func,
920 asciichat_thread_current_id(), clean_msg, false, time_nanoseconds);
921 }
922
923 /* For colored output, apply format with colors enabled */
924 int colored_len = 0;
925 if (use_colors && plain_len > 0) {
926 const log_template_t *format_check = g_log.format;
927 if (format_check && lifecycle_is_initialized(&g_log.lifecycle)) { /* Re-verify lifecycle before use */
928 colored_len = log_template_apply(format_check, colored_log_line, sizeof(colored_log_line), level, timestamp, file,
929 line, func, asciichat_thread_current_id(), clean_msg, true, time_nanoseconds);
930 }
931 /* If applying with colors failed, fall back to plain text */
932 if (colored_len <= 0) {
933 // Applying with colors failed - use plain text (no colors)
934 colored_len = plain_len;
935 if (plain_len > 0 && plain_len < (int)sizeof(colored_log_line)) {
936 memcpy(colored_log_line, plain_log_line, (size_t)plain_len + 1);
937 }
938 }
939 } else {
940 /* No colors - use plain text */
941 colored_len = plain_len;
942 if (plain_len > 0 && plain_len < (int)sizeof(colored_log_line)) {
943 memcpy(colored_log_line, plain_log_line, (size_t)plain_len + 1);
944 }
945 }
946
947 // Feed log to registered session log buffer (use colored version if available, otherwise plain)
948 session_log_buffer_t *buf = (session_log_buffer_t *)atomic_ptr_load(&g_log.session_log_buffer);
949 if (buf) {
950 if (colored_len > 0 && colored_len < (int)sizeof(colored_log_line)) {
951 session_log_buffer_append(buf, colored_log_line);
952 } else if (plain_len > 0 && plain_len < (int)sizeof(plain_log_line)) {
953 session_log_buffer_append(buf, plain_log_line);
954 }
955 }
956
957 // Check if terminal output is enabled (atomic load)
958 bool is_enabled = atomic_load_u64(&g_log.terminal_output_enabled);
959 if (!is_enabled) {
960 // Terminal output disabled - return (status screen already captured)
961 return;
962 }
963
964 // Check if terminal is locked by another thread
965 if (atomic_load_u64(&g_log.terminal_locked)) {
966 uint64_t owner = atomic_load_u64(&g_log.terminal_owner_thread);
967 if (owner != (uint64_t)asciichat_thread_self()) {
968 // Terminal locked by another thread - return
969 return;
970 }
971 }
972
973 // Handle message formatting errors for terminal output
974 if (msg_len <= 0 || msg_len >= (int)sizeof(msg_buffer)) {
975 // Message formatting failed - skip filtering and try direct output
976 (void)vfprintf(output_stream, fmt, args);
977 safe_fprintf(output_stream, "\n");
978 (void)fflush(output_stream);
979 return;
980 }
981
982 // Handle log formatting errors
983 if (colored_len <= 0 || colored_len >= (int)sizeof(colored_log_line) || plain_len <= 0 ||
984 plain_len >= (int)sizeof(plain_log_line)) {
985 // Formatting failed - try to output something
986 safe_fprintf(output_stream, "%s\n", clean_msg);
987 (void)fflush(output_stream);
988 return;
989 }
990
991 // Apply grep filter (terminal output only - file logs are unfiltered)
992 // Match against PLAIN text (no ANSI codes) so offsets are correct
993 size_t match_start = 0, match_len = 0;
994 if (!grep_should_output(plain_log_line, &match_start, &match_len)) {
995 return; // No match - suppress terminal output
996 }
997
998 // Output with colors
999 if (use_colors) {
1000 const char **colors = log_get_color_array();
1001
1002 // When grep matches, highlight in the colored line
1003 if (match_len > 0 && colors != NULL) {
1004 // Apply highlighting to the full line, respecting existing ANSI codes
1005 const char *highlighted_line = grep_highlight_colored(colored_log_line, plain_log_line, match_start, match_len);
1006 safe_fprintf(output_stream, "%s\n", highlighted_line);
1007 } else {
1008 // No grep match - output colored line as-is
1009 safe_fprintf(output_stream, "%s\n", colored_log_line);
1010 }
1011 } else {
1012 // No colors - output plain line
1013 safe_fprintf(output_stream, "%s\n", plain_log_line);
1014 }
1015
1016 (void)fflush(output_stream);
1017}
1018
1019void log_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt, ...) {
1020
1021 // All state access uses atomic operations - fully lock-free
1022 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1023 return;
1024 }
1025
1026 // Safety check: if main format template has been freed during shutdown, skip logging
1027 // This prevents heap-use-after-free when threads call logging after log_destroy() has freed g_log.format
1028 if (!g_log.format) {
1029 return;
1030 }
1031
1032 uint64_t loaded_level = atomic_load_u64(&g_log.level);
1033
1034 if (level < (log_level_t)loaded_level) {
1035 return;
1036 }
1037 /* =========================================================================
1038 * MMAP PATH: When mmap logging is active, writes go to mmap'd file
1039 * ========================================================================= */
1040 bool mmap_active = log_mmap_is_active();
1041
1042 if (mmap_active) {
1043 maybe_rotate_log();
1044
1045 va_list args;
1046 va_start(args, fmt);
1047 char msg_buffer[LOG_MMAP_MSG_BUFFER_SIZE];
1048 int msg_len = safe_vsnprintf(msg_buffer, sizeof(msg_buffer), fmt, args);
1049 va_end(args);
1050
1051 // Truncate at whole line boundaries to avoid UTF-8 issues
1052 if (msg_len > 0) {
1053 msg_len = truncate_at_whole_line(msg_buffer, msg_len, sizeof(msg_buffer));
1054 }
1055
1056 // Validate UTF-8 in formatted message
1057 validate_log_message_utf8(msg_buffer, "mmap log message");
1058
1059 log_mmap_write(level, file, line, func, "%s", msg_buffer);
1060
1061 // Terminal output (check with atomic loads)
1062 if (atomic_load_u64(&g_log.terminal_output_enabled) && !atomic_load_bool(&g_log.terminal_locked)) {
1063 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1064 uint64_t time_ns = time_get_realtime_ns();
1066
1067 // Choose output stream using unified routing logic
1068 int fd = terminal_choose_log_fd(level);
1069 FILE *output_stream = (fd == STDERR_FILENO) ? stderr : stdout;
1070 // Check if colors should be used
1071 // Priority 1: If --color was explicitly passed, force colors
1072 bool use_colors = true; // Default: enable colors
1074 use_colors = false; // --color=false explicitly disables colors
1075 }
1076 // Priority 2: If --color NOT explicitly passed, enable colors by default
1077
1078 char header_buffer[512];
1079 int header_len = format_log_header(header_buffer, sizeof(header_buffer), level, time_buf, file, line, func,
1080 use_colors, time_ns);
1081
1082 if (header_len >= 0 && header_len < (int)sizeof(header_buffer)) {
1083 if (use_colors) {
1084 const char *colorized_msg = colorize_log_message(msg_buffer);
1085 const char **colors = log_get_color_array();
1086 if (colors) {
1087 safe_fprintf(output_stream, "%s%s%s%s\n", header_buffer, colors[LOG_COLOR_RESET], colorized_msg,
1088 colors[LOG_COLOR_RESET]);
1089 } else {
1090 safe_fprintf(output_stream, "%s%s\n", header_buffer, colorized_msg);
1091 }
1092 } else {
1093 safe_fprintf(output_stream, "%s%s\n", header_buffer, msg_buffer);
1094 }
1095 (void)fflush(output_stream);
1096 }
1097 } else {
1098 }
1099 return;
1100 }
1101 /* =========================================================================
1102 * FILE I/O PATH: Lock-free using atomic write() syscalls
1103 * ========================================================================= */
1104 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1105 uint64_t time_ns = time_get_realtime_ns();
1107 // Format message for file output
1108 char log_buffer[LOG_MSG_BUFFER_SIZE];
1109 va_list args;
1110 va_start(args, fmt);
1111 int header_len = format_log_header(log_buffer, sizeof(log_buffer), level, time_buf, file, line, func, false, time_ns);
1112 if (header_len < 0 || header_len >= (int)sizeof(log_buffer)) {
1113 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format log header");
1114 va_end(args);
1115 return;
1116 }
1117 int msg_len = header_len;
1118 int formatted_len = safe_vsnprintf(log_buffer + header_len, sizeof(log_buffer) - (size_t)header_len, fmt, args);
1119 if (formatted_len < 0) {
1120 LOGGING_INTERNAL_ERROR(ERROR_INVALID_STATE, "Failed to format log message");
1121 va_end(args);
1122 return;
1123 }
1124
1125 msg_len += formatted_len;
1126 // Truncate at whole line boundaries to avoid UTF-8 issues
1127 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1128 // Add newline if there's room and message doesn't already end with one
1129 if (msg_len > 0 && msg_len < (int)sizeof(log_buffer) - 1) {
1130 if (log_buffer[msg_len - 1] != '\n') {
1131 log_buffer[msg_len++] = '\n';
1132 log_buffer[msg_len] = '\0';
1133 }
1134 }
1135 va_end(args);
1136 // Validate UTF-8 in formatted message
1137 validate_log_message_utf8(log_buffer, "leveled log message");
1138
1139 // Extract the user message part (without header) for JSON logging
1140 const char *user_message = log_buffer + header_len;
1141
1142 // For JSON output: strip automatic trailing newline (keep explicit ones)
1143 char json_message_buf[LOG_MSG_BUFFER_SIZE];
1144 const char *json_message = user_message;
1145 size_t user_msg_len = strlen(user_message);
1146
1147 // If message ends with newline and it was added automatically (not in original message)
1148 if (user_msg_len > 0 && user_message[user_msg_len - 1] == '\n') {
1149 // The newline was added automatically if the message part didn't end with one
1150 // before we added it on line 1067. We can check this by looking at msg_len - 1
1151 // If msg_len >= header_len + 2, then msg_len - 2 would be the char before the newline
1152 int msg_content_len = msg_len - header_len;
1153 // NOLINTNEXTLINE(clang-analyzer-security.ArrayBound) - msg_content_len > 1 bounds check above
1154 if (msg_content_len > 1 && log_buffer[msg_len - 2] != '\n') {
1155 // Newline was added automatically - strip it for JSON
1156 if (user_msg_len - 1 < sizeof(json_message_buf)) {
1157 memcpy(json_message_buf, user_message, user_msg_len - 1);
1158 json_message_buf[user_msg_len - 1] = '\0';
1159 json_message = json_message_buf;
1160 }
1161 }
1162 }
1163
1164 // Check if JSON format is enabled
1165 int json_fd = atomic_load_int(&g_log.json_file);
1166 bool json_format_enabled = (json_fd >= 0);
1167
1168 // If JSON format is enabled, output ONLY JSON (skip text output)
1169 if (json_format_enabled) {
1170 // Output JSON to the JSON file descriptor
1171 log_json_write(json_fd, level, time_ns, file, line, func, json_message);
1172 // Also output JSON to console using unified routing logic, respecting quiet flag
1173 if (atomic_load_u64(&g_log.terminal_output_enabled)) {
1174 int console_fd = terminal_choose_log_fd(level);
1175 log_json_write(console_fd, level, time_ns, file, line, func, json_message);
1176 }
1177 } else {
1178 // Text format: output to file and terminal
1179 // No heap allocation - use log_buffer directly
1180 // Write to file (atomic write syscall) - use original buffer (with ANSI codes)
1181 int file_fd = (int)atomic_load_u64(&g_log.file);
1182 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1183 write_to_log_file_atomic(log_buffer, msg_len, NULL);
1184 }
1185
1186 // Write to terminal (atomic state checks)
1187 va_list args_terminal;
1188 va_start(args_terminal, fmt);
1189 write_to_terminal_atomic(level, time_buf, file, line, func, fmt, args_terminal, time_ns);
1190 va_end(args_terminal);
1191 }
1192}
1193
1194void log_terminal_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt, ...) {
1195 // Lock-free: only uses atomic loads, no mutex
1196 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1197 return;
1198 }
1199
1200 if (level < (log_level_t)atomic_load_u64(&g_log.level)) {
1201 return;
1202 }
1203
1204 // Terminal output only - no file/mmap writing
1205 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1206 uint64_t time_ns = time_get_realtime_ns();
1208
1209 va_list args;
1210 va_start(args, fmt);
1211 write_to_terminal_atomic(level, time_buf, file, line, func, fmt, args, time_ns);
1212 va_end(args);
1213}
1214
1215void log_plain_msg(const char *fmt, ...) {
1216 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1217 return;
1218 }
1219
1220 if (shutdown_is_requested()) {
1221 return;
1222 }
1223
1224 char log_buffer[LOG_MSG_BUFFER_SIZE];
1225 va_list args;
1226 va_start(args, fmt);
1227 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1228 va_end(args);
1229
1230 if (msg_len <= 0) {
1231 return;
1232 }
1233
1234 // Truncate at whole line boundaries to avoid UTF-8 issues
1235 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1236
1237 // Validate UTF-8 in formatted message
1238 validate_log_message_utf8(log_buffer, "plain text log");
1239
1240 // Write to mmap if active
1241 if (log_mmap_is_active()) {
1242 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1243 } else {
1244 // Write to file with headers (atomic write syscall)
1245 int file_fd = atomic_load_u64(&g_log.file);
1246 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1247 // Add header with timestamp and log level to file output
1248 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1249 uint64_t time_ns = time_get_realtime_ns();
1251
1252 char header_buffer[512];
1253 int header_len = format_log_header(header_buffer, sizeof(header_buffer), LOG_INFO, time_buf, "lib/log/logging.c",
1254 0, "log_plain_msg", false, time_ns);
1255
1256 if (header_len > 0) {
1257 write_to_log_file_atomic(header_buffer, header_len, NULL);
1258 }
1259 write_to_log_file_atomic(log_buffer, msg_len, NULL);
1260 write_to_log_file_atomic("\n", 1, NULL);
1261 }
1262 }
1263
1264 // Terminal output (atomic state checks)
1265 if (!atomic_load_u64(&g_log.terminal_output_enabled)) {
1266 return;
1267 }
1268 if (atomic_load_u64(&g_log.terminal_locked)) {
1269 uint64_t owner = atomic_load_u64(&g_log.terminal_owner_thread);
1270 if (owner != (uint64_t)asciichat_thread_self()) {
1271 return;
1272 }
1273 }
1274
1275 // Check if JSON format is enabled
1276 int json_fd = atomic_load_int(&g_log.json_file);
1277 bool json_format_enabled = (json_fd >= 0);
1278
1279 if (json_format_enabled) {
1280 // Output JSON for plain messages too
1281 uint64_t time_ns = time_get_realtime_ns();
1282 int console_fd = terminal_choose_log_fd(LOG_INFO);
1283 log_json_write(console_fd, LOG_INFO, time_ns, __FILE__, __LINE__, "log_plain_msg", log_buffer);
1284 } else {
1285 // Choose output stream using unified routing logic (LOG_INFO level)
1287 FILE *output_stream = (fd == STDERR_FILENO) ? stderr : stdout;
1288
1289 // Apply colorization for TTY output
1291 const char *colorized_msg = colorize_log_message(log_buffer);
1292 safe_fprintf(output_stream, "%s\n", colorized_msg);
1293 } else {
1294 safe_fprintf(output_stream, "%s\n", log_buffer);
1295 }
1296 (void)fflush(output_stream);
1297 }
1298}
1299
1300// Helper for log_plain_stderr variants (lock-free)
1301static void log_plain_stderr_internal_atomic(const char *fmt, va_list args, bool add_newline) {
1302 char log_buffer[LOG_MSG_BUFFER_SIZE];
1303 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1304
1305 if (msg_len <= 0) {
1306 return;
1307 }
1308
1309 // Truncate at whole line boundaries to avoid UTF-8 issues
1310 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1311
1312 // Validate UTF-8 in formatted message
1313 validate_log_message_utf8(log_buffer, "stderr log");
1314
1315 // Write to mmap if active
1316 if (log_mmap_is_active()) {
1317 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1318 } else {
1319 // Write to file with headers (atomic write syscall)
1320 int file_fd = atomic_load_u64(&g_log.file);
1321 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1322 // Add header with timestamp and log level to file output
1323 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1324 uint64_t time_ns = time_get_realtime_ns();
1326
1327 char header_buffer[512];
1328 int header_len = format_log_header(header_buffer, sizeof(header_buffer), LOG_INFO, time_buf, "lib/log/logging.c",
1329 0, "log_plain_stderr_msg", false, time_ns);
1330
1331 if (header_len > 0) {
1332 write_to_log_file_atomic(header_buffer, header_len, NULL);
1333 }
1334 write_to_log_file_atomic(log_buffer, msg_len, NULL);
1335 if (add_newline) {
1336 write_to_log_file_atomic("\n", 1, NULL);
1337 }
1338 }
1339 }
1340
1341 // Terminal output (atomic state checks)
1342 if (!atomic_load_u64(&g_log.terminal_output_enabled)) {
1343 return;
1344 }
1345 if (atomic_load_u64(&g_log.terminal_locked)) {
1346 uint64_t owner = atomic_load_u64(&g_log.terminal_owner_thread);
1347 if (owner != (uint64_t)asciichat_thread_self()) {
1348 return;
1349 }
1350 }
1351
1352 // Write to stderr without colorization (plain output)
1353 if (add_newline) {
1354 safe_fprintf(stderr, "%s\n", log_buffer);
1355 } else {
1356 safe_fprintf(stderr, "%s", log_buffer);
1357 }
1358 (void)fflush(stderr);
1359}
1360
1361void log_plain_stderr_msg(const char *fmt, ...) {
1362 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1363 return;
1364 }
1365 if (shutdown_is_requested()) {
1366 return;
1367 }
1368
1369 va_list args;
1370 va_start(args, fmt);
1371 log_plain_stderr_internal_atomic(fmt, args, true);
1372 va_end(args);
1373}
1374
1375void log_plain_stderr_nonewline_msg(const char *fmt, ...) {
1376 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1377 return;
1378 }
1379 if (shutdown_is_requested()) {
1380 return;
1381 }
1382
1383 va_list args;
1384 va_start(args, fmt);
1385 log_plain_stderr_internal_atomic(fmt, args, false);
1386 va_end(args);
1387}
1388
1394static void log_plain_stdout_internal_atomic(const char *fmt, va_list args, bool add_newline) {
1395 char log_buffer[LOG_MSG_BUFFER_SIZE];
1396 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1397
1398 if (msg_len <= 0) {
1399 return;
1400 }
1401
1402 // Truncate at whole line boundaries to avoid UTF-8 issues
1403 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1404
1405 // Validate UTF-8 in formatted message
1406 validate_log_message_utf8(log_buffer, "stdout log");
1407
1408 // Write to mmap if active
1409 if (log_mmap_is_active()) {
1410 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1411 } else {
1412 // Write to file with headers (atomic write syscall)
1413 int file_fd = atomic_load_u64(&g_log.file);
1414 if (file_fd >= 0 && file_fd != STDOUT_FILENO) {
1415 // Add header with timestamp and log level to file output
1416 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
1417 uint64_t time_ns = time_get_realtime_ns();
1419
1420 char header_buffer[512];
1421 int header_len = format_log_header(header_buffer, sizeof(header_buffer), LOG_INFO, time_buf, "lib/log/logging.c",
1422 0, "log_plain_stdout_msg", false, time_ns);
1423
1424 if (header_len > 0) {
1425 write_to_log_file_atomic(header_buffer, header_len, NULL);
1426 }
1427 write_to_log_file_atomic(log_buffer, msg_len, NULL);
1428 if (add_newline) {
1429 write_to_log_file_atomic("\n", 1, NULL);
1430 }
1431 }
1432 }
1433
1434 // Terminal output (atomic state checks)
1435 if (!atomic_load_u64(&g_log.terminal_output_enabled)) {
1436 return;
1437 }
1438 if (atomic_load_u64(&g_log.terminal_locked)) {
1439 uint64_t owner = atomic_load_u64(&g_log.terminal_owner_thread);
1440 if (owner != (uint64_t)asciichat_thread_self()) {
1441 return;
1442 }
1443 }
1444
1445 // Write to stdout without colorization (plain output)
1446 if (add_newline) {
1447 safe_fprintf(stdout, "%s\n", log_buffer);
1448 } else {
1449 safe_fprintf(stdout, "%s", log_buffer);
1450 }
1451}
1452
1453void log_plain_stdout_msg(const char *fmt, ...) {
1454 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1455 return;
1456 }
1457 if (shutdown_is_requested()) {
1458 return;
1459 }
1460
1461 va_list args;
1462 va_start(args, fmt);
1463 log_plain_stdout_internal_atomic(fmt, args, true);
1464 va_end(args);
1465}
1466
1467void log_file_msg(const char *fmt, ...) {
1468 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1469 return;
1470 }
1471
1472 char log_buffer[LOG_MSG_BUFFER_SIZE];
1473 va_list args;
1474 va_start(args, fmt);
1475 int msg_len = safe_vsnprintf(log_buffer, sizeof(log_buffer), fmt, args);
1476 va_end(args);
1477
1478 if (msg_len <= 0) {
1479 return;
1480 }
1481
1482 // Truncate at whole line boundaries to avoid UTF-8 issues
1483 msg_len = truncate_at_whole_line(log_buffer, msg_len, sizeof(log_buffer));
1484
1485 // Validate UTF-8 in formatted message
1486 validate_log_message_utf8(log_buffer, "file-only log");
1487
1488 // Write to mmap if active, else to file
1489 if (log_mmap_is_active()) {
1490 log_mmap_write(LOG_INFO, NULL, 0, NULL, "%s", log_buffer);
1491 } else {
1492 int file_fd = atomic_load_u64(&g_log.file);
1493 if (file_fd >= 0 && file_fd != STDERR_FILENO) {
1494 write_to_log_file_atomic(log_buffer, msg_len, NULL);
1495 write_to_log_file_atomic("\n", 1, NULL);
1496 }
1497 }
1498}
1499
1500static const char *log_network_direction_label(remote_log_direction_t direction) {
1501 switch (direction) {
1503 return "server→client";
1505 return "client→server";
1506 default:
1507 return "network";
1508 }
1509}
1510
1511static asciichat_error_t log_network_message_internal(socket_t sockfd, const struct crypto_context_t *crypto_ctx,
1512 log_level_t level, remote_log_direction_t direction,
1513 const char *file, int line, const char *func, const char *fmt,
1514 va_list args) {
1515 if (!fmt) {
1516 return SET_ERRNO(ERROR_INVALID_PARAM, "Format string is NULL");
1517 }
1518
1519 va_list args_copy;
1520 va_copy(args_copy, args);
1521 char *formatted = format_message(fmt, args_copy);
1522 va_end(args_copy);
1523
1524 if (!formatted) {
1525 asciichat_error_t current_error = GET_ERRNO();
1526 if (current_error == ASCIICHAT_OK) {
1527 current_error = SET_ERRNO(ERROR_MEMORY, "Failed to format network log message");
1528 }
1529 return current_error;
1530 }
1531
1532 asciichat_error_t send_result = ASCIICHAT_OK;
1533 if (sockfd == INVALID_SOCKET_VALUE) {
1534 send_result = SET_ERRNO(ERROR_INVALID_PARAM, "Invalid socket descriptor");
1535 log_msg(LOG_WARN, file, line, func, "Skipping remote log message: invalid socket descriptor");
1536 } else {
1537 send_result = packet_send_remote_log(sockfd, (const crypto_context_t *)crypto_ctx, level, direction, 0, formatted);
1538 if (send_result != ASCIICHAT_OK) {
1539 log_msg(LOG_WARN, file, line, func, "Failed to send remote log message: %s", asciichat_error_string(send_result));
1540 }
1541 }
1542
1543 const char *direction_label = log_network_direction_label(direction);
1544 log_msg(level, file, line, func, "[NET %s] %s", direction_label, formatted);
1545
1546 SAFE_FREE(formatted);
1547 return send_result;
1548}
1549
1551 remote_log_direction_t direction, const char *fmt, ...) {
1552 va_list args;
1553 va_start(args, fmt);
1554 asciichat_error_t result =
1555 log_network_message_internal(sockfd, crypto_ctx, level, direction, NULL, 0, NULL, fmt, args);
1556 va_end(args);
1557 return result;
1558}
1559
1561 remote_log_direction_t direction, const char *file, int line, const char *func,
1562 const char *fmt, ...) {
1563 va_list args;
1564 va_start(args, fmt);
1565 asciichat_error_t result =
1566 log_network_message_internal(sockfd, crypto_ctx, level, direction, file, line, func, fmt, args);
1567 va_end(args);
1568 return result;
1569}
1570
1571/* ============================================================================
1572 * Color Helper Functions
1573 * ============================================================================ */
1574
1575/* Initialize terminal capabilities if not already done */
1576static void init_terminal_capabilities(void) {
1577 // Guard against recursion - this can be called indirectly via log_get_color_array()
1578 // from detect_terminal_capabilities() which uses logging
1579 if (g_log.terminal_caps_detecting) {
1580 return;
1581 }
1582
1583 // Terminal capabilities should already be initialized via log_system_init()
1584 // This function is now just a safety guard for any edge cases before init
1585 if (!g_log.terminal_caps_initialized) {
1586 // Set detecting flag to prevent recursion
1587 g_log.terminal_caps_detecting = true;
1588
1589 // Use safe fallback during very early initialization to avoid recursion
1590 g_log.terminal_caps.color_level = TERM_COLOR_16;
1591 g_log.terminal_caps.capabilities = TERM_CAP_COLOR_16;
1592 g_log.terminal_caps.color_count = 16;
1593 g_log.terminal_caps.detection_reliable = false;
1594 g_log.terminal_caps_initialized = true;
1595
1596 // Clear detecting flag
1597 g_log.terminal_caps_detecting = false;
1598 }
1599}
1600
1601/* Re-detect terminal capabilities after logging is initialized */
1603
1604 // Guard against recursion
1605 if (g_log.terminal_caps_detecting) {
1606 return;
1607 }
1608
1609 // Detect if not initialized, or if we're using defaults (not reliably detected)
1610 // This ensures we get proper detection after logging is ready, replacing any defaults
1611 // Once we have reliable detection, never re-detect to keep colors consistent
1612 if (!g_log.terminal_caps_initialized || !g_log.terminal_caps.detection_reliable) {
1613 g_log.terminal_caps_detecting = true;
1614 g_log.terminal_caps = detect_terminal_capabilities();
1615 g_log.terminal_caps_detecting = false;
1616 g_log.terminal_caps_initialized = true;
1617
1618 // Now log the capabilities AFTER colors are set, so this log uses the correct colors
1619 log_debug("Terminal capabilities: color_level=%d, capabilities=0x%x, utf8=%s, fps=%d",
1620 g_log.terminal_caps.color_level, g_log.terminal_caps.capabilities,
1621 g_log.terminal_caps.utf8_support ? "yes" : "no", g_log.terminal_caps.desired_fps);
1622
1623 // Now that we've detected once with reliable results, keep these colors consistent for all future logs
1624 } else {
1625 }
1626 // Once initialized with reliable detection, never re-detect to keep colors consistent
1627}
1628
1629/* Get the appropriate color array based on terminal capabilities */
1630const char **log_get_color_array(void) {
1631 init_terminal_capabilities();
1632
1633 /* Colors should be initialized via log_system_init(); this is just a safety check */
1634 if (!g_log.log_colorscheme_initialized) {
1635 /* Fallback: initialize colors if not already done (should not happen after log_system_init) */
1637 }
1638
1639 /* Safety check: if colors are still not initialized, return NULL to prevent crashes from null pointers */
1640 if (!g_log.log_colorscheme_initialized) {
1641 return NULL;
1642 }
1643
1644 /* Return the compiled color scheme based on terminal capabilities
1645 * codes_16, codes_256, codes_truecolor are now proper arrays of pointers (const char *[8]),
1646 * so we can safely cast them to const char **. */
1647 if (g_log.terminal_caps.color_level >= TERM_COLOR_TRUECOLOR) {
1648 return (const char **)g_log.compiled_colors.codes_truecolor;
1649 } else if (g_log.terminal_caps.color_level >= TERM_COLOR_256) {
1650 return (const char **)g_log.compiled_colors.codes_256;
1651 } else {
1652 return (const char **)g_log.compiled_colors.codes_16;
1653 }
1654}
1655
1656const char *log_level_color(log_color_t color) {
1657 const char **colors = log_get_color_array();
1658 if (colors == NULL) {
1659 return ""; /* Return empty string if colors not available */
1660 }
1661 if (color >= 0 && color <= LOG_COLOR_RESET) {
1662 return colors[color];
1663 }
1664 return colors[LOG_COLOR_RESET]; /* Return reset color if invalid */
1665}
1666
1667/* ============================================================================
1668 * Color Scheme Management
1669 * ============================================================================ */
1670
1672
1673 /* Skip color initialization during terminal detection to avoid mutex deadlock */
1674 if (g_log.terminal_caps_detecting) {
1675 return;
1676 }
1677
1678 /* Skip color initialization before logging is fully initialized */
1679 if (!lifecycle_is_initialized(&g_log.lifecycle)) {
1680 return;
1681 }
1682
1683 if (g_log.log_colorscheme_initialized) {
1684 return;
1685 }
1686
1687 /* Get active color scheme - this ensures color system is initialized */
1689 if (!scheme) {
1690 /* Don't mark as initialized if we can't get a color scheme - return NULL instead */
1691 return;
1692 }
1693
1694 /* Acquire mutex for compilation (mutex is now initialized by colorscheme_init) */
1696
1697 /* Debug: Check if g_log.compiled_colors is actually zero-initialized */
1698 /* Zero the structure on first use to avoid freeing garbage pointers */
1699 /* (static = {0} produces garbage in this build for unknown reasons) */
1700 static bool first_compile = true;
1701 if (first_compile) {
1702 memset(&g_log.compiled_colors, 0, sizeof(g_log.compiled_colors));
1703 first_compile = false;
1704 }
1705
1706 /* Detect terminal background */
1708 /* Determine color mode for compilation */
1710 if (g_log.terminal_caps.color_level >= TERM_COLOR_TRUECOLOR) {
1711 mode = TERM_COLOR_TRUECOLOR;
1712 } else if (g_log.terminal_caps.color_level >= TERM_COLOR_256) {
1713 mode = TERM_COLOR_256;
1714 } else {
1715 mode = TERM_COLOR_16;
1716 }
1717 /* Compile the color scheme to ANSI codes */
1718 asciichat_error_t result = colorscheme_compile_scheme(scheme, mode, background, &g_log.compiled_colors);
1719 g_log.log_colorscheme_initialized = true;
1721
1722 /* Log outside of mutex lock to avoid recursive lock deadlock */
1723 if (result != ASCIICHAT_OK) {
1724 log_debug("Failed to compile color scheme: %d", result);
1725 }
1726}
1727
1729 if (!scheme) {
1730 return;
1731 }
1732
1733 /* Mutex is managed by colors.c - just use it */
1735
1736 /* Detect terminal background */
1738
1739 /* Determine color mode for compilation */
1741 if (g_log.terminal_caps.color_level >= TERM_COLOR_TRUECOLOR) {
1742 mode = TERM_COLOR_TRUECOLOR;
1743 } else if (g_log.terminal_caps.color_level >= TERM_COLOR_256) {
1744 mode = TERM_COLOR_256;
1745 } else {
1746 mode = TERM_COLOR_16;
1747 }
1748
1749 /* Compile the new color scheme */
1750 asciichat_error_t result = colorscheme_compile_scheme(scheme, mode, background, &g_log.compiled_colors);
1751
1752 g_log.log_colorscheme_initialized = true;
1754
1755 /* Log outside of mutex lock to avoid recursive lock deadlock */
1756 if (result != ASCIICHAT_OK) {
1757 log_debug("Failed to compile color scheme: %d", result);
1758 }
1759}
1760
1761/* ============================================================================
1762 * Lock-Free MMAP Logging Integration
1763 * ============================================================================ */
1764
1765asciichat_error_t log_enable_mmap(const char *log_path) {
1766 return log_enable_mmap_sized(log_path, 0); /* Use default size */
1767}
1768
1769asciichat_error_t log_enable_mmap_sized(const char *log_path, size_t max_size) {
1770 if (!log_path) {
1771 return SET_ERRNO(ERROR_INVALID_PARAM, "log_path is required");
1772 }
1773
1774 // Initialize mmap logging - text is written directly to the mmap'd file
1775 asciichat_error_t result = log_mmap_init_simple(log_path, max_size);
1776 if (result != ASCIICHAT_OK) {
1777 return result;
1778 }
1779
1780 log_info("Lock-free mmap logging enabled: %s", log_path);
1781 return ASCIICHAT_OK;
1782}
1783
1785 if (log_mmap_is_active()) {
1787 log_info("Lock-free mmap logging disabled");
1788 }
1789}
1790
1791/* ============================================================================
1792 * Shutdown Logging Control
1793 * ============================================================================ */
1794
1796 if (g_log.shutdown_in_progress) {
1797 return; /* Already in shutdown phase */
1798 }
1799
1800 /* Save current terminal output state and disable console output */
1801 g_log.shutdown_saved_terminal_output = atomic_load_u64(&g_log.terminal_output_enabled);
1802 atomic_store_bool(&g_log.terminal_output_enabled, false);
1803 g_log.shutdown_in_progress = true;
1804}
1805
1807 if (!g_log.shutdown_in_progress) {
1808 return; /* Not in shutdown phase, skip terminal state restoration */
1809 }
1810
1811 /* Restore previous terminal output state */
1812 atomic_store_u64(&g_log.terminal_output_enabled, g_log.shutdown_saved_terminal_output);
1813 g_log.shutdown_in_progress = false;
1814}
1815
1823 colorscheme_cleanup_compiled(&g_log.compiled_colors);
1824}
1825
1826/* ============================================================================
1827 * Session Log Buffer Registration
1828 * ============================================================================ */
1829
1839 atomic_ptr_store(&g_log.session_log_buffer, (void *)buf);
1840}
1841
1848 atomic_ptr_store(&g_log.session_log_buffer, NULL);
1849}
1850
1858 return (session_log_buffer_t *)atomic_ptr_load(&g_log.session_log_buffer);
1859}
1860
1861/* ============================================================================
1862 * Log Recoloring for Plain Text Entries
1863 * ============================================================================ */
1864
1871size_t log_recolor_plain_entry(const char *plain_line, char *colored_buf, size_t buf_size) {
1872 if (!plain_line || !colored_buf || buf_size < 128) {
1873 return 0;
1874 }
1875
1876 static char work_buffer[LOG_MSG_BUFFER_SIZE + 1024];
1877
1878 // Parse format FIRST, regardless of color availability
1879 // Parse format: [TIMESTAMP] [LEVEL] [tid:THREAD_ID] FILE:LINE in FUNC(): MESSAGE
1880 const char *p = plain_line;
1881
1882 // Extract timestamp [TIMESTAMP]
1883 if (*p != '[') {
1884 return 0; // Invalid format
1885 }
1886 p++;
1887 const char *timestamp_start = p;
1888
1889 // Find the closing ] for timestamp - look for pattern that ends with proper timestamp format
1890 // Valid timestamp: HH:MM:SS.UUUUUU (time with microseconds, no date)
1891 while (*p && *p != ']') {
1892 p++;
1893 }
1894 if (*p != ']') {
1895 return 0; // Malformed - no closing bracket
1896 }
1897
1898 size_t timestamp_len = p - timestamp_start;
1899 char timestamp[64];
1900 if (timestamp_len >= sizeof(timestamp) || timestamp_len == 0) {
1901 return 0; // Invalid timestamp - empty or too long
1902 }
1903 SAFE_STRNCPY(timestamp, timestamp_start, timestamp_len);
1904 timestamp[timestamp_len] = '\0';
1905 p++; // Skip ]
1906
1907 // Skip whitespace after timestamp
1908 while (*p && *p == ' ') {
1909 p++;
1910 }
1911
1912 // Extract level [LEVEL]
1913 if (*p != '[') {
1914 return 0; // Missing opening bracket for level
1915 }
1916 p++;
1917 const char *level_start = p;
1918 while (*p && *p != ']') {
1919 p++;
1920 }
1921 if (*p != ']') {
1922 return 0; // Missing closing bracket for level
1923 }
1924 size_t level_len = p - level_start;
1925 char level_str[16];
1926 if (level_len >= sizeof(level_str) || level_len == 0) {
1927 return 0; // Level string too long or empty
1928 }
1929 memcpy(level_str, level_start, level_len);
1930 level_str[level_len] = '\0';
1931
1932 // Determine log level for color selection
1933 log_level_t level = LOG_INFO; // Default
1934 if (strstr(level_str, "DEV") || strstr(level_str, "DEBUG")) {
1935 level = LOG_DEBUG;
1936 } else if (strstr(level_str, "INFO")) {
1937 level = LOG_INFO;
1938 } else if (strstr(level_str, "WARN")) {
1939 level = LOG_WARN;
1940 } else if (strstr(level_str, "ERROR")) {
1941 level = LOG_ERROR;
1942 } else if (strstr(level_str, "FATAL")) {
1943 level = LOG_FATAL;
1944 }
1945
1946 p++; // Skip ]
1947
1948 // Skip whitespace after level
1949 while (*p && *p == ' ') {
1950 p++;
1951 }
1952
1953 // Extract thread ID [tid:THREAD_ID] - optional field
1954 uint64_t tid = 0;
1955 if (*p == '[' && strncmp(p, "[tid:", 5) == 0) {
1956 p += 5;
1957 char *tid_end = NULL;
1958 tid = strtoull(p, &tid_end, 10);
1959 if (!tid_end || *tid_end != ']') {
1960 // tid parsing failed, but continue anyway (might still recover)
1961 // Try to find the closing bracket
1962 while (*p && *p != ']') {
1963 p++;
1964 }
1965 if (*p == ']') {
1966 p++;
1967 }
1968 } else {
1969 p = tid_end + 1; // Skip past the ]
1970 }
1971 // Skip whitespace after tid
1972 while (*p && *p == ' ') {
1973 p++;
1974 }
1975 }
1976 // tid is optional - continue parsing if not present
1977
1978 // Extract file path (everything up to :LINE)
1979 const char *file_start = p;
1980 while (*p && *p != ':') {
1981 p++;
1982 }
1983 if (*p != ':') {
1984 return 0; // Malformed
1985 }
1986 size_t file_len = p - file_start;
1987 char file_path[256];
1988 if (file_len >= sizeof(file_path)) {
1989 return 0;
1990 }
1991 memcpy(file_path, file_start, file_len);
1992 file_path[file_len] = '\0';
1993 p++; // Skip :
1994
1995 // Extract line number (digits only)
1996 int line_num = 0;
1997 const char *line_start = p;
1998 while (*p && isdigit((unsigned char)*p)) {
1999 p++;
2000 }
2001 if (p == line_start) {
2002 return 0; // No line number
2003 }
2004 line_num = (int)strtol(line_start, NULL, 10);
2005
2006 // Skip whitespace and find "in" keyword (be very lenient)
2007 while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
2008 p++;
2009 }
2010
2011 // Try to find "in " - if not found, might still be valid, just harder to parse
2012 if (strncmp(p, "in ", 3) == 0) {
2013 p += 3; // Skip "in "
2014 } else if (*p == 'i' && *(p + 1) == 'n' && (*(p + 2) == ' ' || *(p + 2) == '\t')) {
2015 // Allow tab after "in"
2016 p += 2; // Skip "in"
2017 while (*p && (*p == ' ' || *p == '\t')) {
2018 p++;
2019 }
2020 } else {
2021 // Missing "in" keyword - this is a format error
2022 return 0;
2023 }
2024
2025 // Extract function name (everything up to "()")
2026 const char *func_start = p;
2027 while (*p && *p != '(') {
2028 p++;
2029 }
2030 if (*p != '(' || func_start == p) {
2031 return 0; // Missing function name or parentheses
2032 }
2033 size_t func_len = p - func_start;
2034
2035 // Trim trailing whitespace from function name
2036 while (func_len > 0 && (func_start[func_len - 1] == ' ' || func_start[func_len - 1] == '\t')) {
2037 func_len--;
2038 }
2039
2040 char func_name[256];
2041 if (func_len >= sizeof(func_name)) {
2042 func_len = sizeof(func_name) - 1;
2043 }
2044 if (func_len > 0) {
2045 memcpy(func_name, func_start, func_len);
2046 }
2047 func_name[func_len] = '\0';
2048
2049 // Skip "(" and ")" - be lenient about what's between them
2050 if (*p == '(') {
2051 p++;
2052 while (*p && *p != ')') {
2053 p++;
2054 }
2055 if (*p == ')') {
2056 p++;
2057 }
2058 }
2059
2060 // Skip whitespace and optional colon(s) and other separators
2061 while (*p && (*p == ' ' || *p == ':' || *p == '\t')) {
2062 p++;
2063 }
2064
2065 // Remaining is the message
2066 const char *message = p;
2067
2068 // Format is valid, get colors from logging system
2069 const char **colors = log_get_color_array();
2070 if (!colors) {
2071 // No colors available, return plain text
2072 static bool warned_once = false;
2073 if (!warned_once) {
2074 log_debug("WARNING: log_recolor_plain_entry() called but colors not initialized - returning plain text");
2075 warned_once = true;
2076 }
2077 size_t len = strlen(plain_line);
2078 if (len >= buf_size) {
2079 return 0;
2080 }
2081 SAFE_STRNCPY(colored_buf, plain_line, buf_size - 1);
2082 colored_buf[buf_size - 1] = '\0';
2083 return len;
2084 }
2085
2086 // Build colored output
2087 const char *level_color = colors[level];
2088 const char *reset = colors[LOG_COLOR_RESET];
2089 const char *file_color = colors[1]; // DEBUG/Cyan
2090 const char *line_color = colors[6]; // GREY (matching tid)
2091 const char *func_color = colors[0]; // DEV/Orange
2092 const char *tid_color = colors[6]; // GREY
2093
2094 int len = safe_snprintf(work_buffer, sizeof(work_buffer),
2095 "[%s%s%s] [%s%s%s] [tid:%s%llu%s] %s%s%s:%s%d%s in %s%s%s(): %s", level_color, timestamp,
2096 reset, level_color, level_str, reset, tid_color, (unsigned long long)tid, reset, file_color,
2097 file_path, reset, line_color, line_num, reset, func_color, func_name, reset, message);
2098
2099 if (len <= 0 || len >= (int)sizeof(work_buffer)) {
2100 return 0;
2101 }
2102
2103 // Colorize the message part
2104 const char *colorized_msg = colorize_log_message(message);
2105 len = safe_snprintf(work_buffer, sizeof(work_buffer),
2106 "[%s%s%s] [%s%s%s] [tid:%s%llu%s] %s%s%s:%s%d%s in %s%s%s(): %s", level_color, timestamp, reset,
2107 level_color, level_str, reset, tid_color, (unsigned long long)tid, reset, file_color, file_path,
2108 reset, line_color, line_num, reset, func_color, func_name, reset, colorized_msg);
2109
2110 if (len <= 0 || len >= (int)sizeof(work_buffer) || len >= (int)buf_size) {
2111 return 0;
2112 }
2113
2114 SAFE_STRNCPY(colored_buf, work_buffer, buf_size - 1);
2115 colored_buf[buf_size - 1] = '\0';
2116 return (size_t)len;
2117}
2118
2119// Internal implementation - don't use directly, use log_console_impl macro
2120void log_console_impl(log_level_t level, const char *file, int line, const char *func, const char *message) {
2121 if (!message) {
2122 return;
2123 }
2124
2125 int fd = terminal_choose_log_fd(level);
2126 if (fd < 0) {
2127 return;
2128 }
2129
2130 // Check if JSON output is enabled (json_file >= 0 means enabled)
2131 int json_fd = atomic_load_int(&g_log.json_file);
2132 bool use_json = (json_fd >= 0);
2133
2134 if (use_json) {
2135 // Use async-safe JSON formatter (safe for signal handlers)
2136 log_json_async_safe(fd, level, file, line, func, message);
2137 } else {
2138 // Text output to console using platform_write_all to handle partial writes
2139 size_t msg_len = strlen(message);
2140 platform_write_all(fd, (const uint8_t *)message, msg_len);
2141 if (msg_len == 0 || message[msg_len - 1] != '\n') {
2142 platform_write_all(fd, (const uint8_t *)"\n", 1);
2143 }
2144 }
2145}
2146
2156void *log_get_template(void) {
2157 return (void *)g_log.format;
2158}
Platform abstraction layer umbrella header providing unified cross-platform API.
ANSI escape sequence utilities and fast color code generation.
void platform_log_hook(log_level_t level, const char *message)
Definition apple.c:52
lifecycle_t lifecycle
uint64_t atomic_fetch_add_u64(atomic_t *a, uint64_t delta)
Atomically add to a uint64_t and return the previous value.
Definition atomic.c:248
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
void atomic_ptr_store(atomic_ptr_t *a, void *value)
Atomically store a pointer.
Definition atomic.c:280
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void * atomic_ptr_load(atomic_ptr_t *a)
Atomically load a pointer.
Definition atomic.c:272
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
void atomic_store_int(atomic_t *a, int value)
Atomically store an int value.
Definition atomic.c:202
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
int atomic_load_int(atomic_t *a)
Atomically load an int value.
Definition atomic.c:194
bool atomic_cas_bool(atomic_t *a, bool *expected, bool new_value)
Atomically compare-and-swap a boolean.
Definition atomic.c:184
const char * colorize_log_message(const char *message)
Colorize a log message for terminal output.
Definition colorize.c:509
Log message colorization for terminal output.
const color_scheme_t * colorscheme_get_active_scheme(void)
Get currently active color scheme.
terminal_background_t detect_terminal_background(void)
Detect the user's terminal theme (dark or light background)
asciichat_error_t colorscheme_compile_scheme(const color_scheme_t *scheme, terminal_color_mode_t mode, terminal_background_t background, compiled_color_scheme_t *compiled)
Compile a color scheme to ANSI codes.
void colorscheme_cleanup_compiled(compiled_color_scheme_t *compiled)
Clean up allocated strings in a compiled color scheme.
mutex_t g_colorscheme_mutex
Shared mutex for color scheme compilation.
Definition colorscheme.c:39
terminal_background_t
Terminal theme detection result.
Definition colorscheme.h:50
βš™οΈ Common definitions, error codes, macros, and types shared throughout the application
void grep_destroy(void)
Clean up filter resources.
Definition grep.c:1177
bool grep_should_output(const char *log_line, size_t *match_start, size_t *match_len)
Check if log line should be output (matches filter)
Definition grep.c:597
bool enabled
Is filtering active?
Definition grep.c:84
int buffer_size
Size of circular buffer.
Definition grep.c:90
const char * grep_highlight_colored(const char *colored_text, const char *plain_text, size_t match_start, size_t match_len)
Highlight matches in colored text while preserving original colors.
Definition grep.c:755
bool g_color_flag_value
Value of –color flag (true if –color was in argv) Set by options_init() before RCU is initialized,...
Definition common.c:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
bool g_color_flag_passed
Was –color explicitly passed in command-line arguments? Set by options_init() before RCU is initializ...
Definition common.c:57
#define SAFE_GETENV(name)
Definition common.h:434
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
#define GET_ERRNO()
Get current error code (0 if no error)
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
void log_truncate_if_large(void)
Manually truncate large log files.
Definition log/log.c:807
void log_plain_stderr_nonewline_msg(const char *fmt,...)
Plain logging to stderr without trailing newline.
Definition log/log.c:1375
void log_plain_msg(const char *fmt,...)
Plain logging without timestamps or levels.
Definition log/log.c:1215
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
bool log_get_terminal_output(void)
Get current terminal output setting.
Definition log/log.c:717
asciichat_error_t log_enable_mmap(const char *log_path)
Enable lock-free mmap-based logging.
Definition log/log.c:1765
void log_init_colors(void)
Initialize logging color system with current terminal capabilities.
Definition log/log.c:1671
void log_system_init(void)
Initialize the logging system internal state (system-level init)
Definition log/log.c:669
void log_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt,...)
Log a message at a specific level.
Definition log/log.c:1019
void log_shutdown_end(void)
End shutdown phase - restore previous logging settings.
Definition log/log.c:1806
void log_destroy(void)
Destroy the logging system and close log file.
Definition log/log.c:624
void log_set_force_stderr(bool enabled)
Force all terminal log output to stderr.
Definition log/log.c:721
enum remote_log_direction remote_log_direction_t
Remote log packet direction enumeration.
log_level_t log_get_level(void)
Get the current minimum log level.
Definition log/log.c:696
const char ** log_get_color_array(void)
Get the appropriate color array based on terminal capabilities.
Definition log/log.c:1630
const char * log_level_color(log_color_t color)
Get color string for a given color enum.
Definition log/log.c:1656
int log_template_apply(const log_template_t *format, char *buf, size_t buf_size, log_level_t level, const char *timestamp, const char *file, int line, const char *func, uint64_t tid, const char *message, bool use_colors, uint64_t time_nanoseconds)
Apply format to a log entry and write result to buffer.
Definition log/format.c:491
void log_set_flush_delay(unsigned int delay_ms)
Set the delay between flushing buffered log entries.
Definition log/log.c:803
session_log_buffer_t * log_get_session_log_buffer(void)
Get the currently registered session log buffer.
Definition log/log.c:1857
#define LOG_MSG_BUFFER_SIZE
Maximum size of a single log message (including formatting)
Definition log/log.h:88
char * format_message(const char *format, va_list args)
Format a message using va_list.
Definition log/log.c:248
log_color_t
Color enum for logging - indexes into color arrays.
Definition log/log.h:130
void log_cleanup_colors(void)
Clean up compiled color scheme.
Definition log/log.c:1822
void log_shutdown_begin(void)
Begin shutdown phase - disable console logging but keep file logging.
Definition log/log.c:1795
void log_disable_file_output(void)
Disable file output and use stderr instead.
Definition log/log.c:733
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
bool log_get_force_stderr(void)
Get current force_stderr setting.
Definition log/log.c:725
void log_set_level(log_level_t level)
Set the minimum log level.
Definition log/log.c:691
size_t log_recolor_plain_entry(const char *plain_line, char *colored_buf, size_t buf_size)
Recolor a plain (non-colored) log line with proper ANSI codes.
Definition log/log.c:1871
void log_clear_session_log_buffer(void)
Unregister the session log buffer.
Definition log/log.c:1847
void log_plain_stdout_msg(const char *fmt,...)
Plain logging to stdout with newline.
Definition log/log.c:1453
void log_set_session_log_buffer(session_log_buffer_t *buf)
Register a session log buffer with the logger.
Definition log/log.c:1838
void log_system_destroy(void)
Shutdown the logging system internal state (system-level cleanup)
Definition log/log.c:686
void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap)
Initialize the logging system.
Definition log/log.c:533
asciichat_error_t log_net_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, const char *file, int line, const char *func, const char *fmt,...)
Log a message to all destinations (network, file, and terminal).
Definition log/log.c:1560
bool log_lock_terminal(void)
Lock terminal output for exclusive access by the calling thread.
Definition log/log.c:787
asciichat_error_t log_network_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, const char *fmt,...)
Send a formatted log message over the network.
Definition log/log.c:1550
log_level_t
Logging levels enumeration.
Definition types.h:29
void log_template_free(log_template_t *format)
Free compiled format structure.
Definition log/format.c:325
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
asciichat_error_t log_set_format(const char *format_str, bool console_only)
Set a custom log format string.
Definition log/log.c:744
log_template_t * log_template_parse(const char *format_str, bool console_only)
Parse a format string into compiled format structure.
Definition log/format.c:321
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
size_t get_current_time_formatted(char *time_buf)
Get current time as formatted string.
Definition log/log.c:216
asciichat_error_t log_enable_mmap_sized(const char *log_path, size_t max_size)
Enable lock-free mmap logging with custom file size.
Definition log/log.c:1769
#define MAX_LOG_SIZE
Maximum log file size in bytes (3MB) before rotation.
Definition log/log.h:72
void log_file_msg(const char *fmt,...)
Log to file only, no stderr output.
Definition log/log.c:1467
void log_disable_mmap(void)
Disable mmap logging and return to mutex-based logging.
Definition log/log.c:1784
#define LOG_TIMESTAMP_BUFFER_SIZE
Maximum size of a timestamp string.
Definition log/log.h:97
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
Definition log/log.c:700
void * log_get_template(void)
Get the current log format template.
Definition log/log.c:2156
#define LOG_MMAP_MSG_BUFFER_SIZE
Maximum size of a log message in mmap mode.
Definition log/log.h:91
void log_set_color_scheme(const color_scheme_t *scheme)
Set the color scheme for logging output.
Definition log/log.c:1728
void log_plain_stderr_msg(const char *fmt,...)
Plain logging to stderr with newline.
Definition log/log.c:1361
void log_redetect_terminal_capabilities(void)
Re-detect terminal capabilities after logging is initialized.
Definition log/log.c:1602
void log_unlock_terminal(bool previous_state)
Release terminal lock and flush buffered messages.
Definition log/log.c:796
#define DEFAULT_LOG_LEVEL
Default log level for debug builds (DEBUG and above)
Definition log/log.h:65
void log_terminal_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt,...)
Log a message to terminal only (no file output)
Definition log/log.c:1194
@ REMOTE_LOG_DIRECTION_SERVER_TO_CLIENT
Definition network/log.h:22
@ REMOTE_LOG_DIRECTION_CLIENT_TO_SERVER
Definition network/log.h:23
@ LOG_COLOR_RESET
Definition log/log.h:138
uint64_t time_get_realtime_ns(void)
Get current wall-clock (real) time in nanoseconds.
Definition util/time.c:119
#define NS_PER_SEC_INT
Definition time.h:157
const options_t * options_get(void)
Get current options (lock-free read)
Definition rcu.c:496
#define OPT_LOG_TEMPLATE_DEFAULT
Default log template string (selected based on build mode)
int safe_fprintf(FILE *stream, const char *format,...)
Safe formatted output to file stream.
Definition system.c:172
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
Definition system.c:224
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int safe_vsnprintf(char *buffer, size_t buffer_size, const char *format, va_list ap)
Safe formatted string printing with va_list.
Definition system.c:199
#define INVALID_SOCKET_VALUE
Invalid socket value (POSIX: -1)
Definition socket.h:278
int platform_strcasecmp(const char *s1, const char *s2)
Case-insensitive string comparison.
asciichat_error_t platform_localtime(const time_t *timer, struct tm *result)
Platform-safe localtime wrapper.
Definition util.c:50
int platform_open(const char *name, const char *pathname, int flags,...)
Safe file open (open replacement)
int platform_close(int fd)
Safe file close (close replacement)
ssize_t platform_read(int fd, void *buf, size_t count)
Safe file read (read replacement)
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
ssize_t platform_write(int fd, const void *buf, size_t count)
Safe file write (write replacement)
Definition wasm/system.c:72
#define FILE_PERM_PRIVATE
File permission: Private (owner read/write only)
Definition filesystem.h:187
uint64_t asciichat_thread_current_id(void)
Get the current thread's unique numeric ID.
Definition threading.c:92
bool shutdown_is_requested(void)
Check if shutdown has been requested.
Definition common.c:73
bool utf8_is_valid(const char *str)
Check if a string is valid UTF-8.
Definition utf8.c:156
int socket_t
πŸ“ JSON structured logging output
βš™οΈ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🧡 Cross-platform thread interface for ascii-chat
Thread-safe circular log buffer for session screens (status, splash)
void log_json_write(int fd, log_level_t level, uint64_t time_nanoseconds, const char *file, int line, const char *func, const char *message)
Write a log entry as a JSON object to the json output fd.
Definition json.c:94
void log_json_async_safe(int fd, log_level_t level, const char *file, int line, const char *func, const char *message)
Async-safe JSON logging for signal handlers.
Definition json.c:250
#define asciichat_thread_self()
bool lifecycle_is_initialized(const lifecycle_t *lc)
Definition lifecycle.c:155
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108
Lock-free module lifecycle state machine using pure stdatomic.
@ LIFECYCLE_SYNC_MUTEX
Contains mutex_t pointer.
Definition lifecycle.h:50
#define LIFECYCLE_INIT
Static initializer for module-global lifecycle variables (no sync primitive)
Definition lifecycle.h:68
void log_set_json_output(int fd)
Set the JSON output file descriptor.
Definition log/log.c:729
#define LOGGING_INTERNAL_ERROR(error, message,...)
Definition log/log.c:201
const char * get_level_string_padded(log_level_t level)
Get padded level string for consistent alignment.
Definition log/log.c:149
void log_console_impl(log_level_t level, const char *file, int line, const char *func, const char *message)
Definition log/log.c:2120
πŸ“ Logging API with multiple log levels and terminal output control
Lock-free memory-mapped text logging with crash safety.
char file_path[PLATFORM_MAX_PATH_LENGTH]
Definition mmap.c:39
void log_mmap_rotate(void)
Rotate the mmap log (tail-keeping rotation)
Definition mmap.c:423
bool log_mmap_is_active(void)
Check if mmap logging is active.
Definition mmap.c:390
void log_mmap_write(int level, const char *file, int line, const char *func, const char *fmt,...)
Write a log entry directly to the mmap'd file (lock-free)
Definition mmap.c:312
bool log_mmap_get_usage(size_t *used, size_t *capacity)
Get current mmap log usage.
Definition mmap.c:409
void log_mmap_destroy(void)
Shutdown mmap logging.
Definition mmap.c:260
asciichat_error_t log_mmap_init_simple(const char *log_path, size_t max_size)
Initialize mmap logging with simple parameters.
Definition mmap.c:252
action_args_t args
asciichat_error_t packet_send_remote_log(socket_t sockfd, const crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, uint16_t flags, const char *message)
Send a remote log packet with optional encryption context.
Definition packet.c:999
πŸ“‚ Path Manipulation Utilities
Cross-platform backtrace and stack trace capture.
Cross-platform mutex interface for ascii-chat.
int terminal_choose_log_fd(log_level_t level)
Choose output file descriptor for logging based on level and interactivity.
bool terminal_is_piped_output(void)
Check if stdout is piped or redirected.
bool terminal_should_color_output(int fd)
Determine if color output should be used.
void session_log_buffer_append(session_log_buffer_t *buf, const char *message)
Append a log message to the buffer.
terminal_capabilities_t detect_terminal_capabilities(void)
Detect terminal capabilities.
Network quality metrics for a single participant.
Definition metrics.h:33
Atomic pointer wrapper.
Definition atomic.h:106
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Color scheme definition.
Definition colorscheme.h:62
Compiled ANSI escape codes for a color scheme.
Definition colorscheme.h:85
Cryptographic context structure.
Compiled log format ready for use in log_template_apply()
Definition log/format.h:66
Mutex type (POSIX: pthread_mutex_t with debug tracking)
Consolidated options structure.
bool quiet
Quiet mode (suppress logs)
Internal circular buffer structure.
Complete terminal capabilities structure.
Definition terminal.h:709
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69
Cross-platform system functions interface for ascii-chat.
πŸ–₯️ Cross-platform terminal interface for ascii-chat
@ TERM_CAP_COLOR_16
16-color support (TERM_CAP_COLOR_16)
Definition terminal.h:641
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:578
@ TERM_COLOR_16
16-color support (standard ANSI colors)
Definition terminal.h:584
@ TERM_COLOR_256
256-color support (extended ANSI palette)
Definition terminal.h:586
@ TERM_COLOR_TRUECOLOR
24-bit truecolor support (RGB colors)
Definition terminal.h:588
⏱️ High-precision timing utilities using sokol_time.h and uthash
#define LOG_DEV
Definition types.h:38
#define LOG_DEBUG
Definition types.h:39
#define LOG_FATAL
Definition types.h:43
#define LOG_ERROR
Definition types.h:42
#define LOG_WARN
Definition types.h:41
#define LOG_INFO
Definition types.h:40
πŸ”€ UTF-8 Encoding and Decoding Utilities
πŸ”€ String Manipulation and Shell Escaping Utilities