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

🖥️ Server main entry point: multi-client connection manager with per-client rendering threads (60fps video + 172fps audio) More...

Go to the source code of this file.

Macros

#define KEYBOARD_QUEUE_SIZE   256
 Thread-safe keyboard queue (lock-free SPSC ring buffer)
 

Functions

int server_main (void)
 Server mode entry point for unified binary.
 

Variables

atomic_bool g_server_should_exit = false
 Global atomic shutdown flag shared across all threads.
 
mixer_t *volatile g_audio_mixer = NULL
 Global audio mixer instance for multi-client audio processing.
 
static_cond_t g_shutdown_cond = STATIC_COND_INIT
 Global shutdown condition variable for waking blocked threads.
 
rate_limiter_t * g_rate_limiter = NULL
 Global rate limiter for connection attempts and packet processing.
 
bool g_server_encryption_enabled = false
 Global flag indicating if server encryption is enabled.
 
private_key_t g_server_private_key = {0}
 Global server private key (first identity key, for backward compatibility)
 
private_key_t g_server_identity_keys [MAX_IDENTITY_KEYS] = {0}
 Global server identity keys array (multi-key support)
 
size_t g_num_server_identity_keys = 0
 Number of loaded server identity keys.
 
public_key_t g_client_whitelist [MAX_CLIENTS] = {0}
 Global client public key whitelist.
 
size_t g_num_whitelisted_clients = 0
 Number of whitelisted clients.
 

Detailed Description

🖥️ Server main entry point: multi-client connection manager with per-client rendering threads (60fps video + 172fps audio)

MODULAR COMPONENTS:

  • main.c (this file): Server initialization, signal handling, connection management
  • client.c: Per-client lifecycle, threading, and state management
  • protocol.c: Network packet processing and protocol implementation
  • stream.c: Video mixing, ASCII frame generation, and caching
  • render.c: Per-client rendering threads with rate limiting
  • stats.c: Performance monitoring and resource tracking

CONCURRENCY MODEL:

The server creates multiple thread types per client:

  1. Receive thread: Handles incoming packets from client (protocol.c functions)
  2. Send thread: Manages outgoing packet delivery (client.c)
  3. Video render thread: Generates ASCII frames at 60fps (render.c)
  4. Audio render thread: Mixes audio streams at 172fps (render.c)
  5. Stats logger thread: Periodic performance reporting (stats.c)

Thread Safety:

  • Lock ordering: Always acquire g_client_manager_rwlock BEFORE per-client mutexes
  • Snapshot pattern: Copy client state under mutex, then process without locks
  • Signal-safe shutdown: SIGINT handler only sets flags and closes sockets
  • Deterministic cleanup: Main thread waits for all worker threads before exit

WHY THE REFACTORING:

The original server.c became unmaintainable at 2408+ lines, making it:

  • Too large for LLM context windows (limited AI-assisted development)
  • Difficult for humans to navigate and understand
  • Slow to compile and modify
  • Hard to isolate bugs and add new features

The modular design enables:

  • Faster development cycles (smaller compilation units)
  • Better IDE support (jump-to-definition, IntelliSense)
  • Easier testing and debugging (isolated components)
  • Future extensibility (new protocols, renderers, optimizations)
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
September 2025
Version
2.0 (Post-Modularization)

Definition in file server/main.c.

Macro Definition Documentation

◆ KEYBOARD_QUEUE_SIZE

#define KEYBOARD_QUEUE_SIZE   256

Thread-safe keyboard queue (lock-free SPSC ring buffer)

Definition at line 269 of file server/main.c.

Function Documentation

◆ server_main()

int server_main ( void  )

Server mode entry point for unified binary.

This function implements the complete server lifecycle including:

  • Server-specific initialization (crypto, shutdown callback)
  • Network socket setup and binding
  • Main connection accept loop
  • Client lifecycle management
  • Graceful shutdown and cleanup

Options are already parsed by the main dispatcher before this function is called, so they are available via global opt_* variables.

Returns
0 on success, non-zero error code on failure
Example
# Invoked by dispatcher after options are parsed:
ascii-chat server --port 8080
# Options parsed in main.c, then server_main() called

Definition at line 1731 of file server/main.c.

