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

ascii-chat Unified Binary - Mode Dispatcher and Entry Point More...

Go to the source code of this file.

Data Structures

struct  mode_descriptor_t
 Mode descriptor for string conversion. More...
 

Macros

#define APP_NAME   "ascii-chat"
 
#define VERSION   ASCII_CHAT_VERSION_FULL
 

Typedefs

typedef int(* mode_entry_point_t) (void)
 

Functions

bool should_exit (void)
 
void signal_exit (void)
 
void set_interrupt_callback (void(*cb)(void))
 
void setup_signal_handlers (void)
 
int main (int argc, char *argv[])
 

Detailed Description

ascii-chat Unified Binary - Mode Dispatcher and Entry Point

This file implements the main entry point for the unified ascii-chat binary, which provides server, client, mirror, and discovery service functionality in a single executable.

The dispatcher now delegates ALL option parsing (including mode detection and binary-level options) to options_init(), then simply dispatches to the appropriate mode-specific entry point based on the detected mode.

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
2025
Version
3.0

Definition in file main.c.

Macro Definition Documentation

◆ APP_NAME

#define APP_NAME   "ascii-chat"

Definition at line 73 of file main.c.

◆ VERSION

#define VERSION   ASCII_CHAT_VERSION_FULL

Definition at line 74 of file main.c.

Typedef Documentation

◆ mode_entry_point_t

typedef int(* mode_entry_point_t) (void)

Definition at line 183 of file main.c.

Function Documentation

◆ main()

int main ( int  argc,
char *  argv[] 
)

Definition at line 281 of file main.c.

