321 bool has_ever_connected) {
322 (void)first_connection;
323 if (!address || port <= 0) {
324 log_error(
"Invalid address or port parameters");
329 if (g_sockfd != INVALID_SOCKET_VALUE) {
331 g_sockfd = INVALID_SOCKET_VALUE;
335 if (reconnect_attempt > 0) {
336 unsigned int delay_us = get_reconnect_delay(reconnect_attempt);
342 log_debug(
"Exit requested during reconnection delay");
353 const char *ws_url = address;
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,
361 log_info(
"Connecting via WebSocket: %s", ws_url);
365 log_debug(
"CLIENT_CONNECT: Calling client_crypto_init()");
367 log_error(
"Failed to initialize crypto (password required or incorrect)");
368 log_debug(
"CLIENT_CONNECT: client_crypto_init() failed");
378 if (!g_client_transport) {
379 log_error(
"Failed to create WebSocket ACIP transport");
383 log_debug(
"CLIENT_CONNECT: Created WebSocket ACIP transport with crypto context");
386 atomic_store(&g_connection_active,
true);
387 atomic_store(&g_connection_lost,
false);
394 g_client_transport = NULL;
400 if (!GET_OPTION(snapshot_mode) && has_ever_connected) {
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;
411 if (GET_OPTION(color_mode) != COLOR_MODE_NONE) {
412 my_capabilities |= CLIENT_CAP_COLOR;
414 if (GET_OPTION(stretch)) {
415 my_capabilities |= CLIENT_CAP_STRETCH;
419 const char *display_name = platform_get_username();
420 char my_display_name[MAX_DISPLAY_NAME_LEN];
422 SAFE_SNPRINTF(my_display_name,
sizeof(my_display_name),
"%s-%d", display_name, pid);
426 g_client_transport = NULL;
431 log_info(
"WebSocket connection established successfully");
441 struct addrinfo hints, *res = NULL, *addr_iter;
442 memset(&hints, 0,
sizeof(hints));
443 hints.ai_family = AF_UNSPEC;
444 hints.ai_socktype = SOCK_STREAM;
446 hints.ai_flags = AI_NUMERICSERV;
450 SAFE_SNPRINTF(port_str,
sizeof(port_str),
"%d", port);
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;
458 int ipv6_result = getaddrinfo(
"::1", port_str, &hints, &res);
459 if (ipv6_result == 0 && res != NULL) {
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);
465 log_debug(
"Connection successful using IPv6 loopback");
466 SAFE_STRNCPY(g_server_ip,
"::1",
sizeof(g_server_ip));
469 goto connection_success;
471 if (socket_get_error(g_sockfd) != 0) {
472 log_debug(
"NETWORK_ERROR: %d", (
int)socket_get_error(g_sockfd));
477 g_sockfd = INVALID_SOCKET_VALUE;
485 log_debug(
"Exit requested during connection attempt");
490 log_debug(
"IPv6 failed, trying IPv4 loopback 127.0.0.1:%s...", port_str);
491 hints.ai_family = AF_INET;
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);
499 log_debug(
"Connection successful using IPv4 loopback");
500 SAFE_STRNCPY(g_server_ip,
"127.0.0.1",
sizeof(g_server_ip));
503 goto connection_success;
505 if (socket_get_error(g_sockfd) != 0) {
506 log_debug(
"NETWORK_ERROR: %d", (
int)socket_get_error(g_sockfd));
511 g_sockfd = INVALID_SOCKET_VALUE;
518 log_warn(
"Could not connect to localhost using either IPv6 or IPv4 loopback");
523 log_debug(
"Resolving server address '%s' port %s...", address, port_str);
524 hints.ai_family = AF_UNSPEC;
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));
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) {
537 if (addr_iter->ai_family != 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());
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...");
556 if (
connect_with_timeout(g_sockfd, addr_iter->ai_addr, addr_iter->ai_addrlen, CONNECT_TIMEOUT)) {
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");
563 if (
format_ip_address(addr_iter->ai_family, addr_iter->ai_addr, g_server_ip,
sizeof(g_server_ip)) ==
565 log_debug(
"Resolved server IP: %s", g_server_ip);
567 log_warn(
"Failed to format server IP address");
570 goto connection_success;
574 if (socket_get_error(g_sockfd) != 0) {
575 log_debug(
"NETWORK_ERROR: %d", (
int)socket_get_error(g_sockfd));
580 g_sockfd = INVALID_SOCKET_VALUE;
591 if (g_sockfd == INVALID_SOCKET_VALUE) {
592 log_warn(
"Could not connect to server %s:%d (tried all addresses)", address, port);
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) {
602 g_sockfd = INVALID_SOCKET_VALUE;
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);
613 g_my_client_id = (uint32_t)local_port;
616 atomic_store(&g_connection_active,
true);
617 atomic_store(&g_connection_lost,
false);
618 atomic_store(&g_should_reconnect,
false);
622 log_debug(
"CLIENT_CONNECT: Calling client_crypto_init()");
624 log_error(
"Failed to initialize crypto (password required or incorrect)");
625 log_debug(
"CLIENT_CONNECT: client_crypto_init() failed");
627 g_sockfd = INVALID_SOCKET_VALUE;
632 log_debug(
"CLIENT_CONNECT: Calling client_crypto_handshake()");
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);
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");
642 log_debug(
"CLIENT_CONNECT: client_crypto_handshake() succeeded");
650 if (!g_client_transport) {
651 log_error(
"Failed to create TCP ACIP transport");
653 g_sockfd = INVALID_SOCKET_VALUE;
656 log_debug(
"CLIENT_CONNECT: Created TCP ACIP transport with crypto context");
660 if (!GET_OPTION(snapshot_mode)) {
661 log_debug(
"Connected to server - terminal logging will be disabled after initial setup");
663 log_debug(
"Connected to server - terminal logging kept enabled for snapshot mode");
667 if (socket_set_keepalive(g_sockfd,
true) < 0) {
673 if (sock_config_result != ASCIICHAT_OK) {
682 g_sockfd = INVALID_SOCKET_VALUE;
687 if (!GET_OPTION(snapshot_mode) && has_ever_connected) {
689 log_debug(
"Reconnected to server - terminal logging disabled to prevent interference with ASCII display");
693 uint32_t my_capabilities = CLIENT_CAP_VIDEO;
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;
699 if (GET_OPTION(color_mode) != COLOR_MODE_NONE) {
700 my_capabilities |= CLIENT_CAP_COLOR;
702 if (GET_OPTION(stretch)) {
703 my_capabilities |= CLIENT_CAP_STRETCH;
707 const char *display_name = platform_get_username();
709 char my_display_name[MAX_DISPLAY_NAME_LEN];
711 SAFE_SNPRINTF(my_display_name,
sizeof(my_display_name),
"%s-%d", display_name, pid);
716 g_sockfd = INVALID_SOCKET_VALUE;
1208 log_debug(
"Sending terminal size to server: %ux%u (auto_width=%d, auto_height=%d)", width, height,
1222 bool is_snapshot_mode = GET_OPTION(snapshot_mode);
1224 caps.wants_padding = is_interactive && !is_snapshot_mode;
1226 log_debug(
"Client capabilities: wants_padding=%d (snapshot=%d, interactive=%d, stdin_tty=%d, stdout_tty=%d)",
1230 caps = apply_color_mode_override(caps);
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;
1243 caps.wants_padding = is_interactive && !is_snapshot_mode;
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);
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';
1263 SAFE_MEMSET(net_packet.palette_custom,
sizeof(net_packet.palette_custom), 0,
sizeof(net_packet.palette_custom));
1267 int fps = GET_OPTION(fps);
1269 net_packet.desired_fps = (uint8_t)(fps > 144 ? 144 : fps);
1271 net_packet.desired_fps = caps.desired_fps;
1274 if (net_packet.desired_fps == 0) {
1275 net_packet.desired_fps = DEFAULT_MAX_FPS;
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';
1281 SAFE_STRNCPY(net_packet.colorterm, caps.colorterm,
sizeof(net_packet.colorterm));
1282 net_packet.colorterm[
sizeof(net_packet.colorterm) - 1] =
'\0';
1284 net_packet.detection_reliable = caps.detection_reliable;
1286 net_packet.utf8_support = (GET_OPTION(force_utf8) != UTF8_SETTING_FALSE) ? 1 : 0;
1289 net_packet.wants_padding = caps.wants_padding ? 1 : 0;