ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
mmap.c File Reference

Lock-free memory-mapped text logging implementation. More...

Go to the source code of this file.

Functions

void log_mmap_install_crash_handlers (void)
 
asciichat_error_t log_mmap_init (const log_mmap_config_t *config)
 
asciichat_error_t log_mmap_init_simple (const char *log_path, size_t max_size)
 
void log_mmap_destroy (void)
 
void log_mmap_write (int level, const char *file, int line, const char *func, const char *fmt,...)
 
bool log_mmap_is_active (void)
 
void log_mmap_sync (void)
 
void log_mmap_get_stats (uint64_t *bytes_written, uint64_t *wrap_count)
 
bool log_mmap_get_usage (size_t *used, size_t *capacity)
 
void log_mmap_rotate (void)
 

Detailed Description

Lock-free memory-mapped text logging implementation.

Writes human-readable log text directly to a memory-mapped file. On crash, the log file is immediately readable with cat/tail.

Definition in file mmap.c.

Function Documentation

◆ log_mmap_destroy()

void log_mmap_destroy ( void  )

Definition at line 260 of file mmap.c.

260 {
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}
void log_mmap_write(int level, const char *file, int line, const char *func, const char *fmt,...)
Definition mmap.c:312

References log_mmap_write().

Referenced by log_destroy(), log_disable_mmap(), and log_mmap_init().

◆ log_mmap_get_stats()

void log_mmap_get_stats ( uint64_t *  bytes_written,
uint64_t *  wrap_count 
)

Definition at line 400 of file mmap.c.

400 {
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}
_Atomic uint64_t bytes_written
Definition mmap.c:42
_Atomic uint64_t wrap_count
Definition mmap.c:43

References bytes_written, and wrap_count.

◆ log_mmap_get_usage()

bool log_mmap_get_usage ( size_t *  used,
size_t *  capacity 
)

Definition at line 409 of file mmap.c.

409 {
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}

◆ log_mmap_init()

asciichat_error_t log_mmap_init ( const log_mmap_config_t *  config)

Definition at line 191 of file mmap.c.

191 {
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}
void log_mmap_install_crash_handlers(void)
Definition mmap.c:144
void log_mmap_destroy(void)
Definition mmap.c:260

References log_mmap_destroy(), log_mmap_install_crash_handlers(), and log_mmap_write().

Referenced by log_mmap_init_simple().

◆ log_mmap_init_simple()

asciichat_error_t log_mmap_init_simple ( const char *  log_path,
size_t  max_size 
)

Definition at line 252 of file mmap.c.

252 {
253 log_mmap_config_t config = {
254 .log_path = log_path,
255 .max_size = max_size,
256 };
257 return log_mmap_init(&config);
258}
asciichat_error_t log_mmap_init(const log_mmap_config_t *config)
Definition mmap.c:191

References log_mmap_init().

Referenced by log_enable_mmap_sized(), and log_init().

◆ log_mmap_install_crash_handlers()

void log_mmap_install_crash_handlers ( void  )

Definition at line 144 of file mmap.c.

144 {
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}

Referenced by log_mmap_init().

◆ log_mmap_is_active()

bool log_mmap_is_active ( void  )

Definition at line 390 of file mmap.c.

390 {
391 return g_mmap_log.initialized;
392}

Referenced by log_destroy(), log_disable_mmap(), log_file_msg(), log_msg(), and log_plain_msg().

◆ log_mmap_rotate()

void log_mmap_rotate ( void  )

Definition at line 423 of file mmap.c.

423 {
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}

◆ log_mmap_sync()

void log_mmap_sync ( void  )

Definition at line 394 of file mmap.c.

394 {
395 if (g_mmap_log.initialized) {
396 platform_mmap_sync(&g_mmap_log.mmap, true);
397 }
398}

◆ log_mmap_write()

void log_mmap_write ( int  level,
const char *  file,
int  line,
const char *  func,
const char *  fmt,
  ... 
)

Definition at line 312 of file mmap.c.

312 {
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}
char * ansi_strip_escapes(const char *input, size_t input_len)
Definition ansi.c:13
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

References ansi_strip_escapes(), args, safe_snprintf(), and safe_vsnprintf().

Referenced by log_file_msg(), log_mmap_destroy(), log_mmap_init(), log_msg(), and log_plain_msg().

Variable Documentation

◆ bytes_written

_Atomic uint64_t bytes_written

◆ file_path

◆ initialized

bool initialized

Definition at line 38 of file mmap.c.

Referenced by registry_init_from_builders(), and test_get_binary_path().

◆ mmap

platform_mmap_t mmap

Definition at line 34 of file mmap.c.

◆ text_capacity

size_t text_capacity

Definition at line 36 of file mmap.c.

◆ text_region

char* text_region

Definition at line 35 of file mmap.c.

◆ wrap_count

_Atomic uint64_t wrap_count

Definition at line 43 of file mmap.c.

Referenced by log_mmap_get_stats().

◆ write_pos

_Atomic uint64_t write_pos

Definition at line 37 of file mmap.c.

Referenced by session_log_buffer_get_recent().