1731 {
1732 // Common initialization (options, logging, lock debugging) now happens in main.c before dispatch
1733 // This function focuses on server-specific initialization
1734
1735 // Register shutdown check callback for library code
1736 shutdown_register_callback(check_shutdown);
1737
1738 // Initialize status screen log buffer if enabled AND terminal is interactive
1739 // In non-interactive mode (piped output), logs go directly to stdout/stderr
1740 if (GET_OPTION(status_screen) && terminal_is_interactive()) {
1742
1743 // Initialize interactive grep for status screen
1745 }
1746
1747 // Initialize crypto after logging is ready
1748 log_debug("Initializing crypto...");
1749 if (init_server_crypto() != 0) {
1750 // Print detailed error context if available
1751 LOG_ERRNO_IF_SET("Crypto initialization failed");
1752 FATAL(ERROR_CRYPTO, "Crypto initialization failed");
1753 }
1754 log_debug("Crypto initialized successfully");
1755
1756 // Handle keepawake: check for mutual exclusivity and apply mode default
1757 // Server default: keepawake DISABLED (use --keepawake to enable)
1758 if (GET_OPTION(enable_keepawake) && GET_OPTION(disable_keepawake)) {
1759 FATAL(ERROR_INVALID_PARAM, "--keepawake and --no-keepawake are mutually exclusive");
1760 }
1761 if (GET_OPTION(enable_keepawake)) {
1763 }
1764
1765 log_info("ascii-chat server starting...");
1766
1767 // log_info("SERVER: Options initialized, using log file: %s", log_filename);
1768 int port = GET_OPTION(port);
1769 if (port < 1 || port > 65535) {
1770 log_error("Invalid port configuration: %d", port);
1771 FATAL(ERROR_CONFIG, "Invalid port configuration: %d", port);
1772 }
1773
1776
1777 // Simple signal handling (temporarily disable complex threading signal handling)
1778 log_debug("Setting up simple signal handlers...");
1779
1780 // Handle Ctrl+C for cleanup
1781 platform_signal(SIGINT, server_handle_sigint);
1782 // Handle termination signal (SIGTERM is defined with limited support on Windows)
1783 platform_signal(SIGTERM, server_handle_sigterm);
1784#ifndef _WIN32
1785 // SIGPIPE not supported on Windows
1786 platform_signal(SIGPIPE, SIG_IGN);
1787 // Note: SIGUSR1 is registered in src/main.c for all modes
1788#endif
1789
1790#ifndef NDEBUG
1791 // Lock debug thread is now started in src/main.c (all modes)
1792 // Initialize statistics system
1793 if (stats_init() != 0) {
1794 FATAL(ERROR_THREAD, "Statistics system initialization failed");
1795 }
1796#endif
1797
1798 // Create background worker thread pool for server operations
1799 g_server_worker_pool = thread_pool_create("server_workers");
1800 if (!g_server_worker_pool) {
1801 LOG_ERRNO_IF_SET("Failed to create server worker thread pool");
1802 FATAL(ERROR_MEMORY, "Failed to create server worker thread pool");
1803 }
1804
1805 // Spawn statistics logging thread in worker pool
1806 if (thread_pool_spawn(g_server_worker_pool, stats_logger_thread, NULL, 0, "stats_logger") != ASCIICHAT_OK) {
1807 LOG_ERRNO_IF_SET("Statistics logger thread creation failed");
1808 } else {
1809 log_debug("Statistics logger thread started");
1810 }
1811
1812 // Network setup - Use tcp_server abstraction for dual-stack IPv4/IPv6 binding
1813 log_debug("Config check: GET_OPTION(address)='%s', GET_OPTION(address6)='%s'", GET_OPTION(address),
1814 GET_OPTION(address6));
1815
1816 bool ipv4_has_value = (strlen(GET_OPTION(address)) > 0);
1817 bool ipv6_has_value = (strlen(GET_OPTION(address6)) > 0);
1818 // Check if address is localhost (any loopback address, not just exact default)
1819 bool ipv4_is_default = is_localhost_ipv4(GET_OPTION(address));
1820 bool ipv6_is_default = is_localhost_ipv6(GET_OPTION(address6));
1821
1822 log_debug("Binding decision: ipv4_has_value=%d, ipv6_has_value=%d, ipv4_is_default=%d, ipv6_is_default=%d",
1823 ipv4_has_value, ipv6_has_value, ipv4_is_default, ipv6_is_default);
1824
1825 // Determine bind configuration
1826 bool bind_ipv4 = false;
1827 bool bind_ipv6 = false;
1828 const char *ipv4_address = NULL;
1829 const char *ipv6_address = NULL;
1830
1831 if (ipv4_has_value && ipv6_has_value && ipv4_is_default && ipv6_is_default) {
1832 // Both are defaults: dual-stack with default localhost addresses
1833 bind_ipv4 = true;
1834 bind_ipv6 = true;
1835 ipv4_address = "127.0.0.1";
1836 ipv6_address = "::1";
1837 log_info("Default dual-stack: binding to 127.0.0.1 (IPv4) and ::1 (IPv6)");
1838 } else if (ipv4_has_value && !ipv4_is_default && (!ipv6_has_value || ipv6_is_default)) {
1839 // IPv4 explicitly set, IPv6 is default or empty: bind only IPv4
1840 bind_ipv4 = true;
1841 bind_ipv6 = false;
1842 ipv4_address = GET_OPTION(address);
1843 log_info("Binding only to IPv4 address: %s", ipv4_address);
1844 } else if (ipv6_has_value && !ipv6_is_default && (ipv4_is_default || !ipv4_has_value)) {
1845 // IPv6 explicitly set, IPv4 is default or empty: bind only IPv6
1846 bind_ipv4 = false;
1847 bind_ipv6 = true;
1848 ipv6_address = GET_OPTION(address6);
1849 log_info("Binding only to IPv6 address: %s", ipv6_address);
1850 } else {
1851 // Both explicitly set or one explicit + one default: dual-stack
1852 bind_ipv4 = true;
1853 bind_ipv6 = true;
1854 ipv4_address = ipv4_has_value ? GET_OPTION(address) : "127.0.0.1";
1855 ipv6_address = ipv6_has_value ? GET_OPTION(address6) : "::1";
1856 log_info("Dual-stack binding: IPv4=%s, IPv6=%s", ipv4_address, ipv6_address);
1857 }
1858
1859 // Create server context - encapsulates all server state for passing to client handlers
1860 // This reduces global state and improves modularity by using tcp_server.user_data
1861 server_context_t server_ctx = {
1862 .tcp_server = &g_tcp_server,
1863 .rate_limiter = g_rate_limiter,
1864 .client_manager = &g_client_manager,
1865 .client_manager_rwlock = &g_client_manager_rwlock,
1866 .server_should_exit = &g_server_should_exit,
1867 .audio_mixer = g_audio_mixer,
1868 .stats = &g_stats,
1869 .stats_mutex = &g_stats_mutex,
1870 .encryption_enabled = g_server_encryption_enabled,
1871 .server_private_key = &g_server_private_key,
1872 .client_whitelist = g_client_whitelist,
1873 .num_whitelisted_clients = g_num_whitelisted_clients,
1874 .session_host = NULL, // Will be created after TCP server init
1875 };
1876
1877 // Configure TCP server
1878 tcp_server_config_t tcp_config = {
1879 .port = port,
1880 .ipv4_address = ipv4_address,
1881 .ipv6_address = ipv6_address,
1882 .bind_ipv4 = bind_ipv4,
1883 .bind_ipv6 = bind_ipv6,
1884 .accept_timeout_sec = ACCEPT_TIMEOUT,
1885 .client_handler = ascii_chat_client_handler,
1886 .user_data = &server_ctx, // Pass server context to client handlers
1887 .status_update_fn = NULL, // Status screen runs in its own thread
1888 .status_update_data = NULL,
1889 };
1890
1891 // Initialize TCP server (creates and binds sockets)
1892 memset(&g_tcp_server, 0, sizeof(g_tcp_server));
1893 asciichat_error_t tcp_init_result = tcp_server_init(&g_tcp_server, &tcp_config);
1894 if (tcp_init_result != ASCIICHAT_OK) {
1895 FATAL(ERROR_NETWORK, "Failed to initialize TCP server");
1896 }
1897
1898 // Initialize WebSocket server (for browser clients)
1899 websocket_server_config_t ws_config = {
1900 .port = GET_OPTION(websocket_port),
1901 .client_handler = websocket_client_handler,
1902 .user_data = &server_ctx,
1903 };
1904
1905 memset(&g_websocket_server, 0, sizeof(g_websocket_server));
1906 asciichat_error_t ws_init_result = websocket_server_init(&g_websocket_server, &ws_config);
1907 if (ws_init_result != ASCIICHAT_OK) {
1908 log_warn("Failed to initialize WebSocket server - browser clients will not be supported");
1909 } else {
1910 log_info("WebSocket server initialized on port %d", GET_OPTION(websocket_port));
1911 }
1912
1913 // =========================================================================
1914 // UPnP Port Mapping (Quick Win for Direct TCP)
1915 // =========================================================================
1916 // Track UPnP success for ACDS session type decision
1917 // If UPnP fails, we need to create a WebRTC session to enable client connectivity
1918 bool upnp_succeeded = false;
1919
1920 // Try to open port via UPnP so direct TCP works for ~70% of home users.
1921 // If this fails, clients fall back to WebRTC automatically - not fatal.
1922 //
1923 // Strategy:
1924 // 1. UPnP (works on ~90% of home routers)
1925 // 2. NAT-PMP fallback (Apple routers)
1926 // 3. If both fail: use ACDS + WebRTC (reliable, but slightly higher latency)
1927 if (GET_OPTION(enable_upnp)) {
1928 asciichat_error_t upnp_result = nat_upnp_open(port, "ascii-chat Server", &g_upnp_ctx);
1929
1930 if (upnp_result == ASCIICHAT_OK && g_upnp_ctx) {
1931 char public_addr[22];
1932 if (nat_upnp_get_address(g_upnp_ctx, public_addr, sizeof(public_addr)) == ASCIICHAT_OK) {
1933 char msg[256];
1934 safe_snprintf(msg, sizeof(msg), "🌐 Public endpoint: %s (direct TCP)", public_addr);
1935 log_console(LOG_INFO, msg);
1936 log_info("UPnP: Port mapping successful, public endpoint: %s", public_addr);
1937 upnp_succeeded = true;
1938 }
1939 } else {
1940 log_info("UPnP: Port mapping unavailable or failed - will use WebRTC fallback");
1941 log_console(LOG_INFO, "📡 Clients behind strict NATs will use WebRTC fallback");
1942 }
1943 } else {
1944 log_debug("UPnP: Disabled (use --upnp to enable automatic port mapping)");
1945 }
1946
1947 // Initialize synchronization primitives
1949 FATAL(ERROR_THREAD, "Failed to initialize client manager rwlock");
1950 }
1951
1952 // Lock debug system already initialized earlier in main()
1953
1954 // Check if SIGINT/SIGTERM was received during initialization
1955 // If so, skip the accept loop entirely and go to cleanup
1956 if (atomic_load(&g_server_should_exit)) {
1957 log_info("Shutdown signal received during initialization, skipping server startup");
1958 goto cleanup;
1959 }
1960
1961 // Lock debug thread already started earlier in main()
1962
1963 // NOTE: g_client_manager is already zero-initialized in client.c with = {0}
1964 // We only need to initialize the mutex
1966
1967 // Initialize uthash head pointer for O(1) lookup (uthash requires NULL initialization)
1969
1970 // Initialize connection rate limiter (prevents DoS attacks)
1971 if (!atomic_load(&g_server_should_exit)) {
1972 log_debug("Initializing connection rate limiter...");
1974 if (!g_rate_limiter) {
1975 LOG_ERRNO_IF_SET("Failed to initialize rate limiter");
1976 if (!atomic_load(&g_server_should_exit)) {
1977 FATAL(ERROR_MEMORY, "Failed to create connection rate limiter");
1978 }
1979 } else {
1980 log_info("Connection rate limiter initialized (50 connections/min per IP)");
1981 }
1982 }
1983
1984 // Initialize audio mixer (always enabled on server)
1985 if (!atomic_load(&g_server_should_exit)) {
1986 log_debug("Initializing audio mixer for per-client audio rendering...");
1987 g_audio_mixer = mixer_create(MAX_CLIENTS, AUDIO_SAMPLE_RATE);
1988 if (!g_audio_mixer) {
1989 LOG_ERRNO_IF_SET("Failed to initialize audio mixer");
1990 if (!atomic_load(&g_server_should_exit)) {
1991 FATAL(ERROR_AUDIO, "Failed to initialize audio mixer");
1992 }
1993 } else {
1994 if (!atomic_load(&g_server_should_exit)) {
1995 log_debug("Audio mixer initialized successfully for per-client audio rendering");
1996 }
1997 }
1998 }
1999
2000 // Initialize mDNS context for LAN service discovery (optional)
2001 // mDNS allows clients on the LAN to discover this server without knowing its IP
2002 // Can be disabled with --no-mdns-advertise
2003 // Note: Actual advertisement is deferred until after ACDS session creation (if --acds is enabled)
2004 if (!atomic_load(&g_server_should_exit) && !GET_OPTION(no_mdns_advertise)) {
2005 log_debug("Initializing mDNS for LAN service discovery...");
2006 g_mdns_ctx = asciichat_mdns_init();
2007 if (!g_mdns_ctx) {
2008 LOG_ERRNO_IF_SET("Failed to initialize mDNS (non-fatal, LAN discovery disabled)");
2009 log_warn("mDNS disabled - LAN service discovery will not be available");
2010 g_mdns_ctx = NULL;
2011 } else {
2012 log_debug("mDNS context initialized, advertisement deferred until session string is ready");
2013 }
2014 } else if (GET_OPTION(no_mdns_advertise)) {
2015 log_info("mDNS service advertisement disabled via --no-mdns-advertise");
2016 }
2017
2018 // ========================================================================
2019 // Session Host Creation (for discovery mode support)
2020 // ========================================================================
2021 // Create session_host to track clients in a transport-agnostic way.
2022 // This enables future discovery mode where participants can become hosts.
2023 if (!atomic_load(&g_server_should_exit)) {
2024 session_host_config_t host_config = {
2025 .port = port,
2026 .ipv4_address = ipv4_address,
2027 .ipv6_address = ipv6_address,
2028 .max_clients = GET_OPTION(max_clients),
2029 .encryption_enabled = g_server_encryption_enabled,
2030 .key_path = GET_OPTION(encrypt_key),
2031 .password = GET_OPTION(password),
2032 .callbacks = {0}, // No callbacks for now
2033 .user_data = NULL,
2034 };
2035
2036 server_ctx.session_host = session_host_create(&host_config);
2037 if (!server_ctx.session_host) {
2038 // Non-fatal: session_host is optional, server can work without it
2039 log_warn("Failed to create session_host (discovery mode support disabled)");
2040 } else {
2041 log_debug("Session host created for discovery mode support");
2042 }
2043 }
2044
2045 // ========================================================================
2046 // MAIN CONNECTION LOOP - Delegated to tcp_server
2047 // ========================================================================
2048 //
2049 // The tcp_server module handles:
2050 // 1. Dual-stack IPv4/IPv6 accept loop with select() timeout
2051 // 2. Spawning client_handler threads for each connection
2052 // 3. Responsive shutdown when g_tcp_server.running is set to false
2053 //
2054 // Client lifecycle is managed by ascii_chat_client_handler() which:
2055 // - Performs rate limiting
2056 // - Calls add_client() to initialize structures and spawn workers
2057 // - Blocks until client disconnects
2058 // - Calls remove_client() to cleanup and stop worker threads
2059
2060 // ACDS Session Creation: Register this server with discovery service
2061 // This also determines the session string for mDNS (if --acds is enabled)
2062 char session_string[64] = {0};
2063 bool session_is_mdns_only = false;
2064
2065 // ACDS Registration (conditional on --discovery flag)
2066 if (GET_OPTION(discovery)) {
2067 // Security Requirement Check (Issue #239):
2068 // Server IP must be protected by password, identity verification, or explicit opt-in
2069
2070 // Auto-detection: Check if password or identity verification is configured
2071 const char *password = GET_OPTION(password);
2072 bool has_password = password && strlen(password) > 0;
2073 const char *encrypt_key = GET_OPTION(encrypt_key);
2074 bool has_identity = encrypt_key && strlen(encrypt_key) > 0;
2075 bool explicit_expose = GET_OPTION(discovery_expose_ip) != 0;
2076
2077 // Validate security configuration BEFORE attempting ACDS connection
2078 bool acds_expose_ip_flag = false;
2079
2080 if (has_password || has_identity) {
2081 // Auto-enable privacy: IP revealed only after verification
2082 acds_expose_ip_flag = false;
2083 log_plain("🔒 ACDS privacy enabled: IP disclosed only after %s verification",
2084 has_password ? "password" : "identity");
2085 } else if (explicit_expose) {
2086 // Explicit opt-in to public IP disclosure
2087 // Only prompt if running interactively (can prompt user)
2088 // When non-interactive (automated/scripted), treat explicit flag as confirmation
2089 bool is_interactive = terminal_can_prompt_user();
2090
2091 if (is_interactive) {
2092 log_plain_stderr("");
2093 log_plain_stderr("⚠️ WARNING: You are about to allow PUBLIC IP disclosure!");
2094 log_plain_stderr("⚠️ Anyone with the session string will be able to see your IP address.");
2095 log_plain_stderr("⚠️ This is NOT RECOMMENDED unless you understand the privacy implications.");
2096 log_plain_stderr("");
2097
2098 if (!platform_prompt_yes_no("Do you want to proceed with public IP disclosure", false)) {
2099 log_plain_stderr("");
2100 log_plain_stderr("❌ IP disclosure not confirmed. Server will run WITHOUT discovery service.");
2101 goto skip_acds_session;
2102 }
2103 }
2104
2105 // User confirmed (or running non-interactively with explicit flag) - proceed with public IP disclosure
2106 acds_expose_ip_flag = true;
2107 log_plain_stderr("");
2108 log_plain_stderr("⚠️ Public IP disclosure CONFIRMED");
2109 log_plain_stderr("⚠️ Your IP address will be visible to anyone with the session string");
2110 } else {
2111 // Security violation: No password, no identity, no explicit opt-in
2112 log_plain_stderr("❌ Cannot create ACDS session: No security configured!");
2113 log_plain_stderr(" You must either:");
2114 log_plain_stderr(" 1. Set a password: --password \"your-secret\"");
2115 log_plain_stderr(" 2. Use identity key: --key ~/.ssh/id_ed25519");
2116 log_plain_stderr(" 3. Explicitly allow public IP: --acds-expose-ip (NOT RECOMMENDED)");
2117 log_plain_stderr("");
2118 log_plain_stderr("Server will run WITHOUT discovery service.");
2119 goto skip_acds_session;
2120 }
2121
2122 // Security is configured, proceed with ACDS connection
2123 const char *acds_server = GET_OPTION(discovery_server);
2124 uint16_t acds_port = (uint16_t)GET_OPTION(discovery_port);
2125
2126 log_info("Attempting to create session on ACDS server at %s:%d...", acds_server, acds_port);
2127
2128 acds_client_config_t acds_config;
2130 SAFE_STRNCPY(acds_config.server_address, acds_server, sizeof(acds_config.server_address));
2131 acds_config.server_port = acds_port;
2132 acds_config.timeout_ms = 5 * MS_PER_SEC_INT;
2133
2134 // Allocate ACDS client on heap for server lifecycle
2135 g_acds_client = SAFE_MALLOC(sizeof(acds_client_t), acds_client_t *);
2136 if (!g_acds_client) {
2137 log_error("Failed to allocate ACDS client");
2138 goto skip_acds_session;
2139 }
2140
2141 asciichat_error_t acds_connect_result = acds_client_connect(g_acds_client, &acds_config);
2142 if (acds_connect_result != ASCIICHAT_OK) {
2143 log_error("Failed to connect to ACDS server at %s:%d: %s", acds_server, acds_port,
2144 asciichat_error_string(acds_connect_result));
2145 goto skip_acds_session;
2146 }
2147 if (acds_connect_result == ASCIICHAT_OK) {
2148 // Prepare session creation parameters
2149 acds_session_create_params_t create_params;
2150 memset(&create_params, 0, sizeof(create_params));
2151
2152 // Use server's Ed25519 identity public key if available
2153 if (g_server_encryption_enabled && has_identity) {
2154 memcpy(create_params.identity_pubkey, g_server_private_key.public_key, 32);
2155 log_debug("Using server identity key for ACDS session");
2156 } else {
2157 // No identity key available - use zero key
2158 // ACDS will accept this if identity verification is not required
2159 memset(create_params.identity_pubkey, 0, 32);
2160 log_debug("No server identity key - using zero key for ACDS session");
2161 }
2162
2163 create_params.capabilities = 0x03; // Video + Audio
2164 create_params.max_participants = GET_OPTION(max_clients);
2165 log_debug("ACDS: max_clients option value = %d", GET_OPTION(max_clients));
2166
2167 // Set password if configured
2168 create_params.has_password = has_password;
2169 if (has_password) {
2170 // TODO: Hash password with Argon2id
2171 SAFE_STRNCPY(create_params.password, password, sizeof(create_params.password));
2172 }
2173
2174 // Set IP disclosure policy (determined above)
2175 create_params.acds_expose_ip = acds_expose_ip_flag;
2176 log_info("DEBUG: Server setting acds_expose_ip=%d (explicit_expose=%d, has_password=%d, has_identity=%d)",
2177 create_params.acds_expose_ip, explicit_expose, has_password, has_identity);
2178
2179 // Set session type (Direct TCP or WebRTC)
2180 // Auto-detect: Use WebRTC if UPnP failed OR if explicitly requested via --webrtc
2181 // Exception: If bind address is 0.0.0.0, server is on public IP - use Direct TCP
2182 const char *bind_addr = GET_OPTION(address);
2183 bool bind_all_interfaces = (strcmp(bind_addr, "0.0.0.0") == 0);
2184
2185 // Determine session type: prefer WebRTC by default (unless explicitly disabled)
2186 // Priority: explicit --webrtc flag > connection type detection > UPnP > default
2187 if (GET_OPTION(webrtc)) {
2188 // Explicit WebRTC request
2189 create_params.session_type = SESSION_TYPE_WEBRTC;
2190 log_info("ACDS session type: WebRTC (explicitly requested via --webrtc)");
2191 } else if (bind_all_interfaces) {
2192 // Bind to 0.0.0.0: use WebRTC as default (better NAT compatibility)
2193 create_params.session_type = SESSION_TYPE_WEBRTC;
2194 log_info("ACDS session type: WebRTC (default for 0.0.0.0 binding, provides NAT-agnostic connections)");
2195 } else if (upnp_succeeded) {
2196 // UPnP port mapping worked - can use direct TCP
2197 create_params.session_type = SESSION_TYPE_DIRECT_TCP;
2198 log_info("ACDS session type: Direct TCP (UPnP succeeded, server is publicly accessible)");
2199 } else {
2200 // UPnP failed and not on public IP - use WebRTC for NAT traversal
2201 create_params.session_type = SESSION_TYPE_WEBRTC;
2202 log_info("ACDS session type: WebRTC (UPnP failed, server behind NAT)");
2203 }
2204
2205 // Server connection information (where clients should connect)
2206 // If bind address is 0.0.0.0, leave server_address empty for ACDS to auto-detect public IP
2207 if (bind_all_interfaces) {
2208 create_params.server_address[0] = '\0'; // Empty - ACDS will use connection source IP
2209 log_debug("Bind address is 0.0.0.0, ACDS will auto-detect public IP from connection");
2210 } else {
2211 SAFE_STRNCPY(create_params.server_address, bind_addr, sizeof(create_params.server_address));
2212 }
2213 create_params.server_port = port;
2214
2215 // DEBUG: Log what we're sending to ACDS
2216 log_info("DEBUG: Before SESSION_CREATE - expose_ip_publicly=%d, server_address='%s' port=%u, session_type=%u",
2217 create_params.acds_expose_ip, create_params.server_address, create_params.server_port,
2218 create_params.session_type);
2219
2220 // Create session
2221 acds_session_create_result_t create_result;
2222 asciichat_error_t create_err = acds_session_create(g_acds_client, &create_params, &create_result);
2223
2224 if (create_err == ASCIICHAT_OK) {
2225 SAFE_STRNCPY(session_string, create_result.session_string, sizeof(session_string));
2226 SAFE_STRNCPY(g_session_string, create_result.session_string, sizeof(g_session_string));
2227 session_is_mdns_only = false; // Session is now registered with ACDS (globally discoverable)
2228 log_info("Session created: %s", session_string);
2229
2230 // Server must join its own session so ACDS can route signaling messages
2231 log_debug("Server joining session as first participant for WebRTC signaling...");
2232 acds_session_join_params_t join_params = {0};
2233 join_params.session_string = session_string;
2234
2235 // Use same identity key as session creation
2236 memcpy(join_params.identity_pubkey, create_params.identity_pubkey, 32);
2237
2238 // Include password if session is password-protected
2239 if (has_password) {
2240 join_params.has_password = true;
2241 SAFE_STRNCPY(join_params.password, password, sizeof(join_params.password));
2242 }
2243
2244 acds_session_join_result_t join_result = {0};
2245 asciichat_error_t join_err = acds_session_join(g_acds_client, &join_params, &join_result);
2246 if (join_err != ASCIICHAT_OK || !join_result.success) {
2247 log_error("Failed to join own session: %s (error: %s)", asciichat_error_string(join_err),
2248 join_result.error_message[0] ? join_result.error_message : "unknown");
2249 // Continue anyway - this is not fatal for Direct TCP sessions
2250 } else {
2251 log_debug("Server joined session successfully (participant_id: %02x%02x...)", join_result.participant_id[0],
2252 join_result.participant_id[1]);
2253 // Store participant ID for WebRTC signaling (needed to identify server in SDP/ICE messages)
2254 memcpy(g_server_participant_id, join_result.participant_id, 16);
2255 log_debug("Stored server participant_id for signaling: %02x%02x...", g_server_participant_id[0],
2256 g_server_participant_id[1]);
2257 memcpy(create_result.session_id, join_result.session_id, 16);
2258 }
2259
2260 // Keep ACDS connection alive for WebRTC signaling relay
2261 log_debug("Server staying connected to ACDS for signaling relay");
2262
2263 // Create ACDS transport wrapper for sending signaling packets
2264 g_acds_transport = acip_tcp_transport_create(g_acds_client->socket, NULL);
2265 if (!g_acds_transport) {
2266 log_error("Failed to create ACDS transport wrapper");
2267 } else {
2268 log_debug("ACDS transport wrapper created for signaling");
2269
2270 // Start ACDS ping thread to keep connection alive (for ALL session types)
2271 int ping_thread_result = asciichat_thread_create(&g_acds_ping_thread, acds_ping_thread, NULL);
2272 if (ping_thread_result != 0) {
2273 log_error("Failed to create ACDS ping thread: %d", ping_thread_result);
2274 } else {
2275 log_debug("ACDS ping thread started to keep connection alive");
2276 g_acds_ping_thread_started = true;
2277 }
2278 }
2279
2280 // Initialize WebRTC peer_manager if session type is WebRTC
2281 if (create_params.session_type == SESSION_TYPE_WEBRTC) {
2282 log_debug("Initializing WebRTC library and peer manager for session (role=CREATOR)...");
2283
2284 // Initialize WebRTC library (libdatachannel)
2285 asciichat_error_t webrtc_init_result = webrtc_init();
2286 if (webrtc_init_result != ASCIICHAT_OK) {
2287 log_error("Failed to initialize WebRTC library: %s", asciichat_error_string(webrtc_init_result));
2288 g_webrtc_peer_manager = NULL;
2289 } else {
2290 log_debug("WebRTC library initialized successfully");
2291
2292 // Configure STUN servers for ICE gathering (static to persist for peer_manager lifetime)
2293 static stun_server_t stun_servers[4] = {0};
2294 static unsigned int g_stun_init_refcount = 0;
2295 static static_mutex_t g_stun_init_mutex = STATIC_MUTEX_INIT;
2296 static int stun_count = 0; // Store actual count separately
2297
2298 static_mutex_lock(&g_stun_init_mutex);
2299 if (g_stun_init_refcount == 0) {
2300 log_debug("Parsing STUN servers from options: '%s'", GET_OPTION(stun_servers));
2301 int count =
2302 stun_servers_parse(GET_OPTION(stun_servers), OPT_ENDPOINT_STUN_SERVERS_DEFAULT, stun_servers, 4);
2303 if (count > 0) {
2304 stun_count = count;
2305 log_debug("Parsed %d STUN servers", count);
2306 for (int i = 0; i < count; i++) {
2307 log_debug(" STUN[%d]: '%s' (len=%d)", i, stun_servers[i].host, stun_servers[i].host_len);
2308 }
2309 } else {
2310 log_warn("Failed to parse STUN servers, using defaults");
2311 stun_count = stun_servers_parse(OPT_ENDPOINT_STUN_SERVERS_DEFAULT, OPT_ENDPOINT_STUN_SERVERS_DEFAULT,
2312 stun_servers, 4);
2313 log_debug("Using default STUN servers, count=%d", stun_count);
2314 for (int i = 0; i < stun_count; i++) {
2315 log_debug(" STUN[%d]: '%s' (len=%d)", i, stun_servers[i].host, stun_servers[i].host_len);
2316 }
2317 }
2318 g_stun_init_refcount = 1;
2319 }
2320 static_mutex_unlock(&g_stun_init_mutex);
2321
2322 // Configure peer_manager
2323 webrtc_peer_manager_config_t pm_config = {
2324 .role = WEBRTC_ROLE_CREATOR, // Server accepts offers, generates answers
2325 .stun_servers = stun_servers,
2326 .stun_count = stun_count,
2327 .turn_servers = NULL, // No TURN for server (clients should have public IP or use TURN)
2328 .turn_count = 0,
2329 .on_transport_ready = on_webrtc_transport_ready,
2330 .user_data = &server_ctx,
2331 .crypto_ctx = NULL // WebRTC handles crypto internally
2332 };
2333
2334 // Configure signaling callbacks for relaying SDP/ICE via ACDS
2335 webrtc_signaling_callbacks_t signaling_callbacks = {
2336 .send_sdp = server_send_sdp, .send_ice = server_send_ice, .user_data = NULL};
2337
2338 // Create peer_manager
2339 asciichat_error_t pm_result =
2340 webrtc_peer_manager_create(&pm_config, &signaling_callbacks, &g_webrtc_peer_manager);
2341 if (pm_result != ASCIICHAT_OK) {
2342 log_error("Failed to create WebRTC peer_manager: %s", asciichat_error_string(pm_result));
2343 g_webrtc_peer_manager = NULL;
2344 } else {
2345 log_debug("WebRTC peer_manager initialized successfully");
2346
2347 // Start ACDS receive thread for WebRTC signaling relay
2348 int thread_result = asciichat_thread_create(&g_acds_receive_thread, acds_receive_thread, NULL);
2349 if (thread_result != 0) {
2350 log_error("Failed to create ACDS receive thread: %d", thread_result);
2351 // Cleanup peer_manager since signaling won't work
2352 webrtc_peer_manager_destroy(g_webrtc_peer_manager);
2353 g_webrtc_peer_manager = NULL;
2354 } else {
2355 log_debug("ACDS receive thread started for WebRTC signaling relay");
2356 g_acds_receive_thread_started = true;
2357 }
2358 }
2359 } // Close else block from webrtc_init() success
2360 } else {
2361 log_debug("Session type is DIRECT_TCP, skipping WebRTC peer_manager initialization");
2362 }
2363
2364 // Advertise mDNS with ACDS session string
2365 // This ensures both mDNS and ACDS discovery return the same session string
2366 advertise_mdns_with_session(session_string, (uint16_t)port);
2367 } else {
2368 log_warn("Failed to create session on ACDS server (server will run without discovery)");
2369 // Clean up failed ACDS client
2370 if (g_acds_client) {
2371 acds_client_disconnect(g_acds_client);
2372 SAFE_FREE(g_acds_client);
2373 g_acds_client = NULL;
2374 }
2375 }
2376 } else {
2377 log_warn("Could not connect to ACDS server at %s:%d (server will run without discovery)", acds_server, acds_port);
2378 }
2379 } else {
2380 log_info("ACDS registration disabled (use --acds to enable)");
2381 }
2382
2383skip_acds_session:
2384 // Fallback: If no session string was set by ACDS (either disabled or failed),
2385 // generate a random session string for mDNS discovery only
2386 if (session_string[0] == '\0' && g_mdns_ctx) {
2387 log_debug("No ACDS session string available, generating random session for mDNS");
2388
2389 // Use the proper session string generation from discovery module
2390 // This generates adjective-noun-noun format using the full wordlists
2391 if (acds_string_generate(session_string, sizeof(session_string)) != ASCIICHAT_OK) {
2392 log_error("Failed to generate session string for mDNS");
2393 return 1;
2394 }
2395
2396 log_debug("Generated random session string for mDNS: '%s'", session_string);
2397
2398 // Mark that this session is mDNS-only (not globally discoverable via ACDS)
2399 session_is_mdns_only = true;
2400
2401 // Advertise mDNS with random session string
2402 advertise_mdns_with_session(session_string, (uint16_t)port);
2403 }
2404
2405 // ====================================================================
2406 // Display session string prominently as the FINAL startup message
2407 // This ensures users see the connection info clearly without logs
2408 // wiping it away
2409 // ====================================================================
2410 if (session_string[0] != '\0') {
2411 if (session_is_mdns_only) {
2412 log_plain("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n📋 Session String: %s (LAN only via "
2413 "mDNS)\n🔗 Share with others on your LAN to join:\n ascii-chat "
2414 "%s\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
2415 session_string, session_string);
2416 } else {
2417 log_plain("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n📋 Session String: %s\n🔗 Share this "
2418 "globally to join:\n ascii-chat %s\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
2419 session_string, session_string);
2420 }
2421 }
2422
2423 // Copy session info to globals for status screen display
2424 SAFE_STRNCPY(g_session_string, session_string, sizeof(g_session_string));
2425 g_session_is_mdns_only = session_is_mdns_only;
2426
2427 log_debug("Server entering accept loop (port %d)...", port);
2428
2429 // Initialize status screen
2430 g_server_start_time = time(NULL);
2431 g_last_status_update = platform_get_monotonic_time_us();
2432
2433 // Clear status screen log buffer to discard initialization logs
2434 // This ensures only NEW logs (generated after status screen starts) are displayed
2435 if (GET_OPTION(status_screen)) {
2436 extern void server_status_log_clear(void);
2438 }
2439
2440 // Start status screen thread if enabled
2441 // Runs independently at target FPS (default 60 Hz), decoupled from network accept loop
2442 if (GET_OPTION(status_screen)) {
2443 if (asciichat_thread_create(&g_status_screen_thread, status_screen_thread, NULL) != 0) {
2444 log_error("Failed to create status screen thread");
2445 goto cleanup;
2446 }
2447 log_debug("Status screen thread started");
2448 }
2449
2450 // Start WebSocket server thread if initialized
2451 if (g_websocket_server.context != NULL) {
2452 if (asciichat_thread_create(&g_websocket_server_thread, websocket_server_thread_wrapper, &g_websocket_server) !=
2453 0) {
2454 log_error("Failed to create WebSocket server thread");
2455 } else {
2456 g_websocket_server_thread_started = true;
2457 log_info("WebSocket server thread started");
2458 }
2459 }
2460
2461 // Run TCP server (blocks until shutdown signal received)
2462 // tcp_server_run() handles:
2463 // - select() on IPv4/IPv6 sockets with timeout
2464 // - accept() new connections
2465 // - Spawn ascii_chat_client_handler() thread for each connection
2466 // - Responsive shutdown when atomic_store(&g_tcp_server.running, false)
2467 asciichat_error_t run_result = tcp_server_run(&g_tcp_server);
2468 if (run_result != ASCIICHAT_OK) {
2469 log_error("TCP server exited with error");
2470 }
2471
2472 log_debug("Server accept loop exited");
2473
2474cleanup:
2475 // Signal status screen thread to exit
2476 atomic_store(&g_server_should_exit, true);
2477
2478 // Wait for status screen thread to finish if it was started
2479 if (GET_OPTION(status_screen)) {
2480 log_debug("Waiting for status screen thread to exit...");
2481 int join_result = asciichat_thread_join_timeout(&g_status_screen_thread, NULL, NS_PER_SEC_INT); // 1 second
2482 if (join_result != 0) {
2483 log_warn("Status screen thread did not exit cleanly (timeout)");
2484 } else {
2485 log_debug("Status screen thread exited");
2486 }
2487 }
2488
2489 // Cleanup status screen log capture
2491
2492 // Cleanup
2493 log_debug("Server shutting down...");
2494 memset(g_session_string, 0, sizeof(g_session_string)); // Clear session string for status screen
2495
2496 // Wake up any threads that might be blocked on condition variables
2497 // (like packet queues) to ensure responsive shutdown
2498 // This must happen BEFORE client cleanup to wake up any blocked threads
2499 static_cond_broadcast(&g_shutdown_cond);
2500 // NOTE: Do NOT call cond_destroy() on statically-initialized condition variables
2501 // g_shutdown_cond uses STATIC_COND_INIT which doesn't allocate resources that need cleanup
2502 // Calling cond_destroy() on a static cond is undefined behavior on some platforms
2503
2504 // Close all client sockets immediately to unblock receive threads.
2505 // The signal handler only closed the listening socket, but client receive threads
2506 // are still blocked in recv_with_timeout(). We need to close their sockets to unblock them.
2507 log_debug("Closing all client sockets to unblock receive threads...");
2508
2509 // Use write lock since we're modifying client->socket
2510 rwlock_wrlock(&g_client_manager_rwlock);
2511 for (int i = 0; i < MAX_CLIENTS; i++) {
2512 client_info_t *client = &g_client_manager.clients[i];
2513 if (atomic_load(&client->client_id) != 0 && client->socket != INVALID_SOCKET_VALUE) {
2514 socket_close(client->socket);
2515 client->socket = INVALID_SOCKET_VALUE;
2516 }
2517 }
2518 rwlock_wrunlock(&g_client_manager_rwlock);
2519
2520 log_debug("Signaling all clients to stop (sockets closed, g_server_should_exit set)...");
2521
2522 // Stop and destroy server worker thread pool (stats logger, etc.)
2523 if (g_server_worker_pool) {
2524 thread_pool_destroy(g_server_worker_pool);
2525 g_server_worker_pool = NULL;
2526 log_debug("Server worker thread pool stopped");
2527 }
2528
2529 // Destroy rate limiter
2530 if (g_rate_limiter) {
2532 g_rate_limiter = NULL;
2533 }
2534
2535 // Shutdown WebSocket server BEFORE removing clients.
2536 // The LWS event thread runs callbacks that access transport data (impl_data).
2537 // If we destroy transports via remove_client while LWS is still running,
2538 // callbacks access freed memory (heap-use-after-free).
2539 // lws_context_destroy fires LWS_CALLBACK_CLOSED for all connections, which
2540 // closes transports and NULLs the per-session conn_data->transport pointer.
2541 // After this, remove_client can safely destroy the transport objects.
2542 if (g_websocket_server.context != NULL) {
2543 log_debug("Shutting down WebSocket server before client cleanup");
2544 atomic_store(&g_websocket_server.running, false);
2545 // Wake the LWS event loop immediately instead of waiting for the 50ms
2546 // lws_service timeout. This is safe because the event loop thread can't
2547 // destroy the context until after it exits its loop (which requires
2548 // seeing running=false, which we just set).
2549 websocket_server_cancel_service(&g_websocket_server);
2550
2551 // Join with 2-second timeout only if thread was successfully created.
2552 // lws_context_destroy() can take 5+ seconds if called from a different thread
2553 // (waiting for close handshake), but we're destroying from the event loop thread
2554 // so it's fast. 2s accounts for any slow lws_service() calls or LWS cleanup.
2555 if (g_websocket_server_thread_started) {
2556 int join_result =
2557 asciichat_thread_join_timeout(&g_websocket_server_thread, NULL, 2 * NS_PER_SEC_INT); // 2 seconds in ns
2558 if (join_result != 0) {
2559 log_warn("WebSocket thread did not exit cleanly (timeout), forcing cleanup");
2560 }
2561 g_websocket_server_thread_started = false;
2562 }
2563
2564 // Context is destroyed by websocket_server_run from the event loop thread.
2565 // websocket_server_destroy handles the case where it's already NULL or context already destroyed.
2566 websocket_server_destroy(&g_websocket_server);
2567 log_debug("WebSocket server shut down");
2568 }
2569
2570 // Clean up all connected clients
2571 log_debug("Cleaning up connected clients...");
2572 // FIXED: Simplified to collect client IDs first, then remove them without holding locks
2573 uint32_t clients_to_remove[MAX_CLIENTS];
2574 int client_count = 0;
2575
2576 rwlock_rdlock(&g_client_manager_rwlock);
2577 for (int i = 0; i < MAX_CLIENTS; i++) {
2578 client_info_t *client = &g_client_manager.clients[i];
2579
2580 // Only attempt to clean up clients that were actually connected
2581 // (client_id is 0 for uninitialized clients, starts from 1 for connected clients)
2582 // FIXED: Only access mutex for initialized clients to avoid accessing uninitialized mutex
2583 if (atomic_load(&client->client_id) == 0) {
2584 continue; // Skip uninitialized clients
2585 }
2586
2587 // Use snapshot pattern to avoid holding both locks simultaneously
2588 // This prevents deadlock by not acquiring client_state_mutex while holding rwlock
2589 uint32_t client_id_snapshot = atomic_load(&client->client_id); // Atomic read is safe under rwlock
2590
2591 // Clean up ANY client that was allocated, whether active or not
2592 // (disconnected clients may not be active but still have resources)
2593 clients_to_remove[client_count++] = client_id_snapshot;
2594 }
2595 rwlock_rdunlock(&g_client_manager_rwlock);
2596
2597 // Remove all clients without holding any locks
2598 for (int i = 0; i < client_count; i++) {
2599 if (remove_client(&server_ctx, clients_to_remove[i]) != 0) {
2600 log_error("Failed to remove client %u during shutdown", clients_to_remove[i]);
2601 }
2602 }
2603
2604 // Clean up hash table
2605 // Clean up uthash table (uthash handles deletion when the last item is removed,
2606 // but we should clear it here just in case there are stragglers)
2608 client_info_t *current_client, *tmp;
2609 HASH_ITER(hh, g_client_manager.clients_by_id, current_client, tmp) {
2610 HASH_DELETE(hh, g_client_manager.clients_by_id, current_client);
2611 // Note: We don't free current_client here because it's part of the clients[] array
2612 }
2614 }
2615
2616 // Clean up audio mixer
2617 if (g_audio_mixer) {
2618 // Set to NULL first before destroying.
2619 // Client handler threads may still be running and checking g_audio_mixer.
2620 // Setting it to NULL first prevents use-after-free race condition.
2621 // volatile ensures this write is visible to other threads immediately
2622 mixer_t *mixer_to_destroy = g_audio_mixer;
2623 g_audio_mixer = NULL;
2624 mixer_destroy(mixer_to_destroy);
2625 }
2626
2627 // Clean up mDNS context
2628 if (g_mdns_ctx) {
2629 asciichat_mdns_destroy(g_mdns_ctx);
2630 g_mdns_ctx = NULL;
2631 log_debug("mDNS context shut down");
2632 }
2633
2634 // Clean up synchronization primitives
2635 rwlock_destroy(&g_client_manager_rwlock);
2637
2638#ifdef NDEBUG
2639 // Clean up statistics system
2640 stats_cleanup();
2641#endif
2642
2643#ifndef NDEBUG
2644 // Clean up lock debugging system (always, regardless of build type)
2645 // Lock debug records are allocated in debug builds too, so they must be cleaned up
2647#endif
2648
2649 // Destroy session host (before TCP server shutdown)
2650 if (server_ctx.session_host) {
2651 log_debug("Destroying session host");
2653 server_ctx.session_host = NULL;
2654 }
2655
2656 // Shutdown TCP server (closes listen sockets and cleans up)
2657 tcp_server_destroy(&g_tcp_server);
2658
2659 // WebSocket server already shut down before client cleanup (above).
2660
2661 // Join ACDS threads (if started)
2662 // NOTE: Must be done BEFORE destroying transport to ensure clean shutdown
2663 if (g_acds_ping_thread_started) {
2664 log_debug("Joining ACDS ping thread");
2665 asciichat_thread_join(&g_acds_ping_thread, NULL);
2666 g_acds_ping_thread_started = false;
2667 log_debug("ACDS ping thread joined");
2668 }
2669
2670 if (g_acds_receive_thread_started) {
2671 log_debug("Joining ACDS receive thread");
2672 asciichat_thread_join(&g_acds_receive_thread, NULL);
2673 g_acds_receive_thread_started = false;
2674 log_debug("ACDS receive thread joined");
2675 }
2676
2677 // Clean up WebRTC peer manager (if initialized for ACDS signaling relay)
2678 if (g_webrtc_peer_manager) {
2679 log_debug("Destroying WebRTC peer manager");
2680 webrtc_peer_manager_destroy(g_webrtc_peer_manager);
2681 g_webrtc_peer_manager = NULL;
2682 }
2683
2684 // Clean up ACDS transport wrapper (if created)
2685 if (g_acds_transport) {
2686 log_debug("Destroying ACDS transport wrapper");
2687 acip_transport_destroy(g_acds_transport);
2688 g_acds_transport = NULL;
2689 }
2690
2691 // Disconnect from ACDS server (if connected for WebRTC signaling relay)
2692 if (g_acds_client) {
2693 log_debug("Disconnecting from ACDS server");
2694 acds_client_disconnect(g_acds_client);
2695 SAFE_FREE(g_acds_client);
2696 g_acds_client = NULL;
2697 }
2698
2699 // Clean up SIMD caches
2701
2702 // Clean up symbol cache
2703 // This must be called BEFORE log_destroy() as symbol_cache_destroy() uses log_debug()
2704 // Safe to call even if atexit() runs - it's idempotent (checks g_symbol_cache_initialized)
2705 // Also called via platform_destroy() atexit handler, but explicit call ensures proper ordering
2707
2708 // Clean up global buffer pool (explicitly, as atexit may not run on Ctrl-C)
2709 // Note: This is also registered with atexit(), but calling it explicitly is safe (idempotent)
2710 // Safe to call even if atexit() runs - it checks g_global_buffer_pool and sets it to NULL
2712
2713 // Disable keepawake mode (re-allow OS to sleep)
2715
2716 // Clean up binary path cache explicitly
2717 // Note: This is also called by platform_destroy() via atexit(), but it's idempotent
2718 // (checks g_cache_initialized and sets it to false, sets g_bin_path_cache to NULL)
2719 // Safe to call even if atexit() runs later
2721
2722 // Clean up errno context (allocated strings, backtrace symbols)
2724
2725 // Clean up RCU-based options state
2727
2728 // Clean up platform-specific resources (Windows: Winsock cleanup, timer restoration)
2729 // POSIX: minimal cleanup (symbol cache already handled above on Windows)
2730 socket_cleanup();
2731 platform_restore_timer_resolution(); // Restore timer resolution (no-op on POSIX)
2732
2733#ifndef NDEBUG
2734 // Join the lock debug thread as one of the very last things before exit
2736#endif
2737
2738 log_info("Server shutdown complete");
2739
2741 log_destroy();
2742
2743 // Use exit() to allow atexit() handlers to run
2744 // Cleanup functions are idempotent (check if initialized first)
2745 exit(0);
2746}
void acds_client_config_init_defaults(acds_client_config_t *config)
Definition acds_client.c:45
asciichat_error_t acds_session_create(acds_client_t *client, const acds_session_create_params_t *params, acds_session_create_result_t *result)
asciichat_error_t acds_session_join(acds_client_t *client, const acds_session_join_params_t *params, acds_session_join_result_t *result)
asciichat_error_t acds_client_connect(acds_client_t *client, const acds_client_config_t *config)
Definition acds_client.c:60
void acds_client_disconnect(acds_client_t *client)
void ascii_simd_init(void)
Definition ascii_simd.c:98
void asciichat_error_stats_print(void)
void asciichat_errno_destroy(void)
void buffer_pool_cleanup_global(void)
asciichat_error_t acds_string_generate(char *output, size_t output_size)
size_t g_num_whitelisted_clients
Number of whitelisted clients.
bool g_server_encryption_enabled
Global flag indicating if server encryption is enabled.
public_key_t g_client_whitelist[MAX_CLIENTS]
Global client public key whitelist.
private_key_t g_server_private_key
Global server private key (first identity key, for backward compatibility)
void session_host_destroy(session_host_t *host)
Definition host.c:222
session_host_t * session_host_create(const session_host_config_t *config)
Definition host.c:166
asciichat_error_t interactive_grep_init(void)
int is_localhost_ipv6(const char *ip)
Definition ip.c:1320
int is_localhost_ipv4(const char *ip)
Definition ip.c:1299
void tcp_server_destroy(tcp_server_t *server)
asciichat_error_t tcp_server_init(tcp_server_t *server, const tcp_server_config_t *config)
asciichat_error_t tcp_server_run(tcp_server_t *server)
asciichat_error_t webrtc_init(void)
asciichat_error_t websocket_server_init(websocket_server_t *server, const websocket_server_config_t *config)
void websocket_server_destroy(websocket_server_t *server)
void websocket_server_cancel_service(websocket_server_t *server)
void platform_disable_keepawake(void)
asciichat_error_t platform_enable_keepawake(void)
void lock_debug_cleanup_thread(void)
Definition lock.c:1371
void lock_debug_destroy(void)
Definition lock.c:1370
void log_destroy(void)
void asciichat_mdns_destroy(asciichat_mdns_t *mdns)
Definition mdns.c:66
asciichat_mdns_t * asciichat_mdns_init(void)
Definition mdns.c:35
mixer_t * mixer_create(int max_sources, int sample_rate)
Definition mixer.c:218
void mixer_destroy(mixer_t *mixer)
Definition mixer.c:347
const float weight_blue
const float weight_red
const float weight_green
asciichat_error_t webrtc_peer_manager_create(const webrtc_peer_manager_config_t *config, const webrtc_signaling_callbacks_t *signaling_callbacks, webrtc_peer_manager_t **manager_out)
void webrtc_peer_manager_destroy(webrtc_peer_manager_t *manager)
bool terminal_can_prompt_user(void)
bool terminal_is_interactive(void)
uint64_t platform_get_monotonic_time_us(void)
void rate_limiter_destroy(rate_limiter_t *limiter)
Definition rate_limit.c:116
rate_limiter_t * rate_limiter_create_memory(void)
Definition rate_limit.c:73
void options_state_destroy(void)
Definition rcu.c:309
rate_limiter_t * g_rate_limiter
Global rate limiter for connection attempts and packet processing.
mixer_t *volatile g_audio_mixer
Global audio mixer instance for multi-client audio processing.
atomic_bool g_server_should_exit
Global atomic shutdown flag shared across all threads.
static_cond_t g_shutdown_cond
Global shutdown condition variable for waking blocked threads.
void server_status_log_destroy(void)
void server_status_log_clear(void)
void server_status_log_init(void)
rwlock_t g_client_manager_rwlock
Reader-writer lock protecting the global client manager.
int remove_client(server_context_t *server_ctx, uint32_t client_id)
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
server_stats_t g_stats
Global server statistics structure.
Definition stats.c:162
void * stats_logger_thread(void *arg)
Main statistics collection and reporting thread function.
Definition stats.c:336
int stats_init(void)
Initialize the stats mutex.
Definition stats.c:189
mutex_t g_stats_mutex
Mutex protecting global server statistics.
Definition stats.c:171
void stats_cleanup(void)
Cleanup the stats mutex.
Definition stats.c:203
client_info_t * clients_by_id
uthash head pointer for O(1) client_id -> client_info_t* lookups
Definition client.h:67
mutex_t mutex
Legacy mutex (mostly replaced by rwlock)
Definition client.h:71
client_info_t clients[MAX_CLIENTS]
Array of client_info_t structures (backing storage)
Definition client.h:65
Server context - encapsulates all server state.
tcp_server_t * tcp_server
TCP server managing connections.
session_host_t * session_host
Session host for discovery mode support.
int stun_servers_parse(const char *csv_servers, const char *default_csv, stun_server_t *out_servers, int max_count)
Parse comma-separated STUN server URLs into stun_server_t array.
Definition stun.c:51
void symbol_cache_destroy(void)
Definition symbols.c:292
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
void platform_cleanup_binary_path_cache(void)
Cleanup the binary PATH cache.
Definition system.c:304
acip_transport_t * acip_tcp_transport_create(socket_t sockfd, crypto_context_t *crypto_ctx)
void acip_transport_destroy(acip_transport_t *transport)
void thread_pool_destroy(thread_pool_t *pool)
Definition thread_pool.c:48
thread_pool_t * thread_pool_create(const char *pool_name)
Definition thread_pool.c:17
asciichat_error_t thread_pool_spawn(thread_pool_t *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *thread_name)
Definition thread_pool.c:70
int mutex_init(mutex_t *mutex)
Definition threading.c:16
int asciichat_thread_create(asciichat_thread_t *thread, void *(*start_routine)(void *), void *arg)
Definition threading.c:42
int rwlock_init(rwlock_t *rwlock)
Definition threading.c:63
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Definition threading.c:46
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21
asciichat_error_t nat_upnp_open(uint16_t internal_port, const char *description, nat_upnp_context_t **ctx)
Definition upnp.c:236
asciichat_error_t nat_upnp_get_address(const nat_upnp_context_t *ctx, char *addr, size_t addr_len)
Definition upnp.c:310
bool platform_prompt_yes_no(const char *question, bool default_yes)
Definition util.c:81
void precalc_rgb_palettes(const float red, const float green, const float blue)
void simd_caches_destroy_all(void)