281 {
282 // Set global argc/argv for early argv inspection (e.g., in terminal.c)
283 g_argc = argc;
284 g_argv = argv;
285 // Validate basic argument structure
286 if (argc < 1 || argv == NULL || argv[0] == NULL) {
287 fprintf(stderr, "Error: Invalid argument vector\n");
288 return 1;
289 }
290
291 // VERY FIRST: Scan for --color BEFORE ANY logging initialization
292 // This sets global flags that persist through cleanup, enabling --color to force colors
293 extern bool g_color_flag_passed;
294 extern bool g_color_flag_value;
295 for (int i = 1; i < argc; i++) {
296 if (strcmp(argv[i], "--color") == 0 || strcmp(argv[i], "--color=true") == 0) {
297 g_color_flag_passed = true;
298 g_color_flag_value = true;
299 break;
300 } else if (strcmp(argv[i], "--color=false") == 0) {
301 g_color_flag_passed = true;
302 g_color_flag_value = false;
303 break;
304 } else if (strncmp(argv[i], "--color=", 8) == 0) {
305 g_color_flag_passed = true;
306 // Default to true for any other --color=X value
307 g_color_flag_value = true;
308 break;
309 }
310 }
311
312 // SECOND: Scan for --grep BEFORE ANY logging initialization
313 // This ensures ALL logs (including from shared_init) can be filtered
314 // Supports multiple --grep patterns (ORed together)
315 for (int i = 1; i < argc - 1; i++) {
316 if (strcmp(argv[i], "--grep") == 0) {
317 const char *pattern = argv[i + 1];
318 asciichat_error_t filter_result = grep_init(pattern);
319 if (filter_result != ASCIICHAT_OK) {
320 fprintf(stderr,
321 "ERROR: Invalid --grep pattern or invalid flags: \"%s\" - use /pattern/flags format (e.g., "
322 "\"/query/ig\" or \"/literal/F\")\n",
323 pattern);
324 return 1;
325 }
326 i++; // Skip the pattern argument
327 }
328 }
329
330 // Detect terminal capabilities early so colored help output works
331 // Logging will be initialized by asciichat_shared_init() before options_init()
333 terminal_capabilities_t caps = detect_terminal_capabilities();
334 caps = apply_color_mode_override(caps);
335
336 // Warn if Release build was built from dirty working tree
337#if ASCII_CHAT_GIT_IS_DIRTY
338 if (strcmp(ASCII_CHAT_BUILD_TYPE, "Release") == 0) {
339 fprintf(stderr, "⚠️ WARNING: This Release build was compiled from a dirty git working tree!\n");
340 fprintf(stderr, " Git commit: %s (dirty)\n", ASCII_CHAT_GIT_COMMIT_HASH);
341 fprintf(stderr, " Build date: %s\n", ASCII_CHAT_BUILD_DATE);
342 fprintf(stderr, " For reproducible builds, commit or stash changes before building.\n\n");
343 }
344#endif
345
346 // Load color scheme early (from config files and CLI) before logging initialization
347 // This allows logging to use the correct colors from the start
349
350 // Parse --color-scheme from argv early to set logging colors for help output
351 const char *colorscheme_name = "pastel"; // default
352 for (int i = 1; i < argc - 1; i++) {
353 if (strcmp(argv[i], "--color-scheme") == 0) {
354 colorscheme_name = argv[i + 1];
355 break;
356 }
357 }
358
359 // Load and apply colorscheme to logging BEFORE options_init() so help gets colors
360 color_scheme_t scheme;
361 if (colorscheme_load_builtin(colorscheme_name, &scheme) == ASCIICHAT_OK) {
362 log_set_color_scheme(&scheme);
363 } else {
364 // Fall back to default pastel if scheme not found
365 if (colorscheme_load_builtin("pastel", &scheme) == ASCIICHAT_OK) {
366 log_set_color_scheme(&scheme);
367 }
368 }
369
370 // Initialize logging colors so they're ready for help output
372
373 // EARLY PARSE: Find the mode position (first positional argument)
374 // Binary-level options must appear BEFORE the mode
375 int mode_position = -1;
376 for (int i = 1; i < argc; i++) {
377 if (argv[i][0] != '-') {
378 // Found a positional argument - this is the mode or session string
379 mode_position = i;
380 break;
381 }
382
383 // Skip option and its argument if needed
384 // Check if this is an option that takes a required argument
385 const char *arg = argv[i];
386 const char *opt_name = arg;
387 if (arg[0] == '-') {
388 opt_name = arg + (arg[1] == '-' ? 2 : 1);
389 }
390
391 // Handle --option=value format
392 if (strchr(opt_name, '=')) {
393 continue; // Value is part of this arg, no need to skip next
394 }
395
396 // Options that take required arguments
397 if (strcmp(arg, "--log-file") == 0 || strcmp(arg, "-L") == 0 || strcmp(arg, "--log-level") == 0 ||
398 strcmp(arg, "--config") == 0 || strcmp(arg, "--color-scheme") == 0 || strcmp(arg, "--log-template") == 0) {
399 if (i + 1 < argc) {
400 i++; // Skip the argument value
401 }
402 }
403 }
404
405 // EARLY PARSE: Determine mode from argv to know if this is client-like mode
406 // The first positional argument (after options) is the mode: client, server, mirror, discovery, acds
407 bool is_client_like_mode = false;
408 if (mode_position > 0) {
409 const char *first_arg = argv[mode_position];
410 if (strcmp(first_arg, "client") == 0 || strcmp(first_arg, "mirror") == 0 || strcmp(first_arg, "discovery") == 0) {
411 is_client_like_mode = true;
412 }
413 }
414
415 // EARLY PARSE: Extract log file from argv (--log-file or -L)
416 // Must appear BEFORE the mode
417 const char *log_file = "ascii-chat.log"; // default
418 int max_search = (mode_position > 0) ? mode_position : argc;
419 for (int i = 1; i < max_search - 1; i++) {
420 if ((strcmp(argv[i], "--log-file") == 0 || strcmp(argv[i], "-L") == 0)) {
421 log_file = argv[i + 1];
422 break;
423 }
424 }
425
426 // EARLY PARSE: Check for --json (JSON logging format)
427 // If JSON format is enabled, we'll use the JSON filename during early init
428 // --json MUST appear BEFORE the mode
429 bool early_json_format = false;
430 if (mode_position > 0) {
431 for (int i = 1; i < mode_position; i++) {
432 if (strcmp(argv[i], "--json") == 0) {
433 early_json_format = true;
434 break;
435 }
436 }
437 } else {
438 // If no mode found, search entire argv
439 for (int i = 1; i < argc; i++) {
440 if (strcmp(argv[i], "--json") == 0) {
441 early_json_format = true;
442 break;
443 }
444 }
445 }
446
447 // EARLY PARSE: Extract log template from argv (--log-template)
448 // Note: This is for format templates, different from --log-format which is the output format
449 // Binary-level options must appear BEFORE the mode
450 const char *early_log_template = NULL;
451 bool early_log_template_console_only = false;
452 for (int i = 1; i < max_search - 1; i++) {
453 if (strcmp(argv[i], "--log-template") == 0) {
454 early_log_template = argv[i + 1];
455 break;
456 }
457 }
458 for (int i = 1; i < max_search; i++) {
459 if (strcmp(argv[i], "--log-format-console-only") == 0) {
460 early_log_template_console_only = true;
461 break;
462 }
463 }
464
465 // Initialize shared subsystems BEFORE options_init()
466 // This ensures options parsing can use properly configured logging with colors
467 // If JSON format is requested, don't write text logs to file
468 // All logs will be JSON formatted later once options_init() runs
469 const char *early_log_file = early_json_format ? NULL : log_file;
470 asciichat_error_t init_result = asciichat_shared_init(early_log_file, is_client_like_mode);
471 if (init_result != ASCIICHAT_OK) {
472 return init_result;
473 }
474
475 // Route logs to stderr if stdout is piped (MUST happen early, before options_init logs)
476 // This keeps stdout clean for data output (e.g., --snapshot mode piped to file)
479 }
480
481 // Register cleanup of shared subsystems to run on normal exit
482 // Library code doesn't call atexit() - the application is responsible
483 (void)atexit(asciichat_shared_destroy);
484
485 // SECRET: Check for --backtrace (debug builds only) BEFORE options_init()
486 // Prints a backtrace and exits immediately - useful for debugging hangs
487#ifndef NDEBUG
488 for (int i = 1; i < argc; i++) {
489 if (strcmp(argv[i], "--backtrace") == 0) {
490 log_info("=== Backtrace at startup ===");
492 log_info("=== End Backtrace ===");
494 return 0;
495 }
496 }
497#endif
498
499 // NOW parse all options - can use logging with colors!
500 asciichat_error_t options_result = options_init(argc, argv);
501 if (options_result != ASCIICHAT_OK) {
502 asciichat_error_context_t error_ctx;
503 if (HAS_ERRNO(&error_ctx)) {
504 fprintf(stderr, "Error: %s\n", error_ctx.context_message);
505 } else {
506 fprintf(stderr, "Error: Failed to initialize options\n");
507 }
508 (void)fflush(stderr);
509
510 // Clean up options state before exiting
513
514 exit(options_result);
515 }
516
517 // Get parsed options
518 const options_t *opts = options_get();
519 if (!opts) {
520 fprintf(stderr, "Error: Options not initialized\n");
521 return 1;
522 }
523
524 // Determine final log file path (use mode-specific default from options if available)
525 // Determine output format early to decide on filename and logging strategy
526 bool use_json_logging = GET_OPTION(json);
527
528 // Determine the log filename
529 // Check if user explicitly passed --log-file (not just the mode-specific default from options_init)
530 bool user_specified_log_file = false;
531 for (int i = 1; i < argc - 1; i++) {
532 if (strcmp(argv[i], "--log-file") == 0 || strcmp(argv[i], "-L") == 0) {
533 user_specified_log_file = true;
534 break;
535 }
536 }
537
538 const char *final_log_file = opts->log_file;
539 char json_filename_buf[256];
540
541 if (use_json_logging) {
542 // When JSON logging is enabled, determine JSON output filename
543 if (user_specified_log_file) {
544 // User explicitly specified a log file - use it exactly for JSON output
545 SAFE_STRNCPY(json_filename_buf, final_log_file, sizeof(json_filename_buf) - 1);
546 } else {
547 // Using default: replace .log with .json in the mode-specific default
548 // e.g., server.log -> server.json, mirror.log -> mirror.json, etc.
549 size_t len = strlen(final_log_file);
550 if (len > 4 && strcmp(&final_log_file[len - 4], ".log") == 0) {
551 // File ends with .log - replace with .json
552 SAFE_STRNCPY(json_filename_buf, final_log_file, sizeof(json_filename_buf) - 1);
553 // Replace .log with .json
554 strcpy(&json_filename_buf[len - 4], ".json");
555 } else {
556 // File doesn't end with .log - just append .json
557 SAFE_STRNCPY(json_filename_buf, final_log_file, sizeof(json_filename_buf) - 1);
558 strncat(json_filename_buf, ".json", sizeof(json_filename_buf) - strlen(json_filename_buf) - 1);
559 }
560 }
561 final_log_file = json_filename_buf;
562 // For JSON mode: Initialize logging without file (text will be disabled)
563 // We'll set up JSON output separately below
564 log_init(NULL, GET_OPTION(log_level), false, false);
565 } else {
566 // Text logging mode: use the file from options (which is mode-specific default or user-specified)
567 log_init(final_log_file, GET_OPTION(log_level), false, false);
568 }
569
570 // Apply custom log template if specified (use early parsed value if available, otherwise use options)
571 const char *final_format = early_log_template ? early_log_template : GET_OPTION(log_template);
572 bool final_format_console_only =
573 early_log_template ? early_log_template_console_only : GET_OPTION(log_format_console_only);
574 if (final_format && final_format[0] != '\0') {
575 asciichat_error_t fmt_result = log_set_format(final_format, final_format_console_only);
576 if (fmt_result != ASCIICHAT_OK) {
577 log_error("Failed to apply custom log format");
578 }
579 }
580
581 // Configure JSON output if requested
582 if (use_json_logging) {
583 // Open the JSON file for output
584 int json_fd = platform_open(final_log_file, O_CREAT | O_RDWR | O_TRUNC, FILE_PERM_PRIVATE);
585 if (json_fd >= 0) {
586 log_set_json_output(json_fd);
587 } else {
588 // Failed to open JSON file - report error and exit
589 SET_ERRNO(ERROR_CONFIG, "Failed to open JSON output file: %s", final_log_file);
590 return ERROR_CONFIG;
591 }
592 }
593
594 // Initialize colors now that logging is fully initialized
595 // This must happen after log_init() since log_init_colors() checks if g_log.initialized
597
598 // Apply quiet mode - disables terminal output
599 // Status screen mode only disables terminal output if terminal is interactive
600 // In non-interactive mode (piped output), logs go to stdout/stderr normally
601 if (GET_OPTION(quiet) ||
602 (opts->detected_mode == MODE_SERVER && GET_OPTION(status_screen) && terminal_is_interactive())) {
604 }
605
606 // Initialize palette based on command line options
607 const char *custom_chars = opts && opts->palette_custom_set ? opts->palette_custom : NULL;
608 if (apply_palette_config(GET_OPTION(palette_type), custom_chars) != 0) {
609 FATAL(ERROR_CONFIG, "Failed to apply palette configuration");
610 }
611
612 // Set quiet mode for memory debugging
613#if defined(DEBUG_MEMORY) && !defined(USE_MIMALLOC_DEBUG) && !defined(NDEBUG)
614 debug_memory_set_quiet_mode(GET_OPTION(quiet));
615#endif
616
617 // Truncate log if it's already too large
619
620 // Handle --help and --version (these are detected and flagged by options_init)
621 // Terminal capabilities already initialized before options_init() at startup
622 if (opts->help) {
623 print_usage(opts->detected_mode);
624 fflush(NULL);
625 _Exit(0);
626 }
627
628 if (opts->version) {
631 // action_show_version() calls _Exit(), so we don't reach here
632 }
633
634 // For server mode with status screen: disable terminal output only if interactive
635 // In non-interactive mode (piped output), logs go to stdout/stderr normally
636 // The status screen (when shown) will capture and display logs in its buffer instead
637 if (opts->detected_mode == MODE_SERVER && opts->status_screen && terminal_is_interactive()) {
639 }
640
641 log_dev("Logging initialized to %s", final_log_file);
642
643 // Note: We do NOT auto-disable colors when stdout appears to be piped, because:
644 // 1. Tools like ripgrep can display ANSI colors when piped
645 // 2. Sandboxed/containerized environments may report false positives for isatty()
646 // 3. Users can explicitly disable colors with --color=false if needed
647 // Color behavior is now fully controlled by --color and --color-mode options.
648
649#ifndef NDEBUG
650 // Initialize lock debugging system after logging is fully set up
651 log_debug("Initializing lock debug system...");
652 int lock_debug_result = lock_debug_init();
653 if (lock_debug_result != 0) {
654 LOG_ERRNO_IF_SET("Lock debug system initialization failed");
655 FATAL(ERROR_PLATFORM_INIT, "Lock debug system initialization failed");
656 }
657 log_debug("Lock debug system initialized successfully");
658
659 // Start lock debug thread in all modes (not just server)
660 if (lock_debug_start_thread() != 0) {
661 LOG_ERRNO_IF_SET("Lock debug thread startup failed");
662 FATAL(ERROR_THREAD, "Lock debug thread startup failed");
663 }
664 log_debug("Lock debug thread started");
665
666#ifndef _WIN32
667 // Register SIGUSR1 to trigger lock state printing in all modes
668 platform_signal(SIGUSR1, common_handle_sigusr1);
669#endif
670#endif
671
672 if (opts->fps > 0) {
673 if (opts->fps < 1 || opts->fps > 144) {
674 log_warn("FPS value %d out of range (1-144), using default", opts->fps);
675 } else {
676 log_debug("FPS set from command line: %d", opts->fps);
677 }
678 }
679
680 // Automatic update check at startup (once per week maximum)
681 if (!GET_OPTION(no_check_update)) {
682 update_check_result_t update_result;
683 asciichat_error_t update_err = update_check_startup(&update_result);
684 if (update_err == ASCIICHAT_OK && update_result.update_available) {
685 char notification[1024];
686 update_check_format_notification(&update_result, notification, sizeof(notification));
687 log_info("%s", notification);
688
689 // Set update notification for splash/status screens
690 splash_set_update_notification(notification);
691 }
692 }
693
694 // Set up global signal handlers BEFORE mode dispatch
695 // All modes use the same centralized exit mechanism
697
698 // Register SIGUSR1 for lock debugging in debug builds (uses shared handler)
699#ifndef NDEBUG
700#ifndef _WIN32
701 platform_signal(SIGUSR1, common_handle_sigusr1);
702#endif
703#endif
704
705 // Find and dispatch to mode entry point
706 const mode_descriptor_t *mode = find_mode(opts->detected_mode);
707 if (!mode) {
708 fprintf(stderr, "Error: Mode not found for detected_mode=%d\n", opts->detected_mode);
709 return 1;
710 }
711
712 // Call the mode-specific entry point
713 // Mode entry points use options_get() to access parsed options
714 int exit_code = mode->entry_point();
715
716 if (exit_code == ERROR_USAGE) {
717 exit(ERROR_USAGE);
718 }
719
720 return exit_code;
721}
asciichat_error_t options_colorscheme_init_early(int argc, const char *const argv[])
Initialize color scheme early (before logging)
asciichat_error_t colorscheme_load_builtin(const char *name, color_scheme_t *scheme)
ASCIICHAT_API bool g_color_flag_value
Definition common.c:50
ASCIICHAT_API bool g_color_flag_passed
Definition common.c:49
ASCIICHAT_API char ** g_argv
Definition common.c:46
asciichat_error_t asciichat_shared_init(const char *log_file, bool is_client)
Definition common.c:109
ASCIICHAT_API int g_argc
Definition common.c:45
void asciichat_shared_destroy(void)
Clean up all shared library subsystems.
Definition common.c:164
asciichat_error_t grep_init(const char *pattern)
Definition grep.c:549
asciichat_error_t options_init(int argc, char **argv)
int lock_debug_init(void)
Definition lock.c:1364
int lock_debug_start_thread(void)
Definition lock.c:1367
void log_truncate_if_large(void)
void log_init_colors(void)
void log_set_force_stderr(bool enabled)
void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap)
asciichat_error_t log_set_format(const char *format_str, bool console_only)
void log_set_json_output(int fd)
void log_set_terminal_output(bool enabled)
void log_set_color_scheme(const color_scheme_t *scheme)
void log_redetect_terminal_capabilities(void)
void setup_signal_handlers(void)
Definition main.c:167
void action_show_version(void)
int apply_palette_config(palette_type_t type, const char *custom_chars)
Definition palette.c:250
bool terminal_is_interactive(void)
bool terminal_should_force_stderr(void)
terminal_capabilities_t detect_terminal_capabilities(void)
void options_state_destroy(void)
Definition rcu.c:309
void options_cleanup_schema(void)
Definition rcu.c:343
const options_t * options_get(void)
Definition rcu.c:347
void splash_set_update_notification(const char *notification)
Definition splash.c:612
Mode descriptor for string conversion.
mode_entry_point_t entry_point
Definition main.c:188
asciichat_error_t update_check_startup(update_check_result_t *result)
void update_check_format_notification(const update_check_result_t *result, char *buffer, size_t buffer_size)
void platform_print_backtrace(int skip_frames)
Definition util.c:42
int platform_open(const char *pathname, int flags,...)

