ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
src/client/server.c
Go to the documentation of this file.
1
58#include "server.h"
59#include "main.h"
60#include "../main.h" // Global exit API
61#include "crypto.h"
62#include <ascii-chat/crypto/crypto.h>
63#include <ascii-chat/crypto/handshake/common.h>
64
65#include <ascii-chat/platform/abstraction.h>
66#include <ascii-chat/platform/terminal.h>
67#include <ascii-chat/platform/system.h>
68#include <ascii-chat/network/packet.h>
69#include <ascii-chat/network/network.h>
70#include <ascii-chat/network/acip/send.h>
71#include <ascii-chat/network/acip/transport.h>
72#include <ascii-chat/util/endian.h>
73#include <ascii-chat/util/ip.h>
74#include <ascii-chat/util/time.h>
75#include <ascii-chat/util/url.h> // For WebSocket URL detection
76#include <ascii-chat/common.h>
77#include "display.h"
78#include <ascii-chat/options/options.h>
79#include <ascii-chat/options/rcu.h> // For RCU-based options access
80#include <ascii-chat/video/palette.h>
81#include <ascii-chat/buffer_pool.h>
82
83#include <string.h>
84#include <stdarg.h>
85#include <time.h>
86#include <sys/types.h>
87#include <stdatomic.h>
88
89#include <ascii-chat/platform/network.h> // Consolidates platform-specific network headers (includes TCP options)
90
91// Debug flags
92#define DEBUG_NETWORK 1
93#define DEBUG_THREADS 1
94#define DEBUG_MEMORY 1
95
96/* ============================================================================
97 * Connection State Management
98 * ============================================================================ */
99
108static socket_t g_sockfd = INVALID_SOCKET_VALUE;
109
118static acip_transport_t *g_client_transport = NULL;
119
129static atomic_bool g_connection_active = false;
130
139static atomic_bool g_connection_lost = false;
140
149static atomic_bool g_should_reconnect = false;
150
159static uint32_t g_my_client_id = 0;
160
170static char g_server_ip[256] = {0};
171
181static mutex_t g_send_mutex = {0};
182
183/* ============================================================================
184 * Crypto State
185 * ============================================================================ */
186
196crypto_handshake_context_t g_crypto_ctx = {0};
197
206static bool g_encryption_enabled = false;
207
208/* ============================================================================
209 * Reconnection Logic
210 * ============================================================================ */
211
213#define MAX_RECONNECT_DELAY (5 * 1000 * 1000)
214
227static unsigned int get_reconnect_delay(unsigned int reconnect_attempt) {
228 // Use integer arithmetic for microsecond calculations
229 // Initial delay: 100,000 us (0.1 seconds)
230 // Additional delay per attempt: 200,000 us (0.2 seconds)
231 unsigned int delay_us = 100 * US_PER_MS_INT + (reconnect_attempt - 1) * 200 * US_PER_MS_INT;
232 if (delay_us > MAX_RECONNECT_DELAY)
233 delay_us = MAX_RECONNECT_DELAY;
234 return delay_us;
235}
236
237/* ============================================================================
238 * Socket Management Functions
239 * ============================================================================ */
240
252static int close_socket(socket_t socketfd) {
253 if (socket_is_valid(socketfd)) {
254 log_debug("Closing socket %d", socketfd);
255
256 if (socket_close(socketfd) != 0) {
257 log_error("Failed to close socket: %s", network_error_string());
258 return -1;
259 }
260
261 // Small delay to ensure socket resources are fully released
262 // This prevents WSA error 10038 on subsequent connections
263 platform_sleep_us(50 * US_PER_MS_INT); // 50ms delay
264
265 return 0;
266 }
267
268 return 0; // Socket already closed or invalid
269}
270
271/* ============================================================================
272 * Public Interface Functions
273 * ============================================================================ */
274
286 // Initialize mutex for thread-safe packet sending
287 if (mutex_init(&g_send_mutex) != 0) {
288 log_error("Failed to initialize send mutex");
289 return -1;
290 }
291
292 // Initialize connection state
293 g_sockfd = INVALID_SOCKET_VALUE;
294 g_client_transport = NULL;
295 atomic_store(&g_connection_active, false);
296 atomic_store(&g_connection_lost, false);
297 atomic_store(&g_should_reconnect, false);
298 g_my_client_id = 0;
299
300 return 0;
301}
302
320int server_connection_establish(const char *address, int port, int reconnect_attempt, bool first_connection,
321 bool has_ever_connected) {
322 (void)first_connection; // Currently unused
323 if (!address || port <= 0) {
324 log_error("Invalid address or port parameters");
325 return -1;
326 }
327
328 // Close any existing connection
329 if (g_sockfd != INVALID_SOCKET_VALUE) {
330 close_socket(g_sockfd);
331 g_sockfd = INVALID_SOCKET_VALUE;
332 }
333
334 // Apply reconnection delay if this is a retry
335 if (reconnect_attempt > 0) {
336 unsigned int delay_us = get_reconnect_delay(reconnect_attempt);
337 // Reconnection attempt logged only to file
338 platform_sleep_us(delay_us);
339
340 // Check if user requested exit during reconnection delay
341 if (should_exit()) {
342 log_debug("Exit requested during reconnection delay");
343 return -1;
344 }
345 } else {
346 // Initial connection logged only to file
347 }
348
349 // Check for WebSocket URL - handle separately from TCP
350 if (url_is_websocket(address)) {
351 // WebSocket connection - bypass TCP socket creation
352 // Use the original address (URL already contains port if specified)
353 const char *ws_url = address;
354
355 // Parse for debug logging
356 url_parts_t url_parts = {0};
357 if (url_parse(address, &url_parts) == ASCIICHAT_OK) {
358 log_info("Connecting via WebSocket: %s (scheme=%s, host=%s, port=%d)", ws_url, url_parts.scheme, url_parts.host,
359 url_parts.port);
360 } else {
361 log_info("Connecting via WebSocket: %s", ws_url);
362 }
363
364 // Initialize crypto if encryption is enabled
365 log_debug("CLIENT_CONNECT: Calling client_crypto_init()");
366 if (client_crypto_init() != 0) {
367 log_error("Failed to initialize crypto (password required or incorrect)");
368 log_debug("CLIENT_CONNECT: client_crypto_init() failed");
369 url_parts_destroy(&url_parts);
371 }
372
373 // Get crypto context for transport
374 const crypto_context_t *crypto_ctx = crypto_client_is_ready() ? crypto_client_get_context() : NULL;
375
376 // Create WebSocket transport (handles connection internally)
377 g_client_transport = acip_websocket_client_transport_create(ws_url, (crypto_context_t *)crypto_ctx);
378 if (!g_client_transport) {
379 log_error("Failed to create WebSocket ACIP transport");
380 url_parts_destroy(&url_parts);
381 return -1;
382 }
383 log_debug("CLIENT_CONNECT: Created WebSocket ACIP transport with crypto context");
384
385 // Set connection as active
386 atomic_store(&g_connection_active, true);
387 atomic_store(&g_connection_lost, false);
388
389 // Send initial terminal capabilities to server
390 int result = threaded_send_terminal_size_with_auto_detect(GET_OPTION(width), GET_OPTION(height));
391 if (result < 0) {
392 log_error("Failed to send initial capabilities to server: %s", network_error_string());
393 acip_transport_destroy(g_client_transport);
394 g_client_transport = NULL;
395 url_parts_destroy(&url_parts);
396 return -1;
397 }
398
399 // Disable terminal logging after initial setup (for non-snapshot mode)
400 if (!GET_OPTION(snapshot_mode) && has_ever_connected) {
402 }
403
404 // Build capabilities flags
405 uint32_t my_capabilities = CLIENT_CAP_VIDEO;
406 log_debug("GET_OPTION(audio_enabled) = %d (sending CLIENT_JOIN)", GET_OPTION(audio_enabled));
407 if (GET_OPTION(audio_enabled)) {
408 log_debug("Adding CLIENT_CAP_AUDIO to capabilities");
409 my_capabilities |= CLIENT_CAP_AUDIO;
410 }
411 if (GET_OPTION(color_mode) != COLOR_MODE_NONE) {
412 my_capabilities |= CLIENT_CAP_COLOR;
413 }
414 if (GET_OPTION(stretch)) {
415 my_capabilities |= CLIENT_CAP_STRETCH;
416 }
417
418 // Generate display name from username + PID
419 const char *display_name = platform_get_username();
420 char my_display_name[MAX_DISPLAY_NAME_LEN];
421 int pid = getpid();
422 SAFE_SNPRINTF(my_display_name, sizeof(my_display_name), "%s-%d", display_name, pid);
423 if (threaded_send_client_join_packet(my_display_name, my_capabilities) < 0) {
424 log_error("Failed to send client join packet: %s", network_error_string());
425 acip_transport_destroy(g_client_transport);
426 g_client_transport = NULL;
427 url_parts_destroy(&url_parts);
428 return -1;
429 }
430
431 log_info("WebSocket connection established successfully");
432 url_parts_destroy(&url_parts);
433 return 0;
434 }
435
436 // Resolve server address using getaddrinfo() for IPv4/IPv6 support
437 // Special handling for localhost: ensure we try both IPv6 (::1) and IPv4 (127.0.0.1)
438 // Many systems only map "localhost" to 127.0.0.1 in /etc/hosts
439 bool is_localhost = (strcmp(address, "localhost") == 0 || is_localhost_ipv4(address) || is_localhost_ipv6(address));
440
441 struct addrinfo hints, *res = NULL, *addr_iter;
442 memset(&hints, 0, sizeof(hints));
443 hints.ai_family = AF_UNSPEC; // Allow IPv4 or IPv6
444 hints.ai_socktype = SOCK_STREAM;
445 if (is_localhost) {
446 hints.ai_flags = AI_NUMERICSERV; // Optimize for localhost
447 }
448
449 char port_str[16];
450 SAFE_SNPRINTF(port_str, sizeof(port_str), "%d", port);
451
452 // For localhost, try IPv6 loopback (::1) first, then fall back to DNS resolution
453 if (is_localhost) {
454 log_debug("Localhost detected - trying IPv6 loopback [::1]:%s first...", port_str);
455 hints.ai_family = AF_INET6;
456 hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;
457
458 int ipv6_result = getaddrinfo("::1", port_str, &hints, &res);
459 if (ipv6_result == 0 && res != NULL) {
460 // Try IPv6 loopback connection
461 g_sockfd = socket_create(res->ai_family, res->ai_socktype, res->ai_protocol);
462 if (g_sockfd != INVALID_SOCKET_VALUE) {
463 log_debug("Trying IPv6 loopback connection to [::1]:%s...", port_str);
464 if (connect_with_timeout(g_sockfd, res->ai_addr, res->ai_addrlen, CONNECT_TIMEOUT)) {
465 log_debug("Connection successful using IPv6 loopback");
466 SAFE_STRNCPY(g_server_ip, "::1", sizeof(g_server_ip));
467 freeaddrinfo(res);
468 res = NULL; // Prevent double-free at connection_success label
469 goto connection_success;
470 }
471 if (socket_get_error(g_sockfd) != 0) {
472 log_debug("NETWORK_ERROR: %d", (int)socket_get_error(g_sockfd));
473 } else {
474 // log_debug("IPv6 loopback connection failed: %s", network_error_string());
475 }
476 close_socket(g_sockfd);
477 g_sockfd = INVALID_SOCKET_VALUE;
478 }
479 freeaddrinfo(res);
480 res = NULL;
481 }
482
483 // Check if user requested exit (Ctrl-C) before trying IPv4
484 if (should_exit()) {
485 log_debug("Exit requested during connection attempt");
486 return -1;
487 }
488
489 // IPv6 failed, try IPv4 loopback (127.0.0.1)
490 log_debug("IPv6 failed, trying IPv4 loopback 127.0.0.1:%s...", port_str);
491 hints.ai_family = AF_INET;
492
493 int ipv4_result = getaddrinfo("127.0.0.1", port_str, &hints, &res);
494 if (ipv4_result == 0 && res != NULL) {
495 g_sockfd = socket_create(res->ai_family, res->ai_socktype, res->ai_protocol);
496 if (g_sockfd != INVALID_SOCKET_VALUE) {
497 log_debug("Trying IPv4 loopback connection to 127.0.0.1:%s...", port_str);
498 if (connect_with_timeout(g_sockfd, res->ai_addr, res->ai_addrlen, CONNECT_TIMEOUT)) {
499 log_debug("Connection successful using IPv4 loopback");
500 SAFE_STRNCPY(g_server_ip, "127.0.0.1", sizeof(g_server_ip));
501 freeaddrinfo(res);
502 res = NULL; // Prevent double-free at connection_success label
503 goto connection_success;
504 }
505 if (socket_get_error(g_sockfd) != 0) {
506 log_debug("NETWORK_ERROR: %d", (int)socket_get_error(g_sockfd));
507 } else {
508 // log_debug("IPv4 loopback connection failed: %s", network_error_string());
509 }
510 close_socket(g_sockfd);
511 g_sockfd = INVALID_SOCKET_VALUE;
512 }
513 freeaddrinfo(res);
514 res = NULL;
515 }
516
517 // Both IPv6 and IPv4 loopback failed for localhost
518 log_warn("Could not connect to localhost using either IPv6 or IPv4 loopback");
519 return -1;
520 }
521
522 // For non-localhost addresses, use standard resolution
523 log_debug("Resolving server address '%s' port %s...", address, port_str);
524 hints.ai_family = AF_UNSPEC;
525 hints.ai_flags = 0;
526 int getaddr_result = getaddrinfo(address, port_str, &hints, &res);
527 if (getaddr_result != 0) {
528 log_error("Failed to resolve server address '%s': %s", address, gai_strerror(getaddr_result));
529 return -1;
530 }
531
532 // Try each address returned by getaddrinfo() until one succeeds
533 // Prefer IPv6 over IPv4: try IPv6 addresses first, then fall back to IPv4
534 for (int address_family = AF_INET6; address_family >= AF_INET; address_family -= (AF_INET6 - AF_INET)) {
535 for (addr_iter = res; addr_iter != NULL; addr_iter = addr_iter->ai_next) {
536 // Skip addresses that don't match current pass (IPv6 first, then IPv4)
537 if (addr_iter->ai_family != address_family) {
538 continue;
539 }
540
541 // Create socket with appropriate address family
542 g_sockfd = socket_create(addr_iter->ai_family, addr_iter->ai_socktype, addr_iter->ai_protocol);
543 if (g_sockfd == INVALID_SOCKET_VALUE) {
544 log_debug("Could not create socket for address family %d: %s", addr_iter->ai_family, network_error_string());
545 continue; // Try next address
546 }
547
548 // Log which address family we're trying
549 if (addr_iter->ai_family == AF_INET) {
550 log_debug("Trying IPv4 connection...");
551 } else if (addr_iter->ai_family == AF_INET6) {
552 log_debug("Trying IPv6 connection...");
553 }
554
555 // Attempt connection with timeout
556 if (connect_with_timeout(g_sockfd, addr_iter->ai_addr, addr_iter->ai_addrlen, CONNECT_TIMEOUT)) {
557 // Connection successful!
558 log_debug("Connection successful using %s", addr_iter->ai_family == AF_INET ? "IPv4"
559 : addr_iter->ai_family == AF_INET6 ? "IPv6"
560 : "unknown protocol");
561
562 // Extract server IP address for known_hosts
563 if (format_ip_address(addr_iter->ai_family, addr_iter->ai_addr, g_server_ip, sizeof(g_server_ip)) ==
564 ASCIICHAT_OK) {
565 log_debug("Resolved server IP: %s", g_server_ip);
566 } else {
567 log_warn("Failed to format server IP address");
568 }
569
570 goto connection_success; // Break out of both loops
571 }
572
573 // Connection failed - close socket and try next address
574 if (socket_get_error(g_sockfd) != 0) {
575 log_debug("NETWORK_ERROR: %d", (int)socket_get_error(g_sockfd));
576 } else {
577 // log_debug("Connection failed: %s", network_error_string());
578 }
579 close_socket(g_sockfd);
580 g_sockfd = INVALID_SOCKET_VALUE;
581 }
582 }
583
584connection_success:
585
586 if (res) {
587 freeaddrinfo(res);
588 }
589
590 // If we exhausted all addresses without success, fail
591 if (g_sockfd == INVALID_SOCKET_VALUE) {
592 log_warn("Could not connect to server %s:%d (tried all addresses)", address, port);
593 return -1;
594 }
595
596 // Connection successful - extract local port for client ID
597 struct sockaddr_storage local_addr = {0};
598 socklen_t addr_len = sizeof(local_addr);
599 if (getsockname(g_sockfd, (struct sockaddr *)&local_addr, &addr_len) == -1) {
600 log_error("Failed to get local socket address: %s", network_error_string());
601 close_socket(g_sockfd);
602 g_sockfd = INVALID_SOCKET_VALUE;
603 return -1;
604 }
605
606 // Extract port from either IPv4 or IPv6 address
607 int local_port = 0;
608 if (((struct sockaddr *)&local_addr)->sa_family == AF_INET) {
609 local_port = NET_TO_HOST_U16(((struct sockaddr_in *)&local_addr)->sin_port);
610 } else if (((struct sockaddr *)&local_addr)->sa_family == AF_INET6) {
611 local_port = NET_TO_HOST_U16(((struct sockaddr_in6 *)&local_addr)->sin6_port);
612 }
613 g_my_client_id = (uint32_t)local_port;
614
615 // Mark connection as active immediately after successful socket connection
616 atomic_store(&g_connection_active, true);
617 atomic_store(&g_connection_lost, false);
618 atomic_store(&g_should_reconnect, false);
619
620 // Initialize crypto BEFORE starting protocol handshake
621 // Note: server IP is already set above in the connection loop
622 log_debug("CLIENT_CONNECT: Calling client_crypto_init()");
623 if (client_crypto_init() != 0) {
624 log_error("Failed to initialize crypto (password required or incorrect)");
625 log_debug("CLIENT_CONNECT: client_crypto_init() failed");
626 close_socket(g_sockfd);
627 g_sockfd = INVALID_SOCKET_VALUE;
628 return CONNECTION_ERROR_AUTH_FAILED; // SSH key password was wrong - no retry
629 }
630
631 // Perform crypto handshake if encryption is enabled
632 log_debug("CLIENT_CONNECT: Calling client_crypto_handshake()");
633 int handshake_result = client_crypto_handshake(g_sockfd);
634 if (handshake_result != 0) {
635 log_error("Crypto handshake failed");
636 log_debug("CLIENT_CONNECT: client_crypto_handshake() failed with code %d", handshake_result);
637 close_socket(g_sockfd);
638 g_sockfd = INVALID_SOCKET_VALUE;
639 FATAL(ERROR_CRYPTO_HANDSHAKE,
640 "Crypto handshake failed with server - this usually indicates a protocol mismatch or network issue");
641 }
642 log_debug("CLIENT_CONNECT: client_crypto_handshake() succeeded");
643
644 // Create ACIP transport for protocol-agnostic packet sending
645 // The transport wraps the socket with encryption context from the handshake
646 const crypto_context_t *crypto_ctx = crypto_client_is_ready() ? crypto_client_get_context() : NULL;
647
648 // Create TCP transport (WebSocket is handled earlier in the function)
649 g_client_transport = acip_tcp_transport_create(g_sockfd, (crypto_context_t *)crypto_ctx);
650 if (!g_client_transport) {
651 log_error("Failed to create TCP ACIP transport");
652 close_socket(g_sockfd);
653 g_sockfd = INVALID_SOCKET_VALUE;
654 return -1;
655 }
656 log_debug("CLIENT_CONNECT: Created TCP ACIP transport with crypto context");
657
658 // Turn OFF terminal logging when successfully connected to server
659 // First connection - we'll disable logging after main.c shows the "Connected successfully" message
660 if (!GET_OPTION(snapshot_mode)) {
661 log_debug("Connected to server - terminal logging will be disabled after initial setup");
662 } else {
663 log_debug("Connected to server - terminal logging kept enabled for snapshot mode");
664 }
665
666 // Configure socket options for optimal performance
667 if (socket_set_keepalive(g_sockfd, true) < 0) {
668 log_warn("Failed to set socket keepalive: %s", network_error_string());
669 }
670
671 // Configure socket buffers and TCP_NODELAY for optimal performance
672 asciichat_error_t sock_config_result = socket_configure_buffers(g_sockfd);
673 if (sock_config_result != ASCIICHAT_OK) {
674 log_warn("Failed to configure socket: %s", network_error_string());
675 }
676
677 // Send initial terminal capabilities to server (this may generate debug logs)
678 int result = threaded_send_terminal_size_with_auto_detect(GET_OPTION(width), GET_OPTION(height));
679 if (result < 0) {
680 log_error("Failed to send initial capabilities to server: %s", network_error_string());
681 close_socket(g_sockfd);
682 g_sockfd = INVALID_SOCKET_VALUE;
683 return -1;
684 }
685
686 // Now disable terminal logging after capabilities are sent (for reconnections)
687 if (!GET_OPTION(snapshot_mode) && has_ever_connected) {
689 log_debug("Reconnected to server - terminal logging disabled to prevent interference with ASCII display");
690 }
691
692 // Send client join packet for multi-user support
693 uint32_t my_capabilities = CLIENT_CAP_VIDEO; // Basic video capability
694 log_debug("GET_OPTION(audio_enabled) = %d (sending CLIENT_JOIN)", GET_OPTION(audio_enabled));
695 if (GET_OPTION(audio_enabled)) {
696 log_debug("Adding CLIENT_CAP_AUDIO to capabilities");
697 my_capabilities |= CLIENT_CAP_AUDIO;
698 }
699 if (GET_OPTION(color_mode) != COLOR_MODE_NONE) {
700 my_capabilities |= CLIENT_CAP_COLOR;
701 }
702 if (GET_OPTION(stretch)) {
703 my_capabilities |= CLIENT_CAP_STRETCH;
704 }
705
706 // Generate display name from username + PID
707 const char *display_name = platform_get_username();
708
709 char my_display_name[MAX_DISPLAY_NAME_LEN];
710 int pid = getpid();
711 SAFE_SNPRINTF(my_display_name, sizeof(my_display_name), "%s-%d", display_name, pid);
712
713 if (threaded_send_client_join_packet(my_display_name, my_capabilities) < 0) {
714 log_error("Failed to send client join packet: %s", network_error_string());
715 close_socket(g_sockfd);
716 g_sockfd = INVALID_SOCKET_VALUE;
717 return -1;
718 }
719
720 // Connection already marked as active after socket creation
721
722 return 0;
723}
724
733 // For TCP: check socket validity
734 // For WebRTC: socket is INVALID_SOCKET_VALUE but transport exists
735 return atomic_load(&g_connection_active) && (g_sockfd != INVALID_SOCKET_VALUE || g_client_transport != NULL);
736}
737
746 return g_sockfd;
747}
748
756acip_transport_t *server_connection_get_transport(void) {
757 return g_client_transport;
758}
759
770void server_connection_set_transport(acip_transport_t *transport) {
771 log_debug("server_connection_set_transport() called with transport=%p", (void *)transport);
772
773 // Clean up any existing transport
774 if (g_client_transport) {
775 log_warn("Replacing existing transport with new fallback transport");
776 acip_transport_destroy(g_client_transport);
777 }
778
779 log_debug("Setting g_client_transport to %p", (void *)transport);
780 g_client_transport = transport;
781
782 // Mark connection as active when transport is set
783 if (transport) {
784 log_debug("Transport is non-NULL, extracting socket...");
785 // Extract socket from transport for backward compatibility with socket-based checks
786 g_sockfd = acip_transport_get_socket(transport);
787 log_debug("Socket extracted: %d", (int)g_sockfd);
788
789 atomic_store(&g_connection_active, true);
790 atomic_store(&g_connection_lost, false); // Reset lost flag for new connection
791 log_debug("Server connection transport set and marked active (sockfd=%d)", (int)g_sockfd);
792 } else {
793 g_sockfd = INVALID_SOCKET_VALUE;
794 atomic_store(&g_connection_active, false);
795 log_debug("Server connection transport cleared and marked inactive");
796 }
797
798 log_debug("server_connection_set_transport() completed");
799}
800
809 return g_my_client_id;
810}
811
823 return g_server_ip;
824}
825
836void server_connection_set_ip(const char *ip) {
837 if (ip) {
838 SAFE_STRNCPY(g_server_ip, ip, sizeof(g_server_ip));
839 log_debug("Server IP set to: %s", g_server_ip);
840 } else {
841 g_server_ip[0] = '\0';
842 log_debug("Server IP cleared");
843 }
844}
845
855 atomic_store(&g_connection_active, false);
856
857 // Destroy ACIP transport before closing socket
858 if (g_client_transport) {
859 acip_transport_destroy(g_client_transport);
860 g_client_transport = NULL;
861 }
862
863 if (g_sockfd != INVALID_SOCKET_VALUE) {
864 close_socket(g_sockfd);
865 g_sockfd = INVALID_SOCKET_VALUE;
866 }
867
868 g_my_client_id = 0;
869
870 // Cleanup crypto context if encryption was enabled
871 if (g_encryption_enabled) {
873 g_encryption_enabled = false;
874 }
875
876 // Turn ON terminal logging when connection is closed (unless it was disabled with --quiet)
877 if (!GET_OPTION(quiet)) {
879 }
880}
881
892 // NOTE: This function may be called from:
893 // - Signal handlers on Unix (async-signal-safe context)
894 // - SetConsoleCtrlHandler callback thread on Windows (separate thread context)
895 // Only use atomic operations and simple system calls - NO mutex locks, NO malloc, NO logging.
896
897 atomic_store(&g_connection_active, false);
898 atomic_store(&g_connection_lost, true);
899
900 if (g_sockfd != INVALID_SOCKET_VALUE) {
901 // Only shutdown() the socket to interrupt blocking recv()/send() operations.
902 // Do NOT close() here - on Windows, closing the socket while another thread
903 // is using it is undefined behavior and can cause STATUS_STACK_BUFFER_OVERRUN.
904 // The actual socket close happens in server_connection_close() which is called
905 // from the main thread after worker threads have been joined.
906 socket_shutdown(g_sockfd, SHUT_RDWR);
907 }
908
909 // DO NOT call log_set_terminal_output() here - it uses mutex which is NOT async-signal-safe.
910 // The normal cleanup path in shutdown_client() will handle logging state.
911}
912
922 atomic_store(&g_connection_lost, true);
923 atomic_store(&g_connection_active, false);
924
925 // Don't re-enable terminal logging here - let the splash screen handle it
926 // The reconnection splash will capture and display logs properly
928}
929
938 return atomic_load(&g_connection_lost);
939}
940
950 if (!GET_OPTION(quiet)) {
952 }
954 mutex_destroy(&g_send_mutex);
955}
956
957/* ============================================================================
958 * Thread Safety Interface
959 * ============================================================================ */
960
961/* ============================================================================
962 * Thread-Safe Wrapper Functions
963 * ============================================================================ */
964
978asciichat_error_t threaded_send_packet(packet_type_t type, const void *data, size_t len) {
979 // Get transport reference while holding mutex (brief lock)
980 mutex_lock(&g_send_mutex);
981
982 // Check connection status and get transport reference
983 if (!atomic_load(&g_connection_active) || !g_client_transport) {
984 mutex_unlock(&g_send_mutex);
985 return SET_ERRNO(ERROR_NETWORK, "Connection not active or transport unavailable");
986 }
987
988 // Get transport reference - transport has its own internal synchronization
989 acip_transport_t *transport = g_client_transport;
990 mutex_unlock(&g_send_mutex);
991
992 // Network I/O happens OUTSIDE the mutex to prevent deadlock on TCP buffer full
993 asciichat_error_t result = packet_send_via_transport(transport, type, data, len, 0);
994
995 // If send failed due to network error, signal connection loss
996 if (result != ASCIICHAT_OK) {
998 return result;
999 }
1000
1001 return ASCIICHAT_OK;
1002}
1003
1018int threaded_send_audio_batch_packet(const float *samples, int num_samples, int batch_count) {
1019 // Get transport reference while holding mutex (brief lock)
1020 mutex_lock(&g_send_mutex);
1021
1022 // Check connection status and get transport reference
1023 if (!atomic_load(&g_connection_active) || !g_client_transport) {
1024 mutex_unlock(&g_send_mutex);
1025 return -1;
1026 }
1027
1028 // Get transport reference - transport has its own internal synchronization
1029 acip_transport_t *transport = g_client_transport;
1030 mutex_unlock(&g_send_mutex);
1031
1032 // Network I/O happens OUTSIDE the mutex to prevent deadlock on TCP buffer full
1033 asciichat_error_t result = acip_send_audio_batch(transport, samples, (uint32_t)num_samples, (uint32_t)batch_count);
1034
1035 // If send failed due to network error, signal connection loss
1036 if (result != ASCIICHAT_OK) {
1038 return -1;
1039 }
1040
1041 return 0;
1042}
1043
1058asciichat_error_t threaded_send_audio_opus(const uint8_t *opus_data, size_t opus_size, int sample_rate,
1059 int frame_duration) {
1060 // Get transport reference while holding mutex (brief lock)
1061 mutex_lock(&g_send_mutex);
1062
1063 // Check connection status and get transport reference
1064 if (!atomic_load(&g_connection_active) || !g_client_transport) {
1065 mutex_unlock(&g_send_mutex);
1066 return SET_ERRNO(ERROR_NETWORK, "Connection not active or transport unavailable");
1067 }
1068
1069 // Get transport reference - transport has its own internal synchronization
1070 acip_transport_t *transport = g_client_transport;
1071 mutex_unlock(&g_send_mutex);
1072
1073 // Build Opus packet with header (outside mutex - no blocking I/O yet)
1074 size_t header_size = 16; // sample_rate (4), frame_duration (4), reserved (8)
1075 size_t total_size = header_size + opus_size;
1076 void *packet_data = buffer_pool_alloc(NULL, total_size);
1077 if (!packet_data) {
1078 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate buffer for Opus packet: %zu bytes", total_size);
1079 }
1080
1081 // Write header in network byte order
1082 uint8_t *buf = (uint8_t *)packet_data;
1083 uint32_t sr = HOST_TO_NET_U32((uint32_t)sample_rate);
1084 uint32_t fd = HOST_TO_NET_U32((uint32_t)frame_duration);
1085 memcpy(buf, &sr, 4);
1086 memcpy(buf + 4, &fd, 4);
1087 memset(buf + 8, 0, 8); // Reserved
1088
1089 // Copy Opus data
1090 memcpy(buf + header_size, opus_data, opus_size);
1091
1092 // Network I/O happens OUTSIDE the mutex to prevent deadlock on TCP buffer full
1093 asciichat_error_t result =
1094 packet_send_via_transport(transport, PACKET_TYPE_AUDIO_OPUS_BATCH, packet_data, total_size, 0);
1095
1096 // Clean up
1097 buffer_pool_free(NULL, packet_data, total_size);
1098
1099 // If send failed due to network error, signal connection loss
1100 if (result != ASCIICHAT_OK) {
1102 return result;
1103 }
1104
1105 return ASCIICHAT_OK;
1106}
1107
1122asciichat_error_t threaded_send_audio_opus_batch(const uint8_t *opus_data, size_t opus_size,
1123 const uint16_t *frame_sizes, int frame_count) {
1124 // Get transport reference while holding mutex (brief lock)
1125 mutex_lock(&g_send_mutex);
1126
1127 // Check connection status and get transport reference
1128 if (!atomic_load(&g_connection_active) || !g_client_transport) {
1129 mutex_unlock(&g_send_mutex);
1130 return SET_ERRNO(ERROR_NETWORK, "Connection not active or transport unavailable");
1131 }
1132
1133 // Get transport reference - transport has its own internal synchronization
1134 acip_transport_t *transport = g_client_transport;
1135 mutex_unlock(&g_send_mutex);
1136
1137 // Network I/O happens OUTSIDE the mutex to prevent deadlock on TCP buffer full
1138 // Opus uses 20ms frames at 48kHz (960 samples = 20ms)
1139 asciichat_error_t result =
1140 acip_send_audio_opus_batch(transport, opus_data, opus_size, frame_sizes, frame_count, 48000, 20);
1141
1142 // If send failed due to network error, signal connection loss
1143 if (result != ASCIICHAT_OK) {
1145 return result;
1146 }
1147
1148 return ASCIICHAT_OK;
1149}
1150
1159 // Use threaded_send_packet which handles encryption, mutex locking, and connection state
1160 return threaded_send_packet(PACKET_TYPE_PING, NULL, 0);
1161}
1162
1171 // Use threaded_send_packet which handles encryption, mutex locking, and connection state
1172 return threaded_send_packet(PACKET_TYPE_PONG, NULL, 0);
1173}
1174
1183asciichat_error_t threaded_send_stream_start_packet(uint32_t stream_type) {
1184 // Connection and transport availability is checked by threaded_send_packet()
1185
1186 // Build STREAM_START packet locally
1187 uint32_t type_data = HOST_TO_NET_U32(stream_type);
1188
1189 // Use threaded_send_packet() which handles encryption
1190 return threaded_send_packet(PACKET_TYPE_STREAM_START, &type_data, sizeof(type_data));
1191}
1192
1206asciichat_error_t threaded_send_terminal_size_with_auto_detect(unsigned short width, unsigned short height) {
1207 // Log the dimensions being sent to server (helps debug dimension mismatch issues)
1208 log_debug("Sending terminal size to server: %ux%u (auto_width=%d, auto_height=%d)", width, height,
1209 GET_OPTION(auto_width), GET_OPTION(auto_height));
1210
1211 // Connection and transport availability is checked by threaded_send_packet()
1212
1213 // Build terminal capabilities packet locally
1214 // Detect terminal capabilities automatically
1215 terminal_capabilities_t caps = detect_terminal_capabilities();
1216
1217 // Set wants_padding based on snapshot mode and TTY status
1218 // Disable padding when:
1219 // - In snapshot mode (one frame and exit)
1220 // - When stdout is not a TTY (piped/redirected output)
1221 // Enable padding for interactive terminal sessions
1222 bool is_snapshot_mode = GET_OPTION(snapshot_mode);
1223 bool is_interactive = terminal_is_interactive();
1224 caps.wants_padding = is_interactive && !is_snapshot_mode;
1225
1226 log_debug("Client capabilities: wants_padding=%d (snapshot=%d, interactive=%d, stdin_tty=%d, stdout_tty=%d)",
1227 caps.wants_padding, is_snapshot_mode, is_interactive, terminal_is_stdin_tty(), terminal_is_stdout_tty());
1228
1229 // Apply user's color mode override
1230 caps = apply_color_mode_override(caps);
1231
1232 // Check if detection was reliable, use fallback only for auto-detection
1233 if (!caps.detection_reliable && GET_OPTION(color_mode) == COLOR_MODE_AUTO) {
1234 log_warn("Terminal capability detection not reliable, using fallback");
1235 SAFE_MEMSET(&caps, sizeof(caps), 0, sizeof(caps));
1236 caps.color_level = TERM_COLOR_NONE;
1237 caps.color_count = 2;
1238 caps.capabilities = 0;
1239 SAFE_STRNCPY(caps.term_type, "unknown", sizeof(caps.term_type));
1240 SAFE_STRNCPY(caps.colorterm, "", sizeof(caps.colorterm));
1241 caps.detection_reliable = 0;
1242 // Preserve wants_padding even in fallback mode
1243 caps.wants_padding = is_interactive && !is_snapshot_mode;
1244 }
1245
1246 // Convert to network packet format with proper byte order
1247 terminal_capabilities_packet_t net_packet;
1248 net_packet.capabilities = HOST_TO_NET_U32(caps.capabilities);
1249 net_packet.color_level = HOST_TO_NET_U32(caps.color_level);
1250 net_packet.color_count = HOST_TO_NET_U32(caps.color_count);
1251 net_packet.render_mode = HOST_TO_NET_U32(caps.render_mode);
1252 net_packet.width = HOST_TO_NET_U16(width);
1253 net_packet.height = HOST_TO_NET_U16(height);
1254 net_packet.palette_type = HOST_TO_NET_U32(GET_OPTION(palette_type));
1255 net_packet.utf8_support = HOST_TO_NET_U32(caps.utf8_support ? 1 : 0);
1256
1257 const options_t *opts = options_get();
1258 if (GET_OPTION(palette_type) == PALETTE_CUSTOM && GET_OPTION(palette_custom_set)) {
1259 const char *palette_custom = opts && opts->palette_custom_set ? opts->palette_custom : "";
1260 SAFE_STRNCPY(net_packet.palette_custom, palette_custom, sizeof(net_packet.palette_custom));
1261 net_packet.palette_custom[sizeof(net_packet.palette_custom) - 1] = '\0';
1262 } else {
1263 SAFE_MEMSET(net_packet.palette_custom, sizeof(net_packet.palette_custom), 0, sizeof(net_packet.palette_custom));
1264 }
1265
1266 // Set desired FPS
1267 int fps = GET_OPTION(fps);
1268 if (fps > 0) {
1269 net_packet.desired_fps = (uint8_t)(fps > 144 ? 144 : fps);
1270 } else {
1271 net_packet.desired_fps = caps.desired_fps;
1272 }
1273
1274 if (net_packet.desired_fps == 0) {
1275 net_packet.desired_fps = DEFAULT_MAX_FPS;
1276 }
1277
1278 SAFE_STRNCPY(net_packet.term_type, caps.term_type, sizeof(net_packet.term_type));
1279 net_packet.term_type[sizeof(net_packet.term_type) - 1] = '\0';
1280
1281 SAFE_STRNCPY(net_packet.colorterm, caps.colorterm, sizeof(net_packet.colorterm));
1282 net_packet.colorterm[sizeof(net_packet.colorterm) - 1] = '\0';
1283
1284 net_packet.detection_reliable = caps.detection_reliable;
1285 // Send UTF-8 support flag: true for AUTO (default) and TRUE settings, false for FALSE setting
1286 net_packet.utf8_support = (GET_OPTION(force_utf8) != UTF8_SETTING_FALSE) ? 1 : 0;
1287
1288 // Set wants_padding flag (1=padding enabled, 0=no padding for snapshot/piped modes)
1289 net_packet.wants_padding = caps.wants_padding ? 1 : 0;
1290
1291 // Use threaded_send_packet() which handles encryption
1292 return threaded_send_packet(PACKET_TYPE_CLIENT_CAPABILITIES, &net_packet, sizeof(net_packet));
1293}
1294
1306int threaded_send_client_join_packet(const char *display_name, uint32_t capabilities) {
1307 // Connection and transport availability is checked by threaded_send_packet()
1308
1309 // Build CLIENT_JOIN packet locally
1310 client_info_packet_t join_packet;
1311 SAFE_MEMSET(&join_packet, sizeof(join_packet), 0, sizeof(join_packet));
1312 join_packet.client_id = HOST_TO_NET_U32(0); // Will be assigned by server
1313 SAFE_SNPRINTF(join_packet.display_name, MAX_DISPLAY_NAME_LEN, "%s", display_name ? display_name : "Unknown");
1314 join_packet.capabilities = HOST_TO_NET_U32(capabilities);
1315
1316 // Use threaded_send_packet() which handles encryption
1317 int send_result = threaded_send_packet(PACKET_TYPE_CLIENT_JOIN, &join_packet, sizeof(join_packet));
1318 if (send_result == 0) {
1319 mutex_lock(&g_send_mutex);
1320 bool active = atomic_load(&g_connection_active);
1321 socket_t socket_snapshot = g_sockfd;
1322 const crypto_context_t *crypto_ctx = crypto_client_is_ready() ? crypto_client_get_context() : NULL;
1323 if (active && socket_snapshot != INVALID_SOCKET_VALUE) {
1324 (void)log_network_message(
1325 socket_snapshot, (const struct crypto_context_t *)crypto_ctx, LOG_INFO, REMOTE_LOG_DIRECTION_CLIENT_TO_SERVER,
1326 "CLIENT_JOIN sent (display=\"%s\", capabilities=0x%x)", join_packet.display_name, capabilities);
1327 }
1328 mutex_unlock(&g_send_mutex);
1329 }
1330 return send_result;
1331}
void buffer_pool_free(buffer_pool_t *pool, void *data, size_t size)
void * buffer_pool_alloc(buffer_pool_t *pool, size_t size)
Definition buffer_pool.c:99
@ CONNECTION_ERROR_AUTH_FAILED
Authentication failure (no retry)
bool should_exit(void)
Definition main.c:90
void crypto_handshake_destroy(crypto_handshake_context_t *ctx)
ascii-chat Client Display Management Interface
int threaded_send_pong_packet(void)
Thread-safe pong packet transmission.
void server_connection_close()
Close the server connection gracefully.
bool server_connection_is_active()
Check if server connection is currently active.
void server_connection_shutdown()
Emergency connection shutdown for signal handlers.
asciichat_error_t threaded_send_terminal_size_with_auto_detect(unsigned short width, unsigned short height)
Thread-safe terminal size packet transmission with auto-detection.
int threaded_send_client_join_packet(const char *display_name, uint32_t capabilities)
Thread-safe client join packet transmission.
acip_transport_t * server_connection_get_transport(void)
Get ACIP transport instance.
uint32_t server_connection_get_client_id()
Get client ID assigned by server.
socket_t server_connection_get_socket()
Get current socket file descriptor.
asciichat_error_t threaded_send_audio_opus(const uint8_t *opus_data, size_t opus_size, int sample_rate, int frame_duration)
Thread-safe Opus audio frame transmission.
void server_connection_set_transport(acip_transport_t *transport)
Set ACIP transport instance from connection fallback.
void server_connection_set_ip(const char *ip)
Set the server IP address.
int threaded_send_ping_packet(void)
Thread-safe ping packet transmission.
bool server_connection_is_lost()
Check if connection loss has been detected.
int server_connection_init()
Initialize the server connection management subsystem.
asciichat_error_t threaded_send_audio_opus_batch(const uint8_t *opus_data, size_t opus_size, const uint16_t *frame_sizes, int frame_count)
Thread-safe Opus audio batch packet transmission.
asciichat_error_t threaded_send_packet(packet_type_t type, const void *data, size_t len)
Thread-safe packet transmission.
int threaded_send_audio_batch_packet(const float *samples, int num_samples, int batch_count)
Thread-safe batched audio packet transmission.
void server_connection_cleanup()
Cleanup connection management subsystem.
asciichat_error_t threaded_send_stream_start_packet(uint32_t stream_type)
Thread-safe stream start packet transmission.
void server_connection_lost()
Signal that connection has been lost.
const char * server_connection_get_ip()
Get resolved server IP address.
int server_connection_establish(const char *address, int port, int reconnect_attempt, bool first_connection, bool has_ever_connected)
Establish connection to ascii-chat server.
crypto_handshake_context_t g_crypto_ctx
Per-connection crypto handshake context.
const crypto_context_t * crypto_client_get_context(void)
Get crypto context for encryption/decryption.
int client_crypto_init(void)
Initialize client crypto handshake.
int client_crypto_handshake(socket_t socket)
Perform crypto handshake with server.
bool crypto_client_is_ready(void)
Check if crypto handshake is ready.
void display_full_reset()
Perform full display reset.
int socket_t
#define close_socket
int is_localhost_ipv6(const char *ip)
Definition ip.c:1320
int is_localhost_ipv4(const char *ip)
Definition ip.c:1299
asciichat_error_t format_ip_address(int family, const struct sockaddr *addr, char *output, size_t output_size)
Definition ip.c:196
asciichat_error_t log_network_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, const char *fmt,...)
void log_set_terminal_output(bool enabled)
bool connect_with_timeout(socket_t sockfd, const struct sockaddr *addr, socklen_t addrlen, int timeout_seconds)
Connect with timeout.
const char * network_error_string()
Get human-readable error string for network errors.
asciichat_error_t socket_configure_buffers(socket_t sockfd)
Configure socket buffers and TCP_NODELAY for optimal performance.
ASCIICHAT_API bool auto_width
ASCIICHAT_API bool auto_height
bool terminal_is_interactive(void)
bool terminal_is_stdin_tty(void)
bool terminal_is_stdout_tty(void)
terminal_capabilities_t detect_terminal_capabilities(void)
void platform_sleep_us(unsigned int us)
const options_t * options_get(void)
Definition rcu.c:347
asciichat_error_t acip_send_audio_opus_batch(acip_transport_t *transport, const void *opus_data, size_t opus_len, const uint16_t *frame_sizes, uint32_t frame_count, uint32_t sample_rate, uint32_t frame_duration)
Definition send.c:158
asciichat_error_t acip_send_audio_batch(acip_transport_t *transport, const float *samples, uint32_t num_samples, uint32_t batch_count)
Definition send.c:115
asciichat_error_t packet_send_via_transport(acip_transport_t *transport, packet_type_t type, const void *payload, size_t payload_len, uint32_t client_id)
Send packet via transport with proper header (exported for generic wrappers)
Definition send.c:41
Server cryptographic operations and per-client handshake management.
ascii-chat Server Mode Entry Point Header
#define MAX_RECONNECT_DELAY
acip_transport_t * acip_tcp_transport_create(socket_t sockfd, crypto_context_t *crypto_ctx)
void acip_transport_destroy(acip_transport_t *transport)
int mutex_init(mutex_t *mutex)
Definition threading.c:16
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21
void url_parts_destroy(url_parts_t *parts)
Definition url.c:281
bool url_is_websocket(const char *url)
Definition url.c:307
asciichat_error_t url_parse(const char *url, url_parts_t *parts_out)
Definition url.c:166
acip_transport_t * acip_websocket_client_transport_create(const char *url, crypto_context_t *crypto_ctx)
Create WebSocket client transport.