ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
report.c
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Summarizer for ascii-chat instrumentation runtime logs
3
4#include <ascii-chat/common.h>
5#include <ascii-chat/log/logging.h>
6#include <ascii-chat/platform/util.h>
7#include <ascii-chat/tooling/panic/instrument_log.h>
8
9#include <ascii-chat/uthash/uthash.h>
10#include <ascii-chat/util/parsing.h>
11
12#include <errno.h>
13#include <inttypes.h>
14#include <limits.h>
15#include <stdbool.h>
16#include <stdio.h>
17#include <stdlib.h>
18#include <string.h>
19
20#if defined(_WIN32)
21#include <io.h>
22#include <windows.h>
23#include <ascii-chat/platform/windows/getopt.h>
24#else
25#include <getopt.h>
26#include <dirent.h>
27#include <unistd.h>
28#endif
29
33typedef struct thread_filter_list {
34 uint64_t *values;
35 size_t count;
36 size_t capacity;
38
50
57typedef struct log_record {
58 uint64_t pid;
59 uint64_t tid;
60 uint64_t seq;
61 char *timestamp;
62 char *elapsed;
63 char *file;
64 uint32_t line;
65 char *function;
66 uint32_t macro_flag;
67 char *snippet;
68 char *raw_line;
70
77typedef struct thread_entry {
78 uint64_t thread_id;
80 UT_hash_handle hh;
82
83static const char *macro_flag_label(uint32_t flag) {
84 switch (flag) {
85 case ASCII_INSTR_SOURCE_PRINT_MACRO_EXPANSION:
86 return "expansion";
87 case ASCII_INSTR_SOURCE_PRINT_MACRO_INVOCATION:
88 return "invocation";
89 case ASCII_INSTR_SOURCE_PRINT_MACRO_NONE:
90 default:
91 return "none";
92 }
93}
94
95static void thread_filter_list_destroy(thread_filter_list_t *list) {
96 if (list == NULL) {
97 return;
98 }
99 SAFE_FREE(list->values);
100 list->values = NULL;
101 list->count = 0;
102 list->capacity = 0;
103}
104
105static bool thread_filter_list_append(thread_filter_list_t *list, uint64_t value) {
106 if (list == NULL) {
107 return false;
108 }
109
110 if (list->count == list->capacity) {
111 size_t new_capacity = list->capacity == 0 ? 4 : list->capacity * 2;
112 uint64_t *new_values = SAFE_REALLOC(list->values, new_capacity * sizeof(uint64_t), uint64_t *);
113 if (new_values == NULL) {
114 return false;
115 }
116 list->values = new_values;
117 list->capacity = new_capacity;
118 }
119
120 list->values[list->count++] = value;
121 return true;
122}
123
124static bool thread_filter_list_contains(const thread_filter_list_t *list, uint64_t value) {
125 if (list == NULL || list->count == 0) {
126 return true;
127 }
128
129 for (size_t i = 0; i < list->count; ++i) {
130 if (list->values[i] == value) {
131 return true;
132 }
133 }
134 return false;
135}
136
137static const char *resolve_default_log_dir(void) {
138 const char *dir = SAFE_GETENV("ASCII_INSTR_SOURCE_PRINT_OUTPUT_DIR");
139 if (dir != NULL && dir[0] != '\0') {
140 return dir;
141 }
142 dir = SAFE_GETENV("TMPDIR");
143 if (dir != NULL && dir[0] != '\0') {
144 return dir;
145 }
146 dir = SAFE_GETENV("TEMP");
147 if (dir != NULL && dir[0] != '\0') {
148 return dir;
149 }
150 dir = SAFE_GETENV("TMP");
151 if (dir != NULL && dir[0] != '\0') {
152 return dir;
153 }
154#if defined(_WIN32)
155 // Default to current directory on Windows (trace.log is usually in cwd)
156 return ".";
157#else
158 return "/tmp";
159#endif
160}
161
162static void free_record(log_record_t *record) {
163 if (record == NULL) {
164 return;
165 }
166 SAFE_FREE(record->timestamp);
167 SAFE_FREE(record->elapsed);
168 SAFE_FREE(record->file);
169 SAFE_FREE(record->function);
170 SAFE_FREE(record->snippet);
171 SAFE_FREE(record->raw_line);
172 memset(record, 0, sizeof(*record));
173}
174
175static void destroy_entries(thread_entry_t **entries) {
176 thread_entry_t *current;
177 thread_entry_t *tmp;
178 HASH_ITER(hh, *entries, current, tmp) {
179 HASH_DEL(*entries, current);
180 free_record(&current->record);
181 SAFE_FREE(current);
182 }
183}
184
185static char *duplicate_segment(const char *begin, size_t length) {
186 char *copy = SAFE_MALLOC(length + 1, char *);
187 if (copy == NULL) {
188 return NULL;
189 }
190 memcpy(copy, begin, length);
191 copy[length] = '\0';
192 return copy;
193}
194
195static bool extract_token(const char *line, const char *key, char **out_value) {
196 const char *position = strstr(line, key);
197 if (position == NULL) {
198 return false;
199 }
200 position += strlen(key);
201 const char *end = strchr(position, ' ');
202 if (end == NULL) {
203 end = line + strlen(line);
204 }
205 size_t length = (size_t)(end - position);
206 *out_value = duplicate_segment(position, length);
207 return *out_value != NULL;
208}
209
210static bool extract_snippet(const char *line, char **out_value) {
211 const char *position = strstr(line, "snippet=");
212 if (position == NULL) {
213 return false;
214 }
215 position += strlen("snippet=");
216 size_t length = strlen(position);
217 while (length > 0 && (position[length - 1] == '\n' || position[length - 1] == '\r')) {
218 length--;
219 }
220 *out_value = duplicate_segment(position, length);
221 return *out_value != NULL;
222}
223
224static bool extract_uint64(const char *line, const char *key, uint64_t *out_value) {
225 const char *position = strstr(line, key);
226 if (position == NULL) {
227 return false;
228 }
229 position += strlen(key);
230 unsigned long long value = 0;
231 asciichat_error_t result = parse_ulonglong(position, &value, 0, ULLONG_MAX);
232 if (result != ASCIICHAT_OK) {
233 return false;
234 }
235 *out_value = (uint64_t)value;
236 return true;
237}
238
239static bool extract_uint32(const char *line, const char *key, uint32_t *out_value) {
240 uint64_t tmp_value = 0;
241 if (!extract_uint64(line, key, &tmp_value)) {
242 return false;
243 }
244 if (tmp_value > UINT32_MAX) {
245 return false;
246 }
247 *out_value = (uint32_t)tmp_value;
248 return true;
249}
250
251static bool parse_log_line(const char *line, log_record_t *record) {
252 if (!extract_uint64(line, "pid=", &record->pid)) {
253 return false;
254 }
255 if (!extract_uint64(line, "tid=", &record->tid)) {
256 return false;
257 }
258 if (!extract_uint64(line, "seq=", &record->seq)) {
259 return false;
260 }
261 if (!extract_token(line, "ts=", &record->timestamp)) {
262 return false;
263 }
264 if (!extract_token(line, "elapsed=", &record->elapsed)) {
265 return false;
266 }
267 if (!extract_token(line, "file=", &record->file)) {
268 return false;
269 }
270 if (!extract_uint32(line, "line=", &record->line)) {
271 return false;
272 }
273 if (!extract_token(line, "func=", &record->function)) {
274 return false;
275 }
276 if (!extract_uint32(line, "macro=", &record->macro_flag)) {
277 return false;
278 }
279 if (!extract_snippet(line, &record->snippet)) {
280 return false;
281 }
282
283 size_t len = strlen(line);
284 record->raw_line = duplicate_segment(line, len);
285 return record->raw_line != NULL;
286}
287
288static bool record_matches_filters(const report_config_t *config, const log_record_t *record) {
289 if (config->include_filter != NULL && config->include_filter[0] != '\0') {
290 if (record->file == NULL || strstr(record->file, config->include_filter) == NULL) {
291 return false;
292 }
293 }
294
295 if (config->exclude_filter != NULL && config->exclude_filter[0] != '\0') {
296 if (record->file != NULL && strstr(record->file, config->exclude_filter) != NULL) {
297 return false;
298 }
299 }
300
301 if (!thread_filter_list_contains(&config->threads, record->tid)) {
302 return false;
303 }
304
305 return true;
306}
307
308static void update_entry(thread_entry_t **entries, const log_record_t *record) {
309 thread_entry_t *entry = NULL;
310 HASH_FIND(hh, *entries, &record->tid, sizeof(uint64_t), entry);
311 if (entry == NULL) {
312 entry = SAFE_CALLOC(1, sizeof(*entry), thread_entry_t *);
313 entry->thread_id = record->tid;
314 entry->record = *record;
315 HASH_ADD(hh, *entries, thread_id, sizeof(uint64_t), entry);
316 return;
317 }
318
319 if (record->seq >= entry->record.seq) {
320 free_record(&entry->record);
321 entry->record = *record;
322 } else {
323 free_record((log_record_t *)record);
324 }
325}
326
327static int compare_entries(const void *lhs, const void *rhs) {
328 const thread_entry_t *const *a = lhs;
329 const thread_entry_t *const *b = rhs;
330 if ((*a)->thread_id < (*b)->thread_id) {
331 return -1;
332 }
333 if ((*a)->thread_id > (*b)->thread_id) {
334 return 1;
335 }
336 return 0;
337}
338
339static void print_summary(const report_config_t *config, thread_entry_t **entries) {
340 size_t count = HASH_COUNT(*entries);
341 if (count == 0) {
342 printf("No instrumentation records matched the given filters.\n");
343 return;
344 }
345
346 thread_entry_t **sorted = SAFE_CALLOC(count, sizeof(*sorted), thread_entry_t **);
347 size_t index = 0;
348 thread_entry_t *entry = NULL;
349 thread_entry_t *tmp = NULL;
350 HASH_ITER(hh, *entries, entry, tmp) {
351 sorted[index++] = entry;
352 }
353
354 qsort(sorted, count, sizeof(*sorted), compare_entries);
355
356 printf("Latest instrumentation record per thread (%zu thread%s)\n", count, count == 1 ? "" : "s");
357 printf("======================================================================\n");
358 for (size_t i = 0; i < count; ++i) {
359 const log_record_t *record = &sorted[i]->record;
360 if (config->emit_raw_line) {
361 printf("%s\n", record->raw_line);
362 continue;
363 }
364
365 printf("tid=%" PRIu64 " seq=%" PRIu64 " pid=%" PRIu64 "\n", record->tid, record->seq, record->pid);
366 printf(" timestamp : %s\n", record->timestamp);
367 printf(" elapsed : %s\n", record->elapsed);
368 printf(" location : %s:%u\n", record->file != NULL ? record->file : "<unknown>", record->line);
369 printf(" function : %s\n", record->function != NULL ? record->function : "<unknown>");
370 printf(" macro : %s (%u)\n", macro_flag_label(record->macro_flag), record->macro_flag);
371 printf(" snippet : %s\n", record->snippet != NULL ? record->snippet : "<missing>");
372 printf("----------------------------------------------------------------------\n");
373 }
374
375 SAFE_FREE(sorted);
376}
377
378static void usage(FILE *stream, const char *program) {
379 fprintf(stream,
380 "Usage: %s [options]\n"
381 " --log-file <path> Single log file to analyze (e.g., trace.log)\n"
382 " --log-dir <path> Directory containing ascii-instr-*.log files (default: resolve from environment)\n"
383 " --thread <id> Limit to specific thread ID (repeatable)\n"
384 " --include <substr> Include records whose file path contains substring\n"
385 " --exclude <substr> Exclude records whose file path contains substring\n"
386 " --raw Emit raw log lines instead of formatted summary\n"
387 " --help Show this help and exit\n",
388 program);
389}
390
391// Simple pre-scan for --log-file to avoid corrupted optarg from instrumented getopt
392// This is a workaround for stack corruption caused by panic instrumentation
393static const char *prescan_log_file(int argc, char **argv) {
394 for (int i = 1; i < argc; i++) {
395 if (strcmp(argv[i], "--log-file") == 0 && i + 1 < argc) {
396 return argv[i + 1];
397 }
398 // Handle --log-file=path format
399 if (strncmp(argv[i], "--log-file=", 11) == 0) {
400 return argv[i] + 11;
401 }
402 }
403 return NULL;
404}
405
406// Check if only --log-file is specified (to skip getopt entirely)
407static bool is_simple_log_file_invocation(int argc, char **argv) {
408 // Expected patterns: program --log-file path (argc=3) or program --log-file=path (argc=2)
409 if (argc == 3 && strcmp(argv[1], "--log-file") == 0) {
410 return true;
411 }
412 if (argc == 2 && strncmp(argv[1], "--log-file=", 11) == 0) {
413 return true;
414 }
415 return false;
416}
417
418static bool parse_arguments(int argc, char **argv, report_config_t *config) {
419 // Pre-scan for --log-file before getopt (workaround for instrumentation bug)
420 const char *log_file_arg = prescan_log_file(argc, argv);
421 if (log_file_arg != NULL) {
422 config->log_file = log_file_arg;
423 }
424
425 // If just --log-file with no other args, skip getopt entirely
426 // (instrumented getopt corrupts memory)
427 if (config->log_file != NULL && is_simple_log_file_invocation(argc, argv)) {
428 return true;
429 }
430
431 static const struct option kLongOptions[] = {
432 {"log-file", required_argument, NULL, 'f'}, {"log-dir", required_argument, NULL, 'd'},
433 {"thread", required_argument, NULL, 't'}, {"include", required_argument, NULL, 'i'},
434 {"exclude", required_argument, NULL, 'x'}, {"raw", no_argument, NULL, 'r'},
435 {"help", no_argument, NULL, 'h'}, {0, 0, 0, 0},
436 };
437
438 int option = 0;
439 while ((option = getopt_long(argc, argv, "", kLongOptions, NULL)) != -1) {
440 switch (option) {
441 case 'f':
442 // Already handled by prescan - skip (optarg may be corrupted)
443 break;
444 case 'd':
445 config->log_dir = optarg;
446 break;
447 case 't': {
448 unsigned long long tid_value = 0;
449 asciichat_error_t result = parse_ulonglong(optarg, &tid_value, 0, ULLONG_MAX);
450 if (result != ASCIICHAT_OK) {
451 log_error("Invalid thread id: %s", optarg);
452 return false;
453 }
454 if (!thread_filter_list_append(&config->threads, (uint64_t)tid_value)) {
455 log_error("Failed to record thread filter");
456 return false;
457 }
458 break;
459 }
460 case 'i':
461 config->include_filter = optarg;
462 break;
463 case 'x':
464 config->exclude_filter = optarg;
465 break;
466 case 'r':
467 config->emit_raw_line = true;
468 break;
469 case 'h':
470 usage(stdout, argv[0]);
471 return false;
472 default:
473 usage(stderr, argv[0]);
474 return false;
475 }
476 }
477
478 if (optind < argc) {
479 log_error("Unexpected positional argument: %s", argv[optind]);
480 usage(stderr, argv[0]);
481 return false;
482 }
483
484 // Validate --log-file and --log-dir are mutually exclusive
485 if (config->log_file != NULL && config->log_dir != NULL) {
486 log_error("Cannot specify both --log-file and --log-dir");
487 return false;
488 }
489
490 // Fall back to default log directory only if no log file specified
491 if (config->log_file == NULL && config->log_dir == NULL) {
492 config->log_dir = resolve_default_log_dir();
493 }
494
495 return true;
496}
497
498static bool process_file(const report_config_t *config, const char *path, thread_entry_t **entries) {
499 FILE *handle = platform_fopen(path, "r");
500 if (handle == NULL) {
501 log_warn("Cannot open log file '%s': %s", path, SAFE_STRERROR(errno));
502 return false;
503 }
504
505 char buffer[8192];
506 while (fgets(buffer, sizeof(buffer), handle) != NULL) {
507 log_record_t record = {0};
508 if (!parse_log_line(buffer, &record)) {
509 free_record(&record);
510 continue;
511 }
512 if (!record_matches_filters(config, &record)) {
513 free_record(&record);
514 continue;
515 }
516 update_entry(entries, &record);
517 }
518
519 fclose(handle);
520 return true;
521}
522
523#if defined(_WIN32)
524static bool build_windows_glob_pattern(char *buffer, size_t capacity, const char *directory, const char *suffix) {
525 if (buffer == NULL || directory == NULL || suffix == NULL) {
526 return false;
527 }
528
529 size_t length = 0;
530 for (const char *cursor = directory; *cursor != '\0'; ++cursor) {
531 char ch = *cursor == '/' ? '\\' : *cursor;
532 if (length + 1 >= capacity) {
533 return false;
534 }
535 buffer[length++] = ch;
536 }
537 if (length == 0) {
538 return false;
539 }
540 if (buffer[length - 1] != '\\') {
541 if (length + 1 >= capacity) {
542 return false;
543 }
544 buffer[length++] = '\\';
545 }
546 const size_t suffix_length = strlen(suffix);
547 if (length + suffix_length >= capacity) {
548 return false;
549 }
550 memcpy(buffer + length, suffix, suffix_length + 1);
551 return true;
552}
553#endif
554
555static bool collect_entries(const report_config_t *config, thread_entry_t **entries) {
556 // If a single log file is specified, just process that file
557 if (config->log_file != NULL) {
558 return process_file(config, config->log_file, entries);
559 }
560
561#if defined(_WIN32)
562 char pattern[MAX_PATH];
563 if (!build_windows_glob_pattern(pattern, sizeof(pattern), config->log_dir, "ascii-instr-*.log")) {
564 log_error("Instrumentation log directory path is too long: %s", config->log_dir);
565 return false;
566 }
567
568 struct _finddata_t data = {0};
569 intptr_t handle = _findfirst(pattern, &data);
570 if (handle == -1) {
571 log_error("Unable to open instrumentation log directory '%s': %s", config->log_dir, SAFE_STRERROR(errno));
572 return false;
573 }
574
575 bool success = true;
576 do {
577 if ((data.attrib & _A_SUBDIR) != 0) {
578 continue;
579 }
580 size_t name_len = strlen(data.name);
581 if (name_len < 4 || strcmp(data.name + name_len - 4, ".log") != 0) {
582 continue;
583 }
584
585 char path_buffer[MAX_PATH];
586 int written = safe_snprintf(path_buffer, sizeof(path_buffer), "%s/%s", config->log_dir, data.name);
587 if (written < 0 || written >= (int)sizeof(path_buffer)) {
588 log_warn("Skipping path that exceeds buffer: %s/%s", config->log_dir, data.name);
589 continue;
590 }
591 if (!process_file(config, path_buffer, entries)) {
592 success = false;
593 }
594 } while (_findnext(handle, &data) == 0);
595
596 _findclose(handle);
597 return success;
598#else
599 DIR *directory = opendir(config->log_dir);
600 if (directory == NULL) {
601 log_error("Unable to open instrumentation log directory '%s': %s", config->log_dir, SAFE_STRERROR(errno));
602 return false;
603 }
604
605 struct dirent *entry = NULL;
606 while ((entry = readdir(directory)) != NULL) {
607 if (entry->d_name[0] == '.') {
608 continue;
609 }
610 if (strncmp(entry->d_name, "ascii-instr-", strlen("ascii-instr-")) != 0) {
611 continue;
612 }
613 size_t name_length = strlen(entry->d_name);
614 if (name_length < 5 || strcmp(entry->d_name + name_length - 4, ".log") != 0) {
615 continue;
616 }
617
618 char path_buffer[PATH_MAX];
619 int written = safe_snprintf(path_buffer, sizeof(path_buffer), "%s/%s", config->log_dir, entry->d_name);
620 if (written < 0 || written >= (int)sizeof(path_buffer)) {
621 log_warn("Skipping path that exceeds buffer: %s/%s", config->log_dir, entry->d_name);
622 continue;
623 }
624
625 process_file(config, path_buffer, entries);
626 }
627
628 closedir(directory);
629 return true;
630#endif
631}
632
633int main(int argc, char **argv) {
634 report_config_t config = {
635 .log_dir = NULL,
636 .log_file = NULL,
637 .include_filter = NULL,
638 .exclude_filter = NULL,
639 .threads = {.values = NULL, .count = 0, .capacity = 0},
640 .emit_raw_line = false,
641 };
642
643 log_init(NULL, LOG_INFO, false, false);
644
645 int exit_code = EXIT_SUCCESS;
646 if (!parse_arguments(argc, argv, &config)) {
647 exit_code = ERROR_USAGE;
648 goto cleanup;
649 }
650
651 thread_entry_t *entries = NULL;
652 if (!collect_entries(&config, &entries)) {
653 exit_code = ERROR_GENERAL;
654 destroy_entries(&entries);
655 goto cleanup;
656 }
657
658 print_summary(&config, &entries);
659 destroy_entries(&entries);
660
661cleanup:
662 thread_filter_list_destroy(&config.threads);
663 log_destroy();
664 return exit_code;
665}
int thread_id
void log_destroy(void)
void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap)
void usage(FILE *desc, asciichat_mode_t mode)
asciichat_error_t parse_ulonglong(const char *str, unsigned long long *out_value, unsigned long long min_value, unsigned long long max_value)
Definition parsing.c:181
struct report_config report_config_t
Configuration for panic report generation.
struct thread_entry thread_entry_t
Hash table entry for thread-local last log record.
struct thread_filter_list thread_filter_list_t
Dynamic array of thread IDs for filtering instrumentation logs.
int main(int argc, char **argv)
Definition report.c:633
struct log_record log_record_t
Parsed instrumentation log record.
Parsed instrumentation log record.
Definition report.c:57
uint64_t pid
Process ID.
Definition report.c:58
char * function
Function name.
Definition report.c:65
char * file
Source file path.
Definition report.c:63
char * snippet
Code snippet.
Definition report.c:67
uint32_t macro_flag
Macro expansion flag.
Definition report.c:66
uint32_t line
Source line number.
Definition report.c:64
char * elapsed
Elapsed time since start.
Definition report.c:62
char * raw_line
Original raw log line.
Definition report.c:68
uint64_t tid
Thread ID.
Definition report.c:59
char * timestamp
Timestamp string.
Definition report.c:61
uint64_t seq
Sequence number.
Definition report.c:60
Configuration for panic report generation.
Definition report.c:42
const char * exclude_filter
File path substring filter for exclusion.
Definition report.c:46
const char * log_file
Single log file path (alternative to log_dir)
Definition report.c:44
bool emit_raw_line
Whether to emit raw log lines in output.
Definition report.c:48
thread_filter_list_t threads
Thread IDs to include in report.
Definition report.c:47
const char * log_dir
Directory containing instrumentation log files.
Definition report.c:43
const char * include_filter
File path substring filter for inclusion.
Definition report.c:45
Hash table entry for thread-local last log record.
Definition report.c:77
uint64_t thread_id
Thread ID (hash key)
Definition report.c:78
log_record_t record
Last log record from this thread.
Definition report.c:79
UT_hash_handle hh
uthash handle
Definition report.c:80
Dynamic array of thread IDs for filtering instrumentation logs.
Definition report.c:33
uint64_t * values
Array of thread IDs.
Definition report.c:34
size_t count
Number of thread IDs.
Definition report.c:35
size_t capacity
Allocated capacity.
Definition report.c:36
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
FILE * platform_fopen(const char *filename, const char *mode)