References acds_client_config_init_defaults(), acds_client_connect(), acds_client_disconnect(), acds_session_create(), acds_session_join(), acds_string_generate(), acip_tcp_transport_create(), acip_transport_destroy(), ascii_simd_init(), asciichat_errno_destroy(), asciichat_error_stats_print(), asciichat_mdns_destroy(), asciichat_mdns_init(), asciichat_thread_create(), asciichat_thread_join(), buffer_pool_cleanup_global(), client_manager_t::clients, client_manager_t::clients_by_id, g_audio_mixer, g_client_manager, g_client_manager_rwlock, g_client_whitelist, g_num_whitelisted_clients, g_rate_limiter, g_server_encryption_enabled, g_server_private_key, g_server_should_exit, g_shutdown_cond, g_stats, g_stats_mutex, interactive_grep_init(), is_localhost_ipv4(), is_localhost_ipv6(), lock_debug_cleanup_thread(), lock_debug_destroy(), log_destroy(), mixer_create(), mixer_destroy(), client_manager_t::mutex, mutex_destroy(), mutex_init(), nat_upnp_get_address(), nat_upnp_open(), options_state_destroy(), platform_cleanup_binary_path_cache(), platform_disable_keepawake(), platform_enable_keepawake(), platform_get_monotonic_time_us(), platform_prompt_yes_no(), precalc_rgb_palettes(), rate_limiter_create_memory(), rate_limiter_destroy(), remove_client(), rwlock_init(), safe_snprintf(), server_status_log_clear(), server_status_log_destroy(), server_status_log_init(), server_context_t::session_host, session_host_create(), session_host_destroy(), simd_caches_destroy_all(), stats_cleanup(), stats_init(), stats_logger_thread(), stun_servers_parse(), symbol_cache_destroy(), server_context_t::tcp_server, tcp_server_destroy(), tcp_server_init(), tcp_server_run(), terminal_can_prompt_user(), terminal_is_interactive(), thread_pool_create(), thread_pool_destroy(), thread_pool_spawn(), webrtc_init(), webrtc_peer_manager_create(), webrtc_peer_manager_destroy(), websocket_server_cancel_service(), websocket_server_destroy(), websocket_server_init(), weight_blue, weight_green, and weight_red.

