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

Lock-free memory-mapped text logging with crash safety. More...

Go to the source code of this file.

Data Structures

struct  log_mmap_config
 Configuration for mmap logging. More...
 

Macros

#define LOG_MMAP_DEFAULT_SIZE   (4 * 1024 * 1024)
 Default mmap log file size (4MB)
 

Typedefs

typedef struct log_mmap_config log_mmap_config_t
 Configuration for mmap logging.
 

Functions

asciichat_error_t log_mmap_init (const log_mmap_config_t *config)
 Initialize mmap-based text logging.
 
asciichat_error_t log_mmap_init_simple (const char *log_path, size_t max_size)
 Initialize mmap logging with simple parameters.
 
void log_mmap_destroy (void)
 Shutdown mmap logging.
 
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)
 
bool log_mmap_is_active (void)
 Check if mmap logging is active.
 
void log_mmap_sync (void)
 Force sync the mmap'd file to disk.
 
void log_mmap_install_crash_handlers (void)
 Install signal handlers for crash safety.
 
void log_mmap_get_stats (uint64_t *bytes_written, uint64_t *wrap_count)
 Get statistics about the mmap log.
 
bool log_mmap_get_usage (size_t *used, size_t *capacity)
 Get current mmap log usage.
 
void log_mmap_rotate (void)
 Rotate the mmap log (tail-keeping rotation)
 

Detailed Description

Lock-free memory-mapped text logging with crash safety.

This module provides crash-safe logging by writing human-readable text directly to a memory-mapped log file. Key features:

  • Lock-free logging: Uses atomic fetch_add for position claiming
  • Crash-safe: Text is written directly to mmap'd file, readable after crash
  • No flusher needed: Logs are human-readable in the file immediately
  • Pure text: No binary header, works with grep/rg/cat without special flags
  • Simple design: Just mmap the .log file and write text to it

Usage:

// Initialize mmap logging (call once at startup)
log_mmap_init("/tmp/myapp.log", 4 * 1024 * 1024); // 4MB log file
// Use normal log_* macros - they automatically use mmap backend
log_debug("This uses atomic operations, no mutex!");
// Cleanup on shutdown
#define log_debug(...)
Log a DEBUG message.
asciichat_error_t log_mmap_init(const log_mmap_config_t *config)
Initialize mmap-based text logging.
Definition mmap.c:188
void log_mmap_destroy(void)
Shutdown mmap logging.
Definition mmap.c:257

Definition in file log/mmap.h.

Macro Definition Documentation

◆ LOG_MMAP_DEFAULT_SIZE

#define LOG_MMAP_DEFAULT_SIZE   (4 * 1024 * 1024)

Default mmap log file size (4MB)

Definition at line 46 of file log/mmap.h.

Typedef Documentation

◆ log_mmap_config_t

Configuration for mmap logging.

Function Documentation

◆ log_mmap_destroy()

void log_mmap_destroy ( void  )

Shutdown mmap logging.

Syncs and unmaps the log file.

Definition at line 257 of file mmap.c.

257 {
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}
unsigned long long uint64_t
Definition common.h:59
#define log_info(...)
Log an INFO message.
void platform_mmap_close(platform_mmap_t *mapping)
Unmap and close a memory-mapped file.
void platform_mmap_sync(platform_mmap_t *mapping, bool async)
Flush memory-mapped changes to disk.
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

References log_info, log_mmap_write(), platform_mmap_close(), and platform_mmap_sync().

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 
)

Get statistics about the mmap log.

Parameters
[out]bytes_writtenTotal bytes written since init
[out]wrap_countNumber of times log has wrapped

Definition at line 385 of file mmap.c.

385 {
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}
_Atomic uint64_t bytes_written
Definition mmap.c:40
_Atomic uint64_t wrap_count
Definition mmap.c:41

References bytes_written, and wrap_count.

◆ log_mmap_get_usage()

bool log_mmap_get_usage ( size_t *  used,
size_t *  capacity 
)