References action_show_version(), apply_palette_config(), asciichat_shared_destroy(), asciichat_shared_init(), colorscheme_load_builtin(), detect_terminal_capabilities(), mode_descriptor_t::entry_point, g_argc, g_argv, g_color_flag_passed, g_color_flag_value, grep_init(), lock_debug_init(), lock_debug_start_thread(), log_init(), log_init_colors(), log_redetect_terminal_capabilities(), log_set_color_scheme(), log_set_force_stderr(), log_set_format(), log_set_json_output(), log_set_terminal_output(), log_truncate_if_large(), options_cleanup_schema(), options_colorscheme_init_early(), options_get(), options_init(), options_state_destroy(), platform_open(), platform_print_backtrace(), setup_signal_handlers(), splash_set_update_notification(), terminal_is_interactive(), terminal_should_force_stderr(), update_check_format_notification(), and update_check_startup().

◆ set_interrupt_callback()

void set_interrupt_callback ( void(*)(void)  cb)

Register a mode-specific interrupt callback

Called by mode-specific code to register a callback that will be invoked when signal_exit() is called. Used to shut down network sockets so threads blocked in recv() unblock quickly instead of waiting for timeouts.

The callback is called synchronously from signal_exit(), so it must be async-signal-safe (only atomics, socket_shutdown(), no malloc/etc).