Variable Documentation

◆ g_audio_mixer

mixer_t* volatile g_audio_mixer = NULL

Global audio mixer instance for multi-client audio processing.

Global audio mixer.

The mixer combines audio streams from multiple clients, excluding each client's own audio from their outbound stream (preventing echo). Created once during server initialization and shared by all audio render threads.

THREAD SAFETY: The mixer itself is thread-safe and can be used concurrently by multiple render.c audio threads without external synchronization. During shutdown, set to NULL before destroying to prevent use-after-free.

Definition at line 167 of file server/main.c.

Referenced by add_client(), add_webrtc_client(), client_audio_render_thread(), remove_client(), and server_main().

◆ g_rate_limiter

rate_limiter_t* g_rate_limiter = NULL

Global rate limiter for connection attempts and packet processing.

Global connection rate limiter.

In-memory rate limiter to prevent connection flooding and DoS attacks. Tracks connection attempts and packet rates per IP address with configurable limits.

Default limits (from rate_limit.c):

  • RATE_EVENT_CONNECTION: 50 connections per 60 seconds
  • RATE_EVENT_IMAGE_FRAME: 144 FPS (8640 frames/min)
  • RATE_EVENT_AUDIO: 172 FPS (10320 packets/min)
  • RATE_EVENT_PING: 2 Hz (120 pings/min)
  • RATE_EVENT_CLIENT_JOIN: 10 joins per 60 seconds
  • RATE_EVENT_CONTROL: 100 control packets per 60 seconds