Get current mmap log usage.

Parameters
[out]usedCurrent bytes used in the log
[out]capacityTotal capacity of the log
Returns
true if mmap is active and values are valid

Definition at line 394 of file mmap.c.

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

◆ log_mmap_init()

asciichat_error_t log_mmap_init ( const log_mmap_config_t config)

Initialize mmap-based text logging.

Creates or opens a memory-mapped log file. Text is written directly to the file, so it's readable even after a crash.

Parameters
configConfiguration options
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 188 of file mmap.c.

188 {
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}
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#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.
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_init(platform_mmap_t *mapping)
Initialize a platform_mmap_t structure.
#define LOG_MMAP_DEFAULT_SIZE
Default mmap log file size (4MB)
Definition log/mmap.h:46
void log_mmap_install_crash_handlers(void)
Install signal handlers for crash safety.
Definition mmap.c:141
size_t max_size
Definition log/mmap.h:53
const char * log_path
Definition log/mmap.h:52

References ASCIICHAT_OK, ERROR_INVALID_PARAM, log_info, LOG_MMAP_DEFAULT_SIZE, log_mmap_destroy(), log_mmap_install_crash_handlers(), log_mmap_write(), log_mmap_config::log_path, log_warn, log_mmap_config::max_size, platform_mmap_init(), platform_mmap_open(), SAFE_STRNCPY, and SET_ERRNO.

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 
)

Initialize mmap logging with simple parameters.

Convenience wrapper around log_mmap_init().

Parameters
log_pathPath to log file
max_sizeMaximum file size (0 = default 4MB)
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 249 of file mmap.c.

249 {
250 log_mmap_config_t config = {
251 .log_path = log_path,
252 .max_size = max_size,
253 };
254 return log_mmap_init(&config);
255}
Configuration for mmap logging.
Definition log/mmap.h:51

References log_mmap_init(), and log_mmap_config::log_path.

Referenced by log_enable_mmap_sized(), and log_init().

◆ log_mmap_install_crash_handlers()

void log_mmap_install_crash_handlers ( void  )

Install signal handlers for crash safety.

Registers handlers for SIGSEGV, SIGABRT, etc. that sync the mmap before the process terminates.

Note
Signal handlers are automatically installed by log_mmap_init().

Definition at line 141 of file mmap.c.

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

Referenced by log_mmap_init().

◆ log_mmap_is_active()

bool log_mmap_is_active ( void  )

Check if mmap logging is active.

Returns
true if mmap logging is initialized and active

Definition at line 375 of file mmap.c.

375 {
376 return g_mmap_log.initialized;
377}

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

◆ log_mmap_rotate()

void log_mmap_rotate ( void  )

Rotate the mmap log (tail-keeping rotation)

Keeps the most recent log entries (tail) and discards old ones. This is the mmap equivalent of file-based log rotation.

Note
Caller must hold the rotation mutex from logging.c. This is called by maybe_rotate_log() which handles locking.

Definition at line 408 of file mmap.c.

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

References platform_mmap_sync().

◆ log_mmap_sync()

void log_mmap_sync ( void  )

Force sync the mmap'd file to disk.

Ensures all written data is flushed to the underlying file. Called automatically for ERROR/FATAL levels and on shutdown.

Definition at line 379 of file mmap.c.

379 {
380 if (g_mmap_log.initialized) {
381 platform_mmap_sync(&g_mmap_log.mmap, true);
382 }
383}

References platform_mmap_sync().

◆ log_mmap_write()

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)

Formats the log message and writes it directly as human-readable text. Uses atomic operations only, no mutex.

Parameters
levelLog level
fileSource file (can be NULL)
lineSource line
funcFunction name (can be NULL)
fmtFormat string
...Format arguments

Definition at line 309 of file mmap.c.

309 {
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}
#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

References LOG_MMAP_MSG_BUFFER_SIZE, LOG_TIMESTAMP_BUFFER_SIZE, and platform_mmap_sync().

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