ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
instrument_log.c
Go to the documentation of this file.
1
11// SPDX-License-Identifier: MIT
12// Debug instrumentation logging runtime for ascii-chat line tracing
13
15#include <ascii-chat/common.h>
24
25#include <ctype.h>
26#include <errno.h>
27#include <fcntl.h>
28#include <limits.h>
29#include <stdarg.h>
30#include <stdbool.h>
31#include <stdio.h>
32#include <stdlib.h>
33#include <string.h>
34#include <sys/stat.h>
35#include <sys/types.h>
36#include <time.h>
37
38#ifdef _WIN32
39#include <io.h>
40#else
41#include <unistd.h>
42#endif
43
44#if !defined(_WIN32)
45#define ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX 1
46#include <regex.h>
47#else
48#define ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX 0
49// Windows doesn't have mode_t
50typedef int mode_t;
51#endif
52
53#ifdef _WIN32
54#include <direct.h>
55#ifndef mkdir
56#define mkdir(path, mode) _mkdir(path)
57#endif
58// Windows uses _write instead of write
59#define posix_write _write
60#ifndef STDERR_FILENO
61#define STDERR_FILENO 2
62#endif
63#else
64#define posix_write write
65#endif
66
67#ifndef ASCII_INSTR_SOURCE_PRINT_DEFAULT_BASENAME
68#define ASCII_INSTR_SOURCE_PRINT_DEFAULT_BASENAME "ascii-instr"
69#endif
70
71#ifndef ASCII_INSTR_SOURCE_PRINT_MAX_LINE
72#define ASCII_INSTR_SOURCE_PRINT_MAX_LINE 4096
73#endif
74
75#ifndef ASCII_INSTR_SOURCE_PRINT_MAX_SNIPPET
76#define ASCII_INSTR_SOURCE_PRINT_MAX_SNIPPET 2048
77#endif
78
85
94
103
138
139static tls_key_t g_runtime_key;
140static lifecycle_t g_runtime_lc = LIFECYCLE_INIT;
141static char g_output_dir[PATH_MAX];
142static bool g_output_dir_set = false;
143static bool g_disable_write = false;
144static uint64_t g_start_ns = 0;
145static bool g_ticks_initialized = false;
146static bool g_coverage_enabled = false;
147static bool g_echo_to_stderr = false;
148static bool g_echo_to_stderr_initialized = false;
149
150static void asciichat_instr_runtime_init_once(void);
151static void asciichat_instr_runtime_tls_destructor(void *ptr);
152static int asciichat_instr_open_log_file(asciichat_instr_runtime_t *runtime);
153static void asciichat_instr_runtime_configure(asciichat_instr_runtime_t *runtime);
154static bool asciichat_instr_should_log(const asciichat_instr_runtime_t *runtime, const char *file_path,
155 uint32_t line_number, const char *function_name);
156static int asciichat_instr_write_full(int fd, const char *buffer, size_t len);
157static bool asciichat_instr_env_is_enabled(const char *value);
158static bool asciichat_instr_parse_positive_uint32(const char *value, uint32_t *out_value);
159#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
160static bool asciichat_instr_compile_regex(regex_t *regex, const char *pattern);
161#endif
162static void asciichat_instr_only_list_destroy(asciichat_instr_only_list_t *list);
163static bool asciichat_instr_only_list_append(asciichat_instr_only_list_t *list, asciichat_instr_selector_type_t type,
164 const char *module, const char *pattern);
165static void asciichat_instr_trim(char *value);
166static bool asciichat_instr_parse_only_filters(asciichat_instr_runtime_t *runtime, const char *value);
167static bool asciichat_instr_only_selectors_match(const asciichat_instr_runtime_t *runtime, const char *file_path,
168 const char *function_name);
169static bool asciichat_instr_match_glob(const char *pattern, const char *value);
170static const char *asciichat_instr_basename(const char *path);
171static bool asciichat_instr_path_contains_module(const char *file_path, const char *module_name);
172
173static _Thread_local bool g_logging_reentry_guard = false;
174static bool g_instrumentation_enabled = false;
175static bool g_instrumentation_enabled_checked = false;
176
178 if (g_disable_write) {
179 return NULL;
180 }
181
182 // Initialize runtime once using lock-free lifecycle API
183 if (!lifecycle_is_initialized(&g_runtime_lc)) {
184 if (lifecycle_init_once(&g_runtime_lc)) {
185 asciichat_instr_runtime_init_once();
186 lifecycle_init_commit(&g_runtime_lc);
187 }
188 }
189
190 asciichat_instr_runtime_t *runtime = ascii_tls_get(g_runtime_key);
191 if (runtime != NULL) {
192 return runtime;
193 }
194
195 runtime = SAFE_CALLOC(1, sizeof(*runtime), asciichat_instr_runtime_t *);
196 if (runtime == NULL) {
197 return NULL;
198 }
199
200 runtime->pid = platform_get_pid();
202 runtime->sequence = 0;
203 runtime->call_counter = 0;
204 runtime->fd = -1;
205 runtime->filter_include = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_INCLUDE");
206 runtime->filter_exclude = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_EXCLUDE");
207 runtime->filter_thread = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_THREAD");
208 runtime->filter_function_include = NULL;
209 runtime->filter_function_exclude = NULL;
210 runtime->filters_enabled = false;
211 runtime->rate = 1;
212 runtime->rate_enabled = false;
213
214 asciichat_instr_runtime_configure(runtime);
215
216 if (ascii_tls_set(g_runtime_key, runtime) != 0) {
217 SAFE_FREE(runtime);
218 return NULL;
219 }
220
221 return runtime;
222}
223
225 if (runtime == NULL) {
226 return;
227 }
228
229 if (runtime->fd >= 0) {
230 platform_close(runtime->fd);
231 runtime->fd = -1;
232 }
233
234#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
235 if (runtime->include_regex_valid) {
236 regfree(&runtime->include_regex);
237 runtime->include_regex_valid = false;
238 }
239 if (runtime->exclude_regex_valid) {
240 regfree(&runtime->exclude_regex);
241 runtime->exclude_regex_valid = false;
242 }
243 if (runtime->function_include_regex_valid) {
244 regfree(&runtime->function_include_regex);
245 runtime->function_include_regex_valid = false;
246 }
247 if (runtime->function_exclude_regex_valid) {
248 regfree(&runtime->function_exclude_regex);
249 runtime->function_exclude_regex_valid = false;
250 }
251#endif
252
253 asciichat_instr_only_list_destroy(&runtime->only_selectors);
254 SAFE_FREE(runtime);
255}
256
258 if (lifecycle_shutdown(&g_runtime_lc)) {
259 g_disable_write = true;
260 asciichat_instr_runtime_t *runtime = ascii_tls_get(g_runtime_key);
261 if (runtime != NULL) {
262 ascii_tls_set(g_runtime_key, NULL);
264 }
265 ascii_tls_key_delete(g_runtime_key);
266 g_ticks_initialized = false;
267 g_start_ns = 0;
268 g_coverage_enabled = false;
269 g_output_dir_set = false;
270 g_output_dir[0] = '\0';
271 g_instrumentation_enabled_checked = false;
272 g_instrumentation_enabled = false;
273 g_echo_to_stderr_initialized = false;
274 g_echo_to_stderr = false;
275 }
276
277 // Reset g_disable_write so instrumentation can be re-enabled in subsequent tests
278 g_disable_write = false;
279}
280
281void asciichat_instr_log_line(const char *file_path, uint32_t line_number, const char *function_name,
282 const char *snippet, uint8_t is_macro_expansion) {
283 // Instrumentation is enabled by default when the binary is built with Source Print.
284 // Set ASCII_INSTR_SOURCE_PRINT_ENABLE=0 to disable tracing at runtime.
285 if (!g_instrumentation_enabled_checked) {
286 const char *enable_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ENABLE");
287 // Default to enabled (true) - only disable if explicitly set to "0", "false", "off", or "no"
288 if (enable_env != NULL && enable_env[0] != '\0') {
289 g_instrumentation_enabled = asciichat_instr_env_is_enabled(enable_env);
290 } else {
291 g_instrumentation_enabled = true; // Default to enabled when instrumented
292 }
293 g_instrumentation_enabled_checked = true;
294 }
295
296 if (!g_instrumentation_enabled) {
297 return;
298 }
299
300 if (g_disable_write) {
301 return;
302 }
303
304 if (g_logging_reentry_guard) {
305 return;
306 }
307
308 g_logging_reentry_guard = true;
309
311 if (runtime == NULL) {
312 goto cleanup;
313 }
314
315 if (!asciichat_instr_should_log(runtime, file_path, line_number, function_name)) {
316 goto cleanup;
317 }
318
319 runtime->call_counter++;
320 if (runtime->rate_enabled) {
321 const uint64_t counter = runtime->call_counter;
322 if (((counter - 1U) % runtime->rate) != 0U) {
323 goto cleanup;
324 }
325 }
326
327 if (runtime->fd < 0) {
328 if (asciichat_instr_open_log_file(runtime) != 0) {
329 runtime->stderr_fallback = true;
330 }
331 }
332
333 const int fd = runtime->stderr_fallback ? STDERR_FILENO : runtime->fd;
335 size_t pos = 0;
336
337 uint64_t realtime_ns = time_get_realtime_ns();
338 time_t sec = (time_t)(realtime_ns / NS_PER_SEC_INT);
339 long nsec = (long)(realtime_ns % NS_PER_SEC_INT);
340 struct tm tm_now;
341 memset(&tm_now, 0, sizeof(tm_now));
342 if (platform_gtime(&sec, &tm_now) != ASCIICHAT_OK) {
343 memset(&tm_now, 0, sizeof(tm_now));
344 }
345
346 char timestamp[32];
347 size_t ts_len = strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", &tm_now);
348 if (ts_len == 0) {
349 SAFE_STRNCPY(timestamp, "1970-01-01T00:00:00", sizeof(timestamp));
350 ts_len = strlen(timestamp);
351 }
352
353 char elapsed_buf[32];
354 elapsed_buf[0] = '\0';
355 if (g_ticks_initialized) {
356 uint64_t now_ns = time_get_ns();
357 uint64_t elapsed_ns = time_elapsed_ns(g_start_ns, now_ns);
358 if (time_pretty((uint64_t)elapsed_ns, -1, elapsed_buf, sizeof(elapsed_buf)) < 0) {
359 elapsed_buf[0] = '\0';
360 }
361 }
362
363 runtime->sequence++;
364
365 const char *elapsed_field = (elapsed_buf[0] != '\0') ? elapsed_buf : "-";
366
367 const char *safe_file_path = (file_path != NULL) ? file_path : "<unknown>";
368 pos += safe_snprintf(buffer + pos, sizeof(buffer) - pos,
369 "pid=%d tid=%llu seq=%llu ts=%.*s.%09ldZ elapsed=%s file=%s line=%u func=%s macro=%u snippet=",
370 runtime->pid, (unsigned long long)runtime->thread_id, (unsigned long long)runtime->sequence,
371 (int)ts_len, timestamp, nsec, elapsed_field, safe_file_path, line_number,
372 function_name ? function_name : "<unknown>", (unsigned)is_macro_expansion);
373
374 if (snippet != NULL) {
375 size_t snippet_len = strnlen(snippet, ASCII_INSTR_SOURCE_PRINT_MAX_SNIPPET);
376 for (size_t i = 0; i < snippet_len && pos < sizeof(buffer) - 2; ++i) {
377 const char ch = snippet[i];
378 switch (ch) {
379 case '\n':
380 buffer[pos++] = '\\';
381 buffer[pos++] = 'n';
382 break;
383 case '\r':
384 buffer[pos++] = '\\';
385 buffer[pos++] = 'r';
386 break;
387 case '\t':
388 buffer[pos++] = '\\';
389 buffer[pos++] = 't';
390 break;
391 default:
392 buffer[pos++] = ch;
393 break;
394 }
395 }
396 }
397
398 if (pos >= sizeof(buffer) - 1) {
399 pos = sizeof(buffer) - 2;
400 }
401
402 buffer[pos++] = '\n';
403 buffer[pos] = '\0';
404
405 asciichat_instr_write_full(fd, buffer, pos);
406
407 // Also echo to stderr if requested via environment variable
408 if (!g_echo_to_stderr_initialized) {
409 const char *echo_env = SAFE_GETENV("ASCII_CHAT_DEBUG_SELF_SOURCE_CODE_LOG_STDERR");
410 g_echo_to_stderr = asciichat_instr_env_is_enabled(echo_env);
411 g_echo_to_stderr_initialized = true;
412 }
413
414 if (g_echo_to_stderr && !runtime->stderr_fallback) {
415 // Write to stderr in addition to the log file
416 // Suppress Windows deprecation warning for POSIX write function
417#ifdef _WIN32
418#pragma clang diagnostic push
419#pragma clang diagnostic ignored "-Wdeprecated-declarations"
420#endif
421 (void)posix_write(STDERR_FILENO, buffer, pos);
422#ifdef _WIN32
423#pragma clang diagnostic pop
424#endif
425 }
426
427cleanup:
428 g_logging_reentry_guard = false;
429}
430
432 if (g_disable_write) {
433 return false;
434 }
435
436 // Initialize runtime once using lock-free lifecycle API
437 if (!lifecycle_is_initialized(&g_runtime_lc)) {
438 if (lifecycle_init_once(&g_runtime_lc)) {
439 asciichat_instr_runtime_init_once();
440 lifecycle_init_commit(&g_runtime_lc);
441 }
442 }
443
444 return g_coverage_enabled;
445}
446
447void asciichat_instr_log_pc(uintptr_t program_counter) {
449 return;
450 }
451
452 char snippet[64];
453 safe_snprintf(snippet, sizeof(snippet), "pc=0x%zx", (size_t)program_counter);
454 asciichat_instr_log_line("__coverage__", 0, "<coverage>", snippet, ASCII_INSTR_SOURCE_PRINT_MACRO_NONE);
455}
456
457static void asciichat_instr_runtime_init_once(void) {
458 // Called by lifecycle_init_once winner
459 (void)ascii_tls_key_create(&g_runtime_key, asciichat_instr_runtime_tls_destructor);
460 const char *output_dir_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_OUTPUT_DIR");
461 if (output_dir_env != NULL && output_dir_env[0] != '\0') {
462 if (platform_is_directory(output_dir_env) && strlen(output_dir_env) < sizeof(g_output_dir)) {
463 SAFE_STRNCPY(g_output_dir, output_dir_env, sizeof(g_output_dir));
464 g_output_dir_set = true;
465 } else {
466 log_warn("Ignoring invalid ASCII_INSTR_SOURCE_PRINT_OUTPUT_DIR path: %s", output_dir_env);
467 }
468 }
469 const char *coverage_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ENABLE_COVERAGE");
470 g_coverage_enabled = asciichat_instr_env_is_enabled(coverage_env);
471 g_start_ns = time_get_ns();
472 g_ticks_initialized = true;
473}
474
475static void asciichat_instr_runtime_tls_destructor(void *ptr) {
477}
478
479static bool asciichat_instr_build_log_path(asciichat_instr_runtime_t *runtime) {
480 // Check for custom log file path first
481 const char *custom_log_file = SAFE_GETENV("ASCII_CHAT_DEBUG_SELF_SOURCE_CODE_LOG_FILE");
482 bool is_custom_path = (custom_log_file != NULL && custom_log_file[0] != '\0');
483
484 if (is_custom_path) {
485 const char *debug_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ECHO_STDERR");
486 if (debug_env && debug_env[0] == '1') {
487 fprintf(stderr, "ASCII_INSTR: Using custom log path: %s\n", custom_log_file);
488 }
489
490 // For instrumentation log files, bypass strict path validation since this is a debug feature
491 // Just normalize the path to an absolute path without security checks
492 char *expanded = expand_path(custom_log_file);
493 if (!expanded) {
494 log_warn("Failed to expand ASCII_CHAT_DEBUG_SELF_SOURCE_CODE_LOG_FILE path: %s", custom_log_file);
495 return false;
496 }
497
498 // Convert to absolute path if relative
499 char absolute_buf[PATH_MAX];
500 if (!path_is_absolute(expanded)) {
501 char cwd_buf[PATH_MAX];
502 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
503 SAFE_FREE(expanded);
504 return false;
505 }
506 // Check if expanded already starts with a separator to avoid double separators
507 if (strlen(expanded) > 0 && expanded[0] == PATH_DELIM) {
508 safe_snprintf(absolute_buf, sizeof(absolute_buf), "%s%s", cwd_buf, expanded);
509 } else {
510 safe_snprintf(absolute_buf, sizeof(absolute_buf), "%s%c%s", cwd_buf, PATH_DELIM, expanded);
511 }
512 SAFE_FREE(expanded);
513 expanded = platform_strdup(absolute_buf);
514 if (!expanded) {
515 return false;
516 }
517 }
518
519 SAFE_STRNCPY(runtime->log_path, expanded, sizeof(runtime->log_path));
520 runtime->log_path[sizeof(runtime->log_path) - 1] = '\0';
521 SAFE_FREE(expanded);
522
523 if (debug_env && debug_env[0] == '1') {
524 fprintf(stderr, "ASCII_INSTR: Resolved custom log path: %s\n", runtime->log_path);
525 }
526
527 // Don't check if file exists - allow appending to existing file
528 // Path already normalized, skip validation below
529 } else {
530 // Determine output directory
531 char output_dir_buf[PATH_MAX];
532 if (g_output_dir_set) {
533 // Use the output directory from ASCII_INSTR_SOURCE_PRINT_OUTPUT_DIR
534 SAFE_STRNCPY(output_dir_buf, g_output_dir, sizeof(output_dir_buf));
535 } else if (!platform_get_cwd(output_dir_buf, sizeof(output_dir_buf))) {
536 // Fallback to temp directory if cwd fails
537 const char *fallback = SAFE_GETENV("TMPDIR");
538 if (fallback == NULL) {
539 fallback = SAFE_GETENV("TEMP");
540 }
541 if (fallback == NULL) {
542 fallback = SAFE_GETENV("TMP");
543 }
544 if (fallback == NULL) {
545 fallback = "/tmp";
546 }
547 SAFE_STRNCPY(output_dir_buf, fallback, sizeof(output_dir_buf));
548 }
549
550 // Build log file name
551 if (g_output_dir_set) {
552 // When output directory is explicitly set, use unique naming with pid and tid
553 if (safe_snprintf(runtime->log_path, sizeof(runtime->log_path), "%s%c%s-%d-%llu.log", output_dir_buf, PATH_DELIM,
555 (unsigned long long)runtime->thread_id) >= (int)sizeof(runtime->log_path)) {
556 return false;
557 }
558 } else {
559 // Use simple "trace.log" name in current directory
560 if (safe_snprintf(runtime->log_path, sizeof(runtime->log_path), "%s%ctrace.log", output_dir_buf, PATH_DELIM) >=
561 (int)sizeof(runtime->log_path)) {
562 return false;
563 }
564 }
565 }
566
567 // Explicit output directories are validated during initialization and receive a generated filename.
568 if (!is_custom_path && !g_output_dir_set) {
569 char *validated_log_path = NULL;
570 asciichat_error_t validate_result =
571 path_validate_user_path(runtime->log_path, PATH_ROLE_LOG_FILE, &validated_log_path);
572 if (validate_result != ASCIICHAT_OK || validated_log_path == NULL) {
573 SAFE_FREE(validated_log_path);
574 log_warn("Failed to validate instrumentation log path: %s", runtime->log_path);
575 return false;
576 }
577 SAFE_STRNCPY(runtime->log_path, validated_log_path, sizeof(runtime->log_path));
578 runtime->log_path[sizeof(runtime->log_path) - 1] = '\0';
579 SAFE_FREE(validated_log_path);
580 }
581
582 // Find last path separator
583 const char *last_sep = strrchr(runtime->log_path, PATH_DELIM);
584 if (last_sep != NULL && last_sep != runtime->log_path) {
585 const size_t dir_path_len = (size_t)(last_sep - runtime->log_path);
586 char dir_path[PATH_MAX];
587 memcpy(dir_path, runtime->log_path, dir_path_len);
588 dir_path[dir_path_len] = '\0';
589 if (mkdir(dir_path, DIR_PERM_PRIVATE) != 0) {
590 if (errno != EEXIST) {
591 return false;
592 }
593 }
594 }
595
596 return true;
597}
598
599static int asciichat_instr_open_log_file(asciichat_instr_runtime_t *runtime) {
600 if (!asciichat_instr_build_log_path(runtime)) {
601 const char *debug_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ECHO_STDERR");
602 if (debug_env && debug_env[0] == '1') {
603 fprintf(stderr, "ASCII_INSTR: Failed to build log path\n");
604 }
605 return -1;
606 }
607
608 // Check if this is a custom log file (env var was set)
609 const char *custom_log_file = SAFE_GETENV("ASCII_CHAT_DEBUG_SELF_SOURCE_CODE_LOG_FILE");
610 const bool is_custom_file = (custom_log_file != NULL && custom_log_file[0] != '\0');
611
612 // For custom files, allow appending to existing file (no O_EXCL)
613 // For auto-generated files, use O_EXCL to avoid conflicts
614 int flags;
615 if (is_custom_file) {
617 } else {
619 }
620
621 const char *debug_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ECHO_STDERR");
622 if (debug_env && debug_env[0] == '1') {
623 fprintf(stderr, "ASCII_INSTR: Opening log file: %s (custom=%d)\n", runtime->log_path, is_custom_file);
624 }
625
626 const mode_t mode = S_IRUSR | S_IWUSR;
627 int fd = platform_open("instrumentation_log", runtime->log_path, flags, mode);
628 if (fd < 0) {
629 if (debug_env && debug_env[0] == '1') {
630 fprintf(stderr, "ASCII_INSTR: Failed to open log file: %s (errno=%d)\n", runtime->log_path, errno);
631 }
632 return -1;
633 }
634
635 if (debug_env && debug_env[0] == '1') {
636 fprintf(stderr, "ASCII_INSTR: Successfully opened log file: %s (fd=%d)\n", runtime->log_path, fd);
637 }
638
639 runtime->fd = fd;
640 return 0;
641}
642
643static void asciichat_instr_runtime_configure(asciichat_instr_runtime_t *runtime) {
644 runtime->filter_function_include = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_FUNCTION_INCLUDE");
645 runtime->filter_function_exclude = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_FUNCTION_EXCLUDE");
646
647 const char *only_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_ONLY");
648 asciichat_instr_parse_only_filters(runtime, only_env);
649
650#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
651 const char *include_regex = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_INCLUDE_REGEX");
652 if (include_regex != NULL && include_regex[0] != '\0') {
653 runtime->include_regex_valid = asciichat_instr_compile_regex(&runtime->include_regex, include_regex);
654 }
655
656 const char *exclude_regex = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_EXCLUDE_REGEX");
657 if (exclude_regex != NULL && exclude_regex[0] != '\0') {
658 runtime->exclude_regex_valid = asciichat_instr_compile_regex(&runtime->exclude_regex, exclude_regex);
659 }
660
661 const char *function_include_regex = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_FUNCTION_INCLUDE_REGEX");
662 if (function_include_regex != NULL && function_include_regex[0] != '\0') {
664 asciichat_instr_compile_regex(&runtime->function_include_regex, function_include_regex);
665 }
666
667 const char *function_exclude_regex = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_FUNCTION_EXCLUDE_REGEX");
668 if (function_exclude_regex != NULL && function_exclude_regex[0] != '\0') {
670 asciichat_instr_compile_regex(&runtime->function_exclude_regex, function_exclude_regex);
671 }
672#endif
673
674 const char *rate_env = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_RATE");
675 uint32_t rate_value = 0;
676 if (asciichat_instr_parse_positive_uint32(rate_env, &rate_value) && rate_value > 1U) {
677 runtime->rate = rate_value;
678 runtime->rate_enabled = true;
679 }
680
681 runtime->filters_enabled = (runtime->filter_include != NULL) || (runtime->filter_exclude != NULL) ||
682 (runtime->filter_thread != NULL) || (runtime->filter_function_include != NULL) ||
683 (runtime->filter_function_exclude != NULL)
685 || runtime->include_regex_valid || runtime->exclude_regex_valid ||
687#endif
688 || runtime->only_selectors.count > 0;
689}
690
691static bool asciichat_instr_env_is_enabled(const char *value) {
692 if (value == NULL) {
693 return false;
694 }
695
696 while (*value != '\0' && isspace((unsigned char)*value) != 0) {
697 value++;
698 }
699
700 if (*value == '\0') {
701 return false;
702 }
703
704 if (strcmp(value, STR_ZERO) == 0) {
705 return false;
706 }
707
708 char lowered[8];
709 size_t len = 0;
710 while (value[len] != '\0' && len < sizeof(lowered) - 1) {
711 lowered[len] = (char)tolower((unsigned char)value[len]);
712 len++;
713 }
714 lowered[len] = '\0';
715
716 return !(strcmp(lowered, STR_FALSE) == 0 || strcmp(lowered, STR_OFF) == 0 || strcmp(lowered, STR_NO) == 0);
717}
718
719static bool asciichat_instr_parse_positive_uint32(const char *value, uint32_t *out_value) {
720 if (value == NULL || out_value == NULL) {
721 return false;
722 }
723
724 while (*value != '\0' && isspace((unsigned char)*value) != 0) {
725 value++;
726 }
727
728 if (*value == '\0') {
729 return false;
730 }
731
732 errno = 0;
733 char *endptr = NULL;
734 unsigned long long parsed = strtoull(value, &endptr, 10);
735 if (errno != 0 || endptr == value || parsed == 0ULL || parsed > UINT32_MAX) {
736 return false;
737 }
738
739 *out_value = (uint32_t)parsed;
740 return true;
741}
742
743#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
744static bool asciichat_instr_compile_regex(regex_t *regex, const char *pattern) {
745 if (regex == NULL || pattern == NULL) {
746 return false;
747 }
748 int ret = regcomp(regex, pattern, REG_EXTENDED | REG_NOSUB);
749 return ret == 0;
750}
751#endif
752
753static void asciichat_instr_only_list_destroy(asciichat_instr_only_list_t *list) {
754 if (list == NULL) {
755 return;
756 }
757 if (list->items != NULL) {
758 for (size_t i = 0; i < list->count; ++i) {
759 asciichat_instr_only_selector_t *selector = &list->items[i];
760 SAFE_FREE(selector->pattern);
761 SAFE_FREE(selector->module);
762 }
763 SAFE_FREE(list->items);
764 }
765 list->count = 0;
766 list->capacity = 0;
767}
768
769static bool asciichat_instr_only_list_append(asciichat_instr_only_list_t *list, asciichat_instr_selector_type_t type,
770 const char *module, const char *pattern) {
771 if (list == NULL) {
772 return false;
773 }
774
777 if (pattern == NULL || pattern[0] == '\0') {
778 return false;
779 }
780 }
781
783 if (module == NULL || module[0] == '\0') {
784 return false;
785 }
786 }
787
788 if (list->count == list->capacity) {
789 size_t new_capacity = (list->capacity == 0) ? 4U : list->capacity * 2U;
791 SAFE_REALLOC(list->items, new_capacity * sizeof(*new_items), asciichat_instr_only_selector_t *);
792 if (new_items == NULL) {
793 return false; // SAFE_REALLOC already called FATAL, but satisfy analyzer
794 }
795 list->items = new_items;
796 list->capacity = new_capacity;
797 }
798
799 asciichat_instr_only_selector_t *selector = &list->items[list->count];
800 memset(selector, 0, sizeof(*selector));
801 selector->type = type;
802
803 if (module != NULL && module[0] != '\0') {
804 SAFE_STRDUP(selector->module, module);
805 }
806 if (pattern != NULL && pattern[0] != '\0') {
807 SAFE_STRDUP(selector->pattern, pattern);
808 }
809
810 list->count++;
811 return true;
812}
813
814static void asciichat_instr_trim(char *value) {
815 if (value == NULL) {
816 return;
817 }
818
819 char *start = value;
820 while (*start != '\0' && isspace((unsigned char)*start) != 0) {
821 start++;
822 }
823
824 char *end = start + strlen(start);
825 while (end > start && isspace((unsigned char)end[-1]) != 0) {
826 end--;
827 }
828
829 const size_t length = (size_t)(end - start);
830 if (start != value && length > 0U) {
831 memmove(value, start, length);
832 }
833 value[length] = '\0';
834}
835
836static bool asciichat_instr_parse_only_filters(asciichat_instr_runtime_t *runtime, const char *value) {
837 if (runtime == NULL) {
838 return false;
839 }
840
841 asciichat_instr_only_list_destroy(&runtime->only_selectors);
842
843 if (value == NULL || value[0] == '\0') {
844 return true;
845 }
846
847 char *mutable_value = NULL;
848 SAFE_STRDUP(mutable_value, value);
849 if (mutable_value == NULL) {
850 return false;
851 }
852
853 char *cursor = mutable_value;
854 while (cursor != NULL && *cursor != '\0') {
855 char *token_start = cursor;
856 while (*cursor != '\0' && *cursor != ',') {
857 cursor++;
858 }
859 if (*cursor == ',') {
860 *cursor = '\0';
861 cursor++;
862 } else {
863 cursor = NULL;
864 }
865
866 asciichat_instr_trim(token_start);
867 if (*token_start == '\0') {
868 continue;
869 }
870
871 char *equal_sign = strchr(token_start, '=');
872 if (equal_sign != NULL) {
873 *equal_sign = '\0';
874 char *kind = token_start;
875 char *spec = equal_sign + 1;
876 asciichat_instr_trim(kind);
877 asciichat_instr_trim(spec);
878 if (*spec == '\0') {
879 continue;
880 }
881
882 if (strcmp(kind, "func") == 0 || strcmp(kind, "function") == 0) {
883 (void)asciichat_instr_only_list_append(&runtime->only_selectors,
885 } else if (strcmp(kind, "module") == 0) {
886 char *module_value = spec;
887 char *module_pattern = strchr(module_value, ':');
888 if (module_pattern != NULL) {
889 *module_pattern = '\0';
890 module_pattern++;
891 asciichat_instr_trim(module_pattern);
892 }
893 asciichat_instr_trim(module_value);
894 if (*module_value == '\0') {
895 continue;
896 }
897 const char *pattern_part = (module_pattern != NULL && *module_pattern != '\0') ? module_pattern : NULL;
898 (void)asciichat_instr_only_list_append(&runtime->only_selectors, ASCII_INSTR_SOURCE_PRINT_SELECTOR_MODULE,
899 module_value, pattern_part);
900 } else {
901 // "file" kind or unknown kinds default to FILE_GLOB
902 (void)asciichat_instr_only_list_append(&runtime->only_selectors, ASCII_INSTR_SOURCE_PRINT_SELECTOR_FILE_GLOB,
903 NULL, spec);
904 }
905 continue;
906 }
907
908 char *colon = strchr(token_start, ':');
909 if (colon != NULL) {
910 *colon = '\0';
911 char *module_name = token_start;
912 char *pattern_part = colon + 1;
913 asciichat_instr_trim(module_name);
914 asciichat_instr_trim(pattern_part);
915 if (*module_name == '\0') {
916 continue;
917 }
918 const char *pattern_spec = (*pattern_part != '\0') ? pattern_part : NULL;
919 (void)asciichat_instr_only_list_append(&runtime->only_selectors, ASCII_INSTR_SOURCE_PRINT_SELECTOR_MODULE,
920 module_name, pattern_spec);
921 continue;
922 }
923
924 (void)asciichat_instr_only_list_append(&runtime->only_selectors, ASCII_INSTR_SOURCE_PRINT_SELECTOR_FILE_SUBSTRING,
925 NULL, token_start);
926 }
927
928 SAFE_FREE(mutable_value);
929 return true;
930}
931
932static bool asciichat_instr_match_glob(const char *pattern, const char *value) {
933 if (pattern == NULL || value == NULL) {
934 return false;
935 }
936
937 const char *p = pattern;
938 const char *v = value;
939 const char *star = NULL;
940 const char *match = NULL;
941
942 while (*v != '\0') {
943 if (*p == '*') {
944 star = p++;
945 match = v;
946 } else if (*p == '?' || *p == *v) {
947 p++;
948 v++;
949 } else if (star != NULL) {
950 p = star + 1;
951 match++;
952 v = match;
953 } else {
954 return false;
955 }
956 }
957
958 while (*p == '*') {
959 p++;
960 }
961
962 return *p == '\0';
963}
964
965static const char *asciichat_instr_basename(const char *path) {
966 if (path == NULL) {
967 return NULL;
968 }
969
970 // Find last path separator
971 const char *last_sep = strrchr(path, PATH_DELIM);
972 if (last_sep != NULL && last_sep[1] != '\0') {
973 return last_sep + 1;
974 }
975 return path;
976}
977
978static bool asciichat_instr_path_contains_module(const char *file_path, const char *module_name) {
979 if (file_path == NULL || module_name == NULL || module_name[0] == '\0') {
980 return false;
981 }
982
983 const size_t module_len = strlen(module_name);
984 const char *cursor = file_path;
985 while ((cursor = strstr(cursor, module_name)) != NULL) {
986 bool left_ok = (cursor == file_path);
987 if (!left_ok) {
988 const char prev = cursor[-1];
989 left_ok = (prev == PATH_DELIM);
990 }
991
992 const char tail = cursor[module_len];
993 bool right_ok = (tail == '\0' || tail == PATH_DELIM);
994
995 if (left_ok && right_ok) {
996 return true;
997 }
998
999 cursor = cursor + 1;
1000 }
1001
1002 return false;
1003}
1004
1005static bool asciichat_instr_only_selectors_match(const asciichat_instr_runtime_t *runtime, const char *file_path,
1006 const char *function_name) {
1007 const asciichat_instr_only_list_t *list = &runtime->only_selectors;
1008 if (list->count == 0) {
1009 return true;
1010 }
1011
1012 const char *base_name = asciichat_instr_basename(file_path);
1013 for (size_t i = 0; i < list->count; ++i) {
1014 const asciichat_instr_only_selector_t *selector = &list->items[i];
1015 switch (selector->type) {
1017 if (file_path != NULL && selector->pattern != NULL && strstr(file_path, selector->pattern) != NULL) {
1018 return true;
1019 }
1020 break;
1022 if (file_path != NULL && selector->pattern != NULL && asciichat_instr_match_glob(selector->pattern, file_path)) {
1023 return true;
1024 }
1025 break;
1027 if (function_name != NULL && selector->pattern != NULL &&
1028 asciichat_instr_match_glob(selector->pattern, function_name)) {
1029 return true;
1030 }
1031 break;
1033 if (file_path != NULL && selector->module != NULL &&
1034 asciichat_instr_path_contains_module(file_path, selector->module)) {
1035 if (selector->pattern == NULL ||
1036 (base_name != NULL && asciichat_instr_match_glob(selector->pattern, base_name))) {
1037 return true;
1038 }
1039 }
1040 break;
1041 default:
1042 break;
1043 }
1044 }
1045
1046 return false;
1047}
1048
1049static bool asciichat_instr_should_log(const asciichat_instr_runtime_t *runtime, const char *file_path,
1050 uint32_t line_number, const char *function_name) {
1051 if (!runtime->filters_enabled) {
1052 return true;
1053 }
1054
1055 if (runtime->filter_thread != NULL) {
1056 char tid_buf[32];
1057 safe_snprintf(tid_buf, sizeof(tid_buf), "%llu", (unsigned long long)runtime->thread_id);
1058 if (strstr(runtime->filter_thread, tid_buf) == NULL) {
1059 return false;
1060 }
1061 }
1062
1063 if (runtime->filter_include != NULL) {
1064 if (file_path == NULL || strstr(file_path, runtime->filter_include) == NULL) {
1065 return false;
1066 }
1067 }
1068
1069 if (runtime->filter_exclude != NULL && file_path != NULL) {
1070 if (strstr(file_path, runtime->filter_exclude) != NULL) {
1071 return false;
1072 }
1073 }
1074
1075#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
1076 if (runtime->include_regex_valid) {
1077 if (file_path == NULL || regexec(&runtime->include_regex, file_path, 0, NULL, 0) != 0) {
1078 return false;
1079 }
1080 }
1081
1082 if (runtime->exclude_regex_valid && file_path != NULL) {
1083 if (regexec(&runtime->exclude_regex, file_path, 0, NULL, 0) == 0) {
1084 return false;
1085 }
1086 }
1087#endif
1088
1089 if (runtime->filter_function_include != NULL) {
1090 if (function_name == NULL || strstr(function_name, runtime->filter_function_include) == NULL) {
1091 return false;
1092 }
1093 }
1094
1095 if (runtime->filter_function_exclude != NULL && function_name != NULL) {
1096 if (strstr(function_name, runtime->filter_function_exclude) != NULL) {
1097 return false;
1098 }
1099 }
1100
1101#if ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
1102 if (runtime->function_include_regex_valid) {
1103 if (function_name == NULL || regexec(&runtime->function_include_regex, function_name, 0, NULL, 0) != 0) {
1104 return false;
1105 }
1106 }
1107
1108 if (runtime->function_exclude_regex_valid && function_name != NULL) {
1109 if (regexec(&runtime->function_exclude_regex, function_name, 0, NULL, 0) == 0) {
1110 return false;
1111 }
1112 }
1113#endif
1114
1115 if (runtime->only_selectors.count > 0) {
1116 if (!asciichat_instr_only_selectors_match(runtime, file_path, function_name)) {
1117 return false;
1118 }
1119 }
1120
1121 (void)line_number;
1122 return true;
1123}
1124
1125static int asciichat_instr_write_full(int fd, const char *buffer, size_t len) {
1126 size_t total = 0;
1127 while (total < len) {
1128 ssize_t written = platform_write(fd, buffer + total, len - total);
1129 if (written < 0) {
1130 if (errno == EINTR) {
1131 continue;
1132 }
1133 return -1;
1134 }
1135 total += (size_t)written;
1136 }
1137 return 0;
1138}
โš™๏ธ Common definitions, error codes, macros, and types shared throughout the application
Cross-platform error handling utilities.
Cross-platform filesystem operations.
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:284
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_GETENV(name)
Definition common.h:434
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define SAFE_STRDUP(dst, src)
Definition common.h:418
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
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
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition util/time.c:150
void asciichat_instr_runtime_global_destroy(void)
Global shutdown of the instrumentation system.
bool asciichat_instr_coverage_enabled(void)
Check if coverage logging is enabled.
void asciichat_instr_runtime_destroy(asciichat_instr_runtime_t *runtime)
Destroy a runtime context and release resources.
void asciichat_instr_log_line(const char *file_path, uint32_t line_number, const char *function_name, const char *snippet, uint8_t is_macro_expansion)
Log a source line execution event.
void asciichat_instr_log_pc(uintptr_t program_counter)
Log a program counter for coverage analysis.
asciichat_instr_runtime_t * asciichat_instr_runtime_get(void)
Get or create the thread-local runtime context.
@ ASCII_INSTR_SOURCE_PRINT_MACRO_NONE
int ascii_tls_key_create(tls_key_t *key, void(*destructor)(void *))
Create a thread-local storage key.
Definition threading.c:97
int ascii_tls_set(tls_key_t key, void *value)
Set thread-local value for a key.
Definition threading.c:109
#define DIR_PERM_PRIVATE
Directory permission: Private (owner read/write/execute only)
Definition filesystem.h:199
bool platform_get_cwd(char *cwd, size_t path_size)
Get the current working directory of the process.
Definition util.c:110
pid_t platform_get_pid(void)
Get the current process ID.
Definition wasm/system.c:61
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
#define EINTR
int platform_is_directory(const char *path)
Check if a path is a directory.
#define PATH_DELIM
Platform-specific path separator character.
Definition filesystem.h:112
#define PLATFORM_O_CREAT
Definition filesystem.h:85
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
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)
#define PLATFORM_O_WRONLY
Definition filesystem.h:83
#define PLATFORM_O_BINARY
Definition filesystem.h:89
void * ascii_tls_get(tls_key_t key)
Get thread-local value for a key.
Definition threading.c:105
int ascii_tls_key_delete(tls_key_t key)
Delete a thread-local storage key.
Definition threading.c:101
asciichat_error_t platform_gtime(const time_t *timer, struct tm *result)
Platform-safe gmtime wrapper.
#define PLATFORM_O_APPEND
Definition filesystem.h:88
#define PLATFORM_O_EXCL
Definition filesystem.h:86
int errno
ssize_t platform_write(int fd, const void *buf, size_t count)
Safe file write (write replacement)
Definition wasm/system.c:72
uint64_t asciichat_thread_current_id(void)
Get the current thread's unique numeric ID.
Definition threading.c:92
#define STR_ZERO
String literal: "0" (zero)
#define STR_FALSE
String literal: "false".
#define STR_NO
String literal: "no".
#define STR_OFF
String literal: "off".
bool path_is_absolute(const char *path)
Determine whether a path is absolute on the current platform.
Definition path.c:739
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Validate and canonicalize a user-supplied filesystem path.
Definition path.c:1020
char * expand_path(const char *path)
Expand path with tilde (~) support.
Definition path.c:505
@ PATH_ROLE_LOG_FILE
Definition path.h:318
Platform initialization and static synchronization helpers.
๐Ÿงต Cross-platform thread interface for ascii-chat
asciichat_instr_selector_type
@ ASCII_INSTR_SOURCE_PRINT_SELECTOR_FUNCTION_GLOB
@ ASCII_INSTR_SOURCE_PRINT_SELECTOR_FILE_GLOB
@ ASCII_INSTR_SOURCE_PRINT_SELECTOR_FILE_SUBSTRING
@ ASCII_INSTR_SOURCE_PRINT_SELECTOR_MODULE
#define posix_write
struct asciichat_instr_runtime asciichat_instr_runtime_t
Per-thread instrumentation runtime state.
#define ASCII_INSTR_SOURCE_PRINT_MAX_SNIPPET
struct asciichat_instr_only_selector asciichat_instr_only_selector_t
Selector for filtering instrumentation output by file, function, or module.
#define ASCII_INSTR_SOURCE_PRINT_MAX_LINE
#define ASCII_INSTR_SOURCE_PRINT_DEFAULT_BASENAME
enum asciichat_instr_selector_type asciichat_instr_selector_type_t
struct asciichat_instr_only_list asciichat_instr_only_list_t
Dynamic array of instrumentation selectors for filtering output.
#define ASCII_INSTR_SOURCE_PRINT_HAVE_REGEX
๐Ÿ” Debug instrumentation logging runtime for ascii-chat line tracing
Capture stdout/stderr and redirect to logging system.
bool lifecycle_is_initialized(const lifecycle_t *lc)
Definition lifecycle.c:155
void lifecycle_init_commit(lifecycle_t *lc)
Definition lifecycle.c:90
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108
bool lifecycle_init_once(lifecycle_t *lc)
Definition lifecycle.c:52
Lock-free module lifecycle state machine using pure stdatomic.
#define LIFECYCLE_INIT
Static initializer for module-global lifecycle variables (no sync primitive)
Definition lifecycle.h:68
Application limits and constraints.
char file_path[PLATFORM_MAX_PATH_LENGTH]
Definition mmap.c:39
๐Ÿ“‚ Path Manipulation Utilities
Cross-platform mutex interface for ascii-chat.
PCRE2-based regex patterns for cryptographic parsing.
ClangTool/LibTooling compatibility shim for stdbool.h.
Dynamic array of instrumentation selectors for filtering output.
asciichat_instr_only_selector_t * items
Array of selectors.
size_t capacity
Allocated capacity.
size_t count
Number of active selectors.
Selector for filtering instrumentation output by file, function, or module.
asciichat_instr_selector_type_t type
Type of selector (file substring, glob, function glob, or module)
char * pattern
Pattern to match (file/function glob pattern)
Per-thread instrumentation runtime state.
const char * filter_exclude
File path substring to exclude (from env var)
bool function_exclude_regex_valid
Whether function_exclude_regex was successfully compiled.
asciichat_instr_only_list_t only_selectors
List of "only" selectors for filtering.
const char * filter_include
File path substring to include (from env var)
uint64_t call_counter
Total number of instrumentation calls.
char log_path[PATH_MAX]
Path to log file.
bool rate_enabled
Whether rate limiting is enabled.
regex_t function_exclude_regex
Compiled regex for function name exclusion.
uint64_t sequence
Sequence number for log entries.
regex_t exclude_regex
Compiled regex for file path exclusion.
bool function_include_regex_valid
Whether function_include_regex was successfully compiled.
uint32_t rate
Rate limiting: log every Nth call.
const char * filter_function_exclude
Function name substring to exclude (from env var)
const char * filter_function_include
Function name substring to include (from env var)
regex_t include_regex
Compiled regex for file path inclusion.
int fd
Log file descriptor (-1 if not open)
uint64_t thread_id
Thread ID.
regex_t function_include_regex
Compiled regex for function name inclusion.
const char * filter_thread
Thread ID filter (from env var)
bool stderr_fallback
Use stderr if log file can't be opened.
bool exclude_regex_valid
Whether exclude_regex was successfully compiled.
bool filters_enabled
Whether any filters are active.
bool include_regex_valid
Whether include_regex was successfully compiled.
Cross-platform system functions interface for ascii-chat.
โฑ๏ธ High-precision timing utilities using sokol_time.h and uthash