ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
mmap.c
Go to the documentation of this file.
1
10#include <ascii-chat/log/mmap.h>
11#include <ascii-chat/log/logging.h>
12#include <ascii-chat/platform/mmap.h>
13#include <ascii-chat/platform/system.h>
14#include <ascii-chat/video/ansi.h>
15#include <ascii-chat/util/time.h>
16
17#include <stdarg.h>
18#include <stdio.h>
19#include <string.h>
20#include <time.h>
21#include <signal.h>
22
23#ifdef _WIN32
24#include <windows.h>
25#else
26#include <unistd.h>
27#endif
28
29/* ============================================================================
30 * Global State
31 * ============================================================================ */
32
33static struct {
34 platform_mmap_t mmap; /* Memory-mapped file handle */
35 char *text_region; /* Pointer to text area (entire file is text) */
36 size_t text_capacity; /* Total file size */
37 _Atomic uint64_t write_pos; /* Current write position (in memory only) */
38 bool initialized; /* Initialization flag */
39 char file_path[PLATFORM_MAX_PATH_LENGTH]; /* Path to log file for truncation */
40
41 /* Statistics */
42 _Atomic uint64_t bytes_written;
43 _Atomic uint64_t wrap_count;
44} g_mmap_log = {
45 .initialized = false,
46 .file_path = {0},
47};
48
49/* ============================================================================
50 * Time Utilities
51 * ============================================================================ */
52
53static void format_timestamp(char *buf, size_t buf_size) {
54 uint64_t current_time_ns = time_get_realtime_ns();
55 time_t seconds = (time_t)(current_time_ns / NS_PER_SEC_INT);
56 uint64_t microseconds = time_ns_to_us(current_time_ns % NS_PER_SEC_INT);
57
58 struct tm tm_info;
59 platform_localtime(&seconds, &tm_info);
60
61 size_t len = strftime(buf, buf_size, "%H:%M:%S", &tm_info);
62 if (len > 0 && len < buf_size - 10) {
63 safe_snprintf(buf + len, buf_size - len, ".%06llu", (unsigned long long)microseconds);
64 }
65}
66
67/* ============================================================================
68 * Signal Handlers for Crash Safety
69 * ============================================================================ */
70
71static volatile sig_atomic_t g_crash_in_progress = 0;
72
73#ifndef _WIN32
74static void crash_signal_handler(int sig) {
75 /* Prevent recursive crashes */
76 if (g_crash_in_progress) {
77 _exit(128 + sig);
78 }
79 g_crash_in_progress = 1;
80
81 /* Write crash marker directly to mmap'd log */
82 if (g_mmap_log.initialized && g_mmap_log.text_region) {
83 const char *crash_msg = "\n=== CRASH DETECTED (signal %d) ===\n";
84 char msg_buf[64];
85 int len = safe_snprintf(msg_buf, sizeof(msg_buf), crash_msg, sig);
86 if (len > 0) {
87 uint64_t pos = atomic_fetch_add(&g_mmap_log.write_pos, (uint64_t)len);
88 if (pos + (uint64_t)len <= g_mmap_log.text_capacity) {
89 memcpy(g_mmap_log.text_region + pos, msg_buf, (size_t)len);
90 }
91 }
92 }
93
94 /* Sync mmap to disk */
95 if (g_mmap_log.initialized) {
96 platform_mmap_sync(&g_mmap_log.mmap, true); /* sync = true for immediate flush */
97 }
98
99 /* Re-raise with default handler for core dump */
100 signal(sig, SIG_DFL);
101 raise(sig);
102}
103#endif /* !_WIN32 */
104
105#ifdef _WIN32
111static LONG WINAPI windows_crash_handler(EXCEPTION_POINTERS *exception_info) {
112 (void)exception_info; /* Unused for now, could log exception code */
113
114 /* Prevent recursive crashes */
115 if (g_crash_in_progress) {
116 return EXCEPTION_CONTINUE_SEARCH;
117 }
118 g_crash_in_progress = 1;
119
120 /* Write crash marker directly to mmap'd log */
121 if (g_mmap_log.initialized && g_mmap_log.text_region) {
122 DWORD exception_code = exception_info ? exception_info->ExceptionRecord->ExceptionCode : 0;
123 const char *crash_msg = "\n=== CRASH DETECTED (exception 0x%08lX) ===\n";
124 char msg_buf[64];
125 int len = safe_snprintf(msg_buf, sizeof(msg_buf), crash_msg, (unsigned long)exception_code);
126 if (len > 0) {
127 uint64_t pos = atomic_fetch_add(&g_mmap_log.write_pos, (uint64_t)len);
128 if (pos + (uint64_t)len <= g_mmap_log.text_capacity) {
129 memcpy(g_mmap_log.text_region + pos, msg_buf, (size_t)len);
130 }
131 }
132 }
133
134 /* Sync mmap to disk */
135 if (g_mmap_log.initialized) {
136 platform_mmap_sync(&g_mmap_log.mmap, true);
137 }
138
139 /* Let Windows continue with default crash handling (creates minidump if configured) */
140 return EXCEPTION_CONTINUE_SEARCH;
141}
142#endif /* _WIN32 */
143
145#ifndef _WIN32
146 struct sigaction sa = {0};
147 sa.sa_handler = crash_signal_handler;
148 sigemptyset(&sa.sa_mask);
149 sa.sa_flags = (int)SA_RESETHAND; /* One-shot */
150
151 sigaction(SIGSEGV, &sa, NULL);
152 sigaction(SIGABRT, &sa, NULL);
153 sigaction(SIGBUS, &sa, NULL);
154 sigaction(SIGFPE, &sa, NULL);
155 sigaction(SIGILL, &sa, NULL);
156#else
157 SetUnhandledExceptionFilter(windows_crash_handler);
158#endif
159}
160
161/* ============================================================================
162 * Public API
163 * ============================================================================ */
164
171static size_t find_content_end(const char *text, size_t capacity) {
172 /* Scan backwards to find last non-space/non-null byte */
173 size_t pos = capacity;
174 while (pos > 0 && (text[pos - 1] == ' ' || text[pos - 1] == '\0' || text[pos - 1] == '\n')) {
175 pos--;
176 }
177
178 /* Now find the newline after the last content */
179 while (pos < capacity && text[pos] != '\n' && text[pos] != ' ' && text[pos] != '\0') {
180 pos++;
181 }
182
183 /* Include the newline if present */
184 if (pos < capacity && text[pos] == '\n') {
185 pos++;
186 }
187
188 return pos;
189}
190
191asciichat_error_t log_mmap_init(const log_mmap_config_t *config) {
192 if (!config || !config->log_path) {
193 return SET_ERRNO(ERROR_INVALID_PARAM, "mmap log: config or log_path is NULL");
194 }
195
196 if (g_mmap_log.initialized) {
197 log_warn("mmap log: already initialized, destroying first");
199 }
200
201 /* Determine file size */
202 size_t file_size = config->max_size > 0 ? config->max_size : LOG_MMAP_DEFAULT_SIZE;
203 if (file_size < 1024) {
204 file_size = 1024; /* Minimum reasonable size */
205 }
206
207 /* Store file path for later truncation */
208 SAFE_STRNCPY(g_mmap_log.file_path, config->log_path, sizeof(g_mmap_log.file_path) - 1);
209
210 /* Open mmap file */
211 platform_mmap_init(&g_mmap_log.mmap);
212 asciichat_error_t result = platform_mmap_open(config->log_path, file_size, &g_mmap_log.mmap);
213 if (result != ASCIICHAT_OK) {
214 return result;
215 }
216
217 /* Entire file is text - no header */
218 g_mmap_log.text_region = (char *)g_mmap_log.mmap.addr;
219 g_mmap_log.text_capacity = file_size;
220
221 /* Find where existing content ends (scan for last newline before spaces/nulls) */
222 size_t existing_pos = find_content_end(g_mmap_log.text_region, file_size);
223 atomic_store(&g_mmap_log.write_pos, existing_pos);
224
225 /* Clear unused portion with newlines (grep-friendly without needing -a flag)
226 * We truncate the file on clean shutdown to save space */
227 if (existing_pos < file_size) {
228 memset(g_mmap_log.text_region + existing_pos, '\n', file_size - existing_pos);
229 }
230
231 if (existing_pos > 0) {
232 log_info("mmap log: resumed existing log at position %zu", existing_pos);
233 } else {
234 log_info("mmap log: created new log file %s (%zu bytes)", config->log_path, file_size);
235 }
236
237 /* Reset statistics */
238 atomic_store(&g_mmap_log.bytes_written, 0);
239 atomic_store(&g_mmap_log.wrap_count, 0);
240
241 /* Install crash handlers */
243
244 g_mmap_log.initialized = true;
245
246 /* Write startup marker */
247 log_mmap_write(1 /* LOG_INFO */, NULL, 0, NULL, "=== Log started (mmap text mode, %zu bytes) ===", file_size);
248
249 return ASCIICHAT_OK;
250}
251
252asciichat_error_t log_mmap_init_simple(const char *log_path, size_t max_size) {
253 log_mmap_config_t config = {
254 .log_path = log_path,
255 .max_size = max_size,
256 };
257 return log_mmap_init(&config);
258}
259
261 if (!g_mmap_log.initialized) {
262 return;
263 }
264
265 /* Write shutdown marker */
266 log_mmap_write(1 /* LOG_INFO */, NULL, 0, NULL, "=== Log ended ===");
267
268 /* Sync to disk */
269 platform_mmap_sync(&g_mmap_log.mmap, true);
270
271 /* Truncate file to actual content size to save space
272 * This converts the large mmap file (4MB with newlines) to just the actual log content */
273 uint64_t final_pos = atomic_load(&g_mmap_log.write_pos);
274 if (final_pos < g_mmap_log.text_capacity && strlen(g_mmap_log.file_path) > 0) {
275#ifdef _WIN32
276 /* Windows: Close mmap first, then truncate file, then reopen for truncation */
277 platform_mmap_close(&g_mmap_log.mmap);
278 HANDLE hFile =
279 CreateFileA(g_mmap_log.file_path, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
280 if (hFile != INVALID_HANDLE_VALUE) {
281 LARGE_INTEGER size;
282 size.QuadPart = (LONGLONG)final_pos;
283 if (SetFilePointerEx(hFile, size, NULL, FILE_BEGIN)) {
284 SetEndOfFile(hFile);
285 }
286 CloseHandle(hFile);
287 log_debug("mmap log: truncated %s to %zu bytes (was %zu MB)", g_mmap_log.file_path, (size_t)final_pos,
288 g_mmap_log.text_capacity / 1024 / 1024);
289 }
290#else
291 /* POSIX: Use ftruncate() on the file descriptor */
292 if (g_mmap_log.mmap.fd >= 0) {
293 if (ftruncate(g_mmap_log.mmap.fd, (off_t)final_pos) == 0) {
294 log_debug("mmap log: truncated %s to %zu bytes (was %zu MB)", g_mmap_log.file_path, (size_t)final_pos,
295 g_mmap_log.text_capacity / 1024 / 1024);
296 }
297 }
298 platform_mmap_close(&g_mmap_log.mmap);
299#endif
300 } else {
301 /* No truncation needed or path not set */
302 platform_mmap_close(&g_mmap_log.mmap);
303 }
304
305 g_mmap_log.text_region = NULL;
306 g_mmap_log.file_path[0] = '\0';
307
308 g_mmap_log.initialized = false;
309 log_debug("mmap log: destroyed");
310}
311
312void log_mmap_write(int level, const char *file, int line, const char *func, const char *fmt, ...) {
313 if (!g_mmap_log.initialized || !g_mmap_log.text_region) {
314 return;
315 }
316
317 static const char *level_names[] = {"DEV", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"};
318 const char *level_name = (level >= 0 && level < 6) ? level_names[level] : "???";
319
320 /* Format the complete log line into a local buffer first */
321 char line_buf[LOG_MMAP_MSG_BUFFER_SIZE];
322 char time_buf[LOG_TIMESTAMP_BUFFER_SIZE];
323 format_timestamp(time_buf, sizeof(time_buf));
324
325 int prefix_len;
326 if (file && func) {
327 prefix_len =
328 safe_snprintf(line_buf, sizeof(line_buf), "[%s] [%s] %s:%d in %s(): ", time_buf, level_name, file, line, func);
329 } else {
330 prefix_len = safe_snprintf(line_buf, sizeof(line_buf), "[%s] [%s] ", time_buf, level_name);
331 }
332
333 if (prefix_len < 0) {
334 return;
335 }
336
337 /* Format the message */
338 va_list args;
339 va_start(args, fmt);
340 int msg_len = safe_vsnprintf(line_buf + prefix_len, sizeof(line_buf) - (size_t)prefix_len - 1, fmt, args);
341 va_end(args);
342
343 if (msg_len < 0) {
344 return;
345 }
346
347 /* Add newline */
348 size_t total_len = (size_t)prefix_len + (size_t)msg_len;
349 if (total_len >= sizeof(line_buf) - 1) {
350 total_len = sizeof(line_buf) - 2; /* Truncate if too long */
351 }
352 line_buf[total_len] = '\n';
353 total_len++;
354 line_buf[total_len] = '\0';
355
356 /* Strip ANSI escape codes from the log message before writing to file */
357 char *stripped = ansi_strip_escapes(line_buf, total_len);
358 const char *write_buf = stripped ? stripped : line_buf;
359 size_t write_len = stripped ? strlen(stripped) : total_len;
360
361 /* Atomically claim space in the mmap'd region */
362 uint64_t pos = atomic_fetch_add(&g_mmap_log.write_pos, write_len);
363
364 /* Check if we exceeded capacity - drop this message if so */
365 /* Rotation is handled by maybe_rotate_log() called from logging.c */
366 if (pos + write_len > g_mmap_log.text_capacity) {
367 /* Undo our claim - we can't fit */
368 atomic_fetch_sub(&g_mmap_log.write_pos, write_len);
369 if (stripped) {
370 SAFE_FREE(stripped);
371 }
372 return;
373 }
374
375 /* Copy formatted text to mmap'd region */
376 memcpy(g_mmap_log.text_region + pos, write_buf, write_len);
377
378 atomic_fetch_add(&g_mmap_log.bytes_written, write_len);
379
380 if (stripped) {
381 SAFE_FREE(stripped);
382 }
383
384 /* Sync for ERROR/FATAL to ensure visibility on crash */
385 if (level >= 4 /* LOG_ERROR */) {
386 platform_mmap_sync(&g_mmap_log.mmap, false);
387 }
388}
389
391 return g_mmap_log.initialized;
392}
393
394void log_mmap_sync(void) {
395 if (g_mmap_log.initialized) {
396 platform_mmap_sync(&g_mmap_log.mmap, true);
397 }
398}
399
400void log_mmap_get_stats(uint64_t *bytes_written, uint64_t *wrap_count) {
401 if (bytes_written) {
402 *bytes_written = atomic_load(&g_mmap_log.bytes_written);
403 }
404 if (wrap_count) {
405 *wrap_count = atomic_load(&g_mmap_log.wrap_count);
406 }
407}
408
409bool log_mmap_get_usage(size_t *used, size_t *capacity) {
410 if (!g_mmap_log.initialized) {
411 return false;
412 }
413
414 if (used) {
415 *used = (size_t)atomic_load(&g_mmap_log.write_pos);
416 }
417 if (capacity) {
418 *capacity = g_mmap_log.text_capacity;
419 }
420 return true;
421}
422
423void log_mmap_rotate(void) {
424 if (!g_mmap_log.initialized || !g_mmap_log.text_region) {
425 return;
426 }
427
428 /* NOTE: Caller must hold the rotation mutex from logging.c */
429
430 uint64_t current_pos = atomic_load(&g_mmap_log.write_pos);
431 size_t capacity = g_mmap_log.text_capacity;
432
433 /* Keep last 2/3 of the log (same ratio as file rotation) */
434 size_t keep_size = capacity * 2 / 3;
435 if (current_pos <= keep_size) {
436 return;
437 }
438
439 /* Find where to start keeping (skip to beginning of current_pos - keep_size) */
440 size_t skip_bytes = (size_t)current_pos - keep_size;
441 char *keep_start = g_mmap_log.text_region + skip_bytes;
442
443 /* Skip to next line boundary to avoid partial lines */
444 size_t skipped = 0;
445 while (skipped < keep_size && *keep_start != '\n') {
446 keep_start++;
447 skipped++;
448 }
449 if (skipped < keep_size && *keep_start == '\n') {
450 keep_start++;
451 skipped++;
452 }
453
454 size_t actual_keep = keep_size - skipped;
455 if (actual_keep == 0) {
456 /* Nothing to keep - just reset */
457 atomic_store(&g_mmap_log.write_pos, 0);
458 memset(g_mmap_log.text_region, '\n', capacity);
459 return;
460 }
461
462 /* Move the tail to the beginning using memmove (handles overlap) */
463 memmove(g_mmap_log.text_region, keep_start, actual_keep);
464
465 /* Clear the rest with newlines (grep-friendly without needing -a flag) */
466 memset(g_mmap_log.text_region + actual_keep, '\n', capacity - actual_keep);
467
468 /* Update write position */
469 atomic_store(&g_mmap_log.write_pos, actual_keep);
470
471 /* Write rotation marker */
472 const char *rotate_msg = "\n=== LOG ROTATED ===\n";
473 size_t rotate_len = strlen(rotate_msg);
474 if (actual_keep + rotate_len < capacity) {
475 memcpy(g_mmap_log.text_region + actual_keep, rotate_msg, rotate_len);
476 atomic_store(&g_mmap_log.write_pos, actual_keep + rotate_len);
477 }
478
479 atomic_fetch_add(&g_mmap_log.wrap_count, 1);
480
481 /* Sync after rotation */
482 platform_mmap_sync(&g_mmap_log.mmap, true);
483}
char * ansi_strip_escapes(const char *input, size_t input_len)
Definition ansi.c:13
char file_path[PLATFORM_MAX_PATH_LENGTH]
Definition mmap.c:39
asciichat_error_t log_mmap_init(const log_mmap_config_t *config)
Definition mmap.c:191
void log_mmap_rotate(void)
Definition mmap.c:423
void log_mmap_install_crash_handlers(void)
Definition mmap.c:144
void log_mmap_sync(void)
Definition mmap.c:394
bool log_mmap_is_active(void)
Definition mmap.c:390
char * text_region
Definition mmap.c:35
void log_mmap_write(int level, const char *file, int line, const char *func, const char *fmt,...)
Definition mmap.c:312
bool log_mmap_get_usage(size_t *used, size_t *capacity)
Definition mmap.c:409
platform_mmap_t mmap
Definition mmap.c:34
_Atomic uint64_t bytes_written
Definition mmap.c:42
_Atomic uint64_t wrap_count
Definition mmap.c:43
void log_mmap_destroy(void)
Definition mmap.c:260
void log_mmap_get_stats(uint64_t *bytes_written, uint64_t *wrap_count)
Definition mmap.c:400
_Atomic uint64_t write_pos
Definition mmap.c:37
size_t text_capacity
Definition mmap.c:36
bool initialized
Definition mmap.c:38
asciichat_error_t log_mmap_init_simple(const char *log_path, size_t max_size)
Definition mmap.c:252
action_args_t args
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
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:507
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:64
uint64_t time_get_realtime_ns(void)
Definition util/time.c:59
asciichat_error_t platform_localtime(const time_t *timer, struct tm *result)
Definition util.c:48