ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
lib/network/tcp/server.c
Go to the documentation of this file.
1
6#include <stdio.h>
7#include <stdlib.h>
8#include <string.h>
9#include <stdbool.h>
10#include <errno.h>
11
12#ifdef _WIN32
13#include <winsock2.h>
14#include <ws2tcpip.h>
15#else
16#include <netdb.h>
17#endif
18
20#include <ascii-chat/common.h>
21#include <ascii-chat/log/log.h>
25#include <ascii-chat/util/ip.h>
28
40static socket_t bind_and_listen(const char *address, int family, int port) {
41 struct addrinfo hints, *res = NULL;
42 memset(&hints, 0, sizeof(hints));
43 hints.ai_family = family;
44 hints.ai_socktype = SOCK_STREAM;
45 hints.ai_flags = AI_PASSIVE | AI_NUMERICHOST;
46
47 char port_str[16];
48 SAFE_SNPRINTF(port_str, sizeof(port_str), "%d", port);
49
50 // Use provided address or NULL for wildcard
51 const char *addr_str = (address && address[0] != '\0') ? address : NULL;
52
53 int gai_result = getaddrinfo(addr_str, port_str, &hints, &res);
54 if (gai_result != 0) {
55 log_error("getaddrinfo failed for %s:%d: %s", addr_str ? addr_str : "(wildcard)", port, gai_strerror(gai_result));
57 }
58
59 const char *socket_name = (family == AF_INET6) ? "server_ipv6" : "server_ipv4";
60 socket_t server_socket = socket_create(socket_name, res->ai_family, res->ai_socktype, res->ai_protocol);
61 if (server_socket == INVALID_SOCKET_VALUE) {
62 log_error("Failed to create socket for %s:%d", addr_str ? addr_str : "(wildcard)", port);
63 freeaddrinfo(res);
65 }
66
67 // Enable SO_REUSEADDR to allow quick restarts
68 int reuse = 1;
69 if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)) < 0) {
70 log_warn("Failed to set SO_REUSEADDR on %s:%d", addr_str ? addr_str : "(wildcard)", port);
71 }
72
73 // For IPv6, disable IPv4-mapped addresses (IPV6_V6ONLY=1) to support dual-stack
74 if (family == AF_INET6) {
75 int ipv6only = 1;
76 if (setsockopt(server_socket, IPPROTO_IPV6, IPV6_V6ONLY, (const char *)&ipv6only, sizeof(ipv6only)) < 0) {
77 log_warn("Failed to set IPV6_V6ONLY on [%s]:%d", addr_str ? addr_str : "::", port);
78 }
79 }
80
81 // Bind socket
82 if (bind(server_socket, res->ai_addr, (socklen_t)res->ai_addrlen) < 0) {
83 log_error("Failed to bind %s:%d", addr_str ? addr_str : "(wildcard)", port);
84 socket_close(server_socket);
85 freeaddrinfo(res);
87 }
88
89 freeaddrinfo(res);
90
91 // Listen with backlog of 128 connections
92 if (listen(server_socket, 128) < 0) {
93 log_error("Failed to listen on %s:%d", addr_str ? addr_str : "(wildcard)", port);
94 socket_close(server_socket);
96 }
97
98 log_info("Listening on %s%s%s:%d (%s)", family == AF_INET6 ? "[" : "",
99 addr_str ? addr_str : (family == AF_INET ? "0.0.0.0" : "::"), family == AF_INET6 ? "]" : "", port,
100 family == AF_INET ? "IPv4" : "IPv6");
101
102 return server_socket;
103}
104
106 if (!server || !config) {
107 return SET_ERRNO(ERROR_INVALID_PARAM, "server or config is NULL");
108 }
109
110 // Note: client_handler is optional - some users may use tcp_server just for socket setup
111 // and implement their own accept loop (like ascii-chat server with its cleanup logic)
112
113 // Initialize server state
114 memset(server, 0, sizeof(*server));
117 atomic_store_bool(&server->running, true);
118 server->config = *config; // Copy config
119
120 // Initialize client registry
121 server->clients = NULL; // uthash starts with NULL
122 server->cleanup_fn = NULL;
123 if (rwlock_init(&server->clients_rwlock, "clients") != 0) {
124 return SET_ERRNO(ERROR_THREAD, "Failed to initialize clients read-write lock");
125 }
126
127 // Determine which IP versions to bind
128 bool should_bind_ipv4 = config->bind_ipv4;
129 bool should_bind_ipv6 = config->bind_ipv6;
130
131 // Bind IPv4 socket if requested
132 if (should_bind_ipv4) {
133 const char *ipv4_addr = (config->ipv4_address && config->ipv4_address[0] != '\0') ? config->ipv4_address : NULL;
134 server->listen_socket = bind_and_listen(ipv4_addr, AF_INET, config->port);
135
136 if (server->listen_socket == INVALID_SOCKET_VALUE) {
137 log_warn("Failed to bind IPv4 socket");
138 }
139 }
140
141 // Bind IPv6 socket if requested
142 if (should_bind_ipv6) {
143 const char *ipv6_addr = (config->ipv6_address && config->ipv6_address[0] != '\0') ? config->ipv6_address : NULL;
144 server->listen_socket6 = bind_and_listen(ipv6_addr, AF_INET6, config->port);
145
146 if (server->listen_socket6 == INVALID_SOCKET_VALUE) {
147 log_warn("Failed to bind IPv6 socket");
148 }
149 }
150
151 // Ensure at least one socket bound successfully
153 return SET_ERRNO(ERROR_NETWORK_BIND, "Failed to bind any sockets (IPv4 and IPv6 both failed)");
154 }
155
156 /* Register server with named registry, identified by port */
157 char port_name[32];
158 snprintf(port_name, sizeof(port_name), "server:%d", server->config.port);
159 NAMED_REGISTER(server, port_name, "tcp_server", "0x%tx", NULL);
160
161 /* Register server's sync primitives with hierarchical naming */
162 NAMED_REGISTER_RWLOCK(&server->clients_rwlock, "clients_rwlock", (uintptr_t)(const void *)(server));
163 NAMED_REGISTER_ATOMIC(&server->running, "is_running", (uintptr_t)(const void *)(server));
164
165 return ASCIICHAT_OK;
166}
167
169 if (!server) {
170 return SET_ERRNO(ERROR_INVALID_PARAM, "server is NULL");
171 }
172
173 if (!server->config.client_handler) {
175 "client_handler is required for tcp_server_run() - use custom accept loop if handler is NULL");
176 }
177
178 log_debug("TCP server starting accept loop...");
179
180 while (atomic_load_bool(&server->running)) {
181 // Build fd_set for select()
182 fd_set read_fds;
183 socket_fd_zero(&read_fds);
184 socket_t max_fd = 0;
185
186 // Add IPv4 socket if available
187 if (server->listen_socket != INVALID_SOCKET_VALUE) {
188 socket_fd_set(server->listen_socket, &read_fds);
189 max_fd = server->listen_socket > max_fd ? server->listen_socket : max_fd;
190 }
191
192 // Add IPv6 socket if available
193 if (server->listen_socket6 != INVALID_SOCKET_VALUE) {
194 socket_fd_set(server->listen_socket6, &read_fds);
195 max_fd = server->listen_socket6 > max_fd ? server->listen_socket6 : max_fd;
196 }
197
198 // Use timeout from config (defaults to 1 second if not set)
199 // Convert double seconds to tv_sec and tv_usec
200 double timeout_sec_double = server->config.accept_timeout_sec > 0 ? server->config.accept_timeout_sec : 1.0;
201 time_t timeout_sec = (time_t)timeout_sec_double;
202 long timeout_usec = (long)((timeout_sec_double - timeout_sec) * (double)US_PER_SEC_INT);
203 struct timeval timeout = {.tv_sec = timeout_sec, .tv_usec = timeout_usec};
204
205 int select_result = socket_select((int)max_fd, &read_fds, NULL, NULL, &timeout);
206
207 if (select_result < 0) {
208 // Check if interrupted by signal (expected during shutdown)
209 int err = socket_get_last_error();
210 if (err == EINTR) {
211 // Signal interrupt (e.g., SIGTERM, SIGINT) - check running flag and continue
212 log_debug("select() interrupted by signal");
213 continue;
214 }
215 // Actual error - socket_get_error_string() returns last socket error
216 log_error("select() failed in accept loop: %s (errno=%d)", socket_get_error_string(), err);
217 continue;
218 }
219
220 if (select_result == 0) {
221 // Timeout - invoke status update callback if configured
222 if (server->config.status_update_fn) {
224 }
225 log_debug_every(5000, "select() timeout on TCP server (no incoming connections)");
226 continue;
227 }
228
229 // Check which socket has incoming connection
230 socket_t ready_socket = INVALID_SOCKET_VALUE;
231 if (server->listen_socket != INVALID_SOCKET_VALUE && socket_fd_isset(server->listen_socket, &read_fds)) {
232 ready_socket = server->listen_socket;
233 } else if (server->listen_socket6 != INVALID_SOCKET_VALUE && socket_fd_isset(server->listen_socket6, &read_fds)) {
234 ready_socket = server->listen_socket6;
235 }
236
237 if (ready_socket == INVALID_SOCKET_VALUE) {
238 // Spurious wakeup
239 continue;
240 }
241
242 // Accept connection
243 struct sockaddr_storage client_addr;
244 socklen_t client_addr_len = sizeof(client_addr);
245 socket_t client_socket = accept(ready_socket, (struct sockaddr *)&client_addr, &client_addr_len);
246 if (socket_is_valid(client_socket)) {
247 NAMED_REGISTER_SOCKET(client_socket, "tcp_client", NULL);
248 }
249
250 if (client_socket == INVALID_SOCKET_VALUE) {
251 log_warn("Failed to accept connection");
252 continue;
253 }
254
255 // Format client IP for logging
256 char client_ip[INET6_ADDRSTRLEN];
257 // NOLINTNEXTLINE(clang-analyzer-core.UndefinedBinaryOperatorResult) - client_addr filled by accept()
258 int addr_family = (client_addr.ss_family == AF_INET) ? AF_INET : AF_INET6;
259 if (format_ip_address(addr_family, (struct sockaddr *)&client_addr, client_ip, sizeof(client_ip)) != ASCIICHAT_OK) {
260 SAFE_STRNCPY(client_ip, "(unknown)", sizeof(client_ip));
261 }
262
263 log_debug("Accepted connection from %s", client_ip);
264
265 // Allocate client context
267 if (!ctx) {
268 log_error("Failed to allocate client context");
269 socket_close(client_socket);
270 continue;
271 }
272
273 ctx->client_socket = client_socket;
274 ctx->addr = client_addr;
275 ctx->addr_len = client_addr_len;
276 ctx->user_data = server->config.user_data;
277
278 // Spawn client handler thread
279 // Handler is responsible for:
280 // 1. Allocating client_data
281 // 2. Calling tcp_server_add_client() to register
282 // 3. Spawning additional worker threads via tcp_server_spawn_thread() if needed
283 // 4. Processing client requests
284 // 5. Calling tcp_server_remove_client() on disconnect
285 // 6. Closing socket and freeing ctx
286 asciichat_thread_t thread;
287 if (asciichat_thread_create(&thread, "tcp_client", server->config.client_handler, ctx) != 0) {
288 log_error("Failed to create client handler thread for %s", client_ip);
289 SAFE_FREE(ctx);
290 socket_close(client_socket);
291 continue;
292 }
293
294 // Thread is detached (handler is responsible for cleanup)
295 (void)thread; // Suppress unused warning
296 }
297
298 log_debug("TCP server accept loop exited");
299 return ASCIICHAT_OK;
300}
301
303 if (!server) {
304 return;
305 }
306
307 log_debug("Shutting down TCP server...");
308
309 // Signal server to stop
310 atomic_store_bool(&server->running, false);
311
312 // Close listen sockets
313 if (server->listen_socket != INVALID_SOCKET_VALUE) {
314 log_debug("Closing IPv4 listen socket");
317 }
318
319 if (server->listen_socket6 != INVALID_SOCKET_VALUE) {
320 log_debug("Closing IPv6 listen socket");
323 }
324
325 // Clean up client registry
327
328 tcp_client_entry_t *entry = NULL, *tmp = NULL;
329 HASH_ITER(hh, server->clients, entry, tmp) {
330 // Call cleanup callback if set
331 if (server->cleanup_fn && entry->client_data) {
332 server->cleanup_fn(entry->client_data);
333 }
334
335 // Destroy thread pool (stops all threads and frees resources)
336 if (entry->threads) {
338 entry->threads = NULL;
339 }
340
341 HASH_DEL(server->clients, entry);
342 SAFE_FREE(entry);
343 }
344 server->clients = NULL;
345
348
349 // Note: This function does NOT wait for client threads to exit
350 // Caller is responsible for thread lifecycle management
351
352 log_debug("TCP server shutdown complete");
353}
354
355// ============================================================================
356// Client Management Functions
357// ============================================================================
358
360 if (!server) {
361 return;
362 }
363 server->cleanup_fn = cleanup_fn;
364}
365
367 if (!server) {
368 return SET_ERRNO(ERROR_INVALID_PARAM, "server is NULL");
369 }
370
371 if (socket == INVALID_SOCKET_VALUE) {
372 return SET_ERRNO(ERROR_INVALID_PARAM, "socket is invalid");
373 }
374
375 // Allocate new entry
377 if (!entry) {
378 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate client entry");
379 }
380
381 entry->socket = socket;
382 entry->client_data = client_data;
383
384 // Create thread pool for this client
385 char pool_name[64];
386 SAFE_SNPRINTF(pool_name, sizeof(pool_name), "client_%d", socket);
387 entry->threads = thread_pool_create(pool_name);
388 if (!entry->threads) {
389 SAFE_FREE(entry);
390 return SET_ERRNO(ERROR_MEMORY, "Failed to create thread pool for client");
391 }
392
393 // Add to hash table (thread-safe with write lock)
395 HASH_ADD(hh, server->clients, socket, sizeof(socket_t), entry);
397
398 log_debug("Added client socket=%d to registry", socket);
399 return ASCIICHAT_OK;
400}
401
403 if (!server) {
404 return SET_ERRNO(ERROR_INVALID_PARAM, "server is NULL");
405 }
406
408
409 tcp_client_entry_t *entry = NULL;
410 HASH_FIND(hh, server->clients, &socket, sizeof(socket_t), entry);
411
412 if (!entry) {
414 // Already removed (e.g., during shutdown) - this is fine
415 log_debug("Client socket=%d already removed from registry", socket);
416 return ASCIICHAT_OK;
417 }
418
419 // Call cleanup callback if set
420 if (server->cleanup_fn && entry->client_data) {
421 server->cleanup_fn(entry->client_data);
422 }
423
424 // Destroy thread pool (stops all threads and frees resources)
425 if (entry->threads) {
427 entry->threads = NULL;
428 }
429
430 HASH_DEL(server->clients, entry);
431 SAFE_FREE(entry);
432
434
435 log_debug("Removed client socket=%d from registry", socket);
436 return ASCIICHAT_OK;
437}
438
440 if (!server || !out_data) {
441 return SET_ERRNO(ERROR_INVALID_PARAM, "server or out_data is NULL");
442 }
443
445
446 tcp_client_entry_t *entry = NULL;
447 HASH_FIND(hh, server->clients, &socket, sizeof(socket_t), entry);
448
449 if (!entry) {
450 *out_data = NULL;
452 return SET_ERRNO(ERROR_INVALID_STATE, "Client socket=%d not in registry", socket);
453 }
454
455 *out_data = entry->client_data;
457
458 return ASCIICHAT_OK;
459}
460
461void tcp_server_foreach_client(tcp_server_t *server, tcp_client_foreach_fn callback, void *user_arg) {
462 if (!server || !callback) {
463 return;
464 }
465
467
468 tcp_client_entry_t *entry, *tmp;
469 HASH_ITER(hh, server->clients, entry, tmp) {
470 callback(entry->socket, entry->client_data, user_arg);
471 }
472
474}
475
477 if (!server) {
478 return 0;
479 }
480
482 size_t count = HASH_COUNT(server->clients);
484
485 return count;
486}
487
488// ============================================================================
489// Client Context Utilities
490// ============================================================================
491
492const char *tcp_client_context_get_ip(const tcp_client_context_t *ctx, char *buf, size_t len) {
493 if (!ctx) {
494 SET_ERRNO(ERROR_INVALID_PARAM, "ctx is NULL");
495 return NULL;
496 }
497 if (!buf) {
498 SET_ERRNO(ERROR_INVALID_PARAM, "buf is NULL");
499 return NULL;
500 }
501 if (len == 0) {
502 SET_ERRNO(ERROR_INVALID_PARAM, "len is 0");
503 return NULL;
504 }
505
506 // Determine address family
507 int addr_family = (ctx->addr.ss_family == AF_INET) ? AF_INET : AF_INET6;
508
509 // Format IP address using existing utility
510 if (format_ip_address(addr_family, (struct sockaddr *)&ctx->addr, buf, len) != 0) {
511 return NULL;
512 }
513
514 return buf;
515}
516
518 if (!ctx) {
519 SET_ERRNO(ERROR_INVALID_PARAM, "ctx is NULL");
520 return -1;
521 }
522
523 // Extract port based on address family
524 if (ctx->addr.ss_family == AF_INET) {
525 struct sockaddr_in *addr_in = (struct sockaddr_in *)&ctx->addr;
526 return ntohs(addr_in->sin_port);
527 } else if (ctx->addr.ss_family == AF_INET6) {
528 struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)&ctx->addr;
529 return ntohs(addr_in6->sin6_port);
530 }
531
532 SET_ERRNO(ERROR_INVALID_STATE, "Unknown address family: %d", ctx->addr.ss_family);
533 return -1;
534}
535
536void tcp_server_reject_client(socket_t socket, const char *reason) {
537 if (socket == INVALID_SOCKET_VALUE) {
538 SET_ERRNO(ERROR_INVALID_PARAM, "socket is INVALID_SOCKET_VALUE");
539 return;
540 }
541
542 log_warn("Rejecting client connection: %s", reason ? reason : "unknown reason");
543 socket_close(socket);
544}
545
546// ============================================================================
547// Client Thread Pool Management
548// ============================================================================
549
550asciichat_error_t tcp_server_spawn_thread(tcp_server_t *server, socket_t client_socket, void *(*thread_func)(void *),
551 void *thread_arg, int stop_id, const char *thread_name) {
552 if (!server || !thread_func) {
553 return SET_ERRNO(ERROR_INVALID_PARAM, "server or thread_func is NULL");
554 }
555
556 // For WebRTC clients (no socket): create thread directly without TCP server thread pool tracking
557 // This allows render threads to work for both TCP and WebRTC clients
558 if (client_socket == INVALID_SOCKET_VALUE) {
559 // Extract thread handle from thread_arg if it's a client_info_t
560 // We need to store the thread handle somewhere, but for now just create the thread
561 // The caller must manage the thread handle directly for WebRTC clients
562 log_debug("Spawning standalone thread '%s' (no socket, WebRTC client)", thread_name ? thread_name : "unnamed");
563
564 // For WebRTC clients, we can't use thread_pool since there's no socket entry
565 // Instead, create the thread directly - the caller must handle the thread
566 // This is a hack to allow reusing tcp_server_spawn_thread for WebRTC
567 // In the future, consider a unified thread management system
568 asciichat_thread_t temp_thread;
569 asciichat_error_t result = asciichat_thread_create(&temp_thread, "tcp_worker", thread_func, thread_arg);
570 if (result != ASCIICHAT_OK) {
571 return result;
572 }
573
574 // Note: temp_thread handle is lost here, but the thread is running
575 // The caller must manage thread lifecycle for WebRTC clients differently
576 (void)temp_thread; // Suppress unused warning
577 return ASCIICHAT_OK;
578 }
579
580 // Find client entry
582 tcp_client_entry_t *entry = NULL;
583 HASH_FIND(hh, server->clients, &client_socket, sizeof(socket_t), entry);
584
585 if (!entry) {
587 return SET_ERRNO(ERROR_NOT_FOUND, "Client socket=%d not in registry", client_socket);
588 }
589
590 // Spawn thread in client's thread pool
591 asciichat_error_t result = thread_pool_spawn(entry->threads, thread_func, thread_arg, stop_id, thread_name);
592
594
595 if (result != ASCIICHAT_OK) {
596 return result;
597 }
598
599 size_t thread_count = thread_pool_get_count(entry->threads);
600 log_debug("Spawned thread '%s' (stop_id=%d) for client socket=%d (total_threads=%zu)",
601 thread_name ? thread_name : "unnamed", stop_id, client_socket, thread_count);
602
603 return ASCIICHAT_OK;
604}
605
607 if (!server) {
608 return SET_ERRNO(ERROR_INVALID_PARAM, "server is NULL");
609 }
610
611 if (client_socket == INVALID_SOCKET_VALUE) {
612 return SET_ERRNO(ERROR_INVALID_PARAM, "client_socket is invalid");
613 }
614
615 // Find client entry
617 tcp_client_entry_t *entry = NULL;
618 HASH_FIND(hh, server->clients, &client_socket, sizeof(socket_t), entry);
619
620 if (!entry) {
622 return SET_ERRNO(ERROR_NOT_FOUND, "Client socket=%d not in registry", client_socket);
623 }
624
625 // Stop all threads in client's thread pool (in stop_id order)
627 if (entry->threads) {
628 result = thread_pool_stop_all(entry->threads);
629 }
630
632
633 log_debug("All threads stopped for client socket=%d", client_socket);
634 return result;
635}
636
638 if (!server || !count) {
639 return SET_ERRNO(ERROR_INVALID_PARAM, "server or count is NULL");
640 }
641
642 if (client_socket == INVALID_SOCKET_VALUE) {
643 return SET_ERRNO(ERROR_INVALID_PARAM, "client_socket is invalid");
644 }
645
646 *count = 0;
647
648 // Find client entry
650 tcp_client_entry_t *entry = NULL;
651 HASH_FIND(hh, server->clients, &client_socket, sizeof(socket_t), entry);
652
653 if (!entry) {
655 return SET_ERRNO(ERROR_NOT_FOUND, "Client socket=%d not in registry", client_socket);
656 }
657
658 // Get thread count from pool
659 if (entry->threads) {
660 *count = thread_pool_get_count(entry->threads);
661 }
662
664
665 return ASCIICHAT_OK;
666}
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
โš™๏ธ Common definitions, error codes, macros, and types shared throughout the application
asciichat_error_t thread_pool_stop_all(void *pool)
Stop all threads in the pool in stop_id order.
asciichat_error_t thread_pool_spawn(void *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *name)
Spawn a worker thread in the pool.
Named object registry for debugging โ€” log identifiable resource names.
Cross-platform error handling utilities.
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
#define NAMED_REGISTER_RWLOCK(lock, name, parent_ptr)
Register an rwlock with automatic format specifier.
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define NAMED_REGISTER_SOCKET(socket, name, parent_ptr)
Register a socket with automatic format specifier.
#define NAMED_REGISTER(ptr, name, type, fmt, parent_ptr)
Register any pointer with base name, type, format spec, and location (auto-suffix)
#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_INVALID_STATE
@ ERROR_NETWORK_BIND
Definition error_codes.h:78
@ ERROR_NOT_FOUND
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ ERROR_THREAD
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#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 US_PER_SEC_INT
Definition time.h:161
void socket_fd_zero(fd_set *set)
Clear an fd_set.
#define rwlock_wrunlock(lock)
Release a write lock (with debug tracking in debug builds)
Definition rwlock.h:349
#define EINTR
int rwlock_init(rwlock_t *rwlock, const char *name)
Initialize a read-write lock with a name.
Definition threading.c:65
int rwlock_destroy(rwlock_t *lock)
Destroy a read-write lock.
bool socket_is_valid(socket_t sock)
Check if a socket handle is valid.
#define INVALID_SOCKET_VALUE
Invalid socket value (POSIX: -1)
Definition socket.h:278
void socket_fd_set(socket_t sock, fd_set *set)
Add a socket to an fd_set.
#define rwlock_rdlock(lock)
Acquire a read lock (with debug tracking in debug builds)
Definition rwlock.h:294
int socket_close(socket_t sock)
Close a socket.
int socket_get_last_error(void)
Get last socket error code.
#define rwlock_wrlock(lock)
Acquire a write lock (with debug tracking in debug builds)
Definition rwlock.h:313
int socket_select(socket_t max_fd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
Select sockets for I/O readiness.
const char * socket_get_error_string(void)
Get last socket error as string.
int socket_fd_isset(socket_t sock, fd_set *set)
Check if a socket is in an fd_set.
socket_t socket_create(const char *name, int domain, int type, int protocol)
Create a new named socket.
#define rwlock_rdunlock(lock)
Release a read lock (with debug tracking in debug builds)
Definition rwlock.h:331
asciichat_error_t format_ip_address(int family, const struct sockaddr *addr, char *output, size_t output_size)
Format IP address from socket address structure.
Definition ip.c:196
int socket_t
void(* tcp_client_foreach_fn)(socket_t socket, void *client_data, void *user_arg)
Callback for iterating over clients.
void(* tcp_client_cleanup_fn)(void *client_data)
Callback for client cleanup.
๐Ÿงต Cross-platform thread interface for ascii-chat
๐ŸŒ IP Address Parsing and Formatting Utilities
void tcp_server_destroy(tcp_server_t *server)
Shutdown TCP server.
void tcp_server_set_cleanup_callback(tcp_server_t *server, tcp_client_cleanup_fn cleanup_fn)
Set client cleanup callback.
void tcp_server_reject_client(socket_t socket, const char *reason)
Reject client connection with reason.
asciichat_error_t tcp_server_spawn_thread(tcp_server_t *server, socket_t client_socket, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *thread_name)
Spawn a worker thread for a client.
asciichat_error_t tcp_server_remove_client(tcp_server_t *server, socket_t socket)
Remove client from registry.
void tcp_server_foreach_client(tcp_server_t *server, tcp_client_foreach_fn callback, void *user_arg)
Iterate over all clients.
asciichat_error_t tcp_server_stop_client_threads(tcp_server_t *server, socket_t client_socket)
Stop all threads for a client in stop_id order.
asciichat_error_t tcp_server_init(tcp_server_t *server, const tcp_server_config_t *config)
Initialize TCP server.
asciichat_error_t tcp_server_get_client(tcp_server_t *server, socket_t socket, void **out_data)
Get client data.
int tcp_client_context_get_port(const tcp_client_context_t *ctx)
Get port number from client context.
asciichat_error_t tcp_server_run(tcp_server_t *server)
Run TCP server accept loop.
asciichat_error_t tcp_server_get_thread_count(tcp_server_t *server, socket_t client_socket, size_t *count)
Get thread count for a client.
const char * tcp_client_context_get_ip(const tcp_client_context_t *ctx, char *buf, size_t len)
Get formatted IP address from client context.
size_t tcp_server_get_client_count(tcp_server_t *server)
Get client count.
asciichat_error_t tcp_server_add_client(tcp_server_t *server, socket_t socket, void *client_data)
Add client to registry.
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
void * asciichat_thread_t
๐Ÿ“ Logging API with multiple log levels and terminal output control
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
Cross-platform socket interface for ascii-chat.
ClangTool/LibTooling compatibility shim for stdbool.h.
socket_t client_socket
Client connection socket.
struct sockaddr_storage addr
Client address.
void * user_data
User-provided data from config.
Client registry entry.
void * client_data
User-provided client data.
thread_pool_t * threads
Thread pool for client worker threads.
socket_t socket
Client socket (hash key)
bool bind_ipv6
Whether to bind IPv6 socket.
void * status_update_data
User data passed to status update callback.
void * user_data
User data passed to each client handler.
tcp_client_handler_fn client_handler
Client handler callback.
bool bind_ipv4
Whether to bind IPv4 socket.
const char * ipv4_address
IPv4 bind address (NULL or empty = don't bind)
const char * ipv6_address
IPv6 bind address (NULL or empty = don't bind)
double accept_timeout_sec
select() timeout in seconds (supports decimals, e.g. 0.05 for 50ms)
tcp_status_update_fn status_update_fn
Optional status update callback (called on timeout, NULL to disable)
atomic_t running
Server running flag (set false to shutdown)
tcp_server_config_t config
Server configuration.
tcp_client_entry_t * clients
Hash table of connected clients.
socket_t listen_socket
IPv4 listen socket.
socket_t listen_socket6
IPv6 listen socket.
tcp_client_cleanup_fn cleanup_fn
Callback for cleaning up client data.
rwlock_t clients_rwlock
Read-write lock (allows concurrent readers, exclusive writers)
void thread_pool_destroy(thread_pool_t *pool)
Destroy a thread pool.
thread_pool_t * thread_pool_create(int num_threads)
size_t thread_pool_get_count(const thread_pool_t *pool)
Get thread count in the pool.
๐Ÿงต Generic thread pool abstraction for managing worker threads
โฑ๏ธ High-precision timing utilities using sokol_time.h and uthash