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