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