ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
parallel_connect.c
Go to the documentation of this file.
1
6#include <ascii-chat/network/parallel_connect.h>
7#include <ascii-chat/log/logging.h>
8#include <ascii-chat/platform/abstraction.h>
9#include <ascii-chat/util/time.h>
10#include <string.h>
11#include <errno.h>
12
13#ifdef _WIN32
14#include <winsock2.h>
15#include <ws2tcpip.h>
16#else
17#include <netdb.h>
18#include <sys/socket.h>
19#include <sys/types.h>
20#include <sys/select.h>
21#include <fcntl.h>
22#endif
23
27typedef struct {
29 struct sockaddr_storage addr;
30 socklen_t addr_len;
31 int family;
32 const char *family_name;
33 uint32_t timeout_ms;
34 volatile bool connected;
35 volatile bool done;
37 volatile bool *winner_found;
38 mutex_t *lock;
39 cond_t *signal;
40 parallel_connect_should_exit_fn should_exit_callback;
43
44static void *attempt_connection_thread(void *arg) {
46 if (!attempt)
47 return NULL;
48
49 log_debug("PCONN: [%s] Starting connection attempt", attempt->family_name);
50
51 attempt->socket = socket(attempt->family, SOCK_STREAM, 0);
52 if (attempt->socket == INVALID_SOCKET_VALUE) {
53 log_debug("PCONN: [%s] Failed to create socket", attempt->family_name);
54 goto done;
55 }
56
57 // Check if winner already found
58 mutex_lock(attempt->lock);
59 if (*attempt->winner_found) {
60 log_debug("PCONN: [%s] Winner already found, aborting", attempt->family_name);
61 mutex_unlock(attempt->lock);
62 socket_close(attempt->socket);
63 attempt->socket = INVALID_SOCKET_VALUE;
64 goto done;
65 }
66 mutex_unlock(attempt->lock);
67
68 // Set socket to non-blocking
69 if (socket_set_nonblocking(attempt->socket, true) != ASCIICHAT_OK) {
70 log_debug("PCONN: [%s] Failed to set non-blocking mode", attempt->family_name);
71 socket_close(attempt->socket);
72 attempt->socket = INVALID_SOCKET_VALUE;
73 goto done;
74 }
75
76 // Attempt non-blocking connect
77 log_debug("PCONN: [%s] Attempting connect with %dms timeout", attempt->family_name, attempt->timeout_ms);
78 int connect_result = connect(attempt->socket, (struct sockaddr *)&attempt->addr, attempt->addr_len);
79
80 int connect_error = socket_get_last_error();
81 bool is_in_progress = socket_is_in_progress_error(connect_error) || socket_is_would_block_error(connect_error);
82
83 if (connect_result == 0) {
84 // Immediate success (rare)
85 log_debug("PCONN: [%s] Connected immediately", attempt->family_name);
86 attempt->connected = true;
87 } else if (is_in_progress) {
88 // Wait with select() using short timeouts so we can check for winner, exit callback, and exit early
89 uint32_t elapsed_ms = 0;
90 uint32_t check_interval_ms = 100; // Check every 100ms if winner found or exit requested
91
92 while (elapsed_ms < attempt->timeout_ms) {
93 // Check if caller requested shutdown/exit (e.g., SIGTERM signal handler set flag)
94 if (attempt->should_exit_callback && attempt->should_exit_callback(attempt->callback_data)) {
95 log_debug("PCONN: [%s] Exit requested via callback, aborting connection", attempt->family_name);
96 break;
97 }
98
99 // Check if winner was already found (allow loser to exit early)
100 mutex_lock(attempt->lock);
101 bool should_exit = *attempt->winner_found;
102 mutex_unlock(attempt->lock);
103
104 if (should_exit) {
105 log_debug("PCONN: [%s] Winner already found, exiting early", attempt->family_name);
106 break;
107 }
108
109 fd_set writefds;
110 FD_ZERO(&writefds);
111 FD_SET(attempt->socket, &writefds);
112
113 struct timeval tv;
114 tv.tv_sec = check_interval_ms / 1000;
115 tv.tv_usec = (check_interval_ms % 1000) * 1000;
116
117 int select_result = select((int)attempt->socket + 1, NULL, &writefds, NULL, &tv);
118
119 if (select_result > 0) {
120 // Check if connection actually succeeded
121 int so_error = 0;
122 socklen_t len = sizeof(so_error);
123 getsockopt(attempt->socket, SOL_SOCKET, SO_ERROR, (char *)&so_error, &len);
124
125 if (so_error == 0) {
126 log_debug("PCONN: [%s] Connection succeeded", attempt->family_name);
127 attempt->connected = true;
128 } else {
129 log_debug("PCONN: [%s] Connection failed: %d", attempt->family_name, so_error);
130 }
131 break;
132 } else if (select_result < 0) {
133 log_debug("PCONN: [%s] Select error", attempt->family_name);
134 break;
135 }
136
137 elapsed_ms += check_interval_ms;
138 }
139
140 if (elapsed_ms >= attempt->timeout_ms && !attempt->connected) {
141 log_debug("PCONN: [%s] Connection timeout after %dms", attempt->family_name, attempt->timeout_ms);
142 }
143 } else {
144 log_debug("PCONN: [%s] Connect failed immediately: %d", attempt->family_name, connect_error);
145 }
146
147 // Signal winner if this succeeded
148 if (attempt->connected) {
149 mutex_lock(attempt->lock);
150 if (!*attempt->winner_found) {
151 log_info("PCONN: [%s] Won the race! Setting as winner", attempt->family_name);
152 *attempt->winner_socket = attempt->socket;
153 *attempt->winner_found = true;
154 attempt->socket = INVALID_SOCKET_VALUE; // Don't close it - caller owns it
155 cond_signal(attempt->signal);
156 } else {
157 log_debug("PCONN: [%s] Connected but another won already, closing", attempt->family_name);
158 socket_close(attempt->socket);
159 attempt->socket = INVALID_SOCKET_VALUE;
160 }
161 mutex_unlock(attempt->lock);
162 } else {
163 // Signal that we're done trying
164 mutex_lock(attempt->lock);
165 cond_signal(attempt->signal);
166 mutex_unlock(attempt->lock);
167 }
168
169done:
170 attempt->done = true;
171 return NULL;
172}
173
174asciichat_error_t parallel_connect(const parallel_connect_config_t *config, socket_t *out_socket) {
175 if (!config || !out_socket) {
176 return SET_ERRNO(ERROR_INVALID_PARAM, "config or out_socket is NULL");
177 }
178
179 *out_socket = INVALID_SOCKET_VALUE;
180
181 // Resolve hostname
182 log_debug("PCONN: Resolving %s:%d", config->hostname, config->port);
183
184 struct addrinfo hints;
185 struct addrinfo *result = NULL;
186 memset(&hints, 0, sizeof(hints));
187 hints.ai_family = AF_UNSPEC;
188 hints.ai_socktype = SOCK_STREAM;
189
190 char port_str[16];
191 safe_snprintf(port_str, sizeof(port_str), "%d", config->port);
192
193 int gai_result = getaddrinfo(config->hostname, port_str, &hints, &result);
194 if (gai_result != 0) {
195 return SET_ERRNO(ERROR_NETWORK, "Failed to resolve %s: %s", config->hostname, gai_strerror(gai_result));
196 }
197
198 // Find first IPv4 and IPv6 addresses
199 struct addrinfo *ipv4_addr = NULL;
200 struct addrinfo *ipv6_addr = NULL;
201
202 for (struct addrinfo *rp = result; rp != NULL; rp = rp->ai_next) {
203 if (rp->ai_family == AF_INET && !ipv4_addr) {
204 ipv4_addr = rp;
205 log_debug("PCONN: Found IPv4 address");
206 } else if (rp->ai_family == AF_INET6 && !ipv6_addr) {
207 ipv6_addr = rp;
208 log_debug("PCONN: Found IPv6 address");
209 }
210 if (ipv4_addr && ipv6_addr)
211 break;
212 }
213
214 // Need at least one address
215 if (!ipv4_addr && !ipv6_addr) {
216 freeaddrinfo(result);
217 return SET_ERRNO(ERROR_NETWORK, "No IPv4 or IPv6 addresses found for %s", config->hostname);
218 }
219
220 // Shared state
221 mutex_t lock;
222 cond_t signal;
223 mutex_init(&lock);
224 cond_init(&signal);
225
226 volatile socket_t winner_socket = INVALID_SOCKET_VALUE;
227 volatile bool winner_found = false;
228
229 connection_attempt_t ipv4_attempt = {0};
230 connection_attempt_t ipv6_attempt = {0};
231 asciichat_thread_t ipv4_thread;
232 asciichat_thread_t ipv6_thread;
233
234 asciichat_thread_init(&ipv4_thread);
235 asciichat_thread_init(&ipv6_thread);
236
237 // Start IPv4 attempt if available
238 if (ipv4_addr) {
239 ipv4_attempt.family = AF_INET;
240 ipv4_attempt.family_name = "IPv4";
241 ipv4_attempt.timeout_ms = config->timeout_ms;
242 ipv4_attempt.winner_socket = &winner_socket;
243 ipv4_attempt.winner_found = &winner_found;
244 ipv4_attempt.lock = &lock;
245 ipv4_attempt.signal = &signal;
246 ipv4_attempt.should_exit_callback = config->should_exit_callback;
247 ipv4_attempt.callback_data = config->callback_data;
248 memcpy(&ipv4_attempt.addr, ipv4_addr->ai_addr, ipv4_addr->ai_addrlen);
249 ipv4_attempt.addr_len = ipv4_addr->ai_addrlen;
250
251 asciichat_thread_create(&ipv4_thread, attempt_connection_thread, &ipv4_attempt);
252 }
253
254 // Start IPv6 attempt if available
255 if (ipv6_addr) {
256 ipv6_attempt.family = AF_INET6;
257 ipv6_attempt.family_name = "IPv6";
258 ipv6_attempt.timeout_ms = config->timeout_ms;
259 ipv6_attempt.winner_socket = &winner_socket;
260 ipv6_attempt.winner_found = &winner_found;
261 ipv6_attempt.lock = &lock;
262 ipv6_attempt.signal = &signal;
263 ipv6_attempt.should_exit_callback = config->should_exit_callback;
264 ipv6_attempt.callback_data = config->callback_data;
265 memcpy(&ipv6_attempt.addr, ipv6_addr->ai_addr, ipv6_addr->ai_addrlen);
266 ipv6_attempt.addr_len = ipv6_addr->ai_addrlen;
267
268 asciichat_thread_create(&ipv6_thread, attempt_connection_thread, &ipv6_attempt);
269 }
270
271 // Wait for winner or both to finish
272 uint32_t max_wait_ms = config->timeout_ms + 1000;
273 uint32_t elapsed_ms = 0;
274
275 mutex_lock(&lock);
276 while (!winner_found && elapsed_ms < max_wait_ms) {
277 cond_timedwait(&signal, &lock, 100 * NS_PER_MS_INT);
278 elapsed_ms += 100;
279
280 // Check if both are done
281 if (ipv4_addr && ipv6_addr) {
282 if (ipv4_attempt.done && ipv6_attempt.done) {
283 break;
284 }
285 } else if (ipv4_addr && ipv4_attempt.done) {
286 break;
287 } else if (ipv6_addr && ipv6_attempt.done) {
288 break;
289 }
290 }
291 mutex_unlock(&lock);
292
293 // Join threads (must wait for both to complete before returning)
294 // The threads access shared state on our stack, so we cannot return until they're done
295 if (asciichat_thread_is_initialized(&ipv4_thread)) {
296 asciichat_thread_join(&ipv4_thread, NULL);
297 }
298 if (asciichat_thread_is_initialized(&ipv6_thread)) {
299 asciichat_thread_join(&ipv6_thread, NULL);
300 }
301
302 // Close any remaining sockets
303 if (ipv4_attempt.socket != INVALID_SOCKET_VALUE) {
304 socket_close(ipv4_attempt.socket);
305 }
306 if (ipv6_attempt.socket != INVALID_SOCKET_VALUE) {
307 socket_close(ipv6_attempt.socket);
308 }
309
310 // Cleanup
311 mutex_destroy(&lock);
312 cond_destroy(&signal);
313 freeaddrinfo(result);
314
315 // Check result
316 if (winner_socket == INVALID_SOCKET_VALUE) {
317 return SET_ERRNO(ERROR_NETWORK, "Failed to connect to %s:%d (both IPv4 and IPv6 failed)", config->hostname,
318 config->port);
319 }
320
321 *out_socket = (socket_t)winner_socket;
322 log_info("PCONN: Successfully connected to %s:%d", config->hostname, config->port);
323 return ASCIICHAT_OK;
324}
bool should_exit(void)
Definition main.c:90
int socket_t
asciichat_error_t parallel_connect(const parallel_connect_config_t *config, socket_t *out_socket)
Per-thread connection attempt state for parallel IPv4/IPv6 racing.
volatile bool done
True if this attempt finished.
volatile bool * winner_found
Shared flag indicating a winner exists.
int family
Address family (AF_INET or AF_INET6)
socklen_t addr_len
Length of address structure.
void * callback_data
User data for callback.
mutex_t * lock
Shared mutex for coordination.
parallel_connect_should_exit_fn should_exit_callback
Optional exit callback for graceful shutdown.
const char * family_name
Human-readable family name ("IPv4"/"IPv6")
uint32_t timeout_ms
Connection timeout in milliseconds.
socket_t socket
Socket for this attempt.
volatile bool connected
True if this attempt succeeded.
cond_t * signal
Shared condition variable for signaling.
struct sockaddr_storage addr
Socket address to connect to.
volatile socket_t * winner_socket
Shared pointer to winning socket.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
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 asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Definition threading.c:46
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21