THREAD SAFETY: The rate limiter is thread-safe and can be used concurrently from the main accept loop and packet handlers without external synchronization.

See also
main.h for extern declaration

Definition at line 197 of file server/main.c.

Referenced by process_decrypted_packet(), and server_main().

◆ g_server_should_exit

atomic_bool g_server_should_exit = false

Global atomic shutdown flag shared across all threads.

Server shutdown flag.

This flag is the primary coordination mechanism for clean server shutdown. It's atomic to ensure thread-safe access without mutexes, as it's checked frequently in tight loops across all worker threads.

USAGE PATTERN:

  • Set to true by signal handlers (SIGINT/SIGTERM) or main loop on error
  • Checked by all worker threads to know when to exit gracefully
  • Must be atomic to prevent race conditions during shutdown cascade

Definition at line 143 of file server/main.c.

Referenced by client_audio_render_thread(), client_dispatch_thread(), client_receive_thread(), client_send_thread_func(), client_video_render_thread(), handle_image_frame_packet(), server_main(), stats_logger_thread(), and stop_client_render_threads().

◆ g_shutdown_cond

static_cond_t g_shutdown_cond = STATIC_COND_INIT

Global shutdown condition variable for waking blocked threads.

Used to wake up threads that might be blocked on condition variables (like packet queues) during shutdown. This ensures responsive shutdown even when threads are waiting on blocking operations.

Definition at line 176 of file server/main.c.

Referenced by server_main().