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