Only one callback can be registered at a time. Setting a new callback replaces the previous one.

Parameters
cbFunction to call on exit signal, or NULL to unregister

Definition at line 102 of file main.c.

102 {
103 g_interrupt_callback = cb;
104}

Referenced by client_main(), and discovery_main().

◆ setup_signal_handlers()

void setup_signal_handlers ( void  )

Set up global signal handlers Called once at startup before mode dispatch

Definition at line 167 of file main.c.

167 {
168 platform_set_console_ctrl_handler(console_ctrl_handler);
169
170#ifndef _WIN32
171 platform_signal_handler_t handlers[] = {
172 {SIGTERM, handle_sigterm},
173 {SIGPIPE, SIG_IGN},
174 };
175 platform_register_signal_handlers(handlers, 2);
176#endif
177}

Referenced by main().

◆ should_exit()

bool should_exit ( void  )

Check if the application should exit

Called by mode main loops to detect shutdown requests from signals or errors.

Returns
true if shutdown has been requested, false otherwise

Definition at line 90 of file main.c.

90 {
91 return atomic_load(&g_app_should_exit);
92}

Referenced by connection_attempt_tcp(), connection_attempt_websocket(), display_full_reset(), server_connection_establish(), session_client_like_run(), and session_render_loop().

◆ signal_exit()

void signal_exit ( void  )

Signal that the application should exit

Sets the global exit flag and calls the registered interrupt callback if set. Safe to call from signal handlers (uses only atomics and function pointers). Called by signal handlers (SIGTERM, Ctrl+C) and normal code (errors, timeouts).

Definition at line 94 of file main.c.

94 {
95 atomic_store(&g_app_should_exit, true);
96 void (*cb)(void) = g_interrupt_callback;
97 if (cb) {
98 cb();
99 